import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createEmptyProjectDocument, createEmptyProjectScene } from "../../src/document/scene-document"; import { createPlayerStartEntity, createSceneEntryEntity, createNpcEntity } from "../../src/entities/entity-instances"; import { createActorControlTargetRef, createSetActorPresenceControlEffect } from "../../src/controls/control-surface"; import { createProjectScheduleRoutine } from "../../src/scheduler/project-scheduler"; import { createProjectSequence } from "../../src/sequencer/project-sequences"; const { MockViewportHost, viewportHostInstances } = vi.hoisted(() => { const viewportHostInstances: Array<{ setTransformCommitHandler: ReturnType; setRenderEnabled: ReturnType; mount: ReturnType; dispose: ReturnType; updateWorld: ReturnType; updateAssets: ReturnType; updateDocument: ReturnType; updateSelection: ReturnType; updateSimulation: ReturnType; setPanelId: ReturnType; setViewMode: ReturnType; setDisplayMode: ReturnType; setGridVisible: ReturnType; setCameraState: ReturnType; setBrushSelectionChangeHandler: ReturnType; setCameraStateChangeHandler: ReturnType; setCreationPreviewChangeHandler: ReturnType; setCreationCommitHandler: ReturnType; setTransformSessionChangeHandler: ReturnType; setTransformPreviewChangeHandler: ReturnType; setTransformCancelHandler: ReturnType; setWhiteboxHoverLabelChangeHandler: ReturnType; setWhiteboxSelectionMode: ReturnType; setWhiteboxSnapSettings: ReturnType; setToolMode: ReturnType; setCreationPreview: ReturnType; setTransformSession: ReturnType; focusSelection: ReturnType; }> = []; class MockViewportHost { setPanelId = vi.fn(); setRenderEnabled = vi.fn(); mount = vi.fn(); dispose = vi.fn(); updateWorld = vi.fn(); updateAssets = vi.fn(); updateDocument = vi.fn(); updateSelection = vi.fn(); updateSimulation = vi.fn(); setViewMode = vi.fn(); setDisplayMode = vi.fn(); setGridVisible = vi.fn(); setCameraState = vi.fn(); setBrushSelectionChangeHandler = vi.fn(); setCameraStateChangeHandler = vi.fn(); setCreationPreviewChangeHandler = vi.fn(); setCreationCommitHandler = vi.fn(); setTransformSessionChangeHandler = vi.fn(); setTransformPreviewChangeHandler = vi.fn(); setTransformCommitHandler = vi.fn(); setTransformCancelHandler = vi.fn(); setWhiteboxHoverLabelChangeHandler = vi.fn(); setWhiteboxSelectionMode = vi.fn(); setWhiteboxSnapSettings = vi.fn(); setToolMode = vi.fn(); setCreationPreview = vi.fn(); setTransformSession = vi.fn(); focusSelection = vi.fn(); constructor() { viewportHostInstances.push(this); } } return { MockViewportHost, viewportHostInstances }; }); vi.mock("../../src/viewport-three/viewport-host", () => ({ ViewportHost: MockViewportHost })); vi.mock("../../src/assets/project-asset-storage", () => ({ getBrowserProjectAssetStorageAccess: vi.fn(async () => ({ storage: null, diagnostic: null })) })); vi.mock("../../src/runner-web/RunnerCanvas", () => ({ RunnerCanvas: (props: { sceneName: string; onSceneTransitionActivated(request: { sourceEntityId: string | null; targetSceneId: string; targetEntryEntityId: string; }): void; }) => { return (
{props.sceneName}
); } })); import { App } from "../../src/app/App"; import { createEditorStore } from "../../src/app/editor-store"; function createSceneTransitionProject() { const outdoorScene = createEmptyProjectScene({ id: "scene-outside", name: "Outside" }); const houseScene = createEmptyProjectScene({ id: "scene-house", name: "House" }); const outdoorPlayerStart = createPlayerStartEntity({ id: "entity-player-start-outside", position: { x: 0, y: 0, z: 0 }, yawDegrees: 90 }); const ana = createNpcEntity({ id: "entity-npc-ana-nanto", actorId: "Ana Nanto" }); const anaTarget = createActorControlTargetRef(ana.actorId); const housePlayerStart = createPlayerStartEntity({ id: "entity-player-start-house", position: { x: 0, y: 0, z: 0 }, yawDegrees: 180 }); const houseEntry = createSceneEntryEntity({ id: "entity-scene-entry-house-front", position: { x: 3, y: 0, z: -1 }, yawDegrees: 270 }); const projectDocument = createEmptyProjectDocument({ sceneId: outdoorScene.id, sceneName: outdoorScene.name }); projectDocument.activeSceneId = outdoorScene.id; projectDocument.scenes = { [outdoorScene.id]: { ...outdoorScene, entities: { [outdoorPlayerStart.id]: outdoorPlayerStart, [ana.id]: ana } }, [houseScene.id]: { ...houseScene, entities: { [housePlayerStart.id]: housePlayerStart, [houseEntry.id]: houseEntry } } }; projectDocument.sequences.sequences["sequence-ana-presence"] = createProjectSequence({ id: "sequence-ana-presence", title: "Ana Presence", effects: [ { stepClass: "held", type: "controlEffect", effect: createSetActorPresenceControlEffect({ target: anaTarget, active: true }) } ] }); projectDocument.scheduler.routines["routine-ana-presence"] = createProjectScheduleRoutine({ id: "routine-ana-presence", title: "Ana Presence", target: anaTarget, sequenceId: "sequence-ana-presence", effects: [] }); return projectDocument; } describe("App scene transition flow", () => { beforeEach(() => { viewportHostInstances.length = 0; vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockImplementation( () => ({}) as never ); }); afterEach(() => { vi.restoreAllMocks(); }); it("switches runtime scenes through scene transition requests without changing the editor active scene", async () => { const store = createEditorStore({ initialProjectDocument: createSceneTransitionProject() }); render(); await waitFor(() => { expect(viewportHostInstances.length).toBeGreaterThan(0); }); fireEvent.click(screen.getByRole("button", { name: "Run Scene" })); await waitFor(() => { expect(screen.getByTestId("mock-runner-scene-name")).toHaveTextContent( "Outside" ); }); expect(screen.getByTestId("runner-scene-name")).toHaveTextContent( "Outside" ); expect(screen.getByTestId("runner-transition-count")).toHaveTextContent( "0" ); expect(store.getState().activeSceneId).toBe("scene-outside"); fireEvent.click(screen.getByTestId("mock-trigger-scene-transition")); await waitFor(() => { expect(screen.getByTestId("mock-runner-scene-name")).toHaveTextContent( "House" ); }); expect(screen.getByTestId("runner-scene-name")).toHaveTextContent("House"); expect(screen.getByTestId("runner-transition-count")).toHaveTextContent( "1" ); expect(screen.getByTestId("runner-spawn-state")).toHaveTextContent( "Scene Entry" ); expect(screen.getByText("Last transition: Outside to House")).toBeVisible(); expect(store.getState().activeSceneId).toBe("scene-outside"); }); });