Files
feuerwerk/tests/core/commands.test.ts
T
rootandClaude Opus 5 76aa441dce Enabled switch, text assets, mouse input and the jump-and-run example game
Engine additions that the example game needed:
- `enabled` as a shared property of every placeable asset (default on, an
  override on the scene object). A switched-off object is not drawn, does not
  collide and its behavior does not run - and neither does anything parented
  below it. Disabled objects are now hidden in the scene view as well, so the
  editor shows what the game shows; they stay in the hierarchy panel, which is
  where they are selected and switched back on.
- Asset type `text` (text, fontSize, color, align), rendered in scene and game.
- Pointer input in the game view: `On click`, `Mouse position`, `Mouse button`,
  `Is hovered`, hit-tested against colliders, else sprite/text footprints.
- New nodes: Set/Is enabled, Set text, Is asset, Name of, Equals, To text,
  Join text, Floor/Ceil/Round, Modulo, Random, Time, Restart game.

Tooling: shared CDP harness (tools/harness.mjs), central project registry with
a guard that keeps destructive scripts off the playground (tools/projects.mjs),
plus smoke suites for the demo scene and the jump-and-run game.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 20:52:10 +02:00

152 lines
6.2 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
CreateAssetCommand,
DeleteAssetCommand,
RenameAssetCommand,
ResetAssetPropertyCommand,
SetActiveSceneCommand,
SetAssetPropertyCommand,
} from '../../src/core/commands/assetCommands';
import { CommandStack } from '../../src/core/commands/CommandStack';
import {
ClearOverrideCommand,
MoveSceneObjectCommand,
PlaceAssetCommand,
RemoveSceneObjectCommand,
SetOverrideCommand,
} from '../../src/core/commands/sceneObjectCommands';
import { createEmptyProject, findScene } from '../../src/core/model/Project';
function setup() {
const project = createEmptyProject('p1', 'Test');
const stack = new CommandStack(project);
const scene = new CreateAssetCommand('scene', 'Level 1');
const sprite = new CreateAssetCommand('sprite', 'Box');
stack.execute(scene);
stack.execute(sprite);
return { project, stack, sceneId: scene.asset.id, spriteId: sprite.asset.id };
}
describe('asset commands', () => {
it('creates assets with defaults and activates the first scene', () => {
const { project, sceneId, spriteId } = setup();
expect(project.assets).toHaveLength(2);
expect(project.activeSceneId).toBe(sceneId);
expect(project.assets.find((a) => a.id === spriteId)?.props).toEqual({ enabled: true, width: 100, height: 100, color: 0xe0553f });
});
it('renames and sets properties with undo/redo', () => {
const { project, stack, spriteId } = setup();
stack.execute(new RenameAssetCommand(spriteId, 'Crate'));
stack.execute(new SetAssetPropertyCommand(spriteId, 'width', 250));
const sprite = () => project.assets.find((a) => a.id === spriteId)!;
expect(sprite().name).toBe('Crate');
expect(sprite().props.width).toBe(250);
stack.undo();
expect(sprite().props.width).toBe(100);
stack.undo();
expect(sprite().name).toBe('Box');
stack.redo();
stack.redo();
expect(sprite().name).toBe('Crate');
expect(sprite().props.width).toBe(250);
});
it('rejects unknown properties', () => {
const { stack, spriteId } = setup();
expect(() => stack.execute(new SetAssetPropertyCommand(spriteId, 'x', 5))).toThrow(/not defined/);
expect(() => stack.execute(new ResetAssetPropertyCommand(spriteId, 'x'))).toThrow(/not defined/);
});
it('resets a property to its schema default with undo', () => {
const { project, stack, spriteId } = setup();
const sprite = () => project.assets.find((a) => a.id === spriteId)!;
stack.execute(new SetAssetPropertyCommand(spriteId, 'height', 60));
stack.execute(new ResetAssetPropertyCommand(spriteId, 'height'));
expect(sprite().props.height).toBe(100);
stack.undo();
expect(sprite().props.height).toBe(60);
stack.redo();
expect(sprite().props.height).toBe(100);
});
it('deletes an asset together with its scene objects and restores both on undo', () => {
const { project, stack, sceneId, spriteId } = setup();
stack.execute(new PlaceAssetCommand(sceneId, spriteId, { x: 1, y: 2 }));
stack.execute(new PlaceAssetCommand(sceneId, spriteId, { x: 3, y: 4 }));
expect(findScene(project, sceneId)!.objects).toHaveLength(2);
stack.execute(new DeleteAssetCommand(spriteId));
expect(project.assets).toHaveLength(1);
expect(findScene(project, sceneId)!.objects).toHaveLength(0);
stack.undo();
expect(project.assets).toHaveLength(2);
expect(findScene(project, sceneId)!.objects.map((o) => o.position)).toEqual([
{ x: 1, y: 2, z: 0 },
{ x: 3, y: 4, z: 0 },
]);
});
it('switches the active scene when the active one is deleted', () => {
const { project, stack, sceneId } = setup();
const second = new CreateAssetCommand('scene', 'Level 2');
stack.execute(second);
stack.execute(new SetActiveSceneCommand(second.asset.id));
stack.execute(new DeleteAssetCommand(second.asset.id));
expect(project.activeSceneId).toBe(sceneId);
stack.undo();
expect(project.activeSceneId).toBe(second.asset.id);
});
});
describe('scene object commands', () => {
it('places assets and refuses scenes', () => {
const { project, stack, sceneId, spriteId } = setup();
const place = new PlaceAssetCommand(sceneId, spriteId, { x: 5, y: 6 });
stack.execute(place);
const object = findScene(project, sceneId)!.objects[0]!;
expect(object.id).toBe(place.objectId);
expect(object.assetId).toBe(spriteId);
expect(object.position).toEqual({ x: 5, y: 6, z: 0 });
expect(object.overrides).toEqual({});
expect(() => stack.execute(new PlaceAssetCommand(sceneId, sceneId, { x: 0, y: 0 }))).toThrow(/cannot be placed/);
expect(findScene(project, sceneId)!.objects).toHaveLength(1);
});
it('moves, overrides, clears and removes with undo', () => {
const { project, stack, sceneId, spriteId } = setup();
const place = new PlaceAssetCommand(sceneId, spriteId, { x: 0, y: 0 });
stack.execute(place);
const object = () => findScene(project, sceneId)!.objects.find((o) => o.id === place.objectId)!;
stack.execute(new MoveSceneObjectCommand(sceneId, place.objectId, { x: 40, y: 50, z: 3 }));
expect(object().position).toEqual({ x: 40, y: 50, z: 3 });
stack.undo();
expect(object().position).toEqual({ x: 0, y: 0, z: 0 });
stack.redo();
stack.execute(new SetOverrideCommand(sceneId, place.objectId, 'color', 0x123456));
expect(object().overrides).toEqual({ color: 0x123456 });
expect(() => stack.execute(new SetOverrideCommand(sceneId, place.objectId, 'nope', 1))).toThrow();
stack.execute(new ClearOverrideCommand(sceneId, place.objectId, 'color'));
expect(object().overrides).toEqual({});
stack.undo();
expect(object().overrides).toEqual({ color: 0x123456 });
stack.execute(new RemoveSceneObjectCommand(sceneId, place.objectId));
expect(findScene(project, sceneId)!.objects).toHaveLength(0);
stack.undo();
expect(object().position).toEqual({ x: 40, y: 50, z: 3 });
});
it('clears the redo stack on a new command', () => {
const { stack, sceneId, spriteId } = setup();
stack.execute(new PlaceAssetCommand(sceneId, spriteId, { x: 0, y: 0 }));
stack.undo();
expect(stack.canRedo).toBe(true);
stack.execute(new PlaceAssetCommand(sceneId, spriteId, { x: 1, y: 1 }));
expect(stack.canRedo).toBe(false);
});
});