Add ESLint and Prettier, and close the node test gaps
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>
This commit is contained in:
1 parent
f140c2fd77
commit
d54a620ebd
113 files changed
+5049
-666
No files matched your search
+53
-11
@@ -1,12 +1,30 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CommandError } from '../../src/core/commands/Command';
|
||||
import { AddNodeCommand, DisconnectCommand, RemoveNodeCommand, SetNodeParamCommand } from '../../src/core/commands/graphCommands';
|
||||
import { AddVariableCommand, RemoveVariableCommand, RenameVariableCommand } from '../../src/core/commands/variableCommands';
|
||||
import {
|
||||
AddNodeCommand,
|
||||
DisconnectCommand,
|
||||
RemoveNodeCommand,
|
||||
SetNodeParamCommand,
|
||||
} from '../../src/core/commands/graphCommands';
|
||||
import {
|
||||
AddVariableCommand,
|
||||
RemoveVariableCommand,
|
||||
RenameVariableCommand,
|
||||
} from '../../src/core/commands/variableCommands';
|
||||
import { GraphInterpreter, allNodeDefs, findNode, portsCompatible } from '../../src/core/graph';
|
||||
import type { BehaviorAsset } from '../../src/core/model/Asset';
|
||||
import { findAsset } from '../../src/core/model/Project';
|
||||
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
|
||||
import { type ProjectFixture, fakeContext, graphEditor, makeBehavior, makeProject, makeSpriteScene, place, roundTrip } from '../helpers';
|
||||
import {
|
||||
type ProjectFixture,
|
||||
fakeContext,
|
||||
graphEditor,
|
||||
makeBehavior,
|
||||
makeProject,
|
||||
makeSpriteScene,
|
||||
place,
|
||||
roundTrip,
|
||||
} from '../helpers';
|
||||
|
||||
/** Adds "On key held(ArrowRight) → Move(owner, dx = speed × dt)" to a project, built through commands like the editor would. */
|
||||
function withMover<F extends ProjectFixture>(fixture: F) {
|
||||
@@ -75,7 +93,10 @@ describe('GraphInterpreter', () => {
|
||||
const start = edit.addNode('OnStart');
|
||||
const self = edit.addNode('Self');
|
||||
const move = edit.addNode('Move', { dx: 7, dy: -3 });
|
||||
edit.wire([[start, 'exec', move, 'exec'], [self, 'object', move, 'object']]);
|
||||
edit.wire([
|
||||
[start, 'exec', move, 'exec'],
|
||||
[self, 'object', move, 'object'],
|
||||
]);
|
||||
const ctx = fakeContext();
|
||||
new GraphInterpreter(behavior.graph).fire('OnStart', ctx);
|
||||
expect(ctx.self.x).toBe(7);
|
||||
@@ -88,7 +109,11 @@ describe('GraphInterpreter', () => {
|
||||
const self = edit.addNode('Self');
|
||||
const clamp = edit.addNode('Clamp', { value: 5 });
|
||||
const move = edit.addNode('Move', { dx: 4 });
|
||||
edit.wire([[start, 'exec', move, 'exec'], [self, 'object', move, 'object'], [clamp, 'result', move, 'dy']]);
|
||||
edit.wire([
|
||||
[start, 'exec', move, 'exec'],
|
||||
[self, 'object', move, 'object'],
|
||||
[clamp, 'result', move, 'dy'],
|
||||
]);
|
||||
const ctx = fakeContext();
|
||||
new GraphInterpreter(behavior.graph).fire('OnStart', ctx);
|
||||
expect(ctx.self.x).toBe(4); // scale defaulted to 1, not 0
|
||||
@@ -145,7 +170,10 @@ describe('GraphInterpreter', () => {
|
||||
let previous = start;
|
||||
sources.forEach((source, i) => {
|
||||
const log = edit.addNode('Log');
|
||||
edit.wire([[previous, 'exec', log, 'exec'], [source, ports[i]!, log, 'message']]);
|
||||
edit.wire([
|
||||
[previous, 'exec', log, 'exec'],
|
||||
[source, ports[i]!, log, 'message'],
|
||||
]);
|
||||
previous = log;
|
||||
});
|
||||
const ctx = fakeContext();
|
||||
@@ -198,14 +226,21 @@ describe('GraphInterpreter', () => {
|
||||
const start = edit.addNode('OnStart');
|
||||
const a = edit.addNode('Sequence');
|
||||
const b = edit.addNode('Sequence');
|
||||
edit.wire([[start, 'exec', a, 'exec'], [a, 'a', b, 'exec'], [b, 'a', a, 'exec']]);
|
||||
edit.wire([
|
||||
[start, 'exec', a, 'exec'],
|
||||
[a, 'a', b, 'exec'],
|
||||
[b, 'a', a, 'exec'],
|
||||
]);
|
||||
expect(() => new GraphInterpreter(behavior.graph).fire('OnStart', fakeContext())).toThrow(/step limit/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('graph commands', () => {
|
||||
it('rejects incompatible, self and duplicate connections', () => {
|
||||
const { edit, nodes: { held, mul, move } } = withMover(makeProject());
|
||||
const {
|
||||
edit,
|
||||
nodes: { held, mul, move },
|
||||
} = withMover(makeProject());
|
||||
expect(() => edit.connect(held, 'exec', mul, 'a')).toThrow(/Cannot connect/);
|
||||
expect(() => edit.connect(mul, 'result', mul, 'a')).toThrow(/itself/);
|
||||
expect(() => edit.connect(held, 'exec', move, 'exec')).toThrow(/Already/);
|
||||
@@ -218,7 +253,8 @@ describe('graph commands', () => {
|
||||
const before = behavior.graph.edges.length;
|
||||
edit.connect(literal, 'value', nodes.move, 'dx');
|
||||
expect(behavior.graph.edges.length).toBe(before); // one replaced, one added
|
||||
const sourceOfDx = () => behavior.graph.edges.find((e) => e.to.node === nodes.move.id && e.to.port === 'dx')!.from.node;
|
||||
const sourceOfDx = () =>
|
||||
behavior.graph.edges.find((e) => e.to.node === nodes.move.id && e.to.port === 'dx')!.from.node;
|
||||
expect(sourceOfDx()).toBe(literal.id);
|
||||
stack.undo();
|
||||
expect(sourceOfDx()).toBe(nodes.mul.id);
|
||||
@@ -288,10 +324,16 @@ describe('behavior serialization', () => {
|
||||
expect(loaded.graph).toEqual(behavior.graph);
|
||||
expect(loaded.props).toEqual({ enabled: true, speed: 0 });
|
||||
|
||||
const raw = serializeProject(project) as { assets: { id: string; graph?: { nodes: unknown[]; edges: unknown[] } }[] };
|
||||
const raw = serializeProject(project) as {
|
||||
assets: { id: string; graph?: { nodes: unknown[]; edges: unknown[] } }[];
|
||||
};
|
||||
const rawGraph = raw.assets.find((a) => a.id === behavior.id)!.graph!;
|
||||
rawGraph.nodes.push({ id: 'zzz', type: 'DoesNotExist', x: 0, y: 0, params: {} });
|
||||
rawGraph.edges.push({ id: 'e-bad', from: { node: 'zzz', port: 'exec' }, to: { node: behavior.graph.nodes[0]!.id, port: 'exec' } });
|
||||
rawGraph.edges.push({
|
||||
id: 'e-bad',
|
||||
from: { node: 'zzz', port: 'exec' },
|
||||
to: { node: behavior.graph.nodes[0]!.id, port: 'exec' },
|
||||
});
|
||||
const cleaned = findAsset(deserializeProject(raw), behavior.id) as BehaviorAsset;
|
||||
expect(cleaned.graph.nodes.length).toBe(behavior.graph.nodes.length);
|
||||
expect(cleaned.graph.edges.length).toBe(behavior.graph.edges.length);
|
||||
|
||||
Reference in new issue
Block a user