Files
webeditor3d/tests/domain/interaction-links.validation.test.ts
Victor Giers 09d4f47402 auto-git:
[change] tests/domain/interaction-links.validation.test.ts
2026-04-02 19:47:21 +02:00

275 lines
8.6 KiB
TypeScript

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 AudioAssetRecord, type ModelAssetRecord } from "../../src/assets/project-assets";
import {
createInteractableEntity,
createPlayerStartEntity,
createSoundEmitterEntity,
createTeleportTargetEntity,
createTriggerVolumeEntity
} from "../../src/entities/entity-instances";
import {
createPlayAnimationInteractionLink,
createPlaySoundInteractionLink,
createTeleportPlayerInteractionLink,
createToggleVisibilityInteractionLink,
createStopSoundInteractionLink
} from "../../src/interactions/interaction-links";
describe("interaction link validation", () => {
it("accepts valid Trigger Volume and Interactable links", () => {
const triggerVolume = createTriggerVolumeEntity({
id: "entity-trigger-main"
});
const interactable = createInteractableEntity({
id: "entity-interactable-main",
prompt: "Use Console"
});
const teleportTarget = createTeleportTargetEntity({
id: "entity-teleport-main"
});
const audioAsset = {
id: "asset-audio-main",
kind: "audio" as const,
sourceName: "lobby-loop.ogg",
mimeType: "audio/ogg",
storageKey: createProjectAssetStorageKey("asset-audio-main"),
byteLength: 4096,
metadata: {
kind: "audio" as const,
durationSeconds: 4,
channelCount: 2,
sampleRateHz: 48000,
warnings: []
}
} satisfies AudioAssetRecord;
const soundEmitter = createSoundEmitterEntity({
id: "entity-sound-main",
audioAssetId: audioAsset.id
});
const brush = createBoxBrush({
id: "brush-door"
});
const document = {
...createEmptySceneDocument(),
brushes: {
[brush.id]: brush
},
entities: {
[triggerVolume.id]: triggerVolume,
[interactable.id]: interactable,
[teleportTarget.id]: teleportTarget,
[soundEmitter.id]: soundEmitter
},
assets: {
[audioAsset.id]: audioAsset
},
interactionLinks: {
"link-teleport": createTeleportPlayerInteractionLink({
id: "link-teleport",
sourceEntityId: triggerVolume.id,
trigger: "enter",
targetEntityId: teleportTarget.id
}),
"link-visibility": createToggleVisibilityInteractionLink({
id: "link-visibility",
sourceEntityId: triggerVolume.id,
trigger: "exit",
targetBrushId: brush.id,
visible: false
}),
"link-click-teleport": createTeleportPlayerInteractionLink({
id: "link-click-teleport",
sourceEntityId: interactable.id,
trigger: "click",
targetEntityId: teleportTarget.id
}),
"link-play-sound": createPlaySoundInteractionLink({
id: "link-play-sound",
sourceEntityId: interactable.id,
trigger: "click",
targetSoundEmitterId: soundEmitter.id
}),
"link-stop-sound": createStopSoundInteractionLink({
id: "link-stop-sound",
sourceEntityId: triggerVolume.id,
trigger: "exit",
targetSoundEmitterId: soundEmitter.id
})
}
};
const validation = validateSceneDocument(document);
expect(validation.errors).toEqual([]);
});
it("detects sound playback links that target a sound emitter without an audio asset", () => {
const triggerVolume = createTriggerVolumeEntity({
id: "entity-trigger-main"
});
const soundEmitter = createSoundEmitterEntity({
id: "entity-sound-main"
});
const validation = validateSceneDocument({
...createEmptySceneDocument(),
entities: {
[triggerVolume.id]: triggerVolume,
[soundEmitter.id]: soundEmitter
},
interactionLinks: {
"link-play-sound": createPlaySoundInteractionLink({
id: "link-play-sound",
sourceEntityId: triggerVolume.id,
trigger: "enter",
targetSoundEmitterId: soundEmitter.id
})
}
});
expect(validation.errors).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: "missing-sound-emitter-audio-asset",
path: "interactionLinks.link-play-sound.action.targetSoundEmitterId"
})
])
);
});
it("detects invalid interaction link source and target references", () => {
const playerStart = createPlayerStartEntity({
id: "entity-player-start-main"
});
const triggerVolume = createTriggerVolumeEntity({
id: "entity-trigger-main"
});
const interactable = createInteractableEntity({
id: "entity-interactable-main"
});
const document = {
...createEmptySceneDocument(),
entities: {
[playerStart.id]: playerStart,
[triggerVolume.id]: triggerVolume,
[interactable.id]: interactable
},
interactionLinks: {
"link-invalid-source": createTeleportPlayerInteractionLink({
id: "link-invalid-source",
sourceEntityId: playerStart.id,
trigger: "enter",
targetEntityId: "entity-missing-teleport-target"
}),
"link-invalid-visibility": createToggleVisibilityInteractionLink({
id: "link-invalid-visibility",
sourceEntityId: triggerVolume.id,
trigger: "exit",
targetBrushId: "brush-missing"
}),
"link-invalid-click-trigger": createTeleportPlayerInteractionLink({
id: "link-invalid-click-trigger",
sourceEntityId: interactable.id,
trigger: "enter",
targetEntityId: "entity-missing-teleport-target"
})
}
};
const validation = validateSceneDocument(document);
expect(validation.errors).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: "invalid-interaction-source-kind",
path: "interactionLinks.link-invalid-source.sourceEntityId"
}),
expect.objectContaining({
code: "missing-teleport-target-entity",
path: "interactionLinks.link-invalid-source.action.targetEntityId"
}),
expect.objectContaining({
code: "missing-visibility-target-brush",
path: "interactionLinks.link-invalid-visibility.action.targetBrushId"
}),
expect.objectContaining({
code: "unsupported-interaction-trigger",
path: "interactionLinks.link-invalid-click-trigger.trigger"
}),
expect.objectContaining({
code: "missing-teleport-target-entity",
path: "interactionLinks.link-invalid-click-trigger.action.targetEntityId"
})
])
);
});
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"
})
])
);
});
});