/** * 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); }); });