Files
webeditor3d/tests/domain/editor-simulation-controller.test.ts

163 lines
4.3 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { createEmptySceneDocument } from "../../src/document/scene-document";
import {
EditorSimulationController,
type EditorSimulationUiSnapshot
} from "../../src/runtime-three/editor-simulation-controller";
import { buildRuntimeSceneFromDocument } from "../../src/runtime-three/runtime-scene-build";
function createManualFrameController(options: {
uiSnapshotIntervalSeconds?: number;
onBuild?: () => void;
} = {}) {
return new EditorSimulationController({
uiSnapshotIntervalSeconds: options.uiSnapshotIntervalSeconds,
requestAnimationFrame: () => 1,
cancelAnimationFrame: () => undefined,
buildRuntimeScene: (document, buildOptions) => {
options.onBuild?.();
return buildRuntimeSceneFromDocument(document, buildOptions);
}
});
}
describe("EditorSimulationController", () => {
it("advances the editor clock only while playing", () => {
const document = createEmptySceneDocument();
document.time.dayLengthMinutes = 24;
document.time.startTimeOfDayHours = 6;
const controller = createManualFrameController();
controller.updateInputs({
document,
loadedModelAssets: {}
});
const initialClock = controller.getUiSnapshot().clock;
expect(initialClock?.timeOfDayHours).toBe(6);
controller.play();
controller.advance(60);
const playingClock = controller.getUiSnapshot().clock;
expect(playingClock?.timeOfDayHours).toBeGreaterThan(6);
controller.pause();
controller.advance(60);
expect(controller.getUiSnapshot().clock).toEqual(playingClock);
});
it("publishes coarse UI snapshots at a bounded tick rate", () => {
const document = createEmptySceneDocument();
const controller = createManualFrameController({
uiSnapshotIntervalSeconds: 0.5
});
const snapshots: EditorSimulationUiSnapshot[] = [];
controller.updateInputs({
document,
loadedModelAssets: {}
});
controller.subscribeUiSnapshot((snapshot) => {
snapshots.push(snapshot);
});
snapshots.length = 0;
controller.play();
expect(snapshots).toHaveLength(1);
snapshots.length = 0;
controller.advance(0.2);
controller.advance(0.2);
expect(snapshots).toHaveLength(0);
controller.advance(0.1);
expect(snapshots).toHaveLength(1);
});
it("rebuilds the cached base simulation scene only when inputs change", () => {
let buildCount = 0;
const document = createEmptySceneDocument();
const loadedModelAssets = {};
const controller = createManualFrameController({
onBuild: () => {
buildCount += 1;
}
});
controller.updateInputs({
document,
loadedModelAssets
});
expect(buildCount).toBe(1);
controller.play();
controller.advance(0.1);
controller.advance(0.1);
expect(buildCount).toBe(1);
controller.updateInputs({
document,
loadedModelAssets
});
expect(buildCount).toBe(1);
const renamedDocument = {
...document,
name: "Rebuilt Scene"
};
controller.updateInputs({
document: renamedDocument,
loadedModelAssets
});
expect(buildCount).toBe(2);
const retimedDocument = {
...renamedDocument,
time: {
...renamedDocument.time,
dayLengthMinutes: 12
}
};
controller.updateInputs({
document: retimedDocument,
loadedModelAssets
});
expect(buildCount).toBe(3);
controller.updateInputs({
document: retimedDocument,
loadedModelAssets: {}
});
expect(buildCount).toBe(4);
});
it("emits frame updates without publishing a UI snapshot every frame", () => {
const document = createEmptySceneDocument();
const controller = createManualFrameController({
uiSnapshotIntervalSeconds: 1
});
const frameListener = vi.fn();
const uiListener = vi.fn();
controller.updateInputs({
document,
loadedModelAssets: {}
});
controller.subscribeFrame(frameListener);
controller.subscribeUiSnapshot(uiListener);
frameListener.mockClear();
uiListener.mockClear();
controller.play();
frameListener.mockClear();
uiListener.mockClear();
controller.advance(0.25);
controller.advance(0.25);
expect(frameListener).toHaveBeenCalledTimes(2);
expect(uiListener).not.toHaveBeenCalled();
});
});