Files
feuerwerk/tests/core/apply.test.ts
T
rootandClaude Fable 5.1 1eb28314cf Refactoring: one file per node, consistent comments, shared helpers, split large modules
- Graph: nodes.ts split into nodes/<category>/<Type>.ts (62 files) with
  shared.ts factories and index.ts registry; NodeContext/values/NodeRegistry
  separated, writeTyped() replaces duplicated typed writes
- Comments: one style (JSDoc on non-obvious exports, // for decisions and
  traps), wrong/stale comments fixed or removed, ids.ts now real 10 base-36 chars
- Core: chainOf, shared worldPosition/editorPosition core, commands/require.ts,
  commands/naming.ts, util/list.ts, setBagValue/toPosition/propDef; dead code
  removed (isBehaviorAsset, SCENE_OBJECT_SCHEMA, CommandStack.subscribe,
  Input.axis, setMode, titleText, World.worldPosition, unused ctor params)
- Editor: Inspector split into inspector/ sections, App.ts into Editor.ts +
  debugHook.ts + shortcuts.ts, Viewport into items.ts + symbols.ts, shared
  "Add collider" action, unified type labels/swatches
- UI toolkit: Widget.trackHover/onPointerRelease, drawControlBox, math helpers,
  Popup.openBelow, Menu/Dialog/ColorPicker/ListRow in own files, named Theme sizes
