Make e2e 40, 46, 51 and 52 pass in the full run again
The suite job failed on every push since 6015869. Each module passed on
its own, but the full run showed two kinds of fault.
- e2e 40 still expected the object menu and the empty-cell menu from
before #240 and #241. It now expects Copy, Paste, Group... and Remove
from group, and Paste alone on an empty cell.
- e2e 46g read the outline of the first collider in the scene. In the
full run that is a leftover of module 43, off screen, so it read as
hidden before any switch. The check now places a collider of its own
next to its tilemap and removes both at the end.
- e2e 51 copied the seed's red and blue crate. In the full run module 09
has packed both into a container. The module now places two crates
with colliders of its own and removes them at the end.
- e2e 52 compared whole row lists against the seed scene. In the full
run that scene holds about 50 more rows. The module now works in a
scene of its own, which it removes at the end. It had never run in CI
because 51 died before it.
The check in e2e 41 that failed once (task 347, a dragged body 15 px
off in its first sample) is left alone. It failed in 1 of the last 60
logs, while a local e2e run loaded the same host.
Tests:
- full e2e run in the game-maker-ci image with GPU, as the runner does:
ALL PASSED, 1452 checks, E2E EXIT 0 (the README sum is now measured)
- GM_STEPS=1,46,51,52 on the host: ALL PASSED, 51 checks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3abb6894a7
commit
38f009e035
5 files changed
+119
-58
No files matched your search
@@ -43,7 +43,7 @@ npm run build # writes dist/ and regenerates index.html; the folder is wh
|
|||||||
The browser suite and the three example games need a machine with a GPU:
|
The browser suite and the three example games need a machine with a GPU:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1447 checks (GM_STEPS=20 for one step)
|
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1452 checks (GM_STEPS=20 for one step)
|
||||||
# every tool signs in first – see the Accounts page
|
# every tool signs in first – see the Accounts page
|
||||||
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
|
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
|
||||||
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
||||||
|
|||||||
@@ -829,11 +829,15 @@ export async function tilemap(t) {
|
|||||||
const OBJECT_MENU = [
|
const OBJECT_MENU = [
|
||||||
'Select asset',
|
'Select asset',
|
||||||
'Open asset',
|
'Open asset',
|
||||||
|
'Copy',
|
||||||
|
'Paste',
|
||||||
'Add collider…',
|
'Add collider…',
|
||||||
'Add rigidbody…',
|
'Add rigidbody…',
|
||||||
'Create container',
|
'Create container',
|
||||||
'Unpack container',
|
'Unpack container',
|
||||||
'Unparent (move to top level)',
|
'Unparent (move to top level)',
|
||||||
|
'Group…',
|
||||||
|
'Remove from group',
|
||||||
'Remove from scene',
|
'Remove from scene',
|
||||||
];
|
];
|
||||||
const onHidden = await cellPoint(1, 1);
|
const onHidden = await cellPoint(1, 1);
|
||||||
@@ -869,15 +873,16 @@ export async function tilemap(t) {
|
|||||||
JSON.stringify({ topRows, topPicked }),
|
JSON.stringify({ topRows, topPicked }),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Empty space opens no menu and leaves the selection alone.
|
// Empty space opens only the menu to paste into the scene (#240) and leaves the selection alone.
|
||||||
const emptyPoint = await cellPoint(COLUMNS - 1, ROWS - 1);
|
const emptyPoint = await cellPoint(COLUMNS - 1, ROWS - 1);
|
||||||
await click(emptyPoint.x, emptyPoint.y, 'right');
|
await click(emptyPoint.x, emptyPoint.y, 'right');
|
||||||
await settle();
|
await settle();
|
||||||
const emptyMenu = await json('__gm.menuRows()');
|
const emptyMenu = await json('__gm.menuRows()');
|
||||||
const emptyPicked = await selected();
|
const emptyPicked = await selected();
|
||||||
check(
|
check(
|
||||||
'a right-click on an empty cell opens no menu and keeps the selection',
|
'a right-click on an empty cell opens only Paste and keeps the selection',
|
||||||
emptyMenu === null && JSON.stringify(emptyPicked) === JSON.stringify([MAP_OBJECT]),
|
JSON.stringify(emptyMenu) === JSON.stringify(['Paste']) &&
|
||||||
|
JSON.stringify(emptyPicked) === JSON.stringify([MAP_OBJECT]),
|
||||||
JSON.stringify({ emptyMenu, emptyPicked }),
|
JSON.stringify({ emptyMenu, emptyPicked }),
|
||||||
);
|
);
|
||||||
await press('Escape');
|
await press('Escape');
|
||||||
|
|||||||
@@ -181,11 +181,13 @@ export async function topbar(t) {
|
|||||||
})()`);
|
})()`);
|
||||||
const ids = {
|
const ids = {
|
||||||
sprite: await firstOf('sprite'),
|
sprite: await firstOf('sprite'),
|
||||||
collider: await firstOf('collider'),
|
collider: 'vis-col-obj',
|
||||||
behavior: await firstOf('behavior'),
|
behavior: await firstOf('behavior'),
|
||||||
tilemap: 'vis-map-obj',
|
tilemap: 'vis-map-obj',
|
||||||
};
|
};
|
||||||
// A map of its own with one solid row, laid over the sprite so it is on screen: the seed has none.
|
// A map of its own with one solid row, laid over the sprite so it is on screen: the seed has none.
|
||||||
|
// A collider of its own there too: in the full run the first one in the scene is a leftover of an
|
||||||
|
// earlier module, off screen, and its outline reads as hidden before any switch is touched.
|
||||||
await t.mutate(`
|
await t.mutate(`
|
||||||
const scene = __gm.state.activeScene;
|
const scene = __gm.state.activeScene;
|
||||||
const assets = __gm.state.project.assets;
|
const assets = __gm.state.project.assets;
|
||||||
@@ -197,6 +199,9 @@ export async function topbar(t) {
|
|||||||
Object.assign(o, { id: 'vis-map-obj', assetId: 'vis-map', name: 'Vis map', position: { ...at },
|
Object.assign(o, { id: 'vis-map-obj', assetId: 'vis-map', name: 'Vis map', position: { ...at },
|
||||||
parentId: '', overrides: {}, instanceEdits: {} });
|
parentId: '', overrides: {}, instanceEdits: {} });
|
||||||
scene.objects.push(o);
|
scene.objects.push(o);
|
||||||
|
assets.push({ id: 'vis-col', type: 'collider', name: 'Vis collider',
|
||||||
|
props: { shape: 'box', width: 40, height: 40, radius: 20, isTrigger: false } });
|
||||||
|
scene.objects.push(Object.assign(structuredClone(o), { id: 'vis-col-obj', assetId: 'vis-col', name: 'Vis collider' }));
|
||||||
__gm.state.select(null)`);
|
__gm.state.select(null)`);
|
||||||
const undoBefore = await json('__gm.state.commands.canUndo');
|
const undoBefore = await json('__gm.state.commands.canUndo');
|
||||||
await clickToolbar('visibility');
|
await clickToolbar('visibility');
|
||||||
@@ -295,8 +300,8 @@ export async function topbar(t) {
|
|||||||
await press('Escape');
|
await press('Escape');
|
||||||
await t.mutate(`
|
await t.mutate(`
|
||||||
const scene = __gm.state.activeScene;
|
const scene = __gm.state.activeScene;
|
||||||
scene.objects.splice(scene.objects.findIndex((o) => o.id === 'vis-map-obj'), 1);
|
scene.objects = scene.objects.filter((o) => o.id !== 'vis-map-obj' && o.id !== 'vis-col-obj');
|
||||||
const assets = __gm.state.project.assets;
|
const assets = __gm.state.project.assets;
|
||||||
for (const id of ['vis-map', 'vis-wall']) assets.splice(assets.findIndex((a) => a.id === id), 1)`);
|
for (const id of ['vis-map', 'vis-wall', 'vis-col']) assets.splice(assets.findIndex((a) => a.id === id), 1)`);
|
||||||
await t.saved();
|
await t.saved();
|
||||||
}
|
}
|
||||||
@@ -18,10 +18,28 @@ export async function clipboard(t) {
|
|||||||
await press('Escape');
|
await press('Escape');
|
||||||
await showTab('scene');
|
await showTab('scene');
|
||||||
await evaluate('__gm.state.clipboard = null');
|
await evaluate('__gm.state.clipboard = null');
|
||||||
|
// Two crates of its own, each with a collider, where the seed has its red and blue crate. In the
|
||||||
|
// full run those two are long packed into a container by module 09, so they are no fixture here.
|
||||||
|
const BLUE = 'clip-blue';
|
||||||
|
const RED = 'clip-red';
|
||||||
|
await t.mutate(`
|
||||||
|
const scene = __gm.state.activeScene;
|
||||||
|
const put = (id, assetId, parentId, x, y, name) => {
|
||||||
|
const o = structuredClone(scene.objects[0]);
|
||||||
|
Object.assign(o, { id, assetId, parentId, name, position: { x, y, z: 0 }, overrides: {}, instanceEdits: {} });
|
||||||
|
delete o.groupId;
|
||||||
|
if ('enabled' in o) o.enabled = true;
|
||||||
|
scene.objects.push(o);
|
||||||
|
};
|
||||||
|
put(${JSON.stringify(BLUE)}, 'crate', '', 120, -80, 'Blue crate');
|
||||||
|
put(${JSON.stringify(BLUE)} + '-col', 'collider', ${JSON.stringify(BLUE)}, 0, 0, 'Collider');
|
||||||
|
put(${JSON.stringify(RED)}, 'crate', '', -150, 0, 'Red crate');
|
||||||
|
put(${JSON.stringify(RED)} + '-col', 'collider', ${JSON.stringify(RED)}, 0, 0, 'Collider');
|
||||||
|
__gm.state.select(null)`);
|
||||||
|
|
||||||
// 51a. Ctrl+C on the blue crate, Ctrl+V with the pointer over empty space: the crate and its
|
// 51a. Ctrl+C on the blue crate, Ctrl+V with the pointer over empty space: the crate and its
|
||||||
// collider come back as new objects, the crate standing right under the pointer.
|
// collider come back as new objects, the crate standing right under the pointer.
|
||||||
await select({ kind: 'object', holderId: 'scene1', objectId: 'o2', objectIds: ['o2'] });
|
await select({ kind: 'object', holderId: 'scene1', objectId: BLUE, objectIds: [BLUE] });
|
||||||
await press('c', { ctrl: true });
|
await press('c', { ctrl: true });
|
||||||
const spot = await json('__gm.emptySpot()');
|
const spot = await json('__gm.emptySpot()');
|
||||||
const before = await stored();
|
const before = await stored();
|
||||||
@@ -51,16 +69,16 @@ export async function clipboard(t) {
|
|||||||
|
|
||||||
// 51b. With the pointer over the hierarchy the copy has nowhere to be aimed: it lands one grid
|
// 51b. With the pointer over the hierarchy the copy has nowhere to be aimed: it lands one grid
|
||||||
// step down and right of the original, beside it rather than inside it.
|
// step down and right of the original, beside it rather than inside it.
|
||||||
await select({ kind: 'object', holderId: 'scene1', objectId: 'o2', objectIds: ['o2'] });
|
await select({ kind: 'object', holderId: 'scene1', objectId: BLUE, objectIds: [BLUE] });
|
||||||
await moveTo(await json('__gm.hierarchyRow("o2")'));
|
await moveTo(await json(`__gm.hierarchyRow(${JSON.stringify(BLUE)})`));
|
||||||
await press('v', { ctrl: true });
|
await press('v', { ctrl: true });
|
||||||
const besideId = (await selected()).objectId;
|
const besideId = (await selected()).objectId;
|
||||||
const beside = (await stored()).find((o) => o.id === besideId);
|
const beside = (await stored()).find((o) => o.id === besideId);
|
||||||
const original = (await stored()).find((o) => o.id === 'o2');
|
const original = (await stored()).find((o) => o.id === BLUE);
|
||||||
check(
|
check(
|
||||||
'without the pointer on the view the copy lands a grid step beside the original',
|
'without the pointer on the view the copy lands a grid step beside the original',
|
||||||
beside !== undefined &&
|
beside !== undefined &&
|
||||||
beside.id !== 'o2' &&
|
beside.id !== BLUE &&
|
||||||
beside.parentId === original.parentId &&
|
beside.parentId === original.parentId &&
|
||||||
beside.position.x === original.position.x + 100 &&
|
beside.position.x === original.position.x + 100 &&
|
||||||
beside.position.y === original.position.y + 100,
|
beside.position.y === original.position.y + 100,
|
||||||
@@ -70,7 +88,7 @@ export async function clipboard(t) {
|
|||||||
|
|
||||||
// 51c. The object menu: right-click the red crate, "Paste" puts the copy inside what was hit –
|
// 51c. The object menu: right-click the red crate, "Paste" puts the copy inside what was hit –
|
||||||
// the crate or the collider drawn over it, whichever the pointer picks.
|
// the crate or the collider drawn over it, whichever the pointer picks.
|
||||||
const red = await json('__gm.objectScreen("o1")');
|
const red = await json(`__gm.objectScreen(${JSON.stringify(RED)})`);
|
||||||
await click(red.x, red.y, 'right');
|
await click(red.x, red.y, 'right');
|
||||||
const target = (await selected()).objectId;
|
const target = (await selected()).objectId;
|
||||||
const rows = (await json('__gm.menuRows()')) ?? [];
|
const rows = (await json('__gm.menuRows()')) ?? [];
|
||||||
@@ -81,7 +99,7 @@ export async function clipboard(t) {
|
|||||||
'the object menu offers Copy and Paste, and Paste puts the copy into the object clicked',
|
'the object menu offers Copy and Paste, and Paste puts the copy into the object clicked',
|
||||||
rows.includes('Copy') &&
|
rows.includes('Copy') &&
|
||||||
rows.includes('Paste') &&
|
rows.includes('Paste') &&
|
||||||
['o1', 'oc1'].includes(target) &&
|
[RED, `${RED}-col`].includes(target) &&
|
||||||
inside?.assetId === 'crate' &&
|
inside?.assetId === 'crate' &&
|
||||||
inside.parentId === target,
|
inside.parentId === target,
|
||||||
JSON.stringify({ target, rows, inside }),
|
JSON.stringify({ target, rows, inside }),
|
||||||
@@ -128,4 +146,8 @@ export async function clipboard(t) {
|
|||||||
check('the graph is back as it was', (await nodes()).length === count);
|
check('the graph is back as it was', (await nodes()).length === count);
|
||||||
await evaluate('__gm.state.clipboard = null');
|
await evaluate('__gm.state.clipboard = null');
|
||||||
await showTab('scene');
|
await showTab('scene');
|
||||||
|
await t.mutate(`
|
||||||
|
const scene = __gm.state.activeScene;
|
||||||
|
scene.objects = scene.objects.filter((o) => !o.id.startsWith('clip-'));`);
|
||||||
|
await t.saved();
|
||||||
}
|
}
|
||||||
@@ -2,13 +2,37 @@ import { same } from '../lib/harness.mjs';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Groups in the hierarchy (#241), made, opened, filled, renamed and dissolved with the real menus,
|
* Groups in the hierarchy (#241), made, opened, filled, renamed and dissolved with the real menus,
|
||||||
* clicks, keys and a real drag. The fixture's top level is o1, o2, o3, o5; the order rules and
|
* clicks, keys and a real drag. The order rules and drops are unit-tested, this checks that the
|
||||||
* drops are unit-tested, this checks that the panel and the dialog do what those rules say.
|
* panel and the dialog do what those rules say.
|
||||||
* Everything is taken back by undo, so the fixture leaves as it came.
|
* It works in a scene of its own, whose top level is g1, g2 (with the collider gc2), g3 and g5: the
|
||||||
|
* checks compare whole lists of rows, and in the full run the fixture's scene holds whatever the
|
||||||
|
* modules before left there. The scene is removed at the end.
|
||||||
*/
|
*/
|
||||||
export async function hierarchyGroups(t) {
|
export async function hierarchyGroups(t) {
|
||||||
const { check, json, evaluate, click, clickText, drag, press, typeText, select, showTab, undo, shot } = t;
|
const { check, json, evaluate, click, clickText, drag, press, typeText, select, showTab, undo, shot } = t;
|
||||||
|
|
||||||
|
const SCENE = 'groups-52';
|
||||||
|
const home = await json('__gm.state.project.activeSceneId');
|
||||||
|
await t.mutate(`
|
||||||
|
const p = __gm.state.project;
|
||||||
|
const base = __gm.state.activeScene.objects[0];
|
||||||
|
const make = (id, assetId, parentId, x, name) => {
|
||||||
|
const o = structuredClone(base);
|
||||||
|
Object.assign(o, { id, assetId, parentId, name, position: { x, y: 0, z: 0 }, overrides: {}, instanceEdits: {} });
|
||||||
|
delete o.groupId;
|
||||||
|
if ('enabled' in o) o.enabled = true;
|
||||||
|
return o;
|
||||||
|
};
|
||||||
|
p.assets.push({ id: ${JSON.stringify(SCENE)}, type: 'scene', name: 'Groups 52', props: { backgroundColor: 0x22252c },
|
||||||
|
objects: [
|
||||||
|
make('g1', 'crate', '', -200, 'Crate 1'),
|
||||||
|
make('g2', 'crate', '', 0, 'Crate 2'),
|
||||||
|
make('gc2', 'collider', 'g2', 0, 'Collider'),
|
||||||
|
make('g3', 'crate', '', 200, 'Crate 3'),
|
||||||
|
make('g5', 'mover', '', 300, 'Mover'),
|
||||||
|
] });
|
||||||
|
p.activeSceneId = ${JSON.stringify(SCENE)}`);
|
||||||
|
|
||||||
const rowIds = () => json('__gm.hierarchyOrder()');
|
const rowIds = () => json('__gm.hierarchyOrder()');
|
||||||
const scene = () => json('__gm.state.activeScene');
|
const scene = () => json('__gm.state.activeScene');
|
||||||
const selected = async () => (await json('__gm.state.selection'))?.objectIds ?? [];
|
const selected = async () => (await json('__gm.state.selection'))?.objectIds ?? [];
|
||||||
@@ -23,8 +47,8 @@ export async function hierarchyGroups(t) {
|
|||||||
await evaluate('__gm.state.select(null); __gm.hierarchyCollapseAll()');
|
await evaluate('__gm.state.select(null); __gm.hierarchyCollapseAll()');
|
||||||
|
|
||||||
// 52a. A child cannot go into a group: the entry is there, greyed out.
|
// 52a. A child cannot go into a group: the entry is there, greyed out.
|
||||||
await select({ kind: 'object', holderId: 'scene1', objectId: 'oc2', objectIds: ['oc2'] });
|
await select({ kind: 'object', holderId: SCENE, objectId: 'gc2', objectIds: ['gc2'] });
|
||||||
const child = await rowAt('oc2');
|
const child = await rowAt('gc2');
|
||||||
await click(child.x, child.y, 'right');
|
await click(child.x, child.y, 'right');
|
||||||
const childEntry = await menuRow('Group…');
|
const childEntry = await menuRow('Group…');
|
||||||
check('Group… is greyed out for a child', childEntry.enabled === false, JSON.stringify(childEntry));
|
check('Group… is greyed out for a child', childEntry.enabled === false, JSON.stringify(childEntry));
|
||||||
@@ -32,8 +56,8 @@ export async function hierarchyGroups(t) {
|
|||||||
await evaluate('__gm.state.select(null); __gm.hierarchyCollapseAll()');
|
await evaluate('__gm.state.select(null); __gm.hierarchyCollapseAll()');
|
||||||
|
|
||||||
// 52b. Two top-level objects, the row menu's "Group 2 objects…", a name typed into the dialog.
|
// 52b. Two top-level objects, the row menu's "Group 2 objects…", a name typed into the dialog.
|
||||||
await select({ kind: 'object', holderId: 'scene1', objectId: 'o1', objectIds: ['o1', 'o3'] });
|
await select({ kind: 'object', holderId: SCENE, objectId: 'g1', objectIds: ['g1', 'g3'] });
|
||||||
const first = await rowAt('o1');
|
const first = await rowAt('g1');
|
||||||
await click(first.x, first.y, 'right');
|
await click(first.x, first.y, 'right');
|
||||||
const entry = await menuRow('Group 2 objects…');
|
const entry = await menuRow('Group 2 objects…');
|
||||||
await clickText('Group 2 objects…');
|
await clickText('Group 2 objects…');
|
||||||
@@ -53,13 +77,13 @@ export async function hierarchyGroups(t) {
|
|||||||
made.objects
|
made.objects
|
||||||
.filter((o) => o.groupId === group.id)
|
.filter((o) => o.groupId === group.id)
|
||||||
.map((o) => o.id)
|
.map((o) => o.id)
|
||||||
.join() === 'o1,o3',
|
.join() === 'g1,g3',
|
||||||
JSON.stringify({ entry, dialogOpened, groups: made.groups }),
|
JSON.stringify({ entry, dialogOpened, groups: made.groups }),
|
||||||
);
|
);
|
||||||
// o3 is listed with o1 now; with both members selected the group stays folded.
|
// g3 is listed with g1 now; with both members selected the group stays folded.
|
||||||
check(
|
check(
|
||||||
'the group row stands where o1 stood, folded',
|
'the group row stands where g1 stood, folded',
|
||||||
same(await rowIds(), [row, 'o2', 'o5']),
|
same(await rowIds(), [row, 'g2', 'g5']),
|
||||||
JSON.stringify(await rowIds()),
|
JSON.stringify(await rowIds()),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -69,26 +93,26 @@ export async function hierarchyGroups(t) {
|
|||||||
await click(groupRow.x, groupRow.y);
|
await click(groupRow.x, groupRow.y);
|
||||||
check(
|
check(
|
||||||
'clicking the group row selects all its members and leaves it folded',
|
'clicking the group row selects all its members and leaves it folded',
|
||||||
same(await selected(), ['o1', 'o3']) && same(await rowIds(), [row, 'o2', 'o5']),
|
same(await selected(), ['g1', 'g3']) && same(await rowIds(), [row, 'g2', 'g5']),
|
||||||
JSON.stringify({ selected: await selected(), rows: await rowIds() }),
|
JSON.stringify({ selected: await selected(), rows: await rowIds() }),
|
||||||
);
|
);
|
||||||
const chevron = await json(`__gm.hierarchyChevron(${JSON.stringify(row)})`);
|
const chevron = await json(`__gm.hierarchyChevron(${JSON.stringify(row)})`);
|
||||||
await click(chevron.x, chevron.y);
|
await click(chevron.x, chevron.y);
|
||||||
check(
|
check(
|
||||||
'its chevron lists the members below it',
|
'its chevron lists the members below it',
|
||||||
same(await rowIds(), [row, 'o1', 'o3', 'o2', 'o5']),
|
same(await rowIds(), [row, 'g1', 'g3', 'g2', 'g5']),
|
||||||
JSON.stringify(await rowIds()),
|
JSON.stringify(await rowIds()),
|
||||||
);
|
);
|
||||||
await shot('52-group-open');
|
await shot('52-group-open');
|
||||||
|
|
||||||
// 52d. Dragging o5 onto the group row puts it in behind the last member; undo takes it out.
|
// 52d. Dragging g5 onto the group row puts it in behind the last member; undo takes it out.
|
||||||
const from = await rowAt('o5');
|
const from = await rowAt('g5');
|
||||||
const onto = await rowAt(row);
|
const onto = await rowAt(row);
|
||||||
await drag(from.x, from.y, onto.x, onto.y);
|
await drag(from.x, from.y, onto.x, onto.y);
|
||||||
check(
|
check(
|
||||||
'dragging an object onto the group row adds it to the group',
|
'dragging an object onto the group row adds it to the group',
|
||||||
same(await rowIds(), [row, 'o1', 'o3', 'o5', 'o2']) &&
|
same(await rowIds(), [row, 'g1', 'g3', 'g5', 'g2']) &&
|
||||||
(await scene()).objects.find((o) => o.id === 'o5')?.groupId === group?.id,
|
(await scene()).objects.find((o) => o.id === 'g5')?.groupId === group?.id,
|
||||||
JSON.stringify(await rowIds()),
|
JSON.stringify(await rowIds()),
|
||||||
);
|
);
|
||||||
await undo();
|
await undo();
|
||||||
@@ -114,7 +138,7 @@ export async function hierarchyGroups(t) {
|
|||||||
'Ungroup dissolves the group and keeps its members',
|
'Ungroup dissolves the group and keeps its members',
|
||||||
ungrouped.groups === undefined &&
|
ungrouped.groups === undefined &&
|
||||||
!ungrouped.objects.some((o) => o.groupId) &&
|
!ungrouped.objects.some((o) => o.groupId) &&
|
||||||
same(await rowIds(), ['o1', 'o2', 'o3', 'o5']),
|
same(await rowIds(), ['g1', 'g2', 'g3', 'g5']),
|
||||||
JSON.stringify({ groups: ungrouped.groups, rows: await rowIds() }),
|
JSON.stringify({ groups: ungrouped.groups, rows: await rowIds() }),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -132,48 +156,48 @@ export async function hierarchyGroups(t) {
|
|||||||
|
|
||||||
// 52g. A plain click on a row that is already part of a multi-selection keeps only that row –
|
// 52g. A plain click on a row that is already part of a multi-selection keeps only that row –
|
||||||
// the other member as well as the primary one.
|
// the other member as well as the primary one.
|
||||||
await select({ kind: 'object', holderId: 'scene1', objectId: 'o1', objectIds: ['o1', 'o2', 'o3'] });
|
await select({ kind: 'object', holderId: SCENE, objectId: 'g1', objectIds: ['g1', 'g2', 'g3'] });
|
||||||
const other = await rowAt('o2');
|
const other = await rowAt('g2');
|
||||||
await click(other.x, other.y);
|
await click(other.x, other.y);
|
||||||
const narrowedToOther = await selected();
|
const narrowedToOther = await selected();
|
||||||
await select({ kind: 'object', holderId: 'scene1', objectId: 'o1', objectIds: ['o1', 'o2', 'o3'] });
|
await select({ kind: 'object', holderId: SCENE, objectId: 'g1', objectIds: ['g1', 'g2', 'g3'] });
|
||||||
const primary = await rowAt('o1');
|
const primary = await rowAt('g1');
|
||||||
await click(primary.x, primary.y);
|
await click(primary.x, primary.y);
|
||||||
const narrowedToPrimary = await selected();
|
const narrowedToPrimary = await selected();
|
||||||
check(
|
check(
|
||||||
'clicking a selected row narrows the selection to that row',
|
'clicking a selected row narrows the selection to that row',
|
||||||
same(narrowedToOther, ['o2']) && same(narrowedToPrimary, ['o1']),
|
same(narrowedToOther, ['g2']) && same(narrowedToPrimary, ['g1']),
|
||||||
JSON.stringify({ narrowedToOther, narrowedToPrimary }),
|
JSON.stringify({ narrowedToOther, narrowedToPrimary }),
|
||||||
);
|
);
|
||||||
await evaluate('__gm.state.select(null); __gm.hierarchyCollapseAll()');
|
await evaluate('__gm.state.select(null); __gm.hierarchyCollapseAll()');
|
||||||
|
|
||||||
// 52h. Shift-click takes every row from the last plain click to the clicked one; a second
|
// 52h. Shift-click takes every row from the last plain click to the clicked one; a second
|
||||||
// Shift-click redraws the range from the same anchor, and it runs upwards just as well.
|
// Shift-click redraws the range from the same anchor, and it runs upwards just as well.
|
||||||
// It starts on o2: 52g's last click hit o1, and a second one there would count as a double click.
|
// It starts on g2: 52g's last click hit g1, and a second one there would count as a double click.
|
||||||
const clickRow = async (id, modifiers) => {
|
const clickRow = async (id, modifiers) => {
|
||||||
const at = await rowAt(id);
|
const at = await rowAt(id);
|
||||||
await click(at.x, at.y, 'left', modifiers);
|
await click(at.x, at.y, 'left', modifiers);
|
||||||
};
|
};
|
||||||
await clickRow('o2');
|
await clickRow('g2');
|
||||||
await clickRow('o5', { shift: true });
|
await clickRow('g5', { shift: true });
|
||||||
const down = await selected();
|
const down = await selected();
|
||||||
const primaryAfter = (await json('__gm.state.selection'))?.objectId;
|
const primaryAfter = (await json('__gm.state.selection'))?.objectId;
|
||||||
await clickRow('o1', { shift: true });
|
await clickRow('g1', { shift: true });
|
||||||
const up = await selected();
|
const up = await selected();
|
||||||
check(
|
check(
|
||||||
'Shift-click selects the rows in between',
|
'Shift-click selects the rows in between',
|
||||||
same([...down].sort(), ['o2', 'o3', 'o5']) &&
|
same([...down].sort(), ['g2', 'g3', 'g5']) &&
|
||||||
primaryAfter === 'o5' &&
|
primaryAfter === 'g5' &&
|
||||||
same([...up].sort(), ['o1', 'o2']) &&
|
same([...up].sort(), ['g1', 'g2']) &&
|
||||||
same(await rowIds(), ['o1', 'o2', 'o3', 'o5']),
|
same(await rowIds(), ['g1', 'g2', 'g3', 'g5']),
|
||||||
JSON.stringify({ down, primaryAfter, up, rows: await rowIds() }),
|
JSON.stringify({ down, primaryAfter, up, rows: await rowIds() }),
|
||||||
);
|
);
|
||||||
await evaluate('__gm.state.select(null); __gm.hierarchyCollapseAll()');
|
await evaluate('__gm.state.select(null); __gm.hierarchyCollapseAll()');
|
||||||
|
|
||||||
// 52i. A group "Pile" of o5 alone; its folder stands in the icon column of the objects, the
|
// 52i. A group "Pile" of g5 alone; its folder stands in the icon column of the objects, the
|
||||||
// count left of it. Then o1 and o2 join it through the dialog's group dropdown.
|
// count left of it. Then g1 and g2 join it through the dialog's group dropdown.
|
||||||
await select({ kind: 'object', holderId: 'scene1', objectId: 'o5', objectIds: ['o5'] });
|
await select({ kind: 'object', holderId: SCENE, objectId: 'g5', objectIds: ['g5'] });
|
||||||
const lone = await rowAt('o5');
|
const lone = await rowAt('g5');
|
||||||
await click(lone.x, lone.y, 'right');
|
await click(lone.x, lone.y, 'right');
|
||||||
await clickText('Group…');
|
await clickText('Group…');
|
||||||
await typeText('Pile');
|
await typeText('Pile');
|
||||||
@@ -182,7 +206,7 @@ export async function hierarchyGroups(t) {
|
|||||||
const pileRow = `group:${pile?.id}`;
|
const pileRow = `group:${pile?.id}`;
|
||||||
const folder = await json(`__gm.hierarchyIcon(${JSON.stringify(pileRow)})`);
|
const folder = await json(`__gm.hierarchyIcon(${JSON.stringify(pileRow)})`);
|
||||||
const count = await json(`__gm.hierarchyHint(${JSON.stringify(pileRow)})`);
|
const count = await json(`__gm.hierarchyHint(${JSON.stringify(pileRow)})`);
|
||||||
const objectIcon = await json('__gm.hierarchyIcon("o1")');
|
const objectIcon = await json('__gm.hierarchyIcon("g1")');
|
||||||
check(
|
check(
|
||||||
"a group's folder stands in the objects' icon column, its count left of it",
|
"a group's folder stands in the objects' icon column, its count left of it",
|
||||||
folder !== null &&
|
folder !== null &&
|
||||||
@@ -192,8 +216,8 @@ export async function hierarchyGroups(t) {
|
|||||||
count.x < folder.x - 8,
|
count.x < folder.x - 8,
|
||||||
JSON.stringify({ folder, objectIcon, count }),
|
JSON.stringify({ folder, objectIcon, count }),
|
||||||
);
|
);
|
||||||
await select({ kind: 'object', holderId: 'scene1', objectId: 'o1', objectIds: ['o1', 'o2'] });
|
await select({ kind: 'object', holderId: SCENE, objectId: 'g1', objectIds: ['g1', 'g2'] });
|
||||||
const joiner = await rowAt('o1');
|
const joiner = await rowAt('g1');
|
||||||
await click(joiner.x, joiner.y, 'right');
|
await click(joiner.x, joiner.y, 'right');
|
||||||
await clickText('Group 2 objects…');
|
await clickText('Group 2 objects…');
|
||||||
await clickText('New group');
|
await clickText('New group');
|
||||||
@@ -210,15 +234,15 @@ export async function hierarchyGroups(t) {
|
|||||||
joined.groups?.length === 1 &&
|
joined.groups?.length === 1 &&
|
||||||
same(
|
same(
|
||||||
joined.objects.filter((o) => o.groupId === pile?.id).map((o) => o.id),
|
joined.objects.filter((o) => o.groupId === pile?.id).map((o) => o.id),
|
||||||
['o1', 'o2', 'o5'],
|
['g1', 'g2', 'g5'],
|
||||||
),
|
),
|
||||||
JSON.stringify({ choices, groups: joined.groups, objects: joined.objects.map((o) => [o.id, o.groupId]) }),
|
JSON.stringify({ choices, groups: joined.groups, objects: joined.objects.map((o) => [o.id, o.groupId]) }),
|
||||||
);
|
);
|
||||||
await undo();
|
await undo();
|
||||||
|
|
||||||
// 52j. Dragging one row of a multi-selection takes the others along – here onto the group row.
|
// 52j. Dragging one row of a multi-selection takes the others along – here onto the group row.
|
||||||
await select({ kind: 'object', holderId: 'scene1', objectId: 'o1', objectIds: ['o1', 'o2'] });
|
await select({ kind: 'object', holderId: SCENE, objectId: 'g1', objectIds: ['g1', 'g2'] });
|
||||||
const grab = await rowAt('o2');
|
const grab = await rowAt('g2');
|
||||||
const into = await rowAt(pileRow);
|
const into = await rowAt(pileRow);
|
||||||
await drag(grab.x, grab.y, into.x, into.y);
|
await drag(grab.x, grab.y, into.x, into.y);
|
||||||
const dragged = await scene();
|
const dragged = await scene();
|
||||||
@@ -227,18 +251,23 @@ export async function hierarchyGroups(t) {
|
|||||||
const undone = await scene();
|
const undone = await scene();
|
||||||
check(
|
check(
|
||||||
'dragging a selected row moves the whole selection, in one undo step',
|
'dragging a selected row moves the whole selection, in one undo step',
|
||||||
same(inPile, ['o5', 'o1', 'o2']) &&
|
same(inPile, ['g5', 'g1', 'g2']) &&
|
||||||
same(
|
same(
|
||||||
undone.objects.filter((o) => o.groupId === pile?.id).map((o) => o.id),
|
undone.objects.filter((o) => o.groupId === pile?.id).map((o) => o.id),
|
||||||
['o5'],
|
['g5'],
|
||||||
),
|
),
|
||||||
JSON.stringify({ inPile, undone: undone.objects.map((o) => [o.id, o.groupId]) }),
|
JSON.stringify({ inPile, undone: undone.objects.map((o) => [o.id, o.groupId]) }),
|
||||||
);
|
);
|
||||||
await undo();
|
await undo();
|
||||||
check(
|
check(
|
||||||
'undo takes the group of o5 back too',
|
'undo takes the group of g5 back too',
|
||||||
(await scene()).groups === undefined,
|
(await scene()).groups === undefined,
|
||||||
JSON.stringify((await scene()).groups),
|
JSON.stringify((await scene()).groups),
|
||||||
);
|
);
|
||||||
await evaluate('__gm.state.select(null); __gm.hierarchyCollapseAll()');
|
await evaluate('__gm.state.select(null); __gm.hierarchyCollapseAll()');
|
||||||
|
await t.mutate(`
|
||||||
|
const p = __gm.state.project;
|
||||||
|
p.activeSceneId = ${JSON.stringify(home)};
|
||||||
|
p.assets = p.assets.filter((a) => a.id !== ${JSON.stringify(SCENE)})`);
|
||||||
|
await t.saved();
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user