/** * The List tab: a list property is a button saying how many items it has, clicking it opens the * tab, and there a row per item edits, adds and removes them with the inspector's own widgets. */ export async function listTab(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)`); // A throwaway behavior carrying one list variable; removed again at the end of the step. Its // items are two digits so that no value can be confused with a row's index label. await mutate(` const p = __gm.state.project; p.assets.push({ id: 'listy', type: 'behavior', name: 'Listy', props: { enabled: true, runInEditor: false, nums: [30, 11, 44] }, variables: [{ key: 'nums', label: 'nums', kind: 'list', default: [], items: 'number' }], graph: { nodes: [], edges: [] }, }); __gm.state.select({ kind: 'asset', assetId: 'listy' }); `); await sleep(150); // 1. The inspector shows the count, not a comma-separated line. const button = await find('3 items'); check('a list property reads as its item count', button !== null, JSON.stringify(button)); check('the editor is still on the scene tab', (await json('__gm.tab()')) === 'scene'); await shot('01-count-button'); // 2. Clicking it opens the List tab on that very list. await click(button.x, button.y); await sleep(200); check('clicking it switches to the List tab', (await json('__gm.tab()')) === 'list'); let info = await json('__gm.list.info()'); check('which shows the list it was opened from', info.title === 'Listy ยท nums', JSON.stringify(info)); check('with a row per item', JSON.stringify(info.items) === '["30","11","44"]', JSON.stringify(info.items)); check('and the inspector is still beside it', (await find('VARIABLES')) !== null); await shot('02-list-tab'); // 3. A row edits its item, and it is one undo step. const second = await json('__gm.list.fieldAt(1)'); await click(second.x, second.y); await typeText('42'); await press('Enter'); await sleep(200); info = await json('__gm.list.info()'); check('typing in a row writes that item', JSON.stringify(info.items) === '["30","42","44"]', info.items[1]); await undo(); info = await json('__gm.list.info()'); check('undo takes just that item back', JSON.stringify(info.items) === '["30","11","44"]', info.items[1]); // 4. A short list opened after a long one. Its rows leave the content height pinned to the // viewport, so a layout skipped on "same size" would stack them all at the top. await mutate(` const a = __gm.state.project.assets.find((x) => x.id === 'listy'); a.props.nums = Array.from({ length: 60 }, (_, i) => i); __gm.state.preview(); `); await sleep(250); const longRows = [await json('__gm.list.fieldAt(0)'), await json('__gm.list.fieldAt(1)')]; check('a long list stacks its rows', longRows[0].y !== longRows[1].y, JSON.stringify(longRows)); await mutate(` const a = __gm.state.project.assets.find((x) => x.id === 'listy'); a.props.nums = [30, 11, 44]; __gm.state.preview(); `); await sleep(250); const shortRows = [await json('__gm.list.fieldAt(0)'), await json('__gm.list.fieldAt(1)')]; check( 'and a short list after it still stacks its own', shortRows[0].y !== shortRows[1].y, JSON.stringify(shortRows), ); // 5. Adding and removing whole items. await clickText('+ Item'); await sleep(200); info = await json('__gm.list.info()'); check('+ Item appends one', info.items.length === 4, JSON.stringify(info.items)); await undo(); check('undo removes it again', (await json('__gm.list.info()')).items.length === 3); await shot('03-rows'); // 6. Back on the scene tab the button carries the current count. await mutate('__gm.state.openList(null)'); await sleep(200); check('closing the list leaves the tab', (await json('__gm.tab()')) === 'scene'); await mutate(` __gm.state.project.assets.find((a) => a.id === 'listy').props.nums = [1, 2]; __gm.state.preview(); `); await sleep(200); check('and the count follows the value', (await find('2 items')) !== null); // 7. A list whose target is gone empties the tab rather than showing a stale one. await mutate("__gm.state.openList({ kind: 'asset', assetId: 'listy', key: 'nums' })"); await sleep(200); check('a list can be opened from outside the inspector too', (await json('__gm.tab()')) === 'list'); await mutate(` const p = __gm.state.project; p.assets = p.assets.filter((a) => a.id !== 'listy'); __gm.state.select(null); __gm.state.preview(); `); await sleep(200); info = await json('__gm.list.info()'); check('a deleted target empties the tab', info.title === '' && info.items.length === 0, info.title); await shot('04-empty'); await mutate('__gm.state.openList(null)'); await sleep(200); const left = await json('__gm.state.project.assets.filter((a) => a.id === "listy").length'); check('the step leaves the fixture as it found it', left === 0, String(left)); }