Files
feuerwerk/tools/e2e/38-plugins.mjs
T
adminandClaude Opus 5.5 204c4a41e8
game-maker / gate (push) Successful in 4m41s
game-maker / suite (push) Failing after 15m21s
Expect the Master routing tab and menu entry in e2e 38 and 46 (#351)
CI task 590 for 0e73fd8 (#351) was red in two checks that did not
know the new Master routing yet. The product is right, the checks
were behind.

- e2e 38: the routing tab, opened by e2e 34 in the same run, is a
  document and may sit behind the plugin tab. The check now accepts
  every kind of document there, not only lists and assets.
- e2e 46: the menu lists "Master routing" after "Project settings…".

Tests:
- e2e 38 + 46 (melektaus-atx): ALL PASSED, 48 checks.
- Counter-probe: CI task 590, the old expectations were red in
  exactly these two checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:52:22 +02:00

275 lines
11 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, #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. Master routing (#351) is a document too.
const { order } = await json('__gm.tabRow()');
const at = order.indexOf('plugin:assets');
const isDocument = (tab) => ['list', 'routing', 'management'].includes(tab) || tab.startsWith('asset:');
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, 2)) === JSON.stringify(['scene', 'game']) &&
order.slice(at + 1).every(isDocument),
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 (#114, #119): the switch belongs to the project, and it works while the
// editor runs. Since #179 they are no fixed tab but reached through the menu, in a tab that
// closes like a document.
await t.clickToolbar('menu');
const entry = await t.locate('Plugins', true);
await t.click(entry.x, entry.y);
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(
'Menu → Plugins opens the list, with the demo plugin active, 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 file = 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')) === '[]' &&
file !== undefined &&
file.active === undefined,
JSON.stringify({ plugins: await json('__gm.state.project.plugins'), file }),
);
// 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 evaluate("__gm.closeDocument('management')");
await settle();
check(
'the tab closes like a document and leaves the row',
!(await json('__gm.tabRow()')).order.includes('management') &&
(await json('__gm.tab()')) !== 'management',
JSON.stringify({ tab: await json('__gm.tab()'), order: (await json('__gm.tabRow()')).order }),
);
// The switch is a change to the project; a step that navigates next would otherwise meet the
// autosave's "leave the page?" and wait on it.
await t.saved();
}