Tabs are open documents: Scene, Game and Plugins stay as fixed tabs, every other asset opens in its own tab (double click or Enter in the asset panel, on creation, from the list button), several assets of one kind can be open at once, a tab is labelled with its kind dimmed before the name, cut at 160 px and renamed with its asset, closes with its x or a middle click and goes away when its asset is deleted; a tab always shows its own document instead of following the selection, the picker in the tab head is gone, zoom and scroll are kept per document for the session and dropped on close, a double click on a call node stays in the same tab with breadcrumbs per tab; one editor per kind is switched between documents, the placement rules live in the pure module documents.ts, and the test hook showTab stays as a compatibility path; new e2e module 44 documents (#171)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4f546c0e08
commit
cc285af10a
33 files changed
+895
-225
No files matched your search
+17
-11
@@ -351,21 +351,27 @@ export async function graph(t) {
|
||||
'graph editor keeps its behavior for a non-behavior asset',
|
||||
(await evaluate(`__gm.graph.current()`)) === 'mover',
|
||||
);
|
||||
// The picker is the *last* "Mover" on screen: the assets panel shows one too.
|
||||
const pickerAt = await locate('Mover', true);
|
||||
await click(pickerAt.x, pickerAt.y);
|
||||
const spawnerRow = await locate('Spawner', true);
|
||||
await click(spawnerRow.x, spawnerRow.y);
|
||||
await select({ kind: 'asset', assetId: 'spawner' });
|
||||
check(
|
||||
'the header picker switches the graph to another behavior',
|
||||
(await evaluate(`__gm.graph.current()`)) === 'spawner',
|
||||
'selecting another behavior leaves the tab on its own (#171)',
|
||||
(await evaluate(`__gm.graph.current()`)) === 'mover',
|
||||
);
|
||||
await evaluate(`__gm.state.openAsset('spawner')`);
|
||||
const twoOpen = await json('__gm.documents()');
|
||||
check(
|
||||
'opening it brings it up in a tab of its own',
|
||||
(await evaluate(`__gm.graph.current()`)) === 'spawner' &&
|
||||
twoOpen.active === 'asset:spawner' &&
|
||||
twoOpen.docs.includes('asset:mover'),
|
||||
JSON.stringify(twoOpen),
|
||||
);
|
||||
await evaluate(`__gm.closeDocument('asset:spawner')`);
|
||||
check(
|
||||
'closing it shows the behavior next to it again',
|
||||
(await evaluate(`__gm.graph.current()`)) === 'mover' && (await json('__gm.tab()')) === 'graph',
|
||||
String(await evaluate(`__gm.graph.current()`)),
|
||||
);
|
||||
await select({ kind: 'asset', assetId: 'mover' });
|
||||
check(
|
||||
'graph editor rebinds when a behavior is selected again',
|
||||
(await evaluate(`__gm.graph.current()`)) === 'mover',
|
||||
);
|
||||
|
||||
// 6j. Mouse behaviour, as in the scene view: the middle button pans from anywhere – a node
|
||||
// under it included – and the left button no longer pans at all.
|
||||
|
||||
+30
-34
@@ -1,43 +1,35 @@
|
||||
import { TAB_LABELS } from './lib/scaffold.mjs';
|
||||
|
||||
/**
|
||||
* Labels must be shortened to fit their control, not written past it. Runs last because it renames
|
||||
* assets to absurd lengths to provoke the case.
|
||||
*/
|
||||
|
||||
/** A name far wider than any 200 px picker, so a control that does not truncate must spill. */
|
||||
/** A name far wider than a tab may grow, so a tab that does not cut it must spill. */
|
||||
const LONG_NAME = 'Player movement with jumping and coyote time';
|
||||
|
||||
export async function overflow(t) {
|
||||
const { check, sleep, json, evaluate, shot, mutate, drag, clickText, showTab } = t;
|
||||
|
||||
const pickerWidths = async () => {
|
||||
const out = {};
|
||||
for (const tab of ['graph', 'texture', 'container']) {
|
||||
await showTab(tab);
|
||||
out[tab] = await json(`__gm.${tab}.pickerWidth()`);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
const before = await pickerWidths();
|
||||
|
||||
await mutate(`
|
||||
for (const asset of __gm.state.project.assets) {
|
||||
if (asset.type === 'behavior' || asset.type === 'texture' || asset.type === 'container')
|
||||
asset.name = ${JSON.stringify(LONG_NAME)};
|
||||
}
|
||||
__gm.state.select(null)`);
|
||||
await sleep(600);
|
||||
|
||||
// 14a. The pickers keep the width they were given instead of growing with the name they show.
|
||||
const after = await pickerWidths();
|
||||
// 14a. A document's tab shows its name, but a long one is cut instead of taking the row (#171).
|
||||
// The three do not fit side by side at this width, so each is measured while it shows.
|
||||
const widths = [];
|
||||
for (const tab of ['graph', 'texture', 'container']) {
|
||||
check(
|
||||
`the ${tab} picker keeps its width when the name gets long`,
|
||||
before[tab] === 200 && after[tab] === 200,
|
||||
`before ${before[tab]}, after ${after[tab]}`,
|
||||
);
|
||||
await showTab(tab);
|
||||
const { active } = await json('__gm.documents()');
|
||||
const shown = (await json('__gm.tabRow()')).shown.find((b) => b.tab === active);
|
||||
widths.push(shown ? Math.round(shown.right - shown.x) : null);
|
||||
}
|
||||
await sleep(600);
|
||||
check(
|
||||
'three documents with long names each keep a tab of bounded width',
|
||||
(await json('__gm.documents()')).docs.length === 3 && widths.every((w) => w !== null && w < 300),
|
||||
JSON.stringify(widths),
|
||||
);
|
||||
|
||||
// 14b. The tab row itself: a button past the centre column's edge sits under the inspector, and
|
||||
// a click on it lands there (nine tabs on a 90 px floor did exactly that at 1280 px wide).
|
||||
@@ -53,8 +45,7 @@ export async function overflow(t) {
|
||||
spillOf(wide, await columnRight()).length === 0,
|
||||
JSON.stringify({ spilled: spillOf(wide, await columnRight()), hidden: wide.hidden }),
|
||||
);
|
||||
// The tenth tab (Particles, #73) is the case #84 was built for: 631 px of nine tabs already left
|
||||
// 35 px in a 1280 px window, so at ten the row has to put something away even undisturbed.
|
||||
// Three open documents on top of the fixed tabs no longer fit a 1280 px window.
|
||||
check(
|
||||
'at the test window’s width the row already needs its overflow, and still shows what is edited',
|
||||
wide.hidden.length > 0 && wide.shown.some((b) => b.tab === 'scene'),
|
||||
@@ -84,20 +75,23 @@ export async function overflow(t) {
|
||||
);
|
||||
await shot('35-tab-overflow');
|
||||
// The overflow button names them, and picking one brings it into the row.
|
||||
const wanted = narrow.hidden[narrow.hidden.length - 1];
|
||||
await clickText('»');
|
||||
const offered = await json('__gm.overlayTexts()');
|
||||
const offered = (await json('__gm.menuRows()')).filter(Boolean);
|
||||
check(
|
||||
'the overflow button offers every tab that had to go',
|
||||
narrow.hidden.every((tab) => offered.includes(TAB_LABELS[tab])),
|
||||
offered.length === narrow.hidden.length &&
|
||||
offered.filter((label) => label.includes(' · ')).length ===
|
||||
narrow.hidden.filter((tab) => tab.startsWith('asset:')).length,
|
||||
JSON.stringify({ offered, hidden: narrow.hidden }),
|
||||
);
|
||||
await clickText(TAB_LABELS[wanted]);
|
||||
const wanted = narrow.hidden[narrow.hidden.length - 1];
|
||||
const item = await t.locate(offered[offered.length - 1], true);
|
||||
await t.click(item.x, item.y);
|
||||
const picked = await json('__gm.tabRow()');
|
||||
check(
|
||||
`picking ${TAB_LABELS[wanted]} from the overflow menu opens it and puts it in the row`,
|
||||
(await json('__gm.tab()')) === wanted && picked.shown.some((b) => b.tab === wanted),
|
||||
JSON.stringify({ tab: await json('__gm.tab()'), shown: picked.shown.map((b) => b.tab) }),
|
||||
'picking a document from the overflow menu opens it and puts it in the row',
|
||||
(await json('__gm.documents()')).active === wanted && picked.shown.some((b) => b.tab === wanted),
|
||||
JSON.stringify({ wanted, docs: await json('__gm.documents()'), shown: picked.shown.map((b) => b.tab) }),
|
||||
);
|
||||
await widenPanels(-250);
|
||||
await showTab('scene');
|
||||
@@ -136,7 +130,7 @@ export async function overflow(t) {
|
||||
);
|
||||
|
||||
// 14c. Every tab, because each one owns a different set of controls.
|
||||
for (const tab of ['scene', 'graph', 'texture', 'game', 'container', 'list', 'particles']) {
|
||||
for (const tab of ['scene', 'graph', 'texture', 'game', 'container', 'particles']) {
|
||||
await showTab(tab);
|
||||
const spills = await json('__gm.textOverflows()');
|
||||
check(
|
||||
@@ -150,7 +144,7 @@ export async function overflow(t) {
|
||||
await showTab('graph');
|
||||
const shown = (await json('__gm.textOverflows(true)')).find((e) => e.text.startsWith('Player movement'));
|
||||
check(
|
||||
'a value too wide for its dropdown is cut with an ellipsis',
|
||||
'a name too wide for its tab is cut with an ellipsis',
|
||||
Boolean(shown) && shown.text.endsWith('…') && shown.text.length > 8,
|
||||
JSON.stringify(shown),
|
||||
);
|
||||
@@ -166,7 +160,7 @@ export async function overflow(t) {
|
||||
|
||||
// 14f. The other direction: a control narrow enough to swallow its whole label draws nothing
|
||||
// at all, which the overflow walk cannot see – an empty text spills over no edge.
|
||||
for (const tab of ['scene', 'graph', 'texture', 'container', 'list', 'particles']) {
|
||||
for (const tab of ['scene', 'graph', 'texture', 'container', 'particles']) {
|
||||
await showTab(tab);
|
||||
const dropped = await json('__gm.labelsDropped()');
|
||||
check(
|
||||
@@ -183,4 +177,6 @@ export async function overflow(t) {
|
||||
onVariables.length === 0,
|
||||
JSON.stringify(onVariables),
|
||||
);
|
||||
for (const doc of (await json('__gm.documents()')).docs)
|
||||
await evaluate(`__gm.closeDocument(${JSON.stringify(doc)})`);
|
||||
}
|
||||
@@ -211,17 +211,17 @@ export async function containerTab(t) {
|
||||
JSON.stringify(await json('__gm.hierarchyOrder()')),
|
||||
);
|
||||
|
||||
// 16i. Switching to another container is a plain asset selection – the same thing the header's
|
||||
// picker does. The hierarchy has to follow it, title and rows both.
|
||||
// 16i. Opening another container shows it in a tab of its own (#171); the hierarchy has to
|
||||
// follow it, title and rows both.
|
||||
await showTab('container');
|
||||
await mutate(`const src = __gm.state.project.assets.find(a => a.id === '${containerId}');
|
||||
const copy = JSON.parse(JSON.stringify(src));
|
||||
copy.id = 'c2'; copy.name = 'Spare box';
|
||||
copy.objects = copy.objects.slice(0, 1).map((o) => ({ ...o, id: 'c2o1', parentId: '' }));
|
||||
__gm.state.project.assets.push(copy)`);
|
||||
await select({ kind: 'asset', assetId: 'c2' });
|
||||
await evaluate("__gm.state.openAsset('c2')");
|
||||
check(
|
||||
'picking another container moves the tab and the hierarchy to it',
|
||||
'opening another container moves the tab and the hierarchy to it',
|
||||
(await json('__gm.container.current()')) === 'c2' &&
|
||||
JSON.stringify(await json('__gm.hierarchyOrder()')) === JSON.stringify(['c2o1']) &&
|
||||
Boolean(await evaluate("__gm.locate('CONTAINER: SPARE BOX')")),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { edge, node, port } from '../lib/builders.mjs';
|
||||
import { MAKE_OBJECT } from './lib/scaffold.mjs';
|
||||
import { clickDocumentTab, MAKE_OBJECT } from './lib/scaffold.mjs';
|
||||
|
||||
/**
|
||||
* A behavior used as a node inside another behavior's graph: the ports it grows from the other
|
||||
@@ -321,31 +321,36 @@ export async function nestedBehavior(t) {
|
||||
`current=${await evaluate('__gm.graph.current()')} trail=${await trail()}`,
|
||||
);
|
||||
|
||||
// Opening another behavior any other way ends the trail: the asset panel, then the picker.
|
||||
// Another behavior opened any other way gets a tab of its own (#171), without a trail; the trail
|
||||
// stays with the tab it was made in, and comes back with it.
|
||||
const enterDouble = async () => {
|
||||
const call = await json(`__gm.graph.nodeAt('nb-call')`);
|
||||
await t.doubleClick(call.x, call.y);
|
||||
};
|
||||
await enterDouble();
|
||||
const followed = await json('__gm.documents()');
|
||||
check(
|
||||
'following a call stays in the same tab',
|
||||
followed.active === 'asset:nb-double' && !followed.docs.includes('asset:nb-caller'),
|
||||
JSON.stringify(followed),
|
||||
);
|
||||
const emptyAgain = await json(`__gm.assetRow('nb-empty')`);
|
||||
await click(emptyAgain.x, emptyAgain.y);
|
||||
await t.doubleClick(emptyAgain.x, emptyAgain.y);
|
||||
check(
|
||||
'choosing another behavior in the asset panel ends the trail',
|
||||
(await evaluate('__gm.graph.current()')) === 'nb-empty' && (await crumbs()).length === 0,
|
||||
`current=${await evaluate('__gm.graph.current()')} trail=${await trail()}`,
|
||||
'another behavior opened from the asset panel comes up in its own tab, without a trail',
|
||||
(await evaluate('__gm.graph.current()')) === 'nb-empty' &&
|
||||
(await crumbs()).length === 0 &&
|
||||
(await json('__gm.documents()')).docs.includes('asset:nb-double'),
|
||||
`current=${await evaluate('__gm.graph.current()')} trail=${await trail()} docs=${JSON.stringify(await json('__gm.documents()'))}`,
|
||||
);
|
||||
await evaluate(`__gm.state.select({ kind: 'asset', assetId: 'nb-caller' })`);
|
||||
await sleep(200);
|
||||
await enterDouble();
|
||||
const picker = await json('__gm.graph.pickerAt()');
|
||||
await click(picker.x, picker.y);
|
||||
const emptyItem = await locate('Empty', true);
|
||||
await click(emptyItem.x, emptyItem.y);
|
||||
await clickDocumentTab(t, 'asset:nb-double');
|
||||
check(
|
||||
'choosing another behavior in the picker ends the trail',
|
||||
(await evaluate('__gm.graph.current()')) === 'nb-empty' && (await crumbs()).length === 0,
|
||||
'back in the tab the call was followed in, its trail is there again',
|
||||
(await evaluate('__gm.graph.current()')) === 'nb-double' && (await trail()) === 'Caller › Double*',
|
||||
`current=${await evaluate('__gm.graph.current()')} trail=${await trail()}`,
|
||||
);
|
||||
await evaluate(`__gm.closeDocument('asset:nb-empty')`);
|
||||
await evaluate(`__gm.closeDocument('asset:nb-double')`);
|
||||
|
||||
// 19l. Clean up: the three behaviors and the box are gone again.
|
||||
await evaluate(`__gm.state.select(null)`);
|
||||
|
||||
@@ -103,7 +103,12 @@ export async function listTab(t) {
|
||||
|
||||
// 21e. Back on the scene tab the button carries the current count.
|
||||
await mutate('__gm.state.openList(null)');
|
||||
check('closing the list leaves the tab', (await json('__gm.tab()')) === 'scene');
|
||||
check(
|
||||
'closing the list closes its tab',
|
||||
(await json('__gm.tab()')) !== 'list' && !(await json('__gm.documents()')).docs.includes('list'),
|
||||
JSON.stringify({ tab: await json('__gm.tab()'), docs: await json('__gm.documents()') }),
|
||||
);
|
||||
|
||||
await mutate(`
|
||||
__gm.state.project.assets.find((a) => a.id === 'listy').props.nums = [1, 2];
|
||||
__gm.state.preview();
|
||||
|
||||
@@ -33,14 +33,16 @@ export async function plugins(t) {
|
||||
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));
|
||||
// 38b. Its tab sits after the fixed ones and ahead of the open documents (#171) – in the row or
|
||||
// behind », depending on the width.
|
||||
const { order } = await json('__gm.tabRow()');
|
||||
const at = order.indexOf('plugin:assets');
|
||||
check('the plugin tab is in the tab row', at >= 0, 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),
|
||||
'and it comes after the fixed tabs, ahead of any document',
|
||||
JSON.stringify(order.slice(0, 3)) === JSON.stringify(['scene', 'game', 'plugins']) &&
|
||||
order.slice(at + 1).every((tab) => tab === 'list' || tab.startsWith('asset:')),
|
||||
JSON.stringify(order),
|
||||
);
|
||||
|
||||
// 38c. One node per asset, titled by its name, in the order of the asset list.
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import { clickDocumentTab } from './lib/scaffold.mjs';
|
||||
|
||||
/**
|
||||
* Tabs as open documents (#171): a double-click in the asset panel opens a tab per asset, several of
|
||||
* one kind at once; the tab keeps its document whatever is selected, and its zoom when another tab
|
||||
* was on screen; the close cross and a middle click close it, a rename renames it and a deleted
|
||||
* asset takes its tab along.
|
||||
*/
|
||||
export async function documents(t) {
|
||||
const { check, json, evaluate, mutate, click, doubleClick, wheel, shot, showTab } = t;
|
||||
await showTab('scene');
|
||||
for (const doc of (await json('__gm.documents()')).docs)
|
||||
await evaluate(`__gm.closeDocument(${JSON.stringify(doc)})`);
|
||||
|
||||
// Short names of their own: an earlier step leaves them too long for a tab to show whole.
|
||||
const [a, b] = await json(`(() => {
|
||||
const two = __gm.state.project.assets.filter((x) => x.type === 'texture' && x.width > 0).slice(0, 2);
|
||||
const out = two.map((x, i) => ({ id: x.id, name: 'Sheet ' + 'AB'[i], was: x.name }));
|
||||
two.forEach((x, i) => (x.name = out[i].name));
|
||||
__gm.state.preview();
|
||||
return out;
|
||||
})()`);
|
||||
const behavior = await json(
|
||||
`(() => { const x = __gm.state.project.assets.find((x) => x.type === 'behavior' && x.graph.nodes.length > 0); return { id: x.id, node: x.graph.nodes[0].id }; })()`,
|
||||
);
|
||||
const docs = () => json('__gm.documents()');
|
||||
const current = () => json('__gm.texture.current()');
|
||||
const place = async (doc) => (await json('__gm.tabRow()')).shown.find((p) => p.tab === doc);
|
||||
|
||||
// 44a. Two textures, two tabs; each names its kind in front of the name.
|
||||
for (const texture of [a, b]) {
|
||||
const row = await json(`__gm.assetRow(${JSON.stringify(texture.id)})`);
|
||||
await doubleClick(row.x, row.y);
|
||||
}
|
||||
const opened = await docs();
|
||||
check(
|
||||
'double-clicking two textures opens a tab for each, the second one showing',
|
||||
JSON.stringify(opened.docs) === JSON.stringify([`asset:${a.id}`, `asset:${b.id}`]) &&
|
||||
opened.active === `asset:${b.id}` &&
|
||||
(await current()) === b.id,
|
||||
JSON.stringify({ opened, current: await current() }),
|
||||
);
|
||||
const placeA = await place(`asset:${a.id}`);
|
||||
const nameHits = await json(`__gm.locateAll(${JSON.stringify(a.name)})`);
|
||||
check(
|
||||
'a tab reads its kind, then the asset’s name',
|
||||
placeA?.kind === 'Texture' &&
|
||||
nameHits.some(
|
||||
(p) => p.x > placeA.x && p.x < placeA.right && p.y > placeA.y && p.y < placeA.y + placeA.h,
|
||||
),
|
||||
JSON.stringify({ placeA, nameHits }),
|
||||
);
|
||||
|
||||
// 44b. Selecting the other texture is a look at the inspector, not a change of document.
|
||||
await evaluate(`__gm.state.select({ kind: 'asset', assetId: ${JSON.stringify(a.id)} })`);
|
||||
check('selecting the other texture leaves the tab on its own', (await current()) === b.id, await current());
|
||||
|
||||
// 44c. Zoom and scroll stay with the document.
|
||||
const view = () =>
|
||||
json(
|
||||
`(() => { const o = __gm.texture.pointAt(0, 0); const p = __gm.texture.pointAt(10, 0); return { x: o.x, y: o.y, scale: (p.x - o.x) / 10 }; })()`,
|
||||
);
|
||||
const fitted = await view();
|
||||
const rect = await json('__gm.texture.rect()');
|
||||
await wheel(rect.x + rect.w / 2, rect.y + rect.h / 2, -300);
|
||||
const zoomed = await view();
|
||||
check('the wheel zooms the texture', zoomed.scale > fitted.scale * 1.2, JSON.stringify({ fitted, zoomed }));
|
||||
await clickDocumentTab(t, `asset:${a.id}`);
|
||||
check('its neighbour’s tab shows the other texture', (await current()) === a.id, await current());
|
||||
await clickDocumentTab(t, `asset:${b.id}`);
|
||||
const back = await view();
|
||||
check(
|
||||
'coming back, the texture is zoomed and scrolled as it was left',
|
||||
(await current()) === b.id &&
|
||||
Math.abs(back.scale - zoomed.scale) < 1e-6 &&
|
||||
Math.abs(back.x - zoomed.x) < 0.5 &&
|
||||
Math.abs(back.y - zoomed.y) < 0.5,
|
||||
JSON.stringify({ zoomed, back }),
|
||||
);
|
||||
|
||||
// 44d. A rename renames the tab.
|
||||
await mutate(
|
||||
`__gm.state.project.assets.find((x) => x.id === ${JSON.stringify(b.id)}).name = 'Renamed sheet'; __gm.state.preview();`,
|
||||
);
|
||||
const renamedAt = await place(`asset:${b.id}`);
|
||||
const renamedHits = await json(`__gm.locateAll('Renamed sheet')`);
|
||||
check(
|
||||
'renaming the asset renames its tab',
|
||||
renamedHits.some(
|
||||
(p) =>
|
||||
p.x > renamedAt.x && p.x < renamedAt.right && p.y > renamedAt.y && p.y < renamedAt.y + renamedAt.h,
|
||||
),
|
||||
JSON.stringify({ renamedAt, renamedHits }),
|
||||
);
|
||||
await mutate(
|
||||
`__gm.state.project.assets.find((x) => x.id === ${JSON.stringify(b.id)}).name = ${JSON.stringify(b.name)}; __gm.state.preview();`,
|
||||
);
|
||||
|
||||
// 44e. A node lives only in its graph: selecting one brings that graph up, in a tab of its own.
|
||||
await evaluate(
|
||||
`__gm.state.select({ kind: 'node', assetId: ${JSON.stringify(behavior.id)}, nodeId: ${JSON.stringify(behavior.node)} })`,
|
||||
);
|
||||
const withGraph = await docs();
|
||||
check(
|
||||
'selecting a node opens its behavior next to the texture showing',
|
||||
withGraph.active === `asset:${behavior.id}` &&
|
||||
JSON.stringify(withGraph.docs) ===
|
||||
JSON.stringify([`asset:${a.id}`, `asset:${b.id}`, `asset:${behavior.id}`]) &&
|
||||
(await json('__gm.tab()')) === 'graph',
|
||||
JSON.stringify(withGraph),
|
||||
);
|
||||
await shot('44-documents');
|
||||
|
||||
// 44f. The close cross closes the tab on screen; its left neighbour comes up.
|
||||
const graphTab = await place(`asset:${behavior.id}`);
|
||||
await click(graphTab.close.x, graphTab.close.y);
|
||||
const afterClose = await docs();
|
||||
check(
|
||||
'the close cross closes the tab, and the one left of it shows',
|
||||
!afterClose.docs.includes(`asset:${behavior.id}`) &&
|
||||
afterClose.active === `asset:${b.id}` &&
|
||||
(await current()) === b.id,
|
||||
JSON.stringify(afterClose),
|
||||
);
|
||||
|
||||
// 44g. A middle click closes a tab that is not showing, and leaves the one that is.
|
||||
const tabA = await place(`asset:${a.id}`);
|
||||
await click((tabA.x + tabA.close.x) / 2, tabA.y + tabA.h / 2, 'middle');
|
||||
const afterMiddle = await docs();
|
||||
check(
|
||||
'a middle click closes a tab in the background',
|
||||
JSON.stringify(afterMiddle.docs) === JSON.stringify([`asset:${b.id}`]) &&
|
||||
afterMiddle.active === `asset:${b.id}`,
|
||||
JSON.stringify(afterMiddle),
|
||||
);
|
||||
|
||||
// 44h. A deleted asset takes its tab along, and the last one gone leaves the Scene tab showing.
|
||||
await mutate(`
|
||||
const p = __gm.state.project;
|
||||
window.__deletedSheet = p.assets.find((x) => x.id === ${JSON.stringify(b.id)});
|
||||
p.assets = p.assets.filter((x) => x.id !== ${JSON.stringify(b.id)});
|
||||
__gm.state.preview();
|
||||
`);
|
||||
const afterDelete = await docs();
|
||||
check(
|
||||
'deleting the asset closes its tab and goes back to the scene',
|
||||
afterDelete.docs.length === 0 && (await json('__gm.tab()')) === 'scene',
|
||||
JSON.stringify({ afterDelete, tab: await json('__gm.tab()') }),
|
||||
);
|
||||
await mutate(
|
||||
`__gm.state.project.assets.push(window.__deletedSheet); delete window.__deletedSheet; __gm.state.preview();`,
|
||||
);
|
||||
await mutate(`
|
||||
for (const { id, was } of ${JSON.stringify([a, b])}) __gm.state.project.assets.find((x) => x.id === id).name = was;
|
||||
__gm.state.preview();
|
||||
`);
|
||||
}
|
||||
@@ -46,6 +46,7 @@ import { tilemap } from './40-tilemap.mjs';
|
||||
import { physics } from './41-physics.mjs';
|
||||
import { shapes } from './42-shapes.mjs';
|
||||
import { queries } from './43-queries.mjs';
|
||||
import { documents } from './44-documents.mjs';
|
||||
|
||||
export const STEPS = [
|
||||
hierarchy,
|
||||
@@ -91,4 +92,5 @@ export const STEPS = [
|
||||
physics,
|
||||
shapes,
|
||||
queries,
|
||||
documents,
|
||||
];
|
||||
@@ -128,6 +128,11 @@ export const TAB_LABELS = {
|
||||
* nothing and took the whole suite down with it.
|
||||
*/
|
||||
export async function openTab(t, tab) {
|
||||
// A kind of document has no tab of its own since #171; the hook shows the one the selection is about.
|
||||
if (DOCUMENT_KINDS.includes(tab)) {
|
||||
await t.evaluate(`__gm.showTab(${JSON.stringify(tab)})`);
|
||||
return;
|
||||
}
|
||||
const label = TAB_LABELS[tab];
|
||||
const row = await t.json('__gm.tabRow()');
|
||||
if (row.hidden.includes(tab)) {
|
||||
@@ -148,6 +153,42 @@ export async function openTab(t, tab) {
|
||||
else await t.clickText(label);
|
||||
}
|
||||
|
||||
const DOCUMENT_KINDS = [
|
||||
'graph',
|
||||
'statemachine',
|
||||
'shader',
|
||||
'particles',
|
||||
'texture',
|
||||
'animation',
|
||||
'tile',
|
||||
'container',
|
||||
'list',
|
||||
];
|
||||
|
||||
/**
|
||||
* Clicks the tab of an open document (`asset:<id>` or `list`), through the `»` menu when the row
|
||||
* hides it. The press lands left of the ×, on the name.
|
||||
*/
|
||||
export async function clickDocumentTab(t, doc) {
|
||||
const row = await t.json('__gm.tabRow()');
|
||||
const entry = row.shown.find((e) => e.tab === doc);
|
||||
if (entry) {
|
||||
await t.click(entry.x + (entry.close.x - entry.x) / 2, entry.y + entry.h / 2);
|
||||
return;
|
||||
}
|
||||
if (!row.hidden.includes(doc)) throw new Error(`no tab for ${doc}`);
|
||||
const label = await t.json(`(() => {
|
||||
const id = ${JSON.stringify(doc)};
|
||||
if (id === 'list') return null;
|
||||
const asset = __gm.state.project.assets.find((a) => 'asset:' + a.id === id);
|
||||
return asset ? asset.name : null;
|
||||
})()`);
|
||||
await t.clickText('»');
|
||||
const item = (await t.json('__gm.menuRows()')).find((r) => r && r.endsWith(` · ${label}`));
|
||||
const at = await t.locate(item, true);
|
||||
await t.click(at.x, at.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Picks an asset type from the already-open `+` menu. Since #43 the menu may grow to the height of
|
||||
* the canvas, so on this window every type is usually on screen at once; the scroll stays in as
|
||||
|
||||
Reference in new issue
Block a user