Tooling: - Prettier as the single formatting authority (110 columns, single quotes, measured against the existing style); Markdown excluded because table-cell padding turns the README glossary into 1000-character lines. - Type-aware ESLint ending in eslint-config-prettier, so a rule always means "this is a bug", never "this looks different". - .editorconfig and an `npm run check` that runs typecheck, lint, format and tests in one go. Findings fixed on the first run: three more duplicate imports, four String(unknown) calls (parameter types narrowed instead of casts), two dead initialisers, two redundant casts, a console.log for a real error. Two warnings were false here and are disabled with a reason in place. Tests 132 -> 269: - node-catalog.test.ts covers every node with a run(), with a guard that fails when a new node has no row. - behavior-events.test.ts covers the ten event nodes through a running World, checking timing rather than existence. - Input's state machine (press/release/releaseAll) is reachable without DOM events now; the DOM handlers are thin adapters. Compare fell back to == without a configured operator although the schema default is <; aligned. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
117 lines
4.5 KiB
TypeScript
117 lines
4.5 KiB
TypeScript
/**
|
||
* Container assets hold placed objects just like scenes do. Anything that walks "all placed
|
||
* objects" has to cover both – the tests here pin the places that used to look at scenes only.
|
||
* The history tests pin that a rejected undo leaves the stacks usable.
|
||
*/
|
||
import { describe, expect, it } from 'vitest';
|
||
import { type Command, CommandError } from '../../src/core/commands/Command';
|
||
import { CommandStack } from '../../src/core/commands/CommandStack';
|
||
import { CreateContainerCommand } from '../../src/core/commands/containerCommands';
|
||
import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
|
||
import { RemoveVariableCommand, RenameVariableCommand } from '../../src/core/commands/variableCommands';
|
||
import type { ContainerAsset } from '../../src/core/model/Asset';
|
||
import { findAsset, findUsages, objectHolders } from '../../src/core/model/Project';
|
||
import { makeBehavior, makeSpriteScene, place } from '../helpers';
|
||
|
||
/** A behavior with a `speed` variable, placed and overridden to 42, then wrapped in a container. */
|
||
function containerWithOverride() {
|
||
const f = makeSpriteScene();
|
||
const behavior = makeBehavior(f, 'B', ['speed']);
|
||
const objectId = place(f, behavior.id, { x: 0, y: 0 });
|
||
f.stack.execute(new SetOverrideCommand(f.sceneId, objectId, 'speed', 42));
|
||
const container = new CreateContainerCommand(f.sceneId, [objectId]);
|
||
f.stack.execute(container);
|
||
const template = () => (findAsset(f.project, container.assetId) as ContainerAsset).objects[0]!;
|
||
return { ...f, behaviorId: behavior.id, objectId, template };
|
||
}
|
||
|
||
describe('object holders', () => {
|
||
it('counts scenes and container assets alike', () => {
|
||
const { project } = containerWithOverride();
|
||
expect(
|
||
objectHolders(project)
|
||
.map((h) => h.type)
|
||
.sort(),
|
||
).toEqual(['container', 'scene']);
|
||
});
|
||
|
||
it('finds usages inside container templates, not just in scenes', () => {
|
||
const { project, behaviorId, template } = containerWithOverride();
|
||
// The placed object and the template copy the container was built from.
|
||
expect(findUsages(project, behaviorId)).toHaveLength(2);
|
||
expect(template().overrides.speed).toBe(42);
|
||
});
|
||
});
|
||
|
||
describe('RemoveVariableCommand', () => {
|
||
it('clears the override on container templates too, and puts it back on undo', () => {
|
||
const { stack, behaviorId, objectId, sceneOf, template } = containerWithOverride();
|
||
const placed = () => sceneOf().objects.find((o) => o.id === objectId)!;
|
||
|
||
stack.execute(new RemoveVariableCommand(behaviorId, 'speed'));
|
||
expect(placed().overrides.speed).toBeUndefined();
|
||
expect(template().overrides.speed).toBeUndefined();
|
||
|
||
stack.undo();
|
||
expect(placed().overrides.speed).toBe(42);
|
||
expect(template().overrides.speed).toBe(42);
|
||
});
|
||
});
|
||
|
||
describe('RenameVariableCommand', () => {
|
||
it('renames the override on container templates too', () => {
|
||
const { stack, behaviorId, template } = containerWithOverride();
|
||
stack.execute(new RenameVariableCommand(behaviorId, 'speed', 'velocity'));
|
||
expect(template().overrides.speed).toBeUndefined();
|
||
expect(template().overrides.velocity).toBe(42);
|
||
|
||
stack.undo();
|
||
expect(template().overrides.speed).toBe(42);
|
||
expect(template().overrides.velocity).toBeUndefined();
|
||
});
|
||
});
|
||
|
||
/** Applies a marker to the project; its undo refuses to run. */
|
||
class RefusingUndo implements Command {
|
||
readonly label = 'Refusing';
|
||
execute(project: { name: string }): void {
|
||
project.name = 'executed';
|
||
}
|
||
undo(): void {
|
||
throw new CommandError('target is gone');
|
||
}
|
||
}
|
||
|
||
describe('CommandStack', () => {
|
||
it('keeps a command on the undo stack when its undo is rejected', () => {
|
||
const { project, stack } = makeSpriteScene();
|
||
stack.execute(new RefusingUndo());
|
||
expect(stack.canUndo).toBe(true);
|
||
|
||
expect(() => stack.undo()).toThrow(CommandError);
|
||
// Not swallowed by the history: still undoable, and not queued for redo.
|
||
expect(stack.canUndo).toBe(true);
|
||
expect(stack.canRedo).toBe(false);
|
||
expect(project.name).toBe('executed');
|
||
});
|
||
|
||
it('keeps a command on the redo stack when its redo is rejected', () => {
|
||
const project = { formatVersion: 1, id: 'p', name: 'P', assets: [], activeSceneId: '' };
|
||
const stack = new CommandStack(project);
|
||
let failing = false;
|
||
stack.execute({
|
||
label: 'Toggle',
|
||
execute: () => {
|
||
if (failing) throw new CommandError('nope');
|
||
},
|
||
undo: () => {},
|
||
});
|
||
stack.undo();
|
||
failing = true;
|
||
|
||
expect(() => stack.redo()).toThrow(CommandError);
|
||
expect(stack.canRedo).toBe(true);
|
||
expect(stack.canUndo).toBe(false);
|
||
});
|
||
});
|