/** * Labels must be shortened to fit their control, not written past it. Runs last because it renames * assets to absurd lengths to provoke the case. */ /** A name far wider than a tab may grow, so a tab that does not cut it must spill. */ const LONG_NAME = 'Player movement with jumping and coyote time'; export async function overflow(t) { const { check, sleep, json, evaluate, shot, mutate, drag, clickText, showTab } = t; await mutate(` for (const asset of __gm.state.project.assets) { if (asset.type === 'behavior' || asset.type === 'texture' || asset.type === 'container') asset.name = ${JSON.stringify(LONG_NAME)}; } __gm.state.select(null)`); // 14a. A document's tab shows its name, but a long one is cut instead of taking the row (#171). // The three do not fit side by side at this width, so each is measured while it shows. const widths = []; for (const tab of ['graph', 'texture', 'container']) { await showTab(tab); const { active } = await json('__gm.documents()'); const shown = (await json('__gm.tabRow()')).shown.find((b) => b.tab === active); widths.push(shown ? Math.round(shown.right - shown.x) : null); } await sleep(600); check( 'three documents with long names each keep a tab of bounded width', (await json('__gm.documents()')).docs.length === 3 && widths.every((w) => w !== null && w < 300), JSON.stringify(widths), ); // 14a-ii. What a tab looks like (#176): the one showing is a light grey card with rounded upper // corners standing on the row's line, its name in plain weight, and a press on it keeps // that colour instead of flashing the pressed fill of a button. await showTab('scene'); const ACTIVE = '#3f444e'; const BAR = '#1f2229'; const LINE = '#383c46'; const pixel = (x, y) => json(`__gm.pixelAt(${Math.round(x)}, ${Math.round(y)})`); const styled = await json('__gm.tabRow()'); const card = styled.shown.find((b) => b.tab === 'scene'); const plain = styled.shown.find((b) => b.tab !== 'scene'); const cardFill = await pixel(card.x + 3, card.y + card.h - 3); const plainFill = await pixel(plain.x + 3, plain.y + plain.h - 3); check( 'the tab showing is a light grey card, the others stay on the bar', cardFill === ACTIVE && plainFill === BAR, JSON.stringify({ cardFill, plainFill, card: card.tab, plain: plain.tab }), ); const corner = await pixel(card.x, card.y); const top = await pixel(card.x + card.h / 2, card.y + 3); const line = await pixel(plain.x + 4, plain.y + plain.h - 1); check( 'its upper corners are rounded and it covers the line the other tabs stand on', corner === BAR && top === ACTIVE && line === LINE, JSON.stringify({ corner, top, line }), ); check( 'neither the tab showing nor the others write their name bold', styled.shown.every((b) => b.bold === false), JSON.stringify(styled.shown.map((b) => ({ tab: b.tab, bold: b.bold }))), ); const pressX = Math.round(card.x + 3); const pressY = Math.round(card.y + card.h - 3); await t.mouse('mouseMoved', pressX, pressY); await t.mouse('mousePressed', pressX, pressY); const held = await pixel(pressX, pressY); await t.mouse('mouseReleased', pressX, pressY); check( 'holding the mouse down on the tab showing does not change its colour', held === ACTIVE, JSON.stringify({ held, expected: ACTIVE }), ); // The fixed tabs carry an icon as well (#176); the Scene card shows it in front of its name. const sceneInk = await json(`(() => { const x0 = ${card.x} + 6, y = ${card.y + card.h / 2}; const grid = []; for (let dy = -7; dy <= 7; dy++) for (let dx = 0; dx < 16; dx++) grid.push({ x: x0 + dx, y: y + dy }); return __gm.pixels(grid).filter((p) => p !== '${ACTIVE}').length; })()`); const fixedIcons = styled.shown .filter((b) => ['scene', 'game'].includes(b.tab)) .map((b) => [b.tab, b.icon]); check( 'Scene and Game show an icon before their name', JSON.stringify(fixedIcons) === JSON.stringify([ ['scene', true], ['game', true], ]) && sceneInk >= 20, JSON.stringify({ fixedIcons, sceneInk }), ); // 14b. The tab row itself: a button past the centre column's edge sits under the inspector, and // a click on it lands there (nine tabs on a 90 px floor did exactly that at 1280 px wide). const columnRight = async () => { const column = await json('__gm.viewportRect()'); return column.x + column.w; }; const spillOf = (row, right) => row.shown.filter((b) => b.right > right); const wide = await json('__gm.tabRow()'); check( 'every tab button ends inside the centre column', spillOf(wide, await columnRight()).length === 0, JSON.stringify({ spilled: spillOf(wide, await columnRight()), hidden: wide.hidden }), ); // Three open documents on top of the fixed tabs no longer fit a 1280 px window. check( 'at the test window’s width the row already needs its overflow, and still shows what is edited', wide.hidden.length > 0 && wide.shown.some((b) => b.tab === 'scene'), JSON.stringify({ hidden: wide.hidden, shown: wide.shown.map((b) => b.tab) }), ); // 14b-ii. Narrowed until the row cannot hold nine tabs, the leftovers go behind the overflow // button instead of under the inspector – the guard against the tenth tab (#84). const widenPanels = async (by) => { const bars = await json('__gm.panelSplitters()'); await drag(bars.left.x, bars.left.y, bars.left.x + by, bars.left.y, { steps: 8 }); const after = await json('__gm.panelSplitters()'); await drag(after.right.x, after.right.y, after.right.x - by, after.right.y, { steps: 8 }); }; await widenPanels(250); const narrow = await json('__gm.tabRow()'); const narrowRight = await columnRight(); check( 'a row too narrow for its tabs puts the leftovers away instead of past the edge', narrow.hidden.length > 0 && spillOf(narrow, narrowRight).length === 0, JSON.stringify({ hidden: narrow.hidden, spilled: spillOf(narrow, narrowRight) }), ); check( 'the tab being edited stays in the row', narrow.shown.some((b) => b.tab === 'scene'), JSON.stringify(narrow.shown.map((b) => b.tab)), ); await shot('35-tab-overflow'); // The overflow button names them, and picking one brings it into the row. await clickText('»'); const offered = (await json('__gm.menuRows()')).filter(Boolean); const offeredIcons = (await json('__gm.menuIcons()')).filter((icon) => icon !== null); // Every hidden tab brings its icon along; only a plugin's own tab has none (#176, #119). const withIcon = (tab) => !tab.startsWith('plugin:'); check( 'the overflow button offers every tab that had to go, each with the icon its tab shows', offered.length === narrow.hidden.length && offeredIcons.filter(Boolean).length === narrow.hidden.filter(withIcon).length, JSON.stringify({ offered, offeredIcons, hidden: narrow.hidden }), ); const wanted = narrow.hidden[narrow.hidden.length - 1]; const item = await t.locate(offered[offered.length - 1], true); await t.click(item.x, item.y); const picked = await json('__gm.tabRow()'); check( 'picking a document from the overflow menu opens it and puts it in the row', (await json('__gm.documents()')).active === wanted && picked.shown.some((b) => b.tab === wanted), JSON.stringify({ wanted, docs: await json('__gm.documents()'), shown: picked.shown.map((b) => b.tab) }), ); await widenPanels(-250); await showTab('scene'); const back = await json('__gm.tabRow()'); check( 'giving the room back brings tabs into the row again, and none past the edge', back.hidden.length < narrow.hidden.length && spillOf(back, await columnRight()).length === 0, JSON.stringify({ hidden: back.hidden, narrow: narrow.hidden }), ); // 14b-iii. The divider between asset panel and scene objects moves the height from one to the // other, and dragged shut either keeps its header to grab it by again. const column = () => json('__gm.leftColumn()'); const near = (a, b) => Math.abs(a - b) <= 1; const start = await column(); await drag(start.splitter.x, start.splitter.y, start.splitter.x, start.splitter.y + 80, { steps: 6 }); const lower = await column(); check( 'dragging the left column’s divider down gives the assets 80 px the scene objects lose', near(lower.assets.h, start.assets.h + 80) && near(lower.objects.h, start.objects.h - 80), JSON.stringify({ start, lower }), ); await drag(lower.splitter.x, lower.splitter.y, lower.splitter.x, lower.splitter.y - 2000, { steps: 10 }); const shut = await column(); const header = await json('__gm.panelHeader'); check( 'dragged all the way up the asset panel stops at its header', near(shut.assets.h, header) && shut.objects.y > shut.assets.y, JSON.stringify(shut), ); await drag(shut.splitter.x, shut.splitter.y, shut.splitter.x, start.splitter.y, { steps: 10 }); const restored = await column(); check( 'and dragged back it returns to the split it started with', near(restored.assets.h, start.assets.h) && near(restored.objects.h, start.objects.h), JSON.stringify({ start, restored }), ); // 14c. Every tab, because each one owns a different set of controls. for (const tab of ['scene', 'graph', 'texture', 'game', 'container', 'particles']) { await showTab(tab); const spills = await json('__gm.textOverflows()'); check( `no label is drawn past its control in the ${tab} tab`, spills.length === 0, JSON.stringify(spills), ); } // 14d. Shortened, not simply dropped: the value must still be readable and marked as cut. await showTab('graph'); const shown = (await json('__gm.textOverflows(true)')).find((e) => e.text.startsWith('Player movement')); check( 'a name too wide for its tab is cut with an ellipsis', Boolean(shown) && shown.text.endsWith('…') && shown.text.length > 8, JSON.stringify(shown), ); await shot('36-long-labels'); // 14e. An inspector full of dropdowns – the parent row and the enum fields – with a selection // whose asset name is now overlong too. await showTab('scene'); await evaluate("__gm.state.select({ kind: 'object', objectId: 'otex' })"); await sleep(500); const inspector = await json('__gm.textOverflows()'); check('no label spills in the inspector', inspector.length === 0, JSON.stringify(inspector)); // 14f. The other direction: a control narrow enough to swallow its whole label draws nothing // at all, which the overflow walk cannot see – an empty text spills over no edge. for (const tab of ['scene', 'graph', 'texture', 'container', 'particles']) { await showTab(tab); const dropped = await json('__gm.labelsDropped()'); check( `no button is left without its label in the ${tab} tab`, dropped.length === 0, JSON.stringify(dropped), ); } // The narrowest buttons in the editor sit on a behaviour's variable rows. await evaluate("__gm.state.select({ kind: 'asset', assetId: 'mover' })"); const onVariables = await json('__gm.labelsDropped()'); check( 'the variable rows keep their buttons labelled', onVariables.length === 0, JSON.stringify(onVariables), ); // The weights the theme draws with (#214): a missing face falls back to the next one the browser // has, and 500 then draws as 400 – no check on widths notices, the text just looks lighter. const faces = await json( '[...document.fonts].filter((f) => f.family.replaceAll(\'"\', "") === "Source Sans 3" && f.status === "loaded").map((f) => f.weight).sort()', ); check( 'the editor font is loaded in every weight the theme uses, 500 included', ['400', '500', '600'].every((weight) => faces.includes(weight)), JSON.stringify(faces), ); for (const doc of (await json('__gm.documents()')).docs) await evaluate(`__gm.closeDocument(${JSON.stringify(doc)})`); }