Files
feuerwerk/tools/e2e/38-plugins.mjs
T
2026-09-17 20:15:51 +02:00

230 lines
9.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { openTab, pickAssetType } from './lib/scaffold.mjs';
/**
* Plugins (#113, #114). The demo plugin in `plugins/` is copied to `data/plugins/` by the build and
* the seeder, switched on; the editor loads it at start-up through the API. 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.
*/
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(
'no plugin failed',
records.every((r) => r.status === 'active'),
JSON.stringify(records.filter((r) => r.status !== 'active')),
);
// 38b. Its tab sits after every built-in one – in the row or behind », depending on the width.
const row = await json('__gm.tabRow()');
const order = [...row.shown.map((p) => p.tab), ...row.hidden];
check('the plugin tab is in the tab row', order.includes('plugin:assets'), JSON.stringify(order));
check(
'and it comes after the built-in tabs',
row.hidden.length > 0 ? row.hidden.at(-1) === 'plugin:assets' : row.shown.at(-1)?.tab === 'plugin:assets',
JSON.stringify(row),
);
// 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): the switch is the server's, 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 toggle = async () => {
const at = await json('__gm.plugins.switchAt("demo-plugin.js")');
await t.click(at.x, at.y);
await settle();
// Switching goes to the server and back before it is applied.
for (let i = 0; i < 50; i++) {
const now = await pane();
if (rowOf(now)?.status !== rowOf(rows)?.status) return now;
await sleep(100);
}
return pane();
};
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 server's, not this browser's: the list says so for everyone. (That a
// switched-off module is not handed out either is the API's own check, not one for here – the
// refusal never reaches the page, the site's firewall answers it with a page of its own.)
const listed = await evaluate(
`fetch('/game-maker/api/index.php?r=plugins').then(async (r) => JSON.stringify(await r.json()))`,
);
check(
'the server stores the switch: its list says the plugin is off',
JSON.parse(listed).find((e) => e.file === 'demo-plugin.js')?.active === false,
listed,
);
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');
}