/** * A variable of type struct: the inspector declares its fields and gives each one a value in the * same row, and writing one field keeps the others – the trap a shared snapshot would fall into. */ export async function structVariable(t) { const { check, sleep, json, shot, click, clickText, press, typeText, mutate, undo } = t; /** `locate` throws when the text is absent, which is the wrong shape for "is it there?". */ const find = (text) => json(`__gm.locate(${JSON.stringify(text)}, false)`); const piece = () => json("__gm.state.project.assets.find((a) => a.id === 'structy').props.piece"); // A throwaway behavior carrying one struct variable; removed again at the end of the step. await mutate(` const p = __gm.state.project; p.assets.push({ id: 'structy', type: 'behavior', name: 'Structy', props: { enabled: true, runInEditor: false, piece: { x: 30, tag: 'tee' } }, variables: [ { key: 'piece', label: 'piece', kind: 'struct', default: { x: 0, tag: '' }, fields: [ { key: 'x', kind: 'number' }, { key: 'tag', kind: 'text' }, ], }, ], graph: { nodes: [], edges: [] }, }); __gm.state.select({ kind: 'asset', assetId: 'structy' }); `); await sleep(150); // 1. The declaration is what the inspector draws: a head that can add fields, a row per field. check('a struct variable carries the add-a-field control', (await find('+ Field')) !== null); check('and a row for each declared field', (await find('x')) !== null && (await find('tag')) !== null); check('whose value is shown, not a summary of the bundle', (await find('30.000')) !== null); await shot('01-struct-rows'); // 2. Writing one field keeps the others. Two edits in a row without a rebuild in between is // exactly where handlers sharing one snapshot of the bundle would revive the first value. const xField = await json("__gm.inspectorField('x')"); await click(xField.x, xField.y); await typeText('42'); await press('Enter'); await sleep(200); check('typing in a field writes that field', (await piece()).x === 42, JSON.stringify(await piece())); const tagField = await json("__gm.inspectorField('tag')"); await click(tagField.x, tagField.y); await press('a', { ctrl: true }); await typeText('ell'); await press('Enter'); await sleep(200); check( 'and the next field edited keeps the one before it', JSON.stringify(await piece()) === '{"x":42,"tag":"ell"}', JSON.stringify(await piece()), ); // 3. One undo step per field. await undo(); check('undo takes back just the last field', (await piece()).tag === 'tee' && (await piece()).x === 42); await undo(); check('and the one before it', (await piece()).x === 30, JSON.stringify(await piece())); // 4. Adding a field reaches the declaration and the value at once. await clickText('+ Field'); await sleep(150); await clickText('Boolean'); await sleep(250); const grown = await piece(); check('+ Field grows the bundle', 'field1' in grown && grown.field1 === false, JSON.stringify(grown)); check('and the new field has a row', (await find('field1')) !== null); await shot('02-field-added'); await undo(); await sleep(200); check('undo takes it out of the value too', !('field1' in (await piece())), JSON.stringify(await piece())); // 5. Renaming a field carries its value along. const nameField = await json("__gm.locate('x')"); await click(nameField.x, nameField.y); await press('a', { ctrl: true }); await typeText('column'); await press('Enter'); await sleep(250); check( 'renaming a field keeps its value', JSON.stringify(await piece()) === '{"column":30,"tag":"tee"}', JSON.stringify(await piece()), ); await mutate(` const p = __gm.state.project; p.assets = p.assets.filter((a) => a.id !== 'structy'); __gm.state.select(null); __gm.state.preview(); `); await sleep(200); const left = await json('__gm.state.project.assets.filter((a) => a.id === "structy").length'); check('the step leaves the fixture as it found it', left === 0, String(left)); }