Files
feuerwerk/tests/core/object-references.test.ts
adminandClaude Opus 5.5 5e21d473a4 Fix references into nested placements and on asset delete (#285, #286)
Two findings from #269. A reference from a container into a placement
nested in it was dead from the start, and deleting an asset left
references to the objects it took along showing "(missing)".

- A reference is stored the way its holder shows the object. In a
  container tab that is q~x for content of a nested placement q.
- referenceCandidates translates it when the container is placed: in
  placement r, q~x becomes r~q/x. pathOfId and idOfPath in instance.ts
  do the mapping, objectChoices offers the same spelling the other way.
  The rule holds at any depth.
- Storing q/x as proposed in #285 was dropped. The container tab would
  then have had to translate its own ids.
- No migration. All projects and snapshots hold three references, none
  of them with ~ or /.
- Create container lets a reference to content of a member placement
  follow it. It was left dead before.
- Paste lets such a reference point into the copy. It kept pointing at
  the original before. Both go through the new renamedReference.
- Delete asset records what every holder shows before the cascade and
  clears every reference to an object that is gone afterwards, with
  dropReferencesToGone from #269. The row shows None, undo restores
  the references (#286).

Tests:
- Gate: 198 files, 2882 tests (2873 + 9), exit 0
- Unit object-references: 26 passed (17 + 9)
- e2e 1-17,57: 566 checks passed, 57 has +57i and +57j
- Six unit counter-probes fail between 1 and 3 tests each
- The browser counter-probe fails 57i and 57j as described in the
  issues
- e2e total 1609 is computed, not measured in a full run

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 07:28:49 +02:00

490 lines
23 KiB
TypeScript
Raw Permalink 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.
/**
* An object's place names another object of the same scene (#258): picked on a placed object,
* stored as its id, the object itself once the game runs – and inside a container, each placement's
* own copy of it.
*/
import { describe, expect, it } from 'vitest';
import { CreateAssetCommand, DeleteAssetCommand } from '../../src/core/commands/assetCommands';
import { CreateContainerCommand, UnpackContainerCommand } from '../../src/core/commands/containerCommands';
import { PasteObjectsCommand } from '../../src/core/commands/pasteCommands';
import { AddPortsCommand } from '../../src/core/commands/portCommands';
import {
PlaceAssetCommand,
RemoveSceneObjectsCommand,
RenameSceneObjectCommand,
SetOverrideCommand,
SetParentCommand,
} from '../../src/core/commands/sceneObjectCommands';
import { AddStructFieldCommand, AddVariableCommand } from '../../src/core/commands/variableCommands';
import type { BehaviorAsset, ContainerAsset } from '../../src/core/model/Asset';
import { copyObjects } from '../../src/core/model/clipboard';
import { instanceChildId, resolvedHolder } from '../../src/core/model/instance';
import {
objectChoices,
objectReference,
referenceCandidates,
referencedId,
} from '../../src/core/model/objectRefs';
import { NO_OBJECT } from '../../src/core/model/Properties';
import { findAsset, findObjectHolder } from '../../src/core/model/Project';
import { makeItem } from '../../src/core/properties/listItems';
import { coerceValue, variableDef } from '../../src/core/properties/PropertySchema';
import {
graphEditor,
makeBehavior,
makeSpriteScene,
place,
roundTrip,
type SceneFixture,
startWorld,
} from '../helpers';
/** A behavior whose object variable `target` is logged on every update. */
function watcher(f: SceneFixture): BehaviorAsset {
const behavior = makeBehavior(f, 'Watcher');
f.stack.execute(new AddVariableCommand(behavior.id, 'target', 'object'));
const edit = graphEditor(f, behavior);
const log = edit.addNode('Log');
edit.wire([
[edit.addNode('OnUpdate'), 'exec', log, 'exec'],
[edit.variables(['target']), 'target', log, 'message'],
]);
return behavior;
}
/** A Watcher and a box called "Crate" in the scene, the Watcher pointed at the box. */
function watching() {
const f = makeSpriteScene();
const behavior = watcher(f);
const watcherId = place(f, behavior.id, { x: 0, y: 0 });
const crate = place(f, f.spriteId, { x: 10, y: 0 });
f.stack.execute(new RenameSceneObjectCommand(f.sceneId, crate, 'Crate'));
f.stack.execute(new SetOverrideCommand(f.sceneId, watcherId, 'target', objectReference(crate)));
return { ...f, behavior, watcherId, crate };
}
describe('a picked object at rest', () => {
it('is kept by the override, the file, a struct field and a list item', () => {
const f = watching();
expect(f.object(f.watcherId).overrides.target).toEqual({ $object: f.crate });
const loaded = roundTrip(f.project);
const scene = loaded.assets.find((a) => a.id === f.sceneId) as {
objects: { id: string; overrides: object }[];
};
expect(scene.objects.find((o) => o.id === f.watcherId)!.overrides).toEqual({
target: { $object: f.crate },
});
const def = variableDef('held', 'struct', undefined, [{ key: 'who', kind: 'object' }]);
expect(coerceValue(def, { who: objectReference('x') })).toEqual({ who: { $object: 'x' } });
expect(makeItem('object', objectReference('x'))).toEqual({ $object: 'x' });
});
it('is nothing for anything that names no object', () => {
const def = variableDef('target', 'object');
expect(coerceValue(def, 'Crate')).toEqual(NO_OBJECT);
expect(coerceValue(def, objectReference(''))).toEqual(NO_OBJECT);
expect(referencedId('Crate')).toBe('');
});
});
describe('a picked object in a running game', () => {
it('is the object it names, and nothing once that is destroyed', () => {
const f = watching();
const run = startWorld(f, { behaviors: true });
run.frame();
expect(run.world.log.at(-1)).toBe('Crate');
run.world.remove(run.world.findByObjectId(f.crate)!);
run.frame();
expect(run.world.log.at(-1)).toBe('');
});
it('is the object inside a struct field and a list item too', () => {
const f = makeSpriteScene();
const behavior = makeBehavior(f, 'Holder');
f.stack.execute(new AddVariableCommand(behavior.id, 'held', 'struct'));
f.stack.execute(new AddStructFieldCommand(behavior.id, 'held', { key: 'who', kind: 'object' }));
f.stack.execute(new AddVariableCommand(behavior.id, 'crowd', 'list'));
const edit = graphEditor(f, behavior);
const expand = edit.addNode('ExpandStruct');
f.stack.execute(new AddPortsCommand(behavior.id, expand.id, [{ key: 'who', kind: 'object' }]));
const first = edit.addNode('ItemAt', { index: 0 });
const join = edit.addNode('Join');
const log = edit.addNode('Log');
const get = edit.variables(['held', 'crowd']);
edit.wire([
[edit.addNode('OnUpdate'), 'exec', log, 'exec'],
[get, 'held', expand, 'value'],
[get, 'crowd', first, 'list'],
[expand, 'who', join, 'a'],
[first, 'item', join, 'b'],
[join, 'text', log, 'message'],
]);
const holder = place(f, behavior.id, { x: 0, y: 0 });
const crate = place(f, f.spriteId, { x: 0, y: 0 });
const barrel = place(f, f.spriteId, { x: 0, y: 0 });
f.stack.execute(new RenameSceneObjectCommand(f.sceneId, crate, 'Crate'));
f.stack.execute(new RenameSceneObjectCommand(f.sceneId, barrel, 'Barrel'));
f.stack.execute(new SetOverrideCommand(f.sceneId, holder, 'held', { who: objectReference(crate) }));
f.stack.execute(new SetOverrideCommand(f.sceneId, holder, 'crowd', [objectReference(barrel)]));
const run = startWorld(f, { behaviors: true });
run.frame();
expect(run.world.log.at(-1)).toBe('CrateBarrel');
});
it('stays in the scene when a graph puts another object into the place (editor run)', () => {
const f = watching();
const edit = graphEditor(f, f.behavior);
const spawn = edit.addNode('Spawn', { asset: f.spriteId });
const set = edit.variables(['target'], { write: true });
edit.wire([
[edit.addNode('OnStart'), 'exec', spawn, 'exec'],
[spawn, 'exec', set, 'exec'],
[spawn, 'object', set, 'target'],
]);
const run = startWorld(f, { behaviors: true });
run.frame();
run.world.syncToModel();
run.world.syncFromModel();
run.frame();
expect(run.world.log.at(-1)).toBe('Box');
expect(f.object(f.watcherId).overrides.target).toEqual({ $object: f.crate });
});
it('follows a pick made while the game runs', () => {
const f = watching();
const barrel = place(f, f.spriteId, { x: 0, y: 0 });
f.stack.execute(new RenameSceneObjectCommand(f.sceneId, barrel, 'Barrel'));
const run = startWorld(f, { behaviors: true });
run.frame();
f.stack.execute(new SetOverrideCommand(f.sceneId, f.watcherId, 'target', objectReference(barrel)));
run.world.syncFromModel();
run.frame();
expect(run.world.log.slice(-2)).toEqual(['Crate', 'Barrel']);
});
});
describe('a picked object inside a container', () => {
/** The Watcher and its Crate made into a container, placed a second time beside the first. */
function contained() {
const f = watching();
const create = new CreateContainerCommand(f.sceneId, [f.watcherId, f.crate]);
f.stack.execute(create);
const second = new PlaceAssetCommand(f.sceneId, create.assetId, { x: 500, y: 0 });
f.stack.execute(second);
const asset = findAsset(f.project, create.assetId) as ContainerAsset;
const [tWatcher, tCrate] = asset.objects.map((o) => o.id) as [string, string];
return { ...f, create, asset, tWatcher, tCrate, first: create.objectId, second: second.objectId };
}
it('names the template copy, and each placement reaches its own', () => {
const f = contained();
expect(f.asset.objects[0]!.overrides.target).toEqual({ $object: f.tCrate });
const run = startWorld(f);
for (const placed of [f.first, f.second]) {
const watcher = run.world.findByObjectId(instanceChildId(placed, f.tWatcher))!;
const found = run.world.findReferenced(watcher, f.tCrate);
expect(found?.objectId).toBe(instanceChildId(placed, f.tCrate));
}
});
it('offers the content of its own placement, named as the template names it', () => {
const f = contained();
const choices = objectChoices(f.project, f.sceneOf(), instanceChildId(f.second, f.tWatcher));
expect(choices.map((c) => [c.ref, c.depth])).toEqual([
[f.tWatcher, 0],
[f.tCrate, 0],
]);
});
it('points a reference from outside at what the placed container shows', () => {
const f = watching();
const other = place(f, f.behavior.id, { x: 0, y: 0 });
f.stack.execute(new SetOverrideCommand(f.sceneId, other, 'target', objectReference(f.crate)));
const create = new CreateContainerCommand(f.sceneId, [f.crate]);
f.stack.execute(create);
const tCrate = (findAsset(f.project, create.assetId) as ContainerAsset).objects[0]!.id;
expect(f.object(other).overrides.target).toEqual({ $object: instanceChildId(create.objectId, tCrate) });
f.stack.undo();
expect(f.object(other).overrides.target).toEqual({ $object: f.crate });
});
it('is spelled out as the copy it names when the container is unpacked', () => {
const f = contained();
f.stack.execute(new UnpackContainerCommand(f.sceneId, f.second));
const watcher = f.object(instanceChildId(f.second, f.tWatcher));
expect(watcher.overrides.target).toEqual({ $object: instanceChildId(f.second, f.tCrate) });
});
});
describe('the objects of a scene', () => {
it('are offered in hierarchy order, the object itself included', () => {
const f = watching();
const lid = place(f, f.spriteId, { x: 0, y: 0 });
f.stack.execute(new SetParentCommand(f.sceneId, lid, f.crate));
f.stack.execute(new RenameSceneObjectCommand(f.sceneId, lid, 'Lid'));
const choices = objectChoices(f.project, f.sceneOf(), f.watcherId);
expect(choices.map((c) => [c.label, c.depth])).toEqual([
['Watcher', 0],
['Crate', 0],
['Lid', 1],
]);
expect(choices[1]!.ref).toBe(f.crate);
});
});
describe('removing and pasting', () => {
it('drops a reference to a removed object, and undo brings it back', () => {
const f = watching();
f.stack.execute(new RemoveSceneObjectsCommand(f.sceneId, [f.crate]));
expect(f.object(f.watcherId).overrides.target).toEqual(NO_OBJECT);
f.stack.undo();
expect(f.object(f.watcherId).overrides.target).toEqual({ $object: f.crate });
});
it('points a pasted copy at the copy of what it named, and leaves one out of the copy alone', () => {
const f = watching();
const both = copyObjects(f.project, f.sceneOf(), [f.watcherId, f.crate])!;
const pasteBoth = new PasteObjectsCommand(f.sceneId, both, {}, { x: 5, y: 0 });
f.stack.execute(pasteBoth);
const [watcherCopy, crateCopy] = pasteBoth.rootIds as [string, string];
expect(f.object(watcherCopy).overrides.target).toEqual({ $object: crateCopy });
const alone = copyObjects(f.project, f.sceneOf(), [f.watcherId])!;
const pasteAlone = new PasteObjectsCommand(f.sceneId, alone, {}, { x: 5, y: 0 });
f.stack.execute(pasteAlone);
expect(f.object(pasteAlone.rootIds[0]!).overrides.target).toEqual({ $object: f.crate });
});
});
describe('removing an object from a container template (#269)', () => {
/**
* Watcher, Crate and Barrel made into a container whose Watcher points at nothing, placed in
* "Level" and in "Level 2", each placement's Watcher pointed at its own Crate.
*/
function placedTwice() {
const f = watching();
const barrel = place(f, f.spriteId, { x: 20, y: 0 });
const create = new CreateContainerCommand(f.sceneId, [f.watcherId, f.crate, barrel]);
f.stack.execute(create);
const asset = findAsset(f.project, create.assetId) as ContainerAsset;
const [tWatcher, tCrate, tBarrel] = asset.objects.map((o) => o.id) as [string, string, string];
f.stack.execute(new SetOverrideCommand(asset.id, tWatcher, 'target', NO_OBJECT));
const level2 = new CreateAssetCommand('scene', 'Level 2');
f.stack.execute(level2);
const other = new PlaceAssetCommand(level2.asset.id, asset.id, { x: 0, y: 0 });
f.stack.execute(other);
const placements = [
[f.sceneId, create.objectId],
[level2.asset.id, other.objectId],
] as const;
for (const [sceneId, placed] of placements)
f.stack.execute(
new SetOverrideCommand(sceneId, instanceChildId(placed, tWatcher), 'target', objectReference(tCrate)),
);
return { ...f, asset, tWatcher, tCrate, tBarrel, level2: level2.asset.id, placements };
}
/** What the inspector shows as `target` on the object, container content resolved. */
function targetOf(f: SceneFixture, holderId: string, objectId: string) {
const holder = resolvedHolder(f.project, findObjectHolder(f.project, holderId)!);
return holder.objects.find((o) => o.id === objectId)!.overrides.target;
}
/** The edit a placement holds for its Watcher. */
function editOf(f: ReturnType<typeof placedTwice>, sceneId: string, placed: string) {
const holder = findObjectHolder(f.project, sceneId)!;
return holder.objects.find((o) => o.id === placed)!.instanceEdits[f.tWatcher];
}
it('empties the placements pointing at it in every scene, and undo brings them back', () => {
const f = placedTwice();
f.stack.execute(new RemoveSceneObjectsCommand(f.asset.id, [f.tCrate]));
for (const [sceneId, placed] of f.placements) {
expect(targetOf(f, sceneId, instanceChildId(placed, f.tWatcher))).toEqual(NO_OBJECT);
// None is what the template says too, so the placement follows it again.
expect(editOf(f, sceneId, placed)).toBeUndefined();
}
f.stack.undo();
for (const [sceneId, placed] of f.placements) {
expect(targetOf(f, sceneId, instanceChildId(placed, f.tWatcher))).toEqual({ $object: f.tCrate });
expect(editOf(f, sceneId, placed)?.overrides.target).toEqual({ $object: f.tCrate });
}
});
it('leaves a placement at None where the template names something else', () => {
const f = placedTwice();
f.stack.execute(new SetOverrideCommand(f.asset.id, f.tWatcher, 'target', objectReference(f.tBarrel)));
f.stack.execute(new RemoveSceneObjectsCommand(f.asset.id, [f.tCrate]));
const [sceneId, placed] = f.placements[0];
expect(targetOf(f, sceneId, instanceChildId(placed, f.tWatcher))).toEqual(NO_OBJECT);
expect(editOf(f, sceneId, placed)?.overrides.target).toEqual(NO_OBJECT);
});
it('empties a reference from outside the placement, and leaves one to what stays alone', () => {
const f = placedTwice();
const [, placed] = f.placements[0];
const toCrate = place(f, f.behavior.id, { x: 0, y: 0 });
const toBarrel = place(f, f.behavior.id, { x: 0, y: 0 });
const crate = objectReference(instanceChildId(placed, f.tCrate));
const barrel = objectReference(instanceChildId(placed, f.tBarrel));
f.stack.execute(new SetOverrideCommand(f.sceneId, toCrate, 'target', crate));
f.stack.execute(new SetOverrideCommand(f.sceneId, toBarrel, 'target', barrel));
f.stack.execute(new RemoveSceneObjectsCommand(f.asset.id, [f.tCrate]));
expect(f.object(toCrate).overrides.target).toEqual(NO_OBJECT);
expect(f.object(toBarrel).overrides.target).toEqual(barrel);
f.stack.undo();
expect(f.object(toCrate).overrides.target).toEqual(crate);
});
it('reaches placements nested in another container, template and scene alike', () => {
const f = placedTwice();
const [, placed] = f.placements[0];
// The placement in "Level" becomes the content of Outer and takes its edit into Outer's template.
const outer = new CreateContainerCommand(f.sceneId, [placed]);
f.stack.execute(outer);
const outerAsset = findAsset(f.project, outer.assetId) as ContainerAsset;
const inner = outerAsset.objects[0]!.id;
const outerPlaced = new PlaceAssetCommand(f.level2, outerAsset.id, { x: 0, y: 0 });
f.stack.execute(outerPlaced);
const nestedWatcher = instanceChildId(outerPlaced.objectId, `${inner}/${f.tWatcher}`);
f.stack.execute(new SetOverrideCommand(f.level2, nestedWatcher, 'target', objectReference(f.tBarrel)));
f.stack.execute(new RemoveSceneObjectsCommand(f.asset.id, [f.tCrate]));
expect(targetOf(f, outerAsset.id, instanceChildId(inner, f.tWatcher))).toEqual(NO_OBJECT);
expect(targetOf(f, f.level2, nestedWatcher)).toEqual({ $object: f.tBarrel });
f.stack.execute(new RemoveSceneObjectsCommand(f.asset.id, [f.tBarrel]));
expect(targetOf(f, f.level2, nestedWatcher)).toEqual(NO_OBJECT);
f.stack.undo();
f.stack.undo();
expect(targetOf(f, outerAsset.id, instanceChildId(inner, f.tWatcher))).toEqual({ $object: f.tCrate });
expect(targetOf(f, f.level2, nestedWatcher)).toEqual({ $object: f.tBarrel });
});
it('does the same when the asset of the object is deleted (#286)', () => {
const f = placedTwice();
f.stack.execute(new DeleteAssetCommand(f.spriteId));
for (const [sceneId, placed] of f.placements) {
expect(targetOf(f, sceneId, instanceChildId(placed, f.tWatcher))).toEqual(NO_OBJECT);
expect(editOf(f, sceneId, placed)).toBeUndefined();
}
f.stack.undo();
for (const [sceneId, placed] of f.placements)
expect(editOf(f, sceneId, placed)?.overrides.target).toEqual({ $object: f.tCrate });
});
});
describe('a reference into a placement nested in a container (#285)', () => {
/**
* The Crate made into "Inner", then the Watcher and that placement into "Outer", placed a second
* time. A Lookout outside points at the Crate as well.
*/
function nested() {
const f = watching();
const lookout = place(f, f.behavior.id, { x: 0, y: 0 });
f.stack.execute(new SetOverrideCommand(f.sceneId, lookout, 'target', objectReference(f.crate)));
const inner = new CreateContainerCommand(f.sceneId, [f.crate]);
f.stack.execute(inner);
const innerPlaced = inner.objectId;
const outer = new CreateContainerCommand(f.sceneId, [f.watcherId, innerPlaced]);
f.stack.execute(outer);
const second = new PlaceAssetCommand(f.sceneId, outer.assetId, { x: 500, y: 0 });
f.stack.execute(second);
const asset = findAsset(f.project, outer.assetId) as ContainerAsset;
const tWatcher = asset.objects.find((o) => o.assetId === f.behavior.id)!.id;
const tInner = asset.objects.find((o) => o.assetId === inner.assetId)!.id;
const tCrate = (findAsset(f.project, inner.assetId) as ContainerAsset).objects[0]!.id;
/** The Crate as Outer's own view names it. */
const crate = instanceChildId(tInner, tCrate);
/** The Crate's copy in a placement of Outer. */
const copyIn = (placed: string) => instanceChildId(placed, `${tInner}/${tCrate}`);
const placements = [outer.objectId, second.objectId] as const;
return { ...f, lookout, inner, innerPlaced, asset, tWatcher, crate, copyIn, placements };
}
it('is picked in the container as its view names it, and every placement reaches its own copy', () => {
const f = nested();
f.stack.execute(new SetOverrideCommand(f.asset.id, f.tWatcher, 'target', NO_OBJECT));
const choice = objectChoices(f.project, f.asset, f.tWatcher).find((c) => c.label === 'Crate')!;
expect(choice).toMatchObject({ ref: f.crate, objectId: f.crate });
f.stack.execute(new SetOverrideCommand(f.asset.id, f.tWatcher, 'target', objectReference(choice.ref)));
const run = startWorld(f);
for (const placed of f.placements) {
const watcher = run.world.findByObjectId(instanceChildId(placed, f.tWatcher))!;
expect(run.world.findReferenced(watcher, f.crate)?.objectId).toBe(f.copyIn(placed));
}
});
it('is shown as the Crate on a placement, and offered there with the same name', () => {
const f = nested();
const [, placed] = f.placements;
const choices = objectChoices(f.project, f.sceneOf(), instanceChildId(placed, f.tWatcher));
expect(choices.find((c) => c.ref === f.crate)).toMatchObject({
objectId: f.copyIn(placed),
label: 'Crate',
});
});
it('follows its object when the placement around it becomes container content', () => {
const f = nested();
expect(f.asset.objects.find((o) => o.id === f.tWatcher)!.overrides.target).toEqual({ $object: f.crate });
expect(f.object(f.lookout).overrides.target).toEqual({ $object: f.copyIn(f.placements[0]) });
const run = startWorld(f);
const lookout = run.world.findByObjectId(f.lookout)!;
expect(run.world.findReferenced(lookout, f.copyIn(f.placements[0]))?.objectId).toBe(
f.copyIn(f.placements[0]),
);
});
it('is spelled out as the copy it names when the container is unpacked', () => {
const f = nested();
const [, placed] = f.placements;
f.stack.execute(new UnpackContainerCommand(f.sceneId, placed));
expect(f.object(instanceChildId(placed, f.tWatcher)).overrides.target).toEqual({
$object: f.copyIn(placed),
});
});
it('reaches content nested deeper as well, from content of a placement', () => {
expect(referenceCandidates('r~q/x', 'p~z')).toEqual(['r~q/p/z', 'p~z']);
expect(referenceCandidates('r~x', 'p~z/y')).toEqual(['r~p/z/y', 'p~z/y']);
});
it('points a pasted copy at the content of the copied placement', () => {
const f = watching();
const inner = new CreateContainerCommand(f.sceneId, [f.crate]);
f.stack.execute(inner);
const tCrate = (findAsset(f.project, inner.assetId) as ContainerAsset).objects[0]!.id;
const both = copyObjects(f.project, f.sceneOf(), [f.watcherId, inner.objectId])!;
const paste = new PasteObjectsCommand(f.sceneId, both, {}, { x: 5, y: 0 });
f.stack.execute(paste);
const copies = paste.rootIds.map((id) => f.object(id));
const watcherCopy = copies.find((o) => o.assetId === f.behavior.id)!;
const innerCopy = copies.find((o) => o.assetId === inner.assetId)!;
expect(watcherCopy.overrides.target).toEqual({ $object: instanceChildId(innerCopy.id, tCrate) });
});
});
describe('deleting an asset (#286)', () => {
it('empties a reference to an object placed from it, and undo brings both back', () => {
const f = watching();
f.stack.execute(new DeleteAssetCommand(f.spriteId));
expect(f.object(f.watcherId).overrides.target).toEqual(NO_OBJECT);
f.stack.undo();
expect(f.object(f.watcherId).overrides.target).toEqual({ $object: f.crate });
expect(f.object(f.crate).name).toBe('Crate');
});
it('empties a reference into a placement of a deleted container', () => {
const f = watching();
const create = new CreateContainerCommand(f.sceneId, [f.crate]);
f.stack.execute(create);
const named = f.object(f.watcherId).overrides.target;
expect(referencedId(named)).toContain(create.objectId);
f.stack.execute(new DeleteAssetCommand(create.assetId));
expect(f.object(f.watcherId).overrides.target).toEqual(NO_OBJECT);
f.stack.undo();
expect(f.object(f.watcherId).overrides.target).toEqual(named);
});
});