- Tools: seed-demo/smoke-demo and demo-check project removed, tools/lib
  (harness, projects, builders), e2e.mjs as runner over tools/e2e/*.mjs sections
- Tests: tests/helpers.ts replaces copied fixtures and NodeContext doubles

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 08:37:22 +02:00

154 lines
7.7 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
ApplyOverrideToAssetCommand,
ApplyOverrideToContainerCommand,
ApplyPositionToContainerCommand,
RevertOverrideToTemplateCommand,
RevertPositionToTemplateCommand,
} from '../../src/core/commands/applyCommands';
import { CreateContainerCommand } from '../../src/core/commands/containerCommands';
import { MoveSceneObjectCommand, SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
import type { ContainerAsset } from '../../src/core/model/Asset';
import { templateLinkOf } from '../../src/core/model/container';
import { findAsset } from '../../src/core/model/Project';
import { makeSpriteScene, place } from '../helpers';
/**
* A container made from two boxes (a at (100,100), b at (10,0)) and a second placed instance
* of it. `a` is the original, `a2` its copy in the instance.
*/
function setup() {
const f = makeSpriteScene();
const a = place(f, f.spriteId, { x: 100, y: 100 });
const b = place(f, f.spriteId, { x: 10, y: 0 });
const create = new CreateContainerCommand(f.sceneId, [a, b]);
f.stack.execute(create);
const instance = place(f, create.assetId, { x: 1000, y: 0 });
const container = () => findAsset(f.project, create.assetId) as ContainerAsset;
const templateOfA = () => container().objects.find((t) => t.id === f.object(a).templateId)!;
const a2 = f.sceneOf().objects.find((o) => o.parentId === instance && o.templateId === f.object(a).templateId)!.id;
return { ...f, a, b, a2, container, templateOfA };
}
describe('template link', () => {
it('resolves the container and template for instance objects only', () => {
const { project, a, a2, sceneOf, object, container, templateOfA } = setup();
const scene = sceneOf();
const link = templateLinkOf(project, scene, object(a2));
expect(link?.asset.id).toBe(container().id);
expect(link?.template).toBe(templateOfA());
expect(templateLinkOf(project, scene, object(a))?.template).toBe(templateOfA());
const containerObject = scene.objects.find((o) => o.assetId === container().id)!;
expect(templateLinkOf(project, scene, containerObject)).toBeUndefined();
});
});
describe('position apply / revert', () => {
it('applies one axis to the template; instances still at the template value follow', () => {
const { stack, sceneId, a, a2, object, templateOfA } = setup();
expect(templateOfA().position).toEqual({ x: 45, y: 50, z: 0 });
// a2 was moved by hand on x → it keeps that; on y it still matches the template.
stack.execute(new MoveSceneObjectCommand(sceneId, a2, { x: 0, y: 50, z: 0 }));
stack.execute(new MoveSceneObjectCommand(sceneId, a, { x: 60, y: 70, z: 0 }));
stack.execute(new ApplyPositionToContainerCommand(sceneId, a, 'x'));
expect(templateOfA().position.x).toBe(60);
expect(object(a2).position.x).toBe(0); // modified on its own → untouched
stack.execute(new ApplyPositionToContainerCommand(sceneId, a, 'y'));
expect(templateOfA().position.y).toBe(70);
expect(object(a2).position.y).toBe(70); // was at the template value → follows
stack.undo();
expect(templateOfA().position.y).toBe(50);
expect(object(a2).position.y).toBe(50);
stack.undo();
expect(templateOfA().position.x).toBe(45);
expect(object(a).position.x).toBe(60); // the source object itself is not changed by apply
});
it('reverts an axis (or the whole position) to the template', () => {
const { stack, sceneId, a, object } = setup();
stack.execute(new MoveSceneObjectCommand(sceneId, a, { x: 1, y: 2, z: 3 }));
stack.execute(new RevertPositionToTemplateCommand(sceneId, a, 'z'));
expect(object(a).position).toEqual({ x: 1, y: 2, z: 0 });
stack.execute(new RevertPositionToTemplateCommand(sceneId, a));
expect(object(a).position).toEqual({ x: 45, y: 50, z: 0 });
stack.undo();
expect(object(a).position).toEqual({ x: 1, y: 2, z: 0 });
});
it('rejects objects that are not part of a container', () => {
const f = setup();
const loose = place(f, f.spriteId, { x: 0, y: 0 });
expect(() => f.stack.execute(new ApplyPositionToContainerCommand(f.sceneId, loose))).toThrow(/not part of a placed container/);
expect(f.sceneOf().objects.find((o) => o.id === loose)).toBeDefined();
});
});
describe('override apply / revert', () => {
it('applies an override to the container template and to instances that had no own value', () => {
const { stack, sceneId, a, a2, object, templateOfA } = setup();
stack.execute(new SetOverrideCommand(sceneId, a, 'width', 77));
stack.execute(new ApplyOverrideToContainerCommand(sceneId, a, 'width'));
expect(templateOfA().overrides.width).toBe(77);
expect(object(a2).overrides.width).toBe(77);
expect(object(a).overrides.width).toBe(77);
stack.undo();
expect(templateOfA().overrides.width).toBeUndefined();
expect(object(a2).overrides.width).toBeUndefined();
expect(object(a).overrides.width).toBe(77);
});
it('leaves instances alone that already had their own override', () => {
const { stack, sceneId, a, a2, object, templateOfA } = setup();
stack.execute(new SetOverrideCommand(sceneId, a2, 'width', 5));
stack.execute(new SetOverrideCommand(sceneId, a, 'width', 77));
stack.execute(new ApplyOverrideToContainerCommand(sceneId, a, 'width'));
expect(templateOfA().overrides.width).toBe(77);
expect(object(a2).overrides.width).toBe(5);
});
it('applies an override to the asset, dropping the override on object and template', () => {
const { project, stack, sceneId, spriteId, a, a2, object, templateOfA } = setup();
stack.execute(new SetOverrideCommand(sceneId, a, 'width', 77));
stack.execute(new ApplyOverrideToContainerCommand(sceneId, a, 'width'));
stack.execute(new ApplyOverrideToAssetCommand(sceneId, a, 'width'));
const sprite = () => findAsset(project, spriteId)!;
expect(sprite().props.width).toBe(77);
expect(object(a).overrides.width).toBeUndefined();
expect(templateOfA().overrides.width).toBeUndefined();
expect(object(a2).overrides.width).toBeUndefined(); // shared the template value → follows the asset now
stack.undo();
expect(sprite().props.width).toBe(100);
expect(object(a).overrides.width).toBe(77);
expect(templateOfA().overrides.width).toBe(77);
expect(object(a2).overrides.width).toBe(77);
});
it('applies to the asset for a plain object too', () => {
const f = setup();
const { project, stack, sceneId, spriteId, object } = f;
const loose = place(f, spriteId, { x: 0, y: 0 });
stack.execute(new SetOverrideCommand(sceneId, loose, 'height', 33));
stack.execute(new ApplyOverrideToAssetCommand(sceneId, loose, 'height'));
expect(findAsset(project, spriteId)!.props.height).toBe(33);
expect(object(loose).overrides.height).toBeUndefined();
expect(() => stack.execute(new ApplyOverrideToAssetCommand(sceneId, loose, 'nope'))).toThrow(/not defined/);
});
it('reverts an override to the template value (or removes it when the template has none)', () => {
const { stack, sceneId, a, a2, object, templateOfA } = setup();
stack.execute(new SetOverrideCommand(sceneId, a, 'width', 77));
stack.execute(new ApplyOverrideToContainerCommand(sceneId, a, 'width'));
stack.execute(new SetOverrideCommand(sceneId, a2, 'width', 5));
stack.execute(new RevertOverrideToTemplateCommand(sceneId, a2, 'width'));
expect(object(a2).overrides.width).toBe(77);
expect(templateOfA().overrides.width).toBe(77);
stack.execute(new SetOverrideCommand(sceneId, a2, 'height', 9));
stack.execute(new RevertOverrideToTemplateCommand(sceneId, a2, 'height'));
expect(object(a2).overrides.height).toBeUndefined();
stack.undo();
expect(object(a2).overrides.height).toBe(9);
});
});