230 lines
9.2 KiB
JavaScript
230 lines
9.2 KiB
JavaScript
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');
|
||
}
|