Add multi-selection clipboard commands

This commit is contained in:
Your Name 2026-10-02 16:13:03 -04:00
parent 987e289f89
commit edca82cd5d
10 changed files with 839 additions and 36 deletions

View file

@ -96,6 +96,14 @@ try {
};
const shot = async () => JSON.parse(JSON.stringify(await evaluate('laneSnapshot()'),
(_key, value) => value?.uuid ?? value));
const shortcut = async (key, code, modifiers = 2) => {
const windowsVirtualKeyCode = key.toUpperCase().charCodeAt(0);
await send('Input.dispatchKeyEvent', {type: 'rawKeyDown', key, code, modifiers,
windowsVirtualKeyCode});
await send('Input.dispatchKeyEvent', {type: 'keyUp', key, code, modifiers: 0,
windowsVirtualKeyCode});
await sleep(180);
};
const mainInstances = s => Object.values(s.clip.symbols.main.nodes)
.filter(n => n.kind === 'instance');
const laneSymbols = s => Object.values(s.clip.symbols).filter(sym => sym.display === 'lane');
@ -109,7 +117,7 @@ try {
assert.equal(placed.length, 1, 'new symbol places one instance');
assert.equal(s.clip.symbols[placed[0].source.symbol].display, undefined,
'new symbol remains ordinary');
assert.equal(await evaluate('document.querySelectorAll(".tl-track").length'), 1,
assert.equal(await evaluate('document.querySelectorAll(".tl-track:not(.palette)").length'), 1,
'an ordinary symbol is an ordinary timeline row');
await clickNew('lane');
@ -151,16 +159,17 @@ try {
await send('Input.dispatchMouseEvent', {type: 'mouseMoved', x: drop.sx, y: drop.sy});
await send('Input.dispatchMouseEvent', {type: 'mousePressed', x: drop.sx, y: drop.sy,
button: 'left', buttons: 1, clickCount: 1});
await send('Input.dispatchMouseEvent', {type: 'mouseMoved', x: drop.sx + 12, y: drop.sy,
await send('Input.dispatchMouseEvent', {type: 'mouseMoved', x: drop.sx + 6, y: drop.sy,
button: 'left', buttons: 1});
await sleep(100);
await sleep(60);
await send('Input.dispatchMouseEvent', {type: 'mouseMoved', x: drop.sx + 18, y: drop.sy,
button: 'left', buttons: 1});
await sleep(140);
await send('Input.dispatchMouseEvent', {type: 'mouseMoved', x: drop.tx, y: drop.ty,
button: 'left', buttons: 1});
await sleep(120);
await sleep(300);
assert.equal(await evaluate('document.querySelectorAll(".tl-label.ghost").length'), 0,
'pool drop does not preview an invented lane');
assert.equal(await evaluate('document.querySelectorAll(".tl-cel.ghost").length'), 1,
'pool drop previews inside the existing lane');
await send('Input.dispatchMouseEvent', {type: 'mouseReleased', x: drop.tx, y: drop.ty,
button: 'left', buttons: 0, clickCount: 1});
await sleep(300);
@ -196,8 +205,51 @@ try {
s = await shot();
assert.deepEqual(laneSymbols(s).map(x => Object.keys(x.nodes).length).sort(), [0, 1],
'a clip body moves from one explicit lane to the other');
// Start a clean local document for clipboard and timeline multi-selection.
await evaluate(`re_frame.core.dispatch_sync(cljs.core.vector(
cljs.core.keyword('arthur.events.project/new'))) `);
await sleep(250);
await clickNew('inside');
const clipboardBaseline = (await shot()).clip;
await shortcut('d', 'KeyD');
assert.equal(mainInstances(await shot()).length, 2, 'duplicate copies the selected instance');
await shortcut('z', 'KeyZ');
assert.equal(mainInstances(await shot()).length, 1, 'one undo removes the whole duplicate');
await shortcut('z', 'KeyZ', 10);
assert.equal(mainInstances(await shot()).length, 2, 'one redo restores the duplicate');
assert(await evaluate(`(() => {
const rows = [...document.querySelectorAll('.tl-label')].filter(x => x.draggable);
if (rows.length < 2) return false;
rows[0].click();
rows[1].dispatchEvent(new MouseEvent('click', {bubbles: true, shiftKey: true}));
return true;
})()`), 'two timeline rows are available for multi-selection');
await sleep(200);
assert.equal(await evaluate('document.querySelectorAll(".tl-label.on").length'), 2,
'timeline Shift-click mirrors the two-member selection');
assert(await evaluate(`(() => {
const row = [...document.querySelectorAll('.tl-label')].find(x => x.draggable);
if (!row) return false; row.click(); return true;
})()`), 'a timeline row can select the redone instance');
await sleep(120);
await shortcut('c', 'KeyC');
await shortcut('x', 'KeyX');
assert.equal(mainInstances(await shot()).length, 1, 'cut removes the selection');
await shortcut('z', 'KeyZ');
assert.equal(mainInstances(await shot()).length, 2, 'one undo restores the cut');
await evaluate(`re_frame.core.dispatch_sync(cljs.core.vector(
cljs.core.keyword('arthur.events.ui/aim'), null))`);
await sleep(80);
await shortcut('v', 'KeyV');
assert.equal(mainInstances(await shot()).length, 3, 'paste uses the copied snapshot');
await shortcut('z', 'KeyZ');
await shortcut('z', 'KeyZ');
assert.equal(mainInstances(await shot()).length, 1, 'clipboard edits undo independently');
assert.deepEqual((await shot()).clip, clipboardBaseline,
'clipboard exercises undo back to the byte-for-byte document');
assert.equal(errors.length, 0, JSON.stringify(errors));
console.log('PASS: symbols are ordinary; explicit lanes rename, accept drops, and exchange clips');
console.log('PASS: lanes, mirrored selection, and atomic clipboard commands');
} finally {
if (ws?.readyState === WebSocket.OPEN) ws.close();
chrome.kill('SIGTERM');