Files
feuerwerk/tests/core/struct-shape-ports.test.ts
T
rootandClaude Fable 5.1 8a7185948d Make texture and colour full kinds and drop the load-time migrations
- `Texture` and `Color` can be picked as a variable kind, a struct field
  kind and a declared port kind; a new `Texture` node hands a picture id
  into the graph. Declared, a texture is its bare id – the `$texture` tag
  stays a list-only thing.
- The `String` node is labelled "Text" like its kind; the type id stays.
- Whole-number inputs (`Item at`, `Set item`, `Sub list`, `Fill`,
  `Repeat`, `Substring`, `To text`) show no decimals (`int()` in shared.ts).
- All server projects are reseeded, so `legacyNodes.ts`,
  `migrateDeclaredLists` and the `ownerId` fallback go, with their tests.
- Note `reseed-because-of-list` moved to done/; new note `refactor-types`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 14:06:14 +02:00

143 lines
5.8 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.
/**
* Seeding the fields of a Pack/Expand from a variable that already has the shape: a struct declares
* it, a list shows it in the struct items it holds. The node keeps declaring its own ports – this
* only copies a shape that exists elsewhere in the behavior.
*/
import { describe, expect, it } from 'vitest';
import { AddPortCommand, AddPortsCommand } from '../../src/core/commands/portCommands';
import { AddStructFieldCommand, AddVariableCommand } from '../../src/core/commands/variableCommands';
import { declaredPortType, missingFieldPorts, resolveDef, structShapeOf } from '../../src/core/graph';
import type { BehaviorAsset } from '../../src/core/model/Asset';
import { behaviorLookup } from '../../src/core/model/behaviorPrograms';
import type { StructFieldDef } from '../../src/core/properties/PropertySchema';
import { graphEditor, makeBehavior, makeProject } from '../helpers';
const PIECE: readonly StructFieldDef[] = [
{ key: 'shape', kind: 'number' },
{ key: 'name', kind: 'text' },
{ key: 'held', kind: 'boolean' },
{ key: 'tint', kind: 'color' },
{ key: 'cells', kind: 'list' },
];
function behaviorWithPiece() {
const fixture = makeProject();
const behavior = makeBehavior(fixture, 'B');
fixture.stack.execute(new AddVariableCommand(behavior.id, 'piece', 'struct'));
for (const field of PIECE) fixture.stack.execute(new AddStructFieldCommand(behavior.id, 'piece', field));
return { ...fixture, behavior, edit: graphEditor(fixture, behavior) };
}
const varOf = (behavior: BehaviorAsset, key: string) => behavior.variables.find((v) => v.key === key)!;
describe('the shape a variable declares', () => {
it('is the fields of a struct variable and the shape of the struct items a list holds', () => {
const fixture = behaviorWithPiece();
const { behavior, stack } = fixture;
stack.execute(new AddVariableCommand(behavior.id, 'shapes', 'list'));
// No declaration to read: the shape is the one the items in there actually have.
varOf(behavior, 'shapes').default = [{ x: 0, y: 0 }];
expect(structShapeOf(varOf(behavior, 'piece'))?.map((f) => f.key)).toEqual([
'shape',
'name',
'held',
'tint',
'cells',
]);
expect(structShapeOf(varOf(behavior, 'shapes'))?.map((f) => f.key)).toEqual(['x', 'y']);
});
it('is absent for a number, and for a list with no struct item in it', () => {
const fixture = behaviorWithPiece();
const { behavior, stack } = fixture;
stack.execute(new AddVariableCommand(behavior.id, 'score', 'number'));
stack.execute(new AddVariableCommand(behavior.id, 'names', 'list'));
varOf(behavior, 'names').default = ['a', 'b'];
expect(structShapeOf(varOf(behavior, 'score'))).toBeUndefined();
expect(structShapeOf(varOf(behavior, 'names'))).toBeUndefined();
});
/** A field's kind is its port kind; a color port then travels as a number, since the graph has none. */
it('gives each field a port of its own kind, a color travelling as a number', () => {
const ports = missingFieldPorts({ id: 'n', type: 'PackStruct', x: 0, y: 0, params: {} }, PIECE);
expect(ports.map((p) => `${p.key}:${p.kind}`)).toEqual([
'shape:number',
'name:text',
'held:boolean',
'tint:color',
'cells:list',
]);
expect(ports.map((p) => declaredPortType(p.kind))).toEqual([
'number',
'string',
'boolean',
'number',
'list',
]);
});
});
describe('seeding a node from that shape', () => {
it('leaves a field the node already declares alone', () => {
const { behavior, stack, edit } = behaviorWithPiece();
const pack = edit.addNode('PackStruct');
stack.execute(new AddPortCommand(behavior.id, pack.id, { key: 'shape', kind: 'text' }));
const ports = missingFieldPorts(pack, structShapeOf(varOf(behavior, 'piece')) ?? []);
expect(ports.map((p) => p.key)).toEqual(['name', 'held', 'tint', 'cells']);
});
it('adds every field as one undo step, and the ports become inputs of the Pack', () => {
const fixture = behaviorWithPiece();
const { behavior, stack, edit } = fixture;
const pack = edit.addNode('PackStruct');
const ports = missingFieldPorts(pack, structShapeOf(varOf(behavior, 'piece')) ?? []);
stack.execute(new AddPortsCommand(behavior.id, pack.id, ports));
const inputs = resolveDef(pack, behavior.graph, behaviorLookup(fixture.project))!.inputs;
expect(inputs.map((p) => `${p.key}:${p.type}`)).toEqual([
'shape:number',
'name:string',
'held:boolean',
'tint:number',
'cells:list',
]);
stack.undo();
expect(pack.ports ?? []).toEqual([]);
});
/** All-or-nothing: a clash halfway down must not leave the node with the fields before it. */
it('adds nothing at all when one of the ports clashes', () => {
const { behavior, stack, edit } = behaviorWithPiece();
const pack = edit.addNode('PackStruct');
stack.execute(new AddPortCommand(behavior.id, pack.id, { key: 'held', kind: 'boolean' }));
expect(() =>
stack.execute(
new AddPortsCommand(
behavior.id,
pack.id,
PIECE.map((f) => ({ key: f.key, kind: 'number' })),
),
),
).toThrow('"held" already exists');
expect((pack.ports ?? []).map((p) => p.key)).toEqual(['held']);
});
/** A field may be called `exec`, a port may not – the one name where a shape does not fit a node. */
it('rejects a shape whose field is called exec, and says why', () => {
const { behavior, stack, edit } = behaviorWithPiece();
stack.execute(new AddStructFieldCommand(behavior.id, 'piece', { key: 'exec', kind: 'number' }));
const pack = edit.addNode('PackStruct');
const ports = missingFieldPorts(pack, structShapeOf(varOf(behavior, 'piece')) ?? []);
expect(() => stack.execute(new AddPortsCommand(behavior.id, pack.id, ports))).toThrow(
'taken by the flow port',
);
expect(pack.ports ?? []).toEqual([]);
});
});