import { render, screen, waitFor } from "@testing-library/react"; import { act } from "react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createEmptySceneDocument } from "../../src/document/scene-document"; import { RunnerCanvas } from "../../src/runner-web/RunnerCanvas"; import type { FirstPersonTelemetry } from "../../src/runtime-three/navigation-controller"; import { buildRuntimeSceneFromDocument } from "../../src/runtime-three/runtime-scene-build"; const { MockRuntimeHost, runtimeHostInstances } = vi.hoisted(() => { const runtimeHostInstances: Array<{ mount: ReturnType; dispose: ReturnType; loadScene: ReturnType; updateAssets: ReturnType; setNavigationMode: ReturnType; setRuntimeMessageHandler: ReturnType; setFirstPersonTelemetryHandler: ReturnType; setInteractionPromptHandler: ReturnType; }> = []; class MockRuntimeHost { mount = vi.fn(); dispose = vi.fn(); loadScene = vi.fn(); updateAssets = vi.fn(); setNavigationMode = vi.fn(); setRuntimeMessageHandler = vi.fn(); setFirstPersonTelemetryHandler = vi.fn(); setInteractionPromptHandler = vi.fn(); constructor() { runtimeHostInstances.push(this); } } return { MockRuntimeHost, runtimeHostInstances }; }); vi.mock("../../src/runtime-three/runtime-host", () => ({ RuntimeHost: MockRuntimeHost })); describe("RunnerCanvas", () => { beforeEach(() => { runtimeHostInstances.length = 0; }); afterEach(() => { vi.restoreAllMocks(); }); it("only shows the underwater overlay when the camera is submerged", async () => { const runtimeScene = buildRuntimeSceneFromDocument(createEmptySceneDocument()); const onTelemetryChange = vi.fn(); render( ); await waitFor(() => { expect(runtimeHostInstances).toHaveLength(1); expect(runtimeHostInstances[0]?.setFirstPersonTelemetryHandler).toHaveBeenCalledTimes(1); }); const publishTelemetry = runtimeHostInstances[0]?.setFirstPersonTelemetryHandler.mock.calls[0]?.[0] as ((telemetry: FirstPersonTelemetry | null) => void) | undefined; expect(publishTelemetry).toBeDefined(); act(() => { publishTelemetry?.({ feetPosition: { x: 0, y: 0, z: 0 }, eyePosition: { x: 0, y: 1.7, z: 0 }, grounded: false, locomotionState: "swimming", inWaterVolume: true, cameraSubmerged: false, inFogVolume: false, pointerLocked: true, spawn: runtimeScene.spawn }); }); expect(screen.queryByLabelText("Built-in scene runner")?.className).not.toContain("runner-canvas--underwater"); expect(document.querySelector(".runner-canvas__underwater")).toBeNull(); act(() => { publishTelemetry?.({ feetPosition: { x: 0, y: 0, z: 0 }, eyePosition: { x: 0, y: 0.4, z: 0 }, grounded: false, locomotionState: "swimming", inWaterVolume: true, cameraSubmerged: true, inFogVolume: false, pointerLocked: true, spawn: runtimeScene.spawn }); }); expect(screen.getByLabelText("Built-in scene runner").className).toContain("runner-canvas--underwater"); expect(document.querySelector(".runner-canvas__underwater")).not.toBeNull(); }); });