Add tests for playAnimation interaction link validation and runtime dispatch

This commit is contained in:
2026-04-02 19:20:15 +02:00
parent 74e7be5073
commit 026ed6fb57
3 changed files with 128 additions and 5 deletions

View File

@@ -3,8 +3,14 @@ import { describe, expect, it } from "vitest";
import { createBoxBrush } from "../../src/document/brushes";
import { createEmptySceneDocument } from "../../src/document/scene-document";
import { validateSceneDocument } from "../../src/document/scene-document-validation";
import { createModelInstance } from "../../src/assets/model-instances";
import { createProjectAssetStorageKey, type ModelAssetRecord } from "../../src/assets/project-assets";
import { createInteractableEntity, createPlayerStartEntity, createTeleportTargetEntity, createTriggerVolumeEntity } from "../../src/entities/entity-instances";
import { createTeleportPlayerInteractionLink, createToggleVisibilityInteractionLink } from "../../src/interactions/interaction-links";
import {
createPlayAnimationInteractionLink,
createTeleportPlayerInteractionLink,
createToggleVisibilityInteractionLink
} from "../../src/interactions/interaction-links";
describe("interaction link validation", () => {
it("accepts valid Trigger Volume and Interactable links", () => {
@@ -125,4 +131,67 @@ describe("interaction link validation", () => {
])
);
});
it("detects playAnimation links that reference a missing clip on the target model asset", () => {
const modelAsset = {
id: "asset-model-animated",
kind: "model",
sourceName: "animated.glb",
mimeType: "model/gltf-binary",
storageKey: createProjectAssetStorageKey("asset-model-animated"),
byteLength: 1024,
metadata: {
kind: "model" as const,
format: "glb" as const,
sceneName: null,
nodeCount: 1,
meshCount: 1,
materialNames: [],
textureNames: [],
animationNames: ["Idle", "Run"],
boundingBox: null,
warnings: []
}
} satisfies ModelAssetRecord;
const modelInstance = createModelInstance({
id: "model-instance-animated",
assetId: modelAsset.id
});
const triggerVolume = createTriggerVolumeEntity({
id: "entity-trigger-main"
});
const document = {
...createEmptySceneDocument(),
assets: {
[modelAsset.id]: modelAsset
},
modelInstances: {
[modelInstance.id]: modelInstance
},
entities: {
[triggerVolume.id]: triggerVolume
},
interactionLinks: {
"link-play-missing-clip": createPlayAnimationInteractionLink({
id: "link-play-missing-clip",
sourceEntityId: triggerVolume.id,
trigger: "enter",
targetModelInstanceId: modelInstance.id,
clipName: "Walk"
})
}
};
const validation = validateSceneDocument(document);
expect(validation.errors).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: "missing-play-animation-clip",
path: "interactionLinks.link-play-missing-clip.action.clipName"
})
])
);
});
});

View File

@@ -1,6 +1,11 @@
import { describe, expect, it } from "vitest";
import { createTeleportPlayerInteractionLink, createToggleVisibilityInteractionLink } from "../../src/interactions/interaction-links";
import {
createPlayAnimationInteractionLink,
createTeleportPlayerInteractionLink,
createToggleVisibilityInteractionLink,
createStopAnimationInteractionLink
} from "../../src/interactions/interaction-links";
import { RuntimeInteractionSystem } from "../../src/runtime-three/runtime-interaction-system";
import type { RuntimeSceneDefinition } from "../../src/runtime-three/runtime-scene-build";
@@ -28,6 +33,10 @@ function createRuntimeSceneFixture(): RuntimeSceneDefinition {
brushes: [],
colliders: [],
sceneBounds: null,
localLights: {
pointLights: [],
spotLights: []
},
modelInstances: [],
entities: {
playerStarts: [],
@@ -155,6 +164,49 @@ describe("RuntimeInteractionSystem", () => {
expect(dispatches).toEqual(["link-teleport:entity-teleport-main:8"]);
});
it("dispatches animation actions with the authored target model instance and clip", () => {
const runtimeScene = createRuntimeSceneFixture();
runtimeScene.interactionLinks = [
createPlayAnimationInteractionLink({
id: "link-play-animation",
sourceEntityId: "entity-interactable-console",
trigger: "click",
targetModelInstanceId: "model-instance-animated",
clipName: "Walk",
loop: false
}),
createStopAnimationInteractionLink({
id: "link-stop-animation",
sourceEntityId: "entity-interactable-console",
trigger: "click",
targetModelInstanceId: "model-instance-animated"
})
];
const interactionSystem = new RuntimeInteractionSystem();
const dispatches: string[] = [];
interactionSystem.dispatchClickInteraction("entity-interactable-console", runtimeScene, {
teleportPlayer: () => {
throw new Error("Teleport should not dispatch in this fixture.");
},
toggleBrushVisibility: () => {
throw new Error("Visibility should not dispatch in this fixture.");
},
playAnimation: (instanceId, clipName, loop, link) => {
dispatches.push(`${link.id}:${instanceId}:${clipName}:${loop === false ? "once" : "loop"}`);
},
stopAnimation: (instanceId, link) => {
dispatches.push(`${link.id}:${instanceId}`);
}
});
expect(dispatches).toEqual([
"link-play-animation:model-instance-animated:Walk:once",
"link-stop-animation:model-instance-animated"
]);
});
it("dispatches visibility actions only when exiting an occupied Trigger Volume", () => {
const runtimeScene = createRuntimeSceneFixture();
runtimeScene.interactionLinks = [

View File

@@ -180,7 +180,8 @@ describe("scene document JSON", () => {
};
document.world.background = {
mode: "image",
assetId: imageAsset.id
assetId: imageAsset.id,
environmentIntensity: 0.75
};
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
@@ -630,7 +631,8 @@ describe("scene document JSON", () => {
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
expect(migratedDocument.world.background).toEqual({
mode: "image",
assetId: imageAsset.id
assetId: imageAsset.id,
environmentIntensity: 0.5
});
expect(migratedDocument.entities[pointLight.id]).toEqual(pointLight);
expect(migratedDocument.entities[spotLight.id]).toEqual(spotLight);
@@ -792,7 +794,7 @@ describe("scene document JSON", () => {
boundingBox: null,
warnings: []
}
};
} satisfies ModelAssetRecord;
const migratedDocument = migrateSceneDocument({
version: 11,