Files
feuerwerk/tests/editor/variable-rows.test.ts
T
rootandClaude Opus 5 631cf54c72 Give every behavior variable one row shape on asset and instance
buildStructBlock only covered structs, so a placed instance drew numbers,
text, bools and lists through createFieldRow on a 64 px name column while
its struct block sat on the asset's grid below it. variableRows.ts now
builds the row for every variable kind on both sides; the edit option
decides what the owner may change (asset: rename, x, item kind; instance:
revert and push). Label and Button take a fixed width so the mark column
no longer shifts names when a list shows its item-kind picker.

The two structFields calls that became dead with 9f48257 are gone. The
e2e steps locate the Enabled toggle through __gm.inspectorField instead
of clicking a fixed offset from its label.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 07:27:29 +02:00

147 lines
5.7 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.
/**
* A variable is drawn by one builder on the asset that declares it and on a placed object, and
* every row of the inspector shares one column grid. The asset may rename and reshape; a placed
* object may not, so there the names are labels. `NumberField extends TextField`, so counting
* editable widgets by type would count the value fields too: the tests go by the name's widget.
*/
import { describe, expect, it } from 'vitest';
import { createEmptyProject } from '../../src/core/model/Project';
import type { PropertyDef, StructDef } from '../../src/core/properties/PropertySchema';
import { HStack, Label, TextField, Theme, VStack, type Widget } from '../../src/ui';
import { type FieldHandlers, labelledRow } from '../../src/editor/inspector/fieldFactory';
import type { InspectorHost } from '../../src/editor/inspector/host';
import { buildVariable, type VariableEditing } from '../../src/editor/inspector/variableRows';
import { EditorState } from '../../src/editor/state';
const STRUCT: StructDef = {
key: 'piece',
label: 'piece',
kind: 'struct',
default: { x: 0, tag: '' },
fields: [
{ key: 'x', kind: 'number' },
{ key: 'tag', kind: 'text' },
],
};
const VALUE = { x: 30, tag: 'tee' };
const PLAIN: PropertyDef[] = [
{ key: 'speed', label: 'speed', kind: 'number', default: 0 },
{ key: 'title', label: 'title', kind: 'text', default: '' },
{ key: 'alive', label: 'alive', kind: 'boolean', default: true },
{ key: 'cells', label: 'cells', kind: 'list', default: [], items: 'text' },
{ key: 'a_rather_long_variable_name', label: 'a_rather_long_variable_name', kind: 'number', default: 0 },
];
function makeHost(): InspectorHost {
const state = new EditorState();
state.loadProject(createEmptyProject('p', 'P'), 1);
return { state, body: new VStack(), fields: new Map(), nameField: null, project: () => state.project };
}
const noHandlers = (): FieldHandlers => ({ onInput: () => {}, onChange: () => {} });
const withFields: FieldHandlers = { ...noHandlers(), structFieldHandlers: noHandlers };
const EDIT: VariableEditing = {
rename: () => {},
remove: () => {},
setItems: () => {},
renameField: () => {},
removeField: () => {},
addField: () => {},
};
const rows = (host: InspectorHost): Widget[][] =>
host.body.children.map((row) => ('children' in row ? (row.children as Widget[]) : []));
/** Every widget of the body that carries `text` as its own content, name or value. */
const carrying = (host: InspectorHost, text: string): Widget[] =>
rows(host)
.flat()
.filter((w) => (w instanceof Label && w.text === text) || (w instanceof TextField && w.value === text));
const isName = (w: Widget, name: string) =>
(w instanceof Label && w.text === name) || (w instanceof TextField && w.value === name);
/** Where the widget after the name starts – summed from the measured cells before it. */
function valueX(host: InspectorHost, name: string): number {
for (const row of host.body.children) {
if (!(row instanceof HStack)) continue;
const items = row.items;
const at = items.findIndex((w) => isName(w, name));
if (at < 0) continue;
return items.slice(0, at + 1).reduce((x, w) => x + w.measure().w + row.gap, row.padding);
}
throw new Error(`no row named ${name}`);
}
function buildAll(edit?: VariableEditing): InspectorHost {
const host = makeHost();
for (const def of PLAIN) buildVariable(host, def, def.default, noHandlers(), { edit });
buildVariable(host, STRUCT, VALUE, withFields, { edit });
return host;
}
const NAMES = [...PLAIN.map((d) => d.key), 'x', 'tag'];
describe('the column grid', () => {
it('starts every value at the same x on a placed object, whatever the kind or depth', () => {
const host = buildAll();
const xs = NAMES.map((name) => valueX(host, name));
expect(new Set(xs), JSON.stringify(xs)).toEqual(new Set([xs[0]]));
});
it('and at that same x on the asset, where names are fields and the list mark is a picker', () => {
const placed = valueX(buildAll(), 'speed');
const host = buildAll(EDIT);
const xs = NAMES.map((name) => valueX(host, name));
expect(xs, JSON.stringify(xs)).toEqual(NAMES.map(() => placed));
});
it('which is where a plain labelled row starts its value too', () => {
const { label } = labelledRow('Enabled');
expect(label.minWidth + Theme.gap).toBe(valueX(buildAll(), 'speed'));
});
});
describe('a struct on a placed object', () => {
it('is a head row plus one row per field', () => {
const host = makeHost();
buildVariable(host, STRUCT, VALUE, withFields);
expect(host.body.children.length).toBe(1 + STRUCT.fields.length);
});
it('names the bundle and its fields with labels, so they cannot be edited', () => {
const host = makeHost();
buildVariable(host, STRUCT, VALUE, withFields);
for (const name of ['piece', 'x', 'tag']) {
const [widget] = carrying(host, name);
expect(widget, name).toBeInstanceOf(Label);
}
});
it('still shows the values in editable fields', () => {
const host = makeHost();
buildVariable(host, STRUCT, VALUE, withFields);
expect(carrying(host, 'tee')[0]).toBeInstanceOf(TextField);
});
});
describe('the same struct on the asset that declares it', () => {
it('names the bundle and its fields with text fields, so they can be renamed', () => {
const host = makeHost();
buildVariable(host, STRUCT, VALUE, withFields, { edit: EDIT });
for (const name of ['piece', 'x', 'tag']) {
const [widget] = carrying(host, name);
expect(widget, name).toBeInstanceOf(TextField);
}
});
it('keeps the same row count as the placed one', () => {
const host = makeHost();
buildVariable(host, STRUCT, VALUE, withFields, { edit: EDIT });
expect(host.body.children.length).toBe(1 + STRUCT.fields.length);
});
});