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)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2a390e52ff
commit
57a97ba94f
22 files changed
+1153
-57
No files matched your search
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user