Plugins in data/plugins are loaded at startup and can add a tab with a read-only node board, and the demo plugin shows every asset as a node (#113)
game-maker / gate (push) Successful in 1m38s
game-maker / suite (push) Successful in 5m43s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-17 19:00:03 +02:00
1 parent 2a390e52ff
commit 57a97ba94f
22 files changed
+1153 -57

No files matched your search

+157
View File
@@ -0,0 +1,157 @@
import { openTab, pickAssetType } from './lib/scaffold.mjs';
/**
* Plugins (#113). The demo plugin in `plugins/` is copied to `data/plugins/` by the build and the
* seeder; 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, and 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.
*/
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);
}