/* Copyright 2026 Element Creations Ltd. SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ import { fn, userEvent, waitFor, within, expect } from "storybook/test"; import { useEffect, useState, type FC, type JSX, type ReactNode } from "react"; import type { Meta, StoryObj } from "@storybook/react-vite"; import { MediaMuteAndSwitchButton } from "./MediaMuteAndSwitchButton"; import styles from "./MediaMuteAndSwitchButton.module.css"; import { type BackgroundEffectOption } from "./BackgroundEffectGrid"; import { SelfPreview } from "./SelfPreview"; import { shippedBackgrounds } from "../livekit/backgroundEffects"; import meterStyles from "./MicrophoneLevelMeter.module.css"; import { MediaDevicesContext } from "../MediaDevicesContext"; import { RootElementProvider } from "../RootElementContext"; import { MediaDevices } from "../state/MediaDevices"; import { globalScope } from "../state/ObservableScope"; const mediaDevices = new MediaDevices(globalScope, { controlledAudioDevices: false, }); /** Supplies a microphone, a wavering tone: a story's invented device ids match no hardware. */ const WithAMicrophone: FC<{ children: ReactNode }> = ({ children }) => { useEffect(() => { const context = new AudioContext(); const microphone = context.createMediaStreamDestination(); const tone = context.createOscillator(); const loudness = context.createGain(); // Wavers between a third and two thirds of full scale, so it reads as live. const swing = context.createOscillator(); const depth = context.createGain(); loudness.gain.value = 0.25; depth.gain.value = 0.2; swing.frequency.value = 0.6; tone.frequency.value = 220; swing.connect(depth).connect(loudness.gain); tone.connect(loudness).connect(microphone); tone.start(); swing.start(); const devices = navigator.mediaDevices; const openedForReal = devices.getUserMedia.bind(devices); const opened = Promise.resolve(microphone.stream); // A fresh clone each time, so a caller stopping its tracks doesn't end the next. devices.getUserMedia = async (): Promise => (await opened).clone(); return (): void => { devices.getUserMedia = openedForReal; tone.stop(); swing.stop(); void context.close(); }; }, []); return <>{children}; }; /** A camera for the preview: a figure against a room, drawn to a stream. */ const FakeCamera: FC = () => { const [stream, setStream] = useState(); useEffect(() => { const canvas = document.createElement("canvas"); canvas.width = 640; canvas.height = 360; const context = canvas.getContext("2d")!; const draw = (): void => { context.fillStyle = "#6b7f8e"; context.fillRect(0, 0, 640, 360); context.fillStyle = "#c9a27e"; context.beginPath(); context.arc(320, 150, 62, 0, 2 * Math.PI); context.fill(); context.fillStyle = "#34495e"; context.beginPath(); context.ellipse(320, 380, 150, 140, 0, Math.PI, 2 * Math.PI); context.fill(); }; draw(); // Redrawn, so the stream keeps delivering frames. const timer = setInterval(draw, 200); const captured = canvas.captureStream(10); setStream(captured); return (): void => { clearInterval(timer); captured.getTracks().forEach((track) => track.stop()); }; }, []); return (