/** * One builder draws a variable on its asset (renamable) and on a placed object (names are labels). * `NumberField extends TextField`, so the tests go by the name's widget rather than counting by type. */ 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: [] }, { 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: () => {}, setScope: () => {}, 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 the names are editable fields', () => { 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); }); });