diff --git a/js/hang-ui/src/shared/components/stats/__tests__/Stats.test.tsx b/js/hang-ui/src/shared/components/stats/__tests__/Stats.test.tsx
deleted file mode 100644
index a9269f3ac..000000000
--- a/js/hang-ui/src/shared/components/stats/__tests__/Stats.test.tsx
+++ /dev/null
@@ -1,225 +0,0 @@
-import { createContext, createSignal } from "solid-js";
-import { render } from "solid-js/web";
-import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import { Stats } from "..";
-import type { ProviderProps } from "../types";
-import { createMockProviderProps } from "./utils";
-
-describe("Stats Component", () => {
- let container: HTMLDivElement;
- let dispose: (() => void) | undefined;
-
- beforeEach(() => {
- container = document.createElement("div");
- document.body.appendChild(container);
-
- // Mock fetch to prevent network requests for Icon SVG files
- global.fetch = vi.fn().mockResolvedValue({
- ok: true,
- text: () => Promise.resolve(''),
- });
- });
-
- afterEach(() => {
- dispose?.();
- dispose = undefined;
- vi.restoreAllMocks();
- });
-
- it("renders stats container", () => {
- const mockProps = createMockProviderProps();
- const TestContext = createContext(mockProps);
-
- dispose = render(
- () => (
-
- context={TestContext} getElement={() => mockProps} />
-
- ),
- container,
- );
-
- const stats = container.querySelector(".stats");
- expect(stats).toBeTruthy();
- });
-
- it("waits for audio and video before rendering", async () => {
- const mockDefault = createMockProviderProps();
- const TestContext = createContext(mockDefault);
-
- dispose = render(() => {
- const [mediaElement, setMediaElement] = createSignal(undefined);
-
- // Set media element after initial render
- setTimeout(() => setMediaElement(createMockProviderProps()), 100);
-
- return (
-
- context={TestContext} getElement={() => mediaElement()} />
-
- );
- }, container);
-
- // Initially no StatsPanel should be rendered
- let panel = container.querySelector(".stats__panel");
- expect(panel).toBeFalsy();
-
- // Wait for setTimeout to trigger
- await new Promise((resolve) => setTimeout(resolve, 150));
-
- // Now panel should be rendered
- panel = container.querySelector(".stats__panel");
- expect(panel).toBeTruthy();
- });
-
- it("only renders when both audio and video are available", async () => {
- const mockDefault = createMockProviderProps();
- const TestContext = createContext(mockDefault);
- const mockWithoutVideo = createMockProviderProps({ video: false });
-
- dispose = render(() => {
- const [mediaElement, setMediaElement] = createSignal | undefined>(mockWithoutVideo);
-
- // Set full props after initial render
- setTimeout(() => setMediaElement(createMockProviderProps()), 100);
-
- return (
-
-
- context={TestContext}
- getElement={() => mediaElement() as ProviderProps | undefined}
- />
-
- );
- }, container);
-
- let panel = container.querySelector(".stats__panel");
-
- // Wait for setTimeout to trigger
- await new Promise((resolve) => setTimeout(resolve, 150));
-
- panel = container.querySelector(".stats__panel");
- expect(panel).toBeTruthy();
- });
-
- it("works with different context types", () => {
- interface CustomContext {
- hangWatch: () => ProviderProps | undefined;
- }
-
- const mockProps = createMockProviderProps();
- const contextValue: CustomContext = {
- hangWatch: () => mockProps,
- };
- const CustomTestContext = createContext(contextValue);
-
- dispose = render(
- () => (
-
- context={CustomTestContext} getElement={(ctx) => ctx?.hangWatch()} />
-
- ),
- container,
- );
-
- const stats = container.querySelector(".stats");
- expect(stats).toBeTruthy();
-
- const panel = container.querySelector(".stats__panel");
- expect(panel).toBeTruthy();
- });
-
- it("provides context value to child components", () => {
- const mockProps = createMockProviderProps();
- const TestContext = createContext(mockProps);
-
- dispose = render(
- () => (
-
- context={TestContext} getElement={() => mockProps} />
-
- ),
- container,
- );
-
- const panel = container.querySelector(".stats__panel");
- expect(panel).toBeTruthy();
- });
-
- it("handles undefined context gracefully", () => {
- const mockDefault = createMockProviderProps();
- const TestContext = createContext(mockDefault);
-
- dispose = render(
- () => (
-
- context={TestContext} getElement={() => undefined} />
-
- ),
- container,
- );
-
- // Should not render panel when element is undefined
- const panel = container.querySelector(".stats__panel");
- expect(panel).toBeFalsy();
- });
-
- it("updates when context changes", async () => {
- const mockDefault = createMockProviderProps();
- const TestContext = createContext(mockDefault);
-
- dispose = render(() => {
- const [element, setElement] = createSignal(undefined);
-
- // Set element after initial render
- setTimeout(() => setElement(createMockProviderProps()), 100);
-
- return (
-
- context={TestContext} getElement={() => element()} />
-
- );
- }, container);
-
- let panel = container.querySelector(".stats__panel");
- expect(panel).toBeFalsy();
-
- // Wait for setTimeout to trigger
- await new Promise((resolve) => setTimeout(resolve, 150));
-
- panel = container.querySelector(".stats__panel");
- expect(panel).toBeTruthy();
- });
-
- it("rerenders when getElement function returns different values", async () => {
- const TestContext = createContext("test");
- const element1 = createMockProviderProps();
- const element2 = createMockProviderProps();
-
- dispose = render(() => {
- const [key, setKey] = createSignal<"element1" | "element2">("element1");
-
- const getElement = (_ctx: string) => {
- return key() === "element1" ? element1 : element2;
- };
-
- // Change key after initial render
- setTimeout(() => setKey("element2"), 100);
-
- return (
-
- context={TestContext} getElement={getElement} />
-
- );
- }, container);
-
- let panel = container.querySelector(".stats__panel");
- expect(panel).toBeTruthy();
-
- // Wait for setTimeout to trigger
- await new Promise((resolve) => setTimeout(resolve, 150));
-
- panel = container.querySelector(".stats__panel");
- expect(panel).toBeTruthy();
- });
-});
diff --git a/js/hang-ui/src/shared/components/stats/__tests__/components/StatsItem.test.tsx b/js/hang-ui/src/shared/components/stats/__tests__/components/StatsItem.test.tsx
deleted file mode 100644
index e97d3ae3f..000000000
--- a/js/hang-ui/src/shared/components/stats/__tests__/components/StatsItem.test.tsx
+++ /dev/null
@@ -1,363 +0,0 @@
-import { createSignal } from "solid-js";
-import { render } from "solid-js/web";
-import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import * as Icon from "../../../icon/icon";
-import { StatsItem } from "../../components/StatsItem";
-import type { BaseProvider } from "../../providers/base";
-import * as registry from "../../providers/registry";
-import type { ProviderContext, ProviderProps } from "../../types";
-import { createMockProviderProps } from "../utils";
-
-vi.mock("../../providers/registry", () => ({
- getStatsInformationProvider: vi.fn(),
-}));
-
-describe("StatsItem", () => {
- let container: HTMLDivElement;
- let dispose: (() => void) | undefined;
- let mockAudioVideo: ProviderProps;
-
- beforeEach(() => {
- container = document.createElement("div");
- document.body.appendChild(container);
- mockAudioVideo = createMockProviderProps();
-
- // Mock fetch to prevent network requests for Icon SVG files
- global.fetch = vi.fn().mockResolvedValue({
- ok: true,
- text: () => Promise.resolve(''),
- });
- });
-
- afterEach(() => {
- dispose?.();
- dispose = undefined;
- document.body.removeChild(container);
- vi.clearAllMocks();
- });
-
- it("renders with correct base structure", () => {
- vi.mocked(registry.getStatsInformationProvider).mockReturnValue(undefined);
-
- dispose = render(
- () => (
- }
- audio={mockAudioVideo.audio}
- video={mockAudioVideo.video}
- />
- ),
- container,
- );
-
- const item = container.querySelector(".stats__item");
- expect(item).toBeTruthy();
- expect(item?.classList.contains("stats__item--network")).toBe(true);
- });
-
- it("renders icon wrapper with SVG content", () => {
- vi.mocked(registry.getStatsInformationProvider).mockReturnValue(undefined);
-
- dispose = render(() => {
- const testSvg = (
-
- );
-
- return (
-
- );
- }, container);
-
- const iconWrapper = container.querySelector(".stats__icon-wrapper");
- expect(iconWrapper).toBeTruthy();
-
- const icon = container.querySelector("svg[data-testid='test-icon']");
- expect(icon).toBeTruthy();
- });
-
- it("renders item detail with icon text", () => {
- vi.mocked(registry.getStatsInformationProvider).mockReturnValue(undefined);
-
- dispose = render(
- () => (
- }
- audio={mockAudioVideo.audio}
- video={mockAudioVideo.video}
- />
- ),
- container,
- );
-
- const iconText = container.querySelector(".stats__item-title");
- expect(iconText?.textContent).toBe("audio");
- });
-
- it("displays N/A when no provider is available", () => {
- vi.mocked(registry.getStatsInformationProvider).mockReturnValue(undefined);
-
- dispose = render(
- () => (
- }
- audio={mockAudioVideo.audio}
- video={mockAudioVideo.video}
- />
- ),
- container,
- );
-
- const dataDisplay = container.querySelector(".stats__item-data");
- expect(dataDisplay?.textContent).toBe("N/A");
- });
-
- it("initializes provider with audio and video props", () => {
- const mockProvider: Pick = {
- setup: vi.fn(),
- cleanup: vi.fn(),
- };
-
- const MockProviderClass = vi.fn(() => mockProvider) as unknown as ReturnType<
- typeof registry.getStatsInformationProvider
- >;
- vi.mocked(registry.getStatsInformationProvider).mockReturnValue(MockProviderClass);
-
- dispose = render(
- () => (
- }
- audio={mockAudioVideo.audio}
- video={mockAudioVideo.video}
- />
- ),
- container,
- );
-
- expect(MockProviderClass).toHaveBeenCalledWith(mockAudioVideo);
- });
-
- it("calls provider setup with setDisplayData callback", () => {
- const mockProvider: Pick = {
- setup: vi.fn(),
- cleanup: vi.fn(),
- };
-
- const MockProviderClass = vi.fn(() => mockProvider) as unknown as ReturnType<
- typeof registry.getStatsInformationProvider
- >;
- vi.mocked(registry.getStatsInformationProvider).mockReturnValue(MockProviderClass);
-
- dispose = render(
- () => (
- }
- audio={mockAudioVideo.audio}
- video={mockAudioVideo.video}
- />
- ),
- container,
- );
-
- expect(mockProvider.setup).toHaveBeenCalled();
-
- const setupCall = vi.mocked(mockProvider.setup).mock.calls[0][0] as ProviderContext;
- expect(setupCall.setDisplayData).toBeDefined();
- expect(typeof setupCall.setDisplayData).toBe("function");
- });
-
- it("updates display data when provider calls setDisplayData", () => {
- let capturedSetDisplayData: ((data: string) => void) | undefined;
-
- const mockProvider: Pick = {
- setup: vi.fn((context: ProviderContext) => {
- capturedSetDisplayData = context.setDisplayData;
- }),
- cleanup: vi.fn(),
- };
-
- const MockProviderClass = vi.fn(() => mockProvider) as unknown as ReturnType<
- typeof registry.getStatsInformationProvider
- >;
- vi.mocked(registry.getStatsInformationProvider).mockReturnValue(MockProviderClass);
-
- dispose = render(
- () => (
- }
- audio={mockAudioVideo.audio}
- video={mockAudioVideo.video}
- />
- ),
- container,
- );
-
- expect(capturedSetDisplayData).toBeDefined();
-
- capturedSetDisplayData?.("42 kbps");
-
- const dataDisplay = container.querySelector(".stats__item-data");
- expect(dataDisplay?.textContent).toBe("42 kbps");
- });
-
- it("renders correct class for each icon type", () => {
- vi.mocked(registry.getStatsInformationProvider).mockReturnValue(undefined);
-
- const statsProvider = [
- { name: "network", Icon: Icon.Network },
- { name: "video", Icon: Icon.Video },
- { name: "audio", Icon: Icon.Audio },
- { name: "buffer", Icon: Icon.Buffer },
- ] as const;
-
- statsProvider.forEach((provider) => {
- const testContainer = document.createElement("div");
- document.body.appendChild(testContainer);
-
- const testDispose = render(
- () => (
- }
- audio={mockAudioVideo.audio}
- video={mockAudioVideo.video}
- />
- ),
- testContainer,
- );
-
- const item = testContainer.querySelector(".stats__item");
- expect(item?.classList.contains(`stats__item--${provider.name}`)).toBe(true);
-
- testDispose();
- document.body.removeChild(testContainer);
- });
- });
-
- it("cleans up previous provider before initializing new one", async () => {
- const mockProvider1: Pick = {
- setup: vi.fn(),
- cleanup: vi.fn(),
- };
-
- const mockProvider2: Pick = {
- setup: vi.fn(),
- cleanup: vi.fn(),
- };
-
- const MockProviderClass1 = vi.fn(() => mockProvider1) as unknown as ReturnType<
- typeof registry.getStatsInformationProvider
- >;
- const MockProviderClass2 = vi.fn(() => mockProvider2) as unknown as ReturnType<
- typeof registry.getStatsInformationProvider
- >;
-
- let _callCount = 0;
- vi.mocked(registry.getStatsInformationProvider).mockImplementation(() => {
- if (_callCount === 0) {
- _callCount++;
- return MockProviderClass1;
- }
- return MockProviderClass2;
- });
-
- dispose = render(() => {
- const [statProvider, setStatProvider] = createSignal<"network" | "video">("network");
-
- // Switch provider after initial render
- setTimeout(() => {
- _callCount = 0;
- vi.mocked(registry.getStatsInformationProvider).mockReturnValue(MockProviderClass2);
- setStatProvider("video");
- }, 0);
-
- return (
- : }
- audio={mockAudioVideo.audio}
- video={mockAudioVideo.video}
- />
- );
- }, container);
-
- expect(mockProvider1.setup).toHaveBeenCalled();
-
- // Wait for setTimeout
- await new Promise((resolve) => setTimeout(resolve, 10));
-
- expect(mockProvider1.cleanup).toHaveBeenCalled();
- expect(mockProvider2.setup).toHaveBeenCalled();
- });
-
- it("maintains correct DOM hierarchy", () => {
- vi.mocked(registry.getStatsInformationProvider).mockReturnValue(undefined);
-
- dispose = render(
- () => (
- }
- audio={mockAudioVideo.audio}
- video={mockAudioVideo.video}
- />
- ),
- container,
- );
-
- const item = container.querySelector(".stats__item");
- expect(item?.children.length).toBe(2);
-
- const iconWrapper = item?.querySelector(".stats__icon-wrapper");
- expect(iconWrapper).toBeTruthy();
- expect(iconWrapper?.children.length).toBe(1);
-
- const detail = item?.querySelector(".stats__item-detail");
- expect(detail).toBeTruthy();
- expect(detail?.children.length).toBe(2);
-
- expect(detail?.querySelector(".stats__item-title")).toBeTruthy();
- expect(detail?.querySelector(".stats__item-data")).toBeTruthy();
- });
-
- it("calls getStatsInformationProvider with correct statProvider", () => {
- vi.mocked(registry.getStatsInformationProvider).mockReturnValue(undefined);
-
- dispose = render(
- () => (
- }
- audio={mockAudioVideo.audio}
- video={mockAudioVideo.video}
- />
- ),
- container,
- );
-
- expect(registry.getStatsInformationProvider).toHaveBeenCalledWith("buffer");
- });
-});
diff --git a/js/hang-ui/src/shared/components/stats/__tests__/components/StatsPanel.test.tsx b/js/hang-ui/src/shared/components/stats/__tests__/components/StatsPanel.test.tsx
deleted file mode 100644
index 09a44c71a..000000000
--- a/js/hang-ui/src/shared/components/stats/__tests__/components/StatsPanel.test.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import { render } from "solid-js/web";
-import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import { StatsPanel, statsDetailItems } from "../../components/StatsPanel";
-import type { ProviderProps } from "../../types";
-import { createMockProviderProps } from "../utils";
-
-describe("StatsPanel", () => {
- let container: HTMLDivElement;
- let dispose: (() => void) | undefined;
- let mockAudioVideo: ProviderProps;
-
- beforeEach(() => {
- container = document.createElement("div");
- document.body.appendChild(container);
- mockAudioVideo = createMockProviderProps();
-
- // Mock fetch to prevent network requests for Icon SVG files
- global.fetch = vi.fn().mockResolvedValue({
- ok: true,
- text: () => Promise.resolve(''),
- });
- });
-
- afterEach(() => {
- dispose?.();
- dispose = undefined;
- document.body.removeChild(container);
- vi.restoreAllMocks();
- });
-
- it("renders with correct base class", () => {
- dispose = render(() => , container);
-
- const panel = container.querySelector(".stats__panel");
- expect(panel).toBeTruthy();
- });
-
- it("renders all metric items", () => {
- dispose = render(() => , container);
-
- const items = container.querySelectorAll(".stats__item");
- expect(items.length).toBe(statsDetailItems.length);
- });
-
- it("renders items with correct icon types", () => {
- const expectedIcons = ["network", "video", "audio", "buffer"];
- dispose = render(() => , container);
-
- const items = container.querySelectorAll(".stats__item");
- items.forEach((item, index) => {
- expect(item.classList.contains(`stats__item--${expectedIcons[index]}`)).toBe(true);
- });
- });
-
- it("renders each item with icon wrapper", () => {
- dispose = render(() => , container);
-
- const wrappers = container.querySelectorAll(".stats__icon-wrapper");
- expect(wrappers.length).toBe(4);
- });
-
- it("renders each item with detail section", () => {
- dispose = render(() => , container);
-
- const details = container.querySelectorAll(".stats__item-detail");
- expect(details.length).toBe(4);
- });
-
- it("maintains correct DOM structure", () => {
- dispose = render(() => , container);
-
- const panel = container.querySelector(".stats__panel");
- const items = panel?.querySelectorAll(".stats__item");
-
- expect(panel?.children.length).toBe(4);
- items?.forEach((item) => {
- expect(item.querySelector(".stats__icon-wrapper")).toBeTruthy();
- expect(item.querySelector(".stats__item-detail")).toBeTruthy();
- });
- });
-});
diff --git a/js/hang-ui/src/shared/components/stats/__tests__/providers/audio.test.ts b/js/hang-ui/src/shared/components/stats/__tests__/providers/audio.test.ts
deleted file mode 100644
index 081f542ef..000000000
--- a/js/hang-ui/src/shared/components/stats/__tests__/providers/audio.test.ts
+++ /dev/null
@@ -1,255 +0,0 @@
-import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import { AudioProvider } from "../../providers/audio";
-import type { AudioConfig, AudioSource, AudioStats, ProviderContext, ProviderProps } from "../../types";
-import { createMockProviderProps } from "../utils";
-
-declare global {
- var __advanceTime: (ms: number) => void;
-}
-
-describe("AudioProvider", () => {
- let provider: AudioProvider;
- let context: ProviderContext;
- let setDisplayData: ReturnType;
- let intervalCallback: ((interval: number) => void) | null = null;
- let originalWindow: typeof window;
- let originalPerformance: typeof performance;
- let mockClearInterval: ReturnType;
-
- beforeEach(() => {
- originalWindow = global.window;
- originalPerformance = global.performance as unknown as Performance;
- setDisplayData = vi.fn();
- context = { setDisplayData };
- intervalCallback = null;
-
- // Mock window functions
- const mockSetInterval = vi.fn((callback: () => void) => {
- intervalCallback = callback as unknown as (interval: number) => void;
- return 1;
- });
- mockClearInterval = vi.fn();
-
- global.window = {
- setInterval: mockSetInterval,
- clearInterval: mockClearInterval,
- } as unknown as typeof window;
-
- // Mock performance.now()
- let mockTime = 0;
- global.performance = {
- now: vi.fn(() => mockTime),
- } as unknown as Performance;
-
- // Helper to advance mock time
- global.__advanceTime = (ms: number) => {
- mockTime += ms;
- };
- });
-
- afterEach(() => {
- provider?.cleanup();
- global.window = originalWindow;
- global.performance = originalPerformance as unknown as Performance;
- });
-
- it("should display N/A when audio source is not available", () => {
- const props: ProviderProps = {};
- provider = new AudioProvider(props);
- provider.setup(context);
-
- expect(setDisplayData).toHaveBeenCalledWith("N/A");
- });
-
- it("should display audio config with stats placeholder on first call", () => {
- const audioConfig: AudioConfig = {
- sampleRate: 48000,
- numberOfChannels: 2,
- bitrate: 128000,
- codec: "opus",
- };
-
- const stats: AudioStats = { bytesReceived: 0 };
-
- const audio: AudioSource = {
- source: {
- active: {
- peek: () => "audio",
- subscribe: vi.fn(() => vi.fn()),
- },
- config: {
- peek: () => audioConfig,
- subscribe: vi.fn(() => vi.fn()),
- },
- stats: {
- peek: () => stats,
- subscribe: vi.fn(() => vi.fn()),
- },
- },
- };
-
- const props: ProviderProps = { audio };
- provider = new AudioProvider(props);
- provider.setup(context);
-
- expect(setDisplayData).toHaveBeenCalledWith("48.0kHz\n2ch\nN/A\nopus");
- });
-
- it("should calculate bitrate from bytesReceived delta", () => {
- const audioConfig: AudioConfig = {
- sampleRate: 48000,
- numberOfChannels: 2,
- bitrate: 128000,
- codec: "opus",
- };
-
- const peekFn = vi.fn(() => ({ bytesReceived: 0 }) as AudioStats);
-
- const audio: AudioSource = {
- source: {
- active: {
- peek: () => "audio",
- subscribe: vi.fn(() => vi.fn()),
- },
- config: {
- peek: () => audioConfig,
- subscribe: vi.fn(() => vi.fn()),
- },
- stats: {
- peek: peekFn,
- subscribe: vi.fn(() => vi.fn()),
- },
- },
- };
-
- const props: ProviderProps = { audio };
- provider = new AudioProvider(props);
-
- // First call - start with bytes already flowing
- peekFn.mockReturnValue({ bytesReceived: 5000 } as AudioStats);
- provider.setup(context);
- expect(setDisplayData).toHaveBeenCalledWith("48.0kHz\n2ch\nN/A\nopus");
-
- // Simulate bytesReceived increasing: 6250 bytes delta = 200 kbps at 250ms interval
- // (6250 bytes * 8 bits/byte * 4) / 1000 = 200 kbps
- peekFn.mockReturnValue({ bytesReceived: 11250 } as AudioStats);
- global.__advanceTime(250);
- intervalCallback?.(250);
- expect(setDisplayData).toHaveBeenNthCalledWith(2, "48.0kHz\n2ch\n200kbps\nopus");
-
- // Increase bytes more: delta 3125 = 100 kbps
- peekFn.mockReturnValue({ bytesReceived: 14375 } as AudioStats);
- global.__advanceTime(250);
- intervalCallback?.(250);
- expect(setDisplayData).toHaveBeenNthCalledWith(3, "48.0kHz\n2ch\n100kbps\nopus");
- });
-
- it("should display N/A when active or config is missing", () => {
- const audio: AudioSource = {
- source: {
- active: {
- peek: () => undefined,
- subscribe: vi.fn(() => vi.fn()),
- },
- config: {
- peek: () => undefined,
- subscribe: vi.fn(() => vi.fn()),
- },
- stats: {
- peek: () => ({ bytesReceived: 0 }),
- subscribe: vi.fn(() => vi.fn()),
- },
- },
- };
-
- const props: ProviderProps = { audio };
- provider = new AudioProvider(props);
- provider.setup(context);
-
- expect(setDisplayData).toHaveBeenCalledWith("N/A");
- });
-
- it("should handle mono audio", () => {
- const audioConfig: AudioConfig = {
- sampleRate: 44100,
- numberOfChannels: 1,
- bitrate: 128000,
- codec: "opus",
- };
-
- const audio: AudioSource = {
- source: {
- active: {
- peek: () => "audio",
- subscribe: vi.fn(() => vi.fn()),
- },
- config: {
- peek: () => audioConfig,
- subscribe: vi.fn(() => vi.fn()),
- },
- stats: {
- peek: () => ({ bytesReceived: 0 }),
- subscribe: vi.fn(() => vi.fn()),
- },
- },
- };
-
- const props: ProviderProps = { audio };
- provider = new AudioProvider(props);
- provider.setup(context);
-
- expect(setDisplayData).toHaveBeenCalledWith("44.1kHz\n1ch\nN/A\nopus");
- });
-
- it("should format Mbps for high bitrates", () => {
- const audioConfig: AudioConfig = {
- sampleRate: 48000,
- numberOfChannels: 2,
- bitrate: 128000,
- codec: "opus",
- };
-
- const peekFn = vi.fn(() => ({ bytesReceived: 0 }) as AudioStats);
-
- const audio: AudioSource = {
- source: {
- active: {
- peek: () => "audio",
- subscribe: vi.fn(() => vi.fn()),
- },
- config: {
- peek: () => audioConfig,
- subscribe: vi.fn(() => vi.fn()),
- },
- stats: {
- peek: peekFn,
- subscribe: vi.fn(() => vi.fn()),
- },
- },
- };
-
- const props: ProviderProps = { audio };
- provider = new AudioProvider(props);
-
- // First call - display audio config with stats placeholder on first call
- peekFn.mockReturnValue({ bytesReceived: 48000 } as AudioStats);
- provider.setup(context);
- // Simulate 5 Mbps: 156250 bytes delta = 5000000 bits/s = 5 Mbps
- peekFn.mockReturnValue({ bytesReceived: 204250 } as AudioStats);
- global.__advanceTime(250);
- intervalCallback?.(250);
-
- expect(setDisplayData).toHaveBeenNthCalledWith(2, "48.0kHz\n2ch\n5.0Mbps\nopus");
- });
-
- it("should cleanup interval on dispose", () => {
- const props: ProviderProps = createMockProviderProps({ video: false });
- provider = new AudioProvider(props);
- provider.setup(context);
-
- provider.cleanup();
-
- expect(mockClearInterval).toHaveBeenCalledTimes(1);
- expect(mockClearInterval).toHaveBeenCalledWith(1);
- });
-});
diff --git a/js/hang-ui/src/shared/components/stats/__tests__/providers/base.test.ts b/js/hang-ui/src/shared/components/stats/__tests__/providers/base.test.ts
deleted file mode 100644
index c70a27f27..000000000
--- a/js/hang-ui/src/shared/components/stats/__tests__/providers/base.test.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from "vitest";
-import { BaseProvider } from "../../providers/base";
-import type { ProviderContext, ProviderProps } from "../../types";
-
-class TestProvider extends BaseProvider {
- public setupCalled = false;
- public setupContext: ProviderContext | undefined;
- public cleanupCalled = false;
-
- setup(context: ProviderContext): void {
- this.setupCalled = true;
- this.setupContext = context;
- }
-
- cleanup(): void {
- this.cleanupCalled = true;
- super.cleanup();
- }
-}
-
-describe("BaseProvider", () => {
- let provider: TestProvider;
- let context: ProviderContext;
-
- beforeEach(() => {
- const props: ProviderProps = {};
- provider = new TestProvider(props);
- context = {
- setDisplayData: vi.fn(),
- };
- });
-
- it("should initialize with props", () => {
- const props: ProviderProps = { audio: undefined, video: undefined };
- const testProvider = new TestProvider(props);
- expect(testProvider).toBeDefined();
- });
-
- it("should call setup method", () => {
- provider.setup(context);
- expect(provider.setupCalled).toBe(true);
- expect(provider.setupContext).toEqual(context);
- });
-
- it("should cleanup", () => {
- provider.setup(context);
- expect(provider.cleanupCalled).toBe(false);
- provider.cleanup();
- expect(provider.cleanupCalled).toBe(true);
- expect(provider.setupCalled).toBe(true);
- });
-
- it("should have setup and cleanup methods", () => {
- expect(provider.setup).toBeDefined();
- expect(provider.cleanup).toBeDefined();
- expect(typeof provider.setup).toBe("function");
- expect(typeof provider.cleanup).toBe("function");
- });
-});
diff --git a/js/hang-ui/src/shared/components/stats/__tests__/providers/network.test.ts b/js/hang-ui/src/shared/components/stats/__tests__/providers/network.test.ts
deleted file mode 100644
index 1aed84d59..000000000
--- a/js/hang-ui/src/shared/components/stats/__tests__/providers/network.test.ts
+++ /dev/null
@@ -1,309 +0,0 @@
-import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import { NetworkProvider } from "../../providers/network";
-import type { ProviderContext, ProviderProps } from "../../types";
-
-interface MockConnection {
- effectiveType?: "slow-2g" | "2g" | "3g" | "4g";
- downlinkMax?: number;
- downlink?: number;
- rtt?: number;
- saveData?: boolean;
- addEventListener?: (type: string, listener: () => void) => void;
- removeEventListener?: (type: string, listener: () => void) => void;
-}
-
-const mockNavigator: { onLine: boolean; connection?: MockConnection } = {
- onLine: true,
- connection: undefined,
-};
-
-describe("NetworkProvider", () => {
- let provider: NetworkProvider;
- let context: ProviderContext;
- let setDisplayData: ReturnType;
-
- beforeEach(() => {
- setDisplayData = vi.fn();
- context = { setDisplayData };
-
- Object.defineProperty(window, "navigator", {
- value: mockNavigator,
- writable: true,
- configurable: true,
- });
-
- vi.useFakeTimers();
- });
-
- afterEach(() => {
- provider?.cleanup();
- vi.useRealTimers();
- vi.clearAllMocks();
- });
-
- it("should display N/A initially when no connection info", () => {
- const props: ProviderProps = {};
- provider = new NetworkProvider(props);
- mockNavigator.connection = undefined;
- mockNavigator.onLine = true;
-
- provider.setup(context);
- expect(setDisplayData).toHaveBeenCalledWith("N/A");
- });
-
- it("should display offline status when browser is offline", () => {
- const props: ProviderProps = {};
- provider = new NetworkProvider(props);
- mockNavigator.onLine = false;
- mockNavigator.connection = {
- effectiveType: "4g" as const,
- };
-
- provider.setup(context);
-
- expect(setDisplayData).toHaveBeenCalledWith("offline");
- });
-
- it("should display effective connection type", () => {
- const props: ProviderProps = {};
- provider = new NetworkProvider(props);
- mockNavigator.onLine = true;
- mockNavigator.connection = {
- effectiveType: "4g" as const,
- };
-
- provider.setup(context);
-
- expect(setDisplayData).toHaveBeenCalledWith("4G");
- });
-
- it("should map all effective connection types", () => {
- const effectiveTypes: Array<["slow-2g", string] | ["2g", string] | ["3g", string] | ["4g", string]> = [
- ["slow-2g", "Slow-2G"],
- ["2g", "2G"],
- ["3g", "3G"],
- ["4g", "4G"],
- ];
-
- for (const [type, expected] of effectiveTypes) {
- setDisplayData.mockClear();
- const props: ProviderProps = {};
- provider = new NetworkProvider(props);
- mockNavigator.onLine = true;
- mockNavigator.connection = { effectiveType: type };
-
- provider.setup(context);
-
- expect(setDisplayData).toHaveBeenCalledWith(expected);
- provider.cleanup();
- }
- });
-
- it("should display bandwidth in Gbps", () => {
- const props: ProviderProps = {};
- provider = new NetworkProvider(props);
- mockNavigator.onLine = true;
- mockNavigator.connection = {
- effectiveType: "4g" as const,
- downlink: 5000,
- };
-
- provider.setup(context);
-
- expect(setDisplayData).toHaveBeenCalledWith("4G\n5.0Gbps");
- });
-
- it("should display bandwidth in Mbps", () => {
- const props: ProviderProps = {};
- provider = new NetworkProvider(props);
- mockNavigator.onLine = true;
- mockNavigator.connection = {
- effectiveType: "4g" as const,
- downlink: 50,
- };
-
- provider.setup(context);
-
- expect(setDisplayData).toHaveBeenCalledWith("4G\n50.0Mbps");
- });
-
- it("should display bandwidth in Kbps", () => {
- const props: ProviderProps = {};
- provider = new NetworkProvider(props);
- mockNavigator.onLine = true;
- mockNavigator.connection = {
- effectiveType: "3g" as const,
- downlink: 0.5,
- };
-
- provider.setup(context);
-
- expect(setDisplayData).toHaveBeenCalledWith("3G\n500Kbps");
- });
-
- it("should display latency in milliseconds", () => {
- const props: ProviderProps = {};
- provider = new NetworkProvider(props);
- mockNavigator.onLine = true;
- mockNavigator.connection = {
- effectiveType: "4g" as const,
- rtt: 50,
- };
-
- provider.setup(context);
-
- expect(setDisplayData).toHaveBeenCalledWith("4G\n50ms");
- });
-
- it("should display save-data status when enabled", () => {
- const props: ProviderProps = {};
- provider = new NetworkProvider(props);
- mockNavigator.onLine = true;
- mockNavigator.connection = {
- effectiveType: "4g" as const,
- saveData: true,
- };
-
- provider.setup(context);
-
- expect(setDisplayData).toHaveBeenCalledWith("4G\nSave-Data");
- });
-
- it("should combine all network metrics", () => {
- const props: ProviderProps = {};
- provider = new NetworkProvider(props);
- mockNavigator.onLine = true;
- mockNavigator.connection = {
- effectiveType: "4g" as const,
- downlink: 50,
- rtt: 45,
- saveData: false,
- };
-
- provider.setup(context);
-
- expect(setDisplayData).toHaveBeenCalledWith("4G\n50.0Mbps\n45ms");
- });
-
- it("should update display on online event", () => {
- const props: ProviderProps = {};
- provider = new NetworkProvider(props);
- mockNavigator.onLine = true;
- mockNavigator.connection = {
- effectiveType: "4g" as const,
- addEventListener: vi.fn(),
- removeEventListener: vi.fn(),
- };
-
- provider.setup(context);
- expect(setDisplayData).toHaveBeenLastCalledWith("4G");
-
- setDisplayData.mockClear();
- mockNavigator.onLine = false;
-
- window.dispatchEvent(new Event("offline"));
-
- expect(setDisplayData).toHaveBeenCalledWith("offline");
- });
-
- it("should update display on offline event", () => {
- const props: ProviderProps = {};
- provider = new NetworkProvider(props);
- mockNavigator.onLine = true;
- mockNavigator.connection = { effectiveType: "4g" as const };
-
- provider.setup(context);
- expect(setDisplayData).toHaveBeenLastCalledWith("4G");
-
- setDisplayData.mockClear();
- mockNavigator.onLine = false;
-
- window.dispatchEvent(new Event("offline"));
-
- expect(setDisplayData).toHaveBeenCalledWith("offline");
- });
-
- it("should ignore zero or negative bandwidth", () => {
- const props: ProviderProps = {};
- provider = new NetworkProvider(props);
- mockNavigator.onLine = true;
- mockNavigator.connection = {
- effectiveType: "4g" as const,
- downlink: 0,
- };
-
- provider.setup(context);
-
- expect(setDisplayData).toHaveBeenCalledWith("4G");
- });
-
- it("should ignore zero or negative latency", () => {
- const props: ProviderProps = {};
- provider = new NetworkProvider(props);
- mockNavigator.onLine = true;
- mockNavigator.connection = {
- effectiveType: "4g" as const,
- rtt: 0,
- };
-
- provider.setup(context);
-
- expect(setDisplayData).toHaveBeenCalledWith("4G");
- });
-
- it("should cleanup event listeners", () => {
- const props: ProviderProps = {};
- provider = new NetworkProvider(props);
- mockNavigator.onLine = true;
- mockNavigator.connection = { effectiveType: "4g" as const };
-
- provider.setup(context);
-
- const removeEventListenerSpy = vi.spyOn(window, "removeEventListener");
- provider.cleanup();
-
- expect(removeEventListenerSpy).toHaveBeenCalledWith("online", expect.any(Function));
- expect(removeEventListenerSpy).toHaveBeenCalledWith("offline", expect.any(Function));
- });
-
- it("should update periodically", () => {
- const props: ProviderProps = {};
- provider = new NetworkProvider(props);
- mockNavigator.onLine = true;
- mockNavigator.connection = {
- effectiveType: "4g" as const,
- downlink: 50,
- };
-
- provider.setup(context);
- setDisplayData.mockClear();
-
- vi.advanceTimersByTime(100);
-
- expect(setDisplayData).toHaveBeenCalled();
- });
-
- it("should prefer mozilla and webkit connection fallbacks", () => {
- const props: ProviderProps = {};
- provider = new NetworkProvider(props);
- mockNavigator.onLine = true;
-
- const mozConnection = {
- effectiveType: "3g" as const,
- };
-
- Object.defineProperty(window, "navigator", {
- value: {
- onLine: true,
- connection: undefined,
- mozConnection,
- },
- writable: true,
- configurable: true,
- });
-
- provider.setup(context);
-
- expect(setDisplayData).toHaveBeenCalledWith("3G");
- });
-});
diff --git a/js/hang-ui/src/shared/components/stats/__tests__/providers/registry.test.ts b/js/hang-ui/src/shared/components/stats/__tests__/providers/registry.test.ts
deleted file mode 100644
index 180c997f3..000000000
--- a/js/hang-ui/src/shared/components/stats/__tests__/providers/registry.test.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { describe, expect, it } from "vitest";
-import { AudioProvider } from "../../providers/audio";
-import { BufferProvider } from "../../providers/buffer";
-import { NetworkProvider } from "../../providers/network";
-import { getStatsInformationProvider, providers } from "../../providers/registry";
-import { VideoProvider } from "../../providers/video";
-import type { KnownStatsProviders } from "../../types";
-
-describe("Registry", () => {
- it("should have all required providers registered", () => {
- const expectedProviders: KnownStatsProviders[] = ["network", "video", "audio", "buffer"];
-
- for (const statProvider of expectedProviders) {
- expect(providers[statProvider]).toBeDefined();
- }
- });
-
- it("should map video icon to VideoProvider", () => {
- expect(providers.video).toBe(VideoProvider);
- });
-
- it("should map audio icon to AudioProvider", () => {
- expect(providers.audio).toBe(AudioProvider);
- });
-
- it("should map buffer icon to BufferProvider", () => {
- expect(providers.buffer).toBe(BufferProvider);
- });
-
- it("should map network icon to NetworkProvider", () => {
- expect(providers.network).toBe(NetworkProvider);
- });
-
- it("should return correct provider class with getStatsInformationProvider", () => {
- expect(getStatsInformationProvider("video")).toBe(VideoProvider);
- expect(getStatsInformationProvider("audio")).toBe(AudioProvider);
- expect(getStatsInformationProvider("buffer")).toBe(BufferProvider);
- expect(getStatsInformationProvider("network")).toBe(NetworkProvider);
- });
-
- it("should return undefined for unknown icon", () => {
- expect(getStatsInformationProvider("unknown" as KnownStatsProviders)).toBeUndefined();
- });
-
- it("should instantiate providers correctly", () => {
- const providersList: KnownStatsProviders[] = ["network", "video", "audio", "buffer"];
-
- for (const statProvider of providersList) {
- const ProviderClass = getStatsInformationProvider(statProvider);
- expect(ProviderClass).toBeDefined();
-
- if (ProviderClass) {
- const instance = new ProviderClass({});
- expect(instance).toBeDefined();
- expect(instance.setup).toBeDefined();
- expect(instance.cleanup).toBeDefined();
- }
- }
- });
-});
diff --git a/js/hang-ui/src/shared/components/stats/__tests__/providers/video.test.ts b/js/hang-ui/src/shared/components/stats/__tests__/providers/video.test.ts
deleted file mode 100644
index ed8252a68..000000000
--- a/js/hang-ui/src/shared/components/stats/__tests__/providers/video.test.ts
+++ /dev/null
@@ -1,181 +0,0 @@
-import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import { VideoProvider } from "../../providers/video";
-import type { ProviderContext, ProviderProps, VideoSource, VideoStats } from "../../types";
-import { createMockProviderProps } from "../utils";
-
-declare global {
- var __advanceTime: (ms: number) => void;
-}
-
-describe("VideoProvider", () => {
- let provider: VideoProvider;
- let context: ProviderContext;
- let setDisplayData: ReturnType;
- let intervalCallback: ((interval: number) => void) | null = null;
-
- beforeEach(() => {
- setDisplayData = vi.fn();
- context = { setDisplayData };
- intervalCallback = null;
-
- // Mock window functions
- const mockSetInterval = vi.fn((callback: (interval: number) => void) => {
- intervalCallback = callback;
- return 1 as unknown as NodeJS.Timeout;
- });
-
- const mockClearInterval = vi.fn();
-
- global.window = {
- setInterval: mockSetInterval,
- clearInterval: mockClearInterval,
- } as unknown as typeof window;
-
- // Mock performance.now()
- let mockTime = 0;
- global.performance = {
- now: vi.fn(() => mockTime),
- } as unknown as Performance;
-
- // Helper to advance mock time
- global.__advanceTime = (ms: number) => {
- mockTime += ms;
- };
- });
-
- afterEach(() => {
- provider?.cleanup();
- });
-
- it("should display N/A when video source is not available", () => {
- const props: ProviderProps = {};
- provider = new VideoProvider(props);
- provider.setup(context);
-
- expect(setDisplayData).toHaveBeenCalledWith("N/A");
- });
-
- it("should setup interval for display updates", () => {
- const mockProps = createMockProviderProps({ audio: false });
- const video = mockProps.video as VideoSource;
-
- const props: ProviderProps = { video };
- provider = new VideoProvider(props);
- provider.setup(context);
-
- expect(setDisplayData).toHaveBeenCalled();
- });
-
- it("should display video resolution with stats placeholder on first call", () => {
- const mockProps = createMockProviderProps({ audio: false });
- const video = mockProps.video as VideoSource;
-
- const props: ProviderProps = { video };
- provider = new VideoProvider(props);
- provider.setup(context);
-
- expect(setDisplayData).toHaveBeenCalledWith("1920x1080\nN/A\nN/A");
- });
-
- it("should calculate FPS from frame count and timestamp delta", () => {
- const peekFn = vi.fn();
- const mockProps = createMockProviderProps({ audio: false });
- const video = mockProps.video as VideoSource;
- video.source.stats.peek = peekFn;
-
- const props: ProviderProps = { video };
- provider = new VideoProvider(props);
-
- // First call - use non-zero timestamp so next call can calculate FPS
- peekFn.mockReturnValue({ frameCount: 100, timestamp: 1000000, bytesReceived: 50000 } as VideoStats);
- provider.setup(context);
- expect(setDisplayData).toHaveBeenCalledWith("1920x1080\nN/A\nN/A");
-
- // Second call: 6 frames in 250ms at 24fps = exactly 24 frames per second
- // frameCount delta = 106 - 100 = 6
- // timestamp delta = 250000 microseconds
- // FPS = 6 / 0.25 = 24.0 fps
- // bytesReceived delta = 100000 - 50000 = 50000 bytes
- // bitrate = 50000 * 8 * 4 = 1600000 bits/s = 1.6Mbps
- peekFn.mockReturnValue({ frameCount: 106, timestamp: 1250000, bytesReceived: 100000 } as VideoStats);
- global.__advanceTime(250);
- intervalCallback?.(250);
-
- expect(setDisplayData).toHaveBeenNthCalledWith(2, "1920x1080\n@24.0 fps\n1.6Mbps");
- });
-
- it("should calculate bitrate from bytesReceived delta", () => {
- const peekFn = vi.fn();
- const mockProps = createMockProviderProps({ audio: false });
- const video = mockProps.video as VideoSource;
- video.source.display.peek = () => ({ width: 1280, height: 720 });
- video.source.stats.peek = peekFn;
-
- const props: ProviderProps = { video };
- provider = new VideoProvider(props);
-
- // First call - use non-zero initial values
- peekFn.mockReturnValue({ frameCount: 0, timestamp: 1000000, bytesReceived: 100000 } as VideoStats);
- provider.setup(context);
-
- // Second call: 5 Mbps = 156250 bytes delta at 250ms
- // (156250 * 8 * 4) / 1_000_000 = 5.0 Mbps
- peekFn.mockReturnValue({ frameCount: 6, timestamp: 1250000, bytesReceived: 256250 } as VideoStats);
- global.__advanceTime(250);
- intervalCallback?.(250);
-
- expect(setDisplayData).toHaveBeenNthCalledWith(2, "1280x720\n@24.0 fps\n5.0Mbps");
- });
-
- it("should display N/A for FPS and bitrate on first call", () => {
- const props: ProviderProps = {};
- provider = new VideoProvider(props);
- provider.setup(context);
-
- expect(setDisplayData).toHaveBeenCalledWith("N/A");
- });
-
- it("should display only resolution when stats are not available", () => {
- const mockProps = createMockProviderProps({ audio: false });
- const video = mockProps.video as VideoSource;
- video.source.display.peek = () => ({ width: 1280, height: 720 });
- video.source.stats.peek = () => undefined;
-
- const props: ProviderProps = { video };
- provider = new VideoProvider(props);
- provider.setup(context);
- expect(setDisplayData).toHaveBeenCalledWith("1280x720\nN/A\nN/A");
- });
-
- it("should format kbps for lower bitrates", () => {
- const peekFn = vi.fn();
- const mockProps = createMockProviderProps({ audio: false });
- const video = mockProps.video as VideoSource;
- video.source.stats.peek = peekFn;
-
- const props: ProviderProps = { video };
- provider = new VideoProvider(props);
-
- // First call - use non-zero initial timestamp
- peekFn.mockReturnValue({ frameCount: 0, timestamp: 1000000, bytesReceived: 100000 } as VideoStats);
- provider.setup(context);
-
- // 256 kbps = 8000 bytes at 250ms
- // (8000 * 8 * 4) / 1000 = 256 kbps
- peekFn.mockReturnValue({ frameCount: 6, timestamp: 1250000, bytesReceived: 108000 } as VideoStats);
- global.__advanceTime(250);
- intervalCallback?.(250);
-
- expect(setDisplayData).toHaveBeenNthCalledWith(2, "1920x1080\n@24.0 fps\n256kbps");
- });
-
- it("should cleanup interval on dispose", () => {
- const props: ProviderProps = {};
- provider = new VideoProvider(props);
- provider.setup(context);
-
- provider.cleanup();
-
- expect(provider).toBeDefined();
- });
-});
diff --git a/js/hang-ui/src/shared/components/stats/__tests__/utils.ts b/js/hang-ui/src/shared/components/stats/__tests__/utils.ts
deleted file mode 100644
index 1df4024a2..000000000
--- a/js/hang-ui/src/shared/components/stats/__tests__/utils.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import { vi } from "vitest";
-import type { ProviderProps } from "../types";
-
-export interface MockProviderPropsOptions {
- audio?: boolean;
- video?: boolean;
-}
-
-/**
- * Creates mock ProviderProps with configurable audio and video sources.
- * Each signal includes a peek() method and a subscribe() method with vi.fn().
- *
- * @param options - Configuration options
- * @returns ProviderProps with mocked audio and/or video sources
- *
- * @example
- * ```ts
- * // Create mock with both audio and video
- * const props = createMockProviderProps();
- *
- * // Create mock with audio only
- * const audioOnly = createMockProviderProps({ video: false });
- *
- * // Create mock with video only
- * const videoOnly = createMockProviderProps({ audio: false });
- * ```
- */
-export const createMockProviderProps = (options?: MockProviderPropsOptions): ProviderProps => {
- const { audio = true, video = true } = options ?? {};
-
- return {
- ...(audio && {
- audio: {
- source: {
- active: {
- peek: () => "audio-active",
- subscribe: vi.fn(() => vi.fn()),
- changed: vi.fn(() => vi.fn()),
- },
- config: {
- peek: () => ({
- sampleRate: 48000,
- numberOfChannels: 2,
- bitrate: 128000,
- codec: "opus",
- }),
- subscribe: vi.fn(() => vi.fn()),
- changed: vi.fn(() => vi.fn()),
- },
- stats: {
- peek: () => ({
- bytesReceived: 1024,
- }),
- subscribe: vi.fn(() => vi.fn()),
- changed: vi.fn(() => vi.fn()),
- },
- },
- },
- }),
- ...(video && {
- video: {
- source: {
- display: {
- peek: () => ({
- width: 1920,
- height: 1080,
- }),
- subscribe: vi.fn(() => vi.fn()),
- changed: vi.fn(() => vi.fn()),
- },
- bufferStatus: {
- peek: () => ({ state: "filled" as const }),
- subscribe: vi.fn(() => vi.fn()),
- changed: vi.fn(() => vi.fn()),
- },
- stats: {
- peek: () => ({
- frameCount: 60,
- timestamp: Date.now(),
- bytesReceived: 2048,
- }),
- subscribe: vi.fn(() => vi.fn()),
- changed: vi.fn(() => vi.fn()),
- },
- },
- },
- }),
- };
-};