Files
feuerwerk/tests/editor/struct-block.test.ts
T
root 9f482573e5 Show a placed object's struct the way its asset does
The instance inspector drew its own, flatter version of a struct: one row per
field, no bundle head. It now draws the same block as the asset side, because
there is only one implementation left - both call buildStructBlock. What the
instance does not get is everything that touches the shape: renaming a field,
'+ Field', and the per-field '×'. The shape belongs to the asset; a placed
object overrides values. The revert and override marks sit on the head rather
than on a field, since an override covers the whole bundle at once.

Underneath this, editing a single field of an unoverridden struct used to blank
its siblings: structFieldsOf filled the untouched fields from spec.read, which
is object.overrides[key] and therefore undefined until something is overridden.
structOf then handed back the empty shape. PreviewedValue carries 'current' now
- the resolved value the row actually shows - and the fields come from that.
Without it the old path stays, which is right for the asset side, whose prop
bag is dense.
2026-09-10 23:05:02 +02:00

86 lines
3.4 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 struct is drawn as a block – a head naming the bundle, a row per field – in both places that
* show one. The asset that declares it may rename and add fields; 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 below go by which widget carries the name.
*/
import { describe, expect, it } from 'vitest';
import { createEmptyProject } from '../../src/core/model/Project';
import type { StructDef } from '../../src/core/properties/PropertySchema';
import { Label, TextField, VStack, type Widget } from '../../src/ui';
import type { InspectorHost } from '../../src/editor/inspector/host';
import { buildStructBlock } from '../../src/editor/inspector/structBlock';
import { EditorState } from '../../src/editor/state';
const DEF: 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' };
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 = () => ({ onInput: () => {}, onChange: () => {} });
const rows = (host: InspectorHost): Widget[][] =>
host.body.children.map((row) => ('children' in row ? (row.children as Widget[]) : []));
/** Every widget of the block 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 noop = { renameStruct: () => {}, removeStruct: () => {}, renameField: () => {}, removeField: () => {} };
describe('a struct block on a placed object', () => {
it('is a head row plus one row per field', () => {
const host = makeHost();
buildStructBlock(host, DEF, VALUE, noHandlers);
expect(host.body.children.length).toBe(1 + DEF.fields.length);
});
it('names the bundle and its fields with labels, so they cannot be edited', () => {
const host = makeHost();
buildStructBlock(host, DEF, VALUE, noHandlers);
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();
buildStructBlock(host, DEF, VALUE, noHandlers);
expect(carrying(host, 'tee')[0]).toBeInstanceOf(TextField);
});
});
describe('the same block on the asset that declares it', () => {
it('names the bundle and its fields with text fields, so they can be renamed', () => {
const host = makeHost();
buildStructBlock(host, DEF, VALUE, noHandlers, { edit: { ...noop, addField: () => {} } });
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();
buildStructBlock(host, DEF, VALUE, noHandlers, { edit: { ...noop, addField: () => {} } });
expect(host.body.children.length).toBe(1 + DEF.fields.length);
});
});