Files
feuerwerk/tests/core/apply.test.ts
T
rootandClaude Opus 5 f40b8a0523 Fix instance lookups that trusted stale data
Two regressions from the template-resolution rework, plus the harness gap
that hid them:

- `instanceTargetOf` decided from the shape of the id. Unpacking keeps the
  derived ids, so unpacked content still counted as template content. It now
  asks whether the object is spelled out in the holder.
- The parent picker captured its holder when the row was built. Since
  `viewOf()` returns a derived copy, the menu listed the objects of that
  moment (measured: 40 in the model, 17 in the menu). It resolves on open.
- `evaluate()` swallowed page exceptions, so a mutate that died half-way let
  every later check pass or fail for the wrong reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 21:59:22 +02:00

176 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
import {
ApplyOverrideToAssetCommand,
ApplyOverrideToContainerCommand,
ApplyPositionToContainerCommand,
RevertOverrideToTemplateCommand,
RevertPositionToTemplateCommand,
} from '../../src/core/commands/applyCommands';
import { CreateContainerCommand, UnpackContainerCommand } from '../../src/core/commands/containerCommands';
import { MoveSceneObjectCommand, SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
import type { ContainerAsset } from '../../src/core/model/Asset';
import { instanceChildId, instanceTargetOf } from '../../src/core/model/instance';
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 content of the first instance, `a2` the same template object in the second – neither
* is stored in the scene, both are resolved from the template.
*/
function setup() {
const f = makeSpriteScene();
const create = new CreateContainerCommand(f.sceneId, [
place(f, f.spriteId, { x: 100, y: 100 }),
place(f, f.spriteId, { x: 10, y: 0 }),
]);
f.stack.execute(create);
const instance = place(f, create.assetId, { x: 1000, y: 0 });
const container = () => findAsset(f.project, create.assetId) as ContainerAsset;
const templateA = container().objects[0]!.id;
return {
...f,
instanceId: create.objectId,
instance,
container,
templateA,
a: instanceChildId(create.objectId, templateA),
a2: instanceChildId(instance, templateA),
templateOfA: () => container().objects.find((t) => t.id === templateA)!,
};
}
describe('template link', () => {
it('resolves the container and template for resolved content only', () => {
const { project, a, a2, sceneOf, instanceId, container, templateOfA } = setup();
const scene = sceneOf();
expect(instanceTargetOf(project, scene, a2)?.asset.id).toBe(container().id);
expect(instanceTargetOf(project, scene, a2)?.template).toBe(templateOfA());
expect(instanceTargetOf(project, scene, a)?.template).toBe(templateOfA());
// The container object itself is stored, so it has no template behind it.
expect(instanceTargetOf(project, scene, instanceId)).toBeUndefined();
});
it('drops the link once the instance is unpacked, even though the ids stay', () => {
const { project, stack, sceneId, sceneOf, instanceId, a } = setup();
stack.execute(new UnpackContainerCommand(sceneId, instanceId));
expect(sceneOf().objects.some((o) => o.id === a)).toBe(true);
expect(instanceTargetOf(project, sceneOf(), a)).toBeUndefined();
stack.undo();
expect(instanceTargetOf(project, sceneOf(), a)?.asset.type).toBe('container');
});
});
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);
});
});