Files
feuerwerk/tools/e2e/24-property-nodes.mjs
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

254 lines
9.8 KiB
JavaScript
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 { edge, node, propertiesOf, variable, written } from '../lib/builders.mjs';
import { MAKE_OBJECT } from './lib/scaffold.mjs';
/**
* Properties: properties of the object handed in, each read at any time and written where it is
* switched to Set. Its type decides what `+ Property` offers. The step brings its own box and
* behavior and removes them again.
*/
/** `On update → Properties{width 40, color red} on the owner`, whose width is then read into `seen`. */
const logicGraph = {
nodes: [
node('pp-update', 'OnUpdate', 0, 0),
node('pp-owner', 'Owner', 0, 240),
node('pp-set', 'Properties', 260, 0, { ...propertiesOf('sprite'), width: 40, color: 0xff0000 }, [
written('width', 'number'),
written('color', 'color'),
]),
node('pp-store', 'Variables', 560, 0, {}, [written('seen', 'number')]),
node('pp-any', 'Properties', 260, 420),
node('pp-beh', 'Properties', 560, 420, propertiesOf('behavior')),
],
edges: [
edge('pp-e1', 'pp-update', 'exec', 'pp-set', 'exec'),
edge('pp-e2', 'pp-owner', 'object', 'pp-set', 'object'),
edge('pp-e3', 'pp-set', 'exec', 'pp-store', 'exec'),
edge('pp-e4', 'pp-set', 'width', 'pp-store', 'seen'),
],
};
export async function propertyNodes(t) {
const { check, sleep, evaluate, json, shot, click, doubleClick, locate, find, undo, entity, asset } = t;
const { mutate, stop, typeText, press } = t;
const portsOf = async (nodeId) =>
((await asset('pp-logic')).graph.nodes.find((n) => n.id === nodeId).ports ?? []).map(
(p) => `${p.key}:${p.kind}${p.write ? ':set' : ''}`,
);
const title = (nodeId) => json(`__gm.graph.nodeLabel(${JSON.stringify(nodeId)})`);
const settings = async (nodeId) => (await json(`__gm.graph.nodeSettings(${JSON.stringify(nodeId)})`)) ?? [];
const select = async (nodeId) => {
await evaluate(
`__gm.state.select({ kind: 'node', assetId: 'pp-logic', nodeId: ${JSON.stringify(nodeId)}, nodeIds: [${JSON.stringify(nodeId)}] })`,
);
await sleep(300);
};
/** The inspector is drawn over the graph, so the last match is the row rather than the port. */
const clickLast = async (text) => {
const p = await locate(text, true);
await click(p.x, p.y);
await sleep(200);
};
const offered = async () => {
await clickLast('+ Property');
const texts = await json('__gm.overlayTexts()');
await press('Escape');
await sleep(200);
return texts;
};
const addProperty = async (search) => {
await clickLast('+ Property');
await typeText(search);
await sleep(150);
await press('Enter');
await sleep(300);
};
const choose = async (setting, option) => {
const field = await json(`__gm.inspectorField(${JSON.stringify(setting)}, true)`);
await click(field.x, field.y);
await sleep(200);
await clickLast(option);
await sleep(200);
};
/** The name field, not the node's port or the unconnected-input row below it that say the same. */
const rename = async (from, to) => {
const canvas = await json('__gm.graph.canvasRect()');
const hits = await json(`__gm.locateAll(${JSON.stringify(from)})`);
const field = hits.find((p) => p.x > canvas.x + canvas.w);
await doubleClick(field.x, field.y);
await typeText(to);
await press('Enter');
await sleep(300);
};
// 24a. A box wearing a behavior that reshapes it through one node.
await mutate(`
const p = __gm.state.project;
const scene = __gm.state.activeScene;
const a = structuredClone(p.assets.find((x) => x.id === 'mover'));
Object.assign(a, {
id: 'pp-logic',
name: 'Reshape',
props: { enabled: true, runInEditor: false, seen: 0 },
variables: ${JSON.stringify([variable('seen', 'number', 0)])},
graph: ${JSON.stringify(logicGraph)},
});
p.assets.unshift(a);
${MAKE_OBJECT}
scene.objects.push(make('pp-box', 'crate', '', 900, 'Reshaped box'), make('pp-brain', 'pp-logic', 'pp-box', 900, 'Reshape'));
`);
await sleep(400);
// 24b. The run: one node writes both properties, and its own output reads the width back after.
await t.play();
await sleep(400);
const box = (await entity('pp-box'))?.props;
const seen = (await entity('pp-brain'))?.props.seen;
await stop();
await sleep(300);
check(
'one node writes several properties and reads them back',
box?.width === 40 && box?.color === 0xff0000 && seen === 40,
JSON.stringify({ width: box?.width, color: box?.color, seen }),
);
// 24c. Titled Properties; the type stands on the node as a setting row, chosen above the list it decides.
await select('pp-set');
const typeRow = await json(`__gm.inspectorField('Type', true)`);
const header = await locate('PROPERTIES', true);
check(
'the node is titled Properties and shows its type, chosen above its properties',
(await title('pp-set')) === 'Properties' &&
(await settings('pp-set')).join() === 'Type: Sprite' &&
typeRow &&
typeRow.y < header.y,
JSON.stringify({
title: await title('pp-set'),
rows: await settings('pp-set'),
type: typeRow?.y,
header: header.y,
}),
);
check(
'the node shows a typed-in color as a color',
(await find('color #ff0000')) !== null,
JSON.stringify(await json(`__gm.graph.nodeInputs('pp-set')`)),
);
await shot('01-sprite-properties');
// 24d. A Sprite offers the fields of a sprite, and nothing of the other types.
const sprite = await offered();
check(
"a Sprite offers a sprite's properties",
['enabled', 'width', 'height', 'color', 'texture'].every((k) => sprite.includes(k)) &&
!sprite.includes('radius') &&
!sprite.includes('zoom'),
sprite.join(' | '),
);
// 24e. Picked, then switched to Set: its typed-in value starts at the property's own default –
// a height of 100, not a bare 0 that would flatten the box.
await addProperty('height');
const grown = await portsOf('pp-set');
check(
'picking a property adds it as an output',
grown.join(',') === 'width:number:set,color:color:set,height:number',
grown.join(','),
);
const toggle = await json(`__gm.inspectorField('Set', true)`);
await click(toggle.x, toggle.y);
await sleep(300);
const inputs = await json(`__gm.graph.nodeInputs('pp-set')`);
check(
"switched to Set, its typed-in value starts at the property's own default",
inputs.includes('height 100'),
JSON.stringify(inputs),
);
await shot('02-height-set');
await undo();
await undo();
await sleep(300);
check('undo takes both back', (await portsOf('pp-set')).length === 2, (await portsOf('pp-set')).join());
// 24f. Another type offers its own fields; a port it does not have is marked, not dropped.
await choose('Type', 'Camera');
const camera = await offered();
check(
'switching to Camera shows it on the node and offers the fields of a camera',
(await settings('pp-set')).join() === 'Type: Camera' &&
camera.includes('zoom') &&
!camera.includes('texture'),
JSON.stringify({ rows: await settings('pp-set'), offered: camera }),
);
check(
'a port the new type does not have stays, marked',
(await portsOf('pp-set')).length === 2 && (await find('"color" is not a property of a Camera.')) !== null,
(await portsOf('pp-set')).join(),
);
await shot('03-camera');
await undo();
await sleep(300);
check('undo switches back', (await settings('pp-set')).join() === 'Type: Sprite', await settings('pp-set'));
// 24g. Any names its ports by hand, and the kind is picked when adding one. A name that happens
// to be a property's stays of the kind picked.
await select('pp-any');
const kinds = await offered();
check(
'Any offers the kinds to pick from, not property names',
['Number', 'Text', 'Boolean', 'Color', 'Texture', 'List', 'Struct'].every((k) => kinds.includes(k)) &&
!kinds.includes('width'),
kinds.join(' | '),
);
await addProperty('text');
check('a port is added of the kind picked', (await portsOf('pp-any'))[0] === 'property1:text');
await rename('property1', 'width');
check(
'and renamed by typing, keeping its kind',
(await portsOf('pp-any'))[0] === 'width:text',
(await portsOf('pp-any')).join(),
);
await evaluate("__gm.state.notice = ''");
await rename('width', 'object');
const notice = await json('__gm.state.notice');
check(
'the object input keeps its key: a port cannot take it',
(await portsOf('pp-any'))[0] === 'width:text' && notice.includes('"object" is taken'),
JSON.stringify({ ports: await portsOf('pp-any'), notice }),
);
await shot('04-any');
// 24h. A Behavior offers the variables of the behavior picked.
await select('pp-beh');
const before = await offered();
check(
'a Behavior offers nothing until one is picked',
before.length === 0 && (await find('Pick a behavior first.')) !== null,
JSON.stringify(before),
);
await choose('Behavior', 'Reshape (behavior)');
const vars = await offered();
check(
'picking the behavior offers its variables and shows it on the node',
vars.includes('seen') &&
vars.includes('enabled') &&
(await settings('pp-beh')).join() === 'Type: Behavior,Behavior: Reshape',
JSON.stringify({ rows: await settings('pp-beh'), offered: vars }),
);
await addProperty('seen');
check('a variable is picked as a port', (await portsOf('pp-beh')).join() === 'seen:number');
await shot('05-behavior');
await mutate(`
const p = __gm.state.project;
p.assets = p.assets.filter((a) => a.id !== 'pp-logic');
const scene = __gm.state.activeScene;
scene.objects = scene.objects.filter((o) => o.id !== 'pp-box' && o.id !== 'pp-brain');
__gm.state.select(null);
__gm.state.preview();
`);
await sleep(300);
const left = await json('__gm.state.project.assets.filter((a) => a.id === "pp-logic").length');
check('the step leaves the fixture as it found it', left === 0, String(left));
}