import { openTab, pickAssetType } from './lib/scaffold.mjs'; /** * Plugins (#113, #114, #119). The demo plugin in `plugins/` is copied to `data/plugins/` by the * build and the seeder; the fixture project switches it on, and the editor loads it before the * project itself. What only a running editor can show: that the module really loads and activates, * that its tab joins the row, that its node board draws one node per asset and follows the asset * list – created through the + menu, renamed, and taken away again by undo – and that the switch in * the Plugins tab stops and starts it while the editor runs, as an undo step of the project. */ export async function plugins(t) { const { check, sleep, json, evaluate, mutate, clickText, drag, wheel, press, shot, showTab, settle, undo } = t; await press('Escape'); await evaluate('__gm.state.select(null)'); // 38a. Loading runs after the project has booted, so on a fresh page it may still be under way. let records = []; for (let i = 0; i < 50 && records.length === 0; i++) { records = await json('__gm.plugins.records()'); if (records.length === 0) await sleep(100); } const demo = records.find((r) => r.file === 'demo-plugin.js'); check( 'the demo plugin is loaded from data/plugins and active', demo?.status === 'active' && demo.meta?.id === 'demo' && demo.meta.api === 1, JSON.stringify(records), ); check( 'and it runs because the project switches it on, not the server', JSON.stringify(await json('__gm.state.project.plugins')) === JSON.stringify(['demo-plugin.js']), JSON.stringify(await json('__gm.state.project.plugins')), ); check( 'no plugin failed', records.every((r) => r.status === 'active'), JSON.stringify(records.filter((r) => r.status !== 'active')), ); // 38b. Its tab sits after the fixed ones and ahead of the open documents (#171) – in the row or // behind », depending on the width. const { order } = await json('__gm.tabRow()'); const at = order.indexOf('plugin:assets'); check('the plugin tab is in the tab row', at >= 0, JSON.stringify(order)); check( 'and it comes after the fixed tabs, ahead of any document', JSON.stringify(order.slice(0, 3)) === JSON.stringify(['scene', 'game', 'plugins']) && order.slice(at + 1).every((tab) => tab === 'list' || tab.startsWith('asset:')), JSON.stringify(order), ); // 38c. One node per asset, titled by its name, in the order of the asset list. await openTab(t, 'plugin:assets'); await settle(); const names = () => json('__gm.state.project.assets.map((a) => a.name)'); const board = () => json("__gm.plugins.board('assets')"); const titles = (b) => b.nodes.map((n) => n.title); let shown = await board(); check( 'clicking the tab opens the node board', (await json('__gm.tab()')) === 'plugin:assets' && shown?.visible === true, JSON.stringify({ tab: await json('__gm.tab()'), visible: shown?.visible }), ); const expected = await names(); check( 'the board shows every asset as a node titled with its name', JSON.stringify(titles(shown)) === JSON.stringify(expected), JSON.stringify({ nodes: titles(shown), assets: expected }), ); // 38d. Pan, zoom and Fit. The framing to come back to is taken from Fit itself, not from the first // show: in a chained run step 14 opened this tab in a narrowed column, and the board framed // its nodes for that width then. await t.click(shown.fit.x, shown.fit.y); await settle(); shown = await board(); const fitted = { zoom: shown.zoom, pan: shown.pan }; // Any hit inside the board: the same name is also in the asset panel and the scene picker. const inside = (p) => p.x > shown.rect.x && p.x < shown.rect.x + shown.rect.w && p.y > shown.rect.y && p.y < shown.rect.y + shown.rect.h; const drawn = await json(`__gm.locateAll(${JSON.stringify(expected[0])})`); check( 'after Fit the node titles are drawn inside the board', drawn.some(inside), JSON.stringify({ drawn, rect: shown.rect }), ); await shot('38-plugin-tab'); const centre = { x: shown.rect.x + shown.rect.w / 2, y: shown.rect.y + shown.rect.h - 40 }; await drag(centre.x, centre.y, centre.x + 80, centre.y - 30); shown = await board(); check( 'dragging on the board pans it', Math.round(shown.pan.x - fitted.pan.x) === 80 && Math.round(shown.pan.y - fitted.pan.y) === -30, JSON.stringify({ fitted, now: shown.pan }), ); await wheel(centre.x, centre.y, -300); await settle(); shown = await board(); check( 'the wheel zooms it', shown.zoom > fitted.zoom, JSON.stringify({ fitted: fitted.zoom, now: shown.zoom }), ); await t.click(shown.fit.x, shown.fit.y); await settle(); shown = await board(); check( 'Fit frames the nodes as before the pan and zoom', Math.abs(shown.zoom - fitted.zoom) < 1e-6 && Math.abs(shown.pan.x - fitted.pan.x) < 1e-6 && Math.abs(shown.pan.y - fitted.pan.y) < 1e-6, JSON.stringify({ fitted, now: { zoom: shown.zoom, pan: shown.pan } }), ); // 38e. The board follows the asset list: a sprite created through the + menu, while the tab // stays open, is a node at once. // Through the menu's own entry: in a chained run an asset named "Sprite" is in the list already. await clickText('+'); await pickAssetType(t, 'Sprite'); await settle(); const withNew = await names(); shown = await board(); check( 'creating an asset adds its node', withNew.length === expected.length + 1 && JSON.stringify(titles(shown)) === JSON.stringify(withNew), JSON.stringify({ nodes: titles(shown), assets: withNew }), ); check('and the tab stays open', (await json('__gm.tab()')) === 'plugin:assets', await json('__gm.tab()')); // 38f. Renaming shows under the new name. const created = await json('(({ id, name }) => ({ id, name }))(__gm.state.project.assets.at(-1))'); const newId = created.id; await mutate( `__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(newId)}).name = 'Renamed by e2e'`, ); shown = await board(); check( 'renaming an asset renames its node', shown.nodes.find((n) => n.id === newId)?.title === 'Renamed by e2e', JSON.stringify(shown.nodes.at(-1)), ); await mutate( `__gm.state.project.assets.find((a) => a.id === ${JSON.stringify(newId)}).name = ${JSON.stringify(created.name)}`, ); // 38g. Undo takes the created asset away again, and its node with it. await undo(); await settle(); shown = await board(); check( 'deleting the asset (undo of the create) removes its node', (await names()).length === expected.length && JSON.stringify(titles(shown)) === JSON.stringify(expected) && !shown.nodes.some((n) => n.id === newId), JSON.stringify({ nodes: titles(shown), assets: await names() }), ); // 38h. Another tab hides the board; the plugin's pane is a tab like any other. await showTab('scene'); check('switching to another tab hides the board', (await board()).visible === false); // 38i. The Plugins tab (#114, #119): the switch belongs to the project, and it works while the // editor runs. await openTab(t, 'plugins'); await settle(); const pane = () => json('__gm.plugins.pane()'); let rows = await pane(); const rowOf = (shown) => shown.rows.find((r) => r.file === 'demo-plugin.js'); check( 'the Plugins tab lists the demo plugin as active, with its name and its switch on', rows.visible === true && rowOf(rows)?.status === 'active' && rowOf(rows)?.name === 'Demo plugin' && rowOf(rows)?.checked === true, JSON.stringify(rows), ); const settled = async (was) => { // Loading a module is a fetch, so the switch is applied a moment after the click. for (let i = 0; i < 50; i++) { const now = await pane(); if (rowOf(now)?.status !== rowOf(was)?.status) return now; await sleep(100); } return pane(); }; const toggle = async () => { const at = await json('__gm.plugins.switchAt("demo-plugin.js")'); await t.click(at.x, at.y); await settle(); return settled(rows); }; rows = await toggle(); const rowAfterOff = await json('__gm.tabRow()'); check( 'clicking the switch off stops the plugin', rowOf(rows)?.status === 'inactive' && rowOf(rows)?.checked === false, JSON.stringify(rows), ); check( 'and its tab is gone from the row, with nothing left of its board', ![...rowAfterOff.shown.map((p) => p.tab), ...rowAfterOff.hidden].includes('plugin:assets') && (await board()) === null, JSON.stringify(rowAfterOff), ); // The switch is the project's (#119): it stands in the project file and travels with it. The // server's own list knows nothing about it any more – it only says which files there are. const listed = await evaluate( `fetch('/game-maker/api/index.php?r=plugins').then(async (r) => JSON.stringify(await r.json()))`, ); const entry = JSON.parse(listed).find((e) => e.file === 'demo-plugin.js'); check( 'the project says it is off, while the server still lists the file itself', JSON.stringify(await json('__gm.state.project.plugins')) === '[]' && entry !== undefined && entry.active === undefined, JSON.stringify({ plugins: await json('__gm.state.project.plugins'), entry }), ); // And because it is a change to the project, undo takes it back – plugin, tab and all. await undo(); rows = await settled(rows); const afterUndo = await json('__gm.tabRow()'); check( 'undo switches it on again, as a step of the project', JSON.stringify(await json('__gm.state.project.plugins')) === JSON.stringify(['demo-plugin.js']) && rowOf(rows)?.status === 'active' && [...afterUndo.shown.map((p) => p.tab), ...afterUndo.hidden].includes('plugin:assets'), JSON.stringify({ plugins: await json('__gm.state.project.plugins'), row: rowOf(rows) }), ); rows = await toggle(); check( 'and clicking the switch off again stops it once more', rowOf(rows)?.status === 'inactive' && JSON.stringify(await json('__gm.state.project.plugins')) === '[]', JSON.stringify({ row: rowOf(rows), plugins: await json('__gm.state.project.plugins') }), ); rows = await toggle(); await settle(); const rowAfterOn = await json('__gm.tabRow()'); shown = await board(); check( 'switching it on again activates it there and then', rowOf(rows)?.status === 'active' && [...rowAfterOn.shown.map((p) => p.tab), ...rowAfterOn.hidden].includes('plugin:assets'), JSON.stringify({ row: rowOf(rows), tabs: rowAfterOn }), ); check( 'and its board is back with a node per asset', JSON.stringify(titles(shown)) === JSON.stringify(await names()), JSON.stringify({ nodes: titles(shown), assets: await names() }), ); await shot('39-plugins-tab'); await showTab('scene'); }