diff --git a/tests/unit/runtime-host.test.ts b/tests/unit/runtime-host.test.ts new file mode 100644 index 00000000..e4d42bac --- /dev/null +++ b/tests/unit/runtime-host.test.ts @@ -0,0 +1,84 @@ +import { waitFor } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { createEmptySceneDocument } from "../../src/document/scene-document"; +import { RapierCollisionWorld } from "../../src/runtime-three/rapier-collision-world"; +import { + RuntimeHost, + type RuntimeSceneLoadState +} from "../../src/runtime-three/runtime-host"; +import { buildRuntimeSceneFromDocument } from "../../src/runtime-three/runtime-scene-build"; + +function createDeferred() { + let resolve: ((value: T) => void) | null = null; + let reject: ((error: unknown) => void) | null = null; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + return { + promise, + resolve(value: T) { + resolve?.(value); + }, + reject(error: unknown) { + reject?.(error); + } + }; +} + +describe("RuntimeHost", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("delays controller activation until collision setup reports the scene as ready", async () => { + const runtimeScene = buildRuntimeSceneFromDocument(createEmptySceneDocument()); + const collisionWorld = { + dispose: vi.fn() + } as unknown as RapierCollisionWorld; + const deferredCollisionWorld = createDeferred(); + vi.spyOn(RapierCollisionWorld, "create").mockReturnValue( + deferredCollisionWorld.promise + ); + + const runtimeMessages: Array = []; + const sceneLoadStates: RuntimeSceneLoadState[] = []; + const host = new RuntimeHost({ + enableRendering: false + }); + host.setRuntimeMessageHandler((message) => { + runtimeMessages.push(message); + }); + host.setSceneLoadStateHandler((state) => { + sceneLoadStates.push(state); + }); + + host.loadScene(runtimeScene); + host.setNavigationMode("orbitVisitor"); + + expect(sceneLoadStates).toEqual([ + { + status: "loading", + message: null + } + ]); + expect(runtimeMessages).toEqual([null]); + + deferredCollisionWorld.resolve(collisionWorld); + + await waitFor(() => { + expect(sceneLoadStates).toContainEqual({ + status: "ready", + message: null + }); + expect(runtimeMessages).toContain( + "Orbit Visitor active. Drag to orbit around the scene and use the mouse wheel to zoom." + ); + }); + + host.dispose(); + expect(collisionWorld.dispose).toHaveBeenCalledTimes(1); + }); +});