Keep overview keyframes passive and edit expanded automation lanes
This commit is contained in:
parent
606055382b
commit
4d441ae606
12 changed files with 856 additions and 69 deletions
|
|
@ -161,3 +161,45 @@
|
|||
(is (nil? (get-in made [:symbols :main :nodes :plate])))
|
||||
(is (nil? (get-in made [:symbols :main :nodes :child])))
|
||||
(is (empty? (clip/problems made)))))
|
||||
|
||||
(defn- reparent-document []
|
||||
(-> (fixture/document)
|
||||
(update-in [:symbols :main] dissoc :display)
|
||||
(assoc-in [:symbols :box] {:id :box :frames 40 :nodes {}})
|
||||
(assoc-in [:symbols :main :nodes :target]
|
||||
(fixture/cel :target :box 0 40 1))))
|
||||
|
||||
(deftest reparenting-a-forest-preserves-relative-times
|
||||
(let [doc (reparent-document)
|
||||
selections [(address :main :a [:a]) (address :main :b [:b])]
|
||||
r (clipboard/move-many doc {} :main selections [:target] 6 0)]
|
||||
(is (nil? (:refused r)))
|
||||
(is (= [0 4] (mapv #(get-in r [:clip :symbols :box :nodes % :time :at]) [:a :b])))
|
||||
(is (nil? (get-in r [:clip :symbols :main :nodes :a])))
|
||||
(is (nil? (get-in r [:clip :symbols :main :nodes :b])))
|
||||
(is (= [[:node :box :a [:target :a]] [:node :box :b [:target :b]]] (:selections r)))))
|
||||
|
||||
(deftest reparenting-refuses-the-entire-forest-on-a-cycle
|
||||
(let [doc (reparent-document)
|
||||
r (clipboard/move-many doc {} :main
|
||||
[(address :main :a [:a]) (address :main :target [:target])]
|
||||
[:target] 6 0)]
|
||||
(is (:refused r))
|
||||
(is (nil? (:clip r)))))
|
||||
|
||||
(deftest reparenting-into-a-lane-refuses-collision-atomically
|
||||
(let [doc (-> (reparent-document)
|
||||
(assoc-in [:symbols :box :display] :lane)
|
||||
(assoc-in [:symbols :box :nodes :occupied] (fixture/cel :occupied :drawing-a 4 4 0)))
|
||||
r (clipboard/move-many doc {} :main
|
||||
[(address :main :a [:a]) (address :main :b [:b])]
|
||||
[:target] 6 0)]
|
||||
(is (:refused r))
|
||||
(is (nil? (:clip r)))))
|
||||
|
||||
(deftest dropping-the-forest-shifts-it-as-a-unit
|
||||
(let [r (clipboard/move-many (reparent-document) {} :main
|
||||
[(address :main :a [:a]) (address :main :b [:b])]
|
||||
[:target] 6 3)]
|
||||
(is (nil? (:refused r)))
|
||||
(is (= [3 7] (mapv #(get-in r [:clip :symbols :box :nodes % :time :at]) [:a :b])))))
|
||||
|
|
|
|||
30
frontend/test/arthur/domain/keyframes_test.cljs
Normal file
30
frontend/test/arthur/domain/keyframes_test.cljs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
(ns arthur.domain.keyframes-test
|
||||
(:require [cljs.test :refer [deftest is]]
|
||||
[arthur.domain.keyframes :as k]))
|
||||
|
||||
(def items [{:sid :main :id :a :channel [:xform :pos] :frame 2 :at 2 :scale 1}
|
||||
{:sid :main :id :a :channel [:xform :pos] :frame 4 :at 4 :scale 1}])
|
||||
(def document {:symbols {:main {:nodes {:a {:channels {[:xform :pos]
|
||||
{:animated? true :interp :linear :keys {2 [2 0] 4 [4 0] 8 [8 0]}
|
||||
:segments {2 :hold}}}}}}}})
|
||||
(defn channel [d] (get-in d [:symbols :main :nodes :a :channels [:xform :pos]]))
|
||||
|
||||
(deftest simultaneous-moves-preserve-values-and-segments
|
||||
(let [c (channel (k/edit-keys document items 2))]
|
||||
(is (= {4 [2 0] 6 [4 0] 8 [8 0]} (:keys c)))
|
||||
(is (= {4 :hold} (:segments c)))))
|
||||
|
||||
(deftest deleting-last-key-keeps-its-value
|
||||
(let [d (k/edit-keys document items nil)
|
||||
last-key (assoc (first items) :frame 8 :at 8)]
|
||||
(is (= {8 [8 0]} (:keys (channel d))))
|
||||
(is (= {:animated? false :value [8 0]} (channel (k/edit-keys d [last-key] nil))))))
|
||||
|
||||
(deftest moving-left-clamps-the-whole-selection
|
||||
(is (= [0 2] (mapv :frame (k/shifted items -10))))
|
||||
(is (= {0 [2 0] 2 [4 0] 8 [8 0]} (:keys (channel (k/edit-keys document items -10))))))
|
||||
|
||||
(deftest retimed-keys-snap-in-their-own-clock
|
||||
(let [key (assoc (first items) :scale 2 :at 14)]
|
||||
(is (= 3 (:frame (first (k/shifted [key] 2)))))
|
||||
(is (= 16 (:at (first (k/shifted [key] 2)))))))
|
||||
|
|
@ -857,3 +857,25 @@
|
|||
{:extent :grow-symbol :remainder-id :rest}))]
|
||||
(is (empty? (symbol/overlaps (get-in mixed [:symbols :main]))))
|
||||
(is (empty? (clip/problems mixed))))))
|
||||
|
||||
(deftest moving-a-selection-in-a-lane-is-simultaneous
|
||||
(let [doc (document)
|
||||
moved (:clip (nest/slide-many doc :main [[:a] [:b]] 1))]
|
||||
(is (nil? moved))
|
||||
(is (:refused (nest/slide-many doc :main [[:a] [:b]] 1))
|
||||
"the whole group refuses collision with the unselected insert")))
|
||||
|
||||
(deftest moving-all-lane-clips-preserves-their-spans
|
||||
(let [doc (document)
|
||||
moved (:clip (nest/slide-many doc :main [[:a] [:b] [:insert]] 2))]
|
||||
(is (some? moved))
|
||||
(is (= [2 6 10] (mapv #(get-in moved [:symbols :main :nodes % :time :at]) [:a :b :insert])))
|
||||
(is (= [0 4] (get-in moved [:symbols :main :nodes :a :span])))))
|
||||
|
||||
(deftest a-selected-instance-carries-its-selected-contents-once
|
||||
(let [doc (document)
|
||||
result (nest/slide-many doc :main [[:insert] [:insert :mark]] 2)]
|
||||
(is (nil? (:refused result)))
|
||||
(is (= 10 (get-in result [:clip :symbols :main :nodes :insert :time :at])))
|
||||
(is (= (get-in doc [:symbols :wave :nodes])
|
||||
(get-in result [:clip :symbols :wave :nodes])))))
|
||||
|
|
|
|||
264
frontend/test/browser/automation.mjs
Normal file
264
frontend/test/browser/automation.mjs
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
// Browser smoke test for timeline keyframes and group editing. It uses the in-memory
|
||||
// document and disables project routing, so it performs no server-side write.
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
const url = process.env.ARTHUR_URL ?? 'http://localhost:8778/';
|
||||
const profile = mkdtempSync(join(tmpdir(), 'arthur-timeline-'));
|
||||
const port = 9336;
|
||||
const chrome = spawn(process.env.CHROME ?? '/usr/bin/chromium', [
|
||||
'--headless=new', '--no-sandbox', '--disable-gpu', '--no-first-run',
|
||||
'--no-default-browser-check', '--mute-audio', '--window-size=1440,1000',
|
||||
`--user-data-dir=${profile}`, `--remote-debugging-port=${port}`, url,
|
||||
], { stdio: 'ignore' });
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
let ws;
|
||||
|
||||
try {
|
||||
let target;
|
||||
for (let i = 0; i < 100 && !target; i++) {
|
||||
await sleep(100);
|
||||
try {
|
||||
target = (await fetch(`http://127.0.0.1:${port}/json/list`).then(r => r.json()))
|
||||
.find(t => t.type === 'page' && t.url.startsWith(url));
|
||||
} catch { /* Chromium is still starting. */ }
|
||||
}
|
||||
assert(target, 'browser exposes the editor page');
|
||||
ws = new WebSocket(target.webSocketDebuggerUrl);
|
||||
await new Promise((resolve, reject) => { ws.onopen = resolve; ws.onerror = reject; });
|
||||
|
||||
let serial = 0;
|
||||
const pending = new Map();
|
||||
const errors = [];
|
||||
ws.onmessage = ({ data }) => {
|
||||
const msg = JSON.parse(data);
|
||||
if (msg.method === 'Runtime.exceptionThrown') errors.push(msg.params.exceptionDetails);
|
||||
if (msg.id && pending.has(msg.id)) {
|
||||
const waiting = pending.get(msg.id);
|
||||
pending.delete(msg.id);
|
||||
if (msg.error) waiting.reject(new Error(JSON.stringify(msg.error)));
|
||||
else waiting.resolve(msg.result);
|
||||
}
|
||||
};
|
||||
const send = (method, params = {}) => new Promise((resolve, reject) => {
|
||||
const id = ++serial;
|
||||
pending.set(id, { resolve, reject });
|
||||
ws.send(JSON.stringify({ id, method, params }));
|
||||
});
|
||||
const evaluate = async expression => {
|
||||
const result = await send('Runtime.evaluate', {
|
||||
expression, returnByValue: true, awaitPromise: true,
|
||||
});
|
||||
if (result.exceptionDetails) throw new Error(JSON.stringify(result.exceptionDetails));
|
||||
return result.result.value;
|
||||
};
|
||||
|
||||
await send('Runtime.enable');
|
||||
for (let i = 0; i < 100; i++) {
|
||||
try {
|
||||
if (await evaluate('typeof arthur !== "undefined" && !!document.querySelector("canvas.stage")')) break;
|
||||
} catch (error) {
|
||||
if (!error.message.includes("Cannot find default execution context")) throw error;
|
||||
}
|
||||
await sleep(100);
|
||||
}
|
||||
await evaluate(`(() => {
|
||||
const k = cljs.core.keyword;
|
||||
cljs.core.swap_BANG_(re_frame.db.app_db, db => cljs.core.assoc(db, k('route'), k('local-test')));
|
||||
window.laneSnapshot = () => {
|
||||
const db = cljs.core.deref(re_frame.db.app_db);
|
||||
return cljs.core.clj__GT_js(arthur.footage.store.entry(cljs.core.get(db, k('clip/current'))));
|
||||
};
|
||||
return true;
|
||||
})()`);
|
||||
await sleep(250);
|
||||
|
||||
await evaluate(`(() => {
|
||||
const k = cljs.core.keyword, v = cljs.core.vector, m = cljs.core.hash_map;
|
||||
let c = arthur.domain.clip.blank();
|
||||
const rect = (id, at, keys) => m(k('id'), k(id), k('kind'), k('rect'), k('z'), id,
|
||||
k('time'), m(k('at'), at, k('rate'), 1), k('span'), v(0, 20),
|
||||
k('channels'), keys ? m(v(k('xform'), k('pos')), arthur.domain.channel.keyed(m(2, v(2,0), 4, v(4,0)), k('linear'))) : m());
|
||||
const target = m(k('id'), k('target'), k('kind'), k('instance'), k('z'), 'z',
|
||||
k('source'), m(k('symbol'), k('box')), k('span'), v(0, 60),
|
||||
k('time'), m(k('at'), 0, k('rate'), 1),
|
||||
k('playback'), m(k('in'), 0, k('speed'), 1, k('end'), k('stop')));
|
||||
c = cljs.core.assoc_in(c, v(k('symbols'), k('box')), m(k('id'), k('box'), k('frames'), 60, k('nodes'), m()));
|
||||
c = cljs.core.assoc_in(c, v(k('symbols'), k('main'), k('nodes')), m(k('a'), rect('a',0,true), k('b'), rect('b',4,false), k('target'), target));
|
||||
const id = arthur.footage.store.install_BANG_(m(k('clip'), c, k('store'), m()), 'timeline-browser');
|
||||
const a = v(k('node'),k('main'),k('a'),v(k('a'))), b = v(k('node'),k('main'),k('b'),v(k('b')));
|
||||
cljs.core.swap_BANG_(re_frame.db.app_db, db => cljs.core.assoc(db,
|
||||
k('clip/current'), id, k('paint/revision'), 100,
|
||||
k('ui'), m(k('open'),k('main'),k('selection'), b,k('selections'),v(a,b),k('expanded'),cljs.core.hash_set(v(k('a')))),
|
||||
k('playback'),m(k('frame'),0,k('playing?'),false)));
|
||||
return true;
|
||||
})()`);
|
||||
await sleep(250);
|
||||
const shot = async () => JSON.parse(JSON.stringify((await evaluate('laneSnapshot()')).clip,
|
||||
(key, value) => key === 'channels' && Object.keys(value).length === 0 ? undefined : value));
|
||||
assert.equal(await evaluate('document.querySelectorAll(".tl-track:not(.automation) button.tl-key").length'), 0,
|
||||
'symbol overview keyframes are passive');
|
||||
assert.equal(await evaluate('document.querySelectorAll(".tl-track:not(.automation) .tl-key").length'), 2,
|
||||
'symbol overview still shows keyframe ticks');
|
||||
const initial = await shot();
|
||||
await evaluate(`(() => {
|
||||
const track = document.querySelector('.tl-span.on').closest('.tl-track');
|
||||
track.focus(); track.dispatchEvent(new KeyboardEvent('keydown', {key:'ArrowRight',bubbles:true})); return true;
|
||||
})()`);
|
||||
await sleep(100);
|
||||
assert.equal((await shot()).symbols.main.nodes.a.time.at, 1);
|
||||
assert.equal((await shot()).symbols.main.nodes.b.time.at, 5);
|
||||
await evaluate(`re_frame.core.dispatch_sync(cljs.core.vector(cljs.core.keyword('arthur.events.history/undo')))`);
|
||||
await sleep(100);
|
||||
assert.deepEqual(await shot(), initial, 'group shift undoes atomically');
|
||||
await evaluate(`(() => {
|
||||
const span=document.querySelector('.tl-span.on'), track=span.closest('.tl-track');
|
||||
const box=track.getBoundingClientRect(), x=box.left+box.width*10/120, dx=box.width*2/120;
|
||||
span.dispatchEvent(new PointerEvent('pointerdown',{bubbles:true,pointerId:20,clientX:x}));
|
||||
track.dispatchEvent(new PointerEvent('pointermove',{bubbles:true,pointerId:20,clientX:x+dx}));
|
||||
track.dispatchEvent(new PointerEvent('pointerup',{bubbles:true,pointerId:20,clientX:x+dx}));
|
||||
return true;
|
||||
})()`);
|
||||
await sleep(150);
|
||||
assert.equal((await shot()).symbols.main.nodes.a.time.at, 2, 'drag shifts the first selected node');
|
||||
assert.equal((await shot()).symbols.main.nodes.b.time.at, 6, 'drag shifts the other selected node');
|
||||
assert.equal(await evaluate('document.querySelectorAll(".tl-label.on").length'), 2,
|
||||
'group dragging preserves selection');
|
||||
await evaluate(`re_frame.core.dispatch_sync(cljs.core.vector(cljs.core.keyword('arthur.events.history/undo')))`);
|
||||
await sleep(100);
|
||||
assert.deepEqual(await shot(), initial, 'group dragging undoes atomically');
|
||||
assert.equal(await evaluate('document.querySelectorAll("button.tl-key").length'), 2);
|
||||
await evaluate(`(() => {
|
||||
const keys = [...document.querySelectorAll('button.tl-key')];
|
||||
for (let i=0;i<2;i++) {
|
||||
keys[i].dispatchEvent(new PointerEvent('pointerdown',{bubbles:true,pointerId:i+1,clientX:200+i*20,shiftKey:!!i}));
|
||||
keys[i].dispatchEvent(new PointerEvent('pointerup',{bubbles:true,pointerId:i+1,clientX:200+i*20,shiftKey:!!i}));
|
||||
} return true;
|
||||
})()`);
|
||||
await sleep(100);
|
||||
assert.equal(await evaluate('document.querySelectorAll("button.tl-key.selected").length'), 2);
|
||||
await evaluate(`(() => {
|
||||
const key=document.querySelector('button.tl-key.selected');
|
||||
const box=key.parentElement.getBoundingClientRect(), x=box.left+box.width*2/120;
|
||||
key.dispatchEvent(new PointerEvent('pointerdown',{bubbles:true,pointerId:21,clientX:x}));
|
||||
key.dispatchEvent(new PointerEvent('pointermove',{bubbles:true,pointerId:21,clientX:x+box.width*2/120}));
|
||||
key.dispatchEvent(new PointerEvent('pointerup',{bubbles:true,pointerId:21,clientX:x+box.width*2/120}));
|
||||
return true;
|
||||
})()`);
|
||||
await sleep(100);
|
||||
assert.deepEqual(await evaluate(`(() => {
|
||||
const k=cljs.core.keyword,v=cljs.core.vector;
|
||||
const c=arthur.footage.store.entry(cljs.core.get(cljs.core.deref(re_frame.db.app_db),k('clip/current')));
|
||||
return cljs.core.clj__GT_js(cljs.core.sort(cljs.core.keys(cljs.core.get_in(c,
|
||||
v(k('clip'),k('symbols'),k('main'),k('nodes'),k('a'),k('channels'),v(k('xform'),k('pos')),k('keys'))))));
|
||||
})()`), [4,6], 'drag moves both selected keys without losing colliding source frames');
|
||||
await evaluate(`document.activeElement.dispatchEvent(new KeyboardEvent('keydown',{key:'ArrowRight',bubbles:true}))`);
|
||||
await sleep(100);
|
||||
assert.equal(await evaluate('document.querySelectorAll("button.tl-key.selected").length'), 2);
|
||||
await evaluate(`document.activeElement.dispatchEvent(new KeyboardEvent('keydown',{key:'Delete',bubbles:true}))`);
|
||||
await sleep(100);
|
||||
assert.equal(await evaluate('document.querySelectorAll("button.tl-key").length'), 0);
|
||||
assert((await shot()).symbols.main.nodes.a, 'deleting keys preserves their node');
|
||||
await evaluate(`re_frame.core.dispatch_sync(cljs.core.vector(cljs.core.keyword('arthur.events.history/undo')))`);
|
||||
await evaluate(`re_frame.core.dispatch_sync(cljs.core.vector(cljs.core.keyword('arthur.events.history/undo')))`);
|
||||
await sleep(100);
|
||||
await evaluate(`re_frame.core.dispatch_sync(cljs.core.vector(cljs.core.keyword('arthur.events.history/undo')))`);
|
||||
await sleep(100);
|
||||
assert.deepEqual(await shot(), initial, 'key dragging, nudge and delete undo independently');
|
||||
await evaluate(`(() => {
|
||||
const k=cljs.core.keyword,v=cljs.core.vector;
|
||||
const a=v(k('node'),k('main'),k('a'),v(k('a'))),b=v(k('node'),k('main'),k('b'),v(k('b')));
|
||||
re_frame.core.dispatch_sync(v(k('arthur.events.ui/select-many'),v(a,b)));
|
||||
arthur.ui.drag.row_BANG_(v(k('a')),k('rect'),a,v(a,b));
|
||||
const row=[...document.querySelectorAll('.tl-label')].find(x=>x.textContent.includes('box'));
|
||||
const r=row.getBoundingClientRect();
|
||||
row.dispatchEvent(new DragEvent('drop',{bubbles:true,clientY:r.top+r.height/2,dataTransfer:new DataTransfer()}));
|
||||
return true;
|
||||
})()`);
|
||||
await sleep(150);
|
||||
const moved=await shot();
|
||||
assert(moved.symbols.box.nodes.a && moved.symbols.box.nodes.b, 'row drop reparents the entire group');
|
||||
assert.equal(moved.symbols.box.nodes.b.time.at, 4);
|
||||
assert(!moved.symbols.main.nodes.a && !moved.symbols.main.nodes.b);
|
||||
await evaluate(`re_frame.core.dispatch_sync(cljs.core.vector(cljs.core.keyword('arthur.events.history/undo')))`);
|
||||
await sleep(100);
|
||||
assert.deepEqual(await shot(), initial, 'group reparenting is one undo step');
|
||||
await evaluate(`(() => {
|
||||
const rows=[...document.querySelectorAll('.tl-label')].filter(x=>x.querySelector('.tl-delete') || /box|rect/.test(x.textContent));
|
||||
rows[0].dispatchEvent(new MouseEvent('click',{bubbles:true}));
|
||||
rows.at(-1).dispatchEvent(new MouseEvent('click',{bubbles:true,shiftKey:true}));
|
||||
})()`);
|
||||
await sleep(100);
|
||||
assert.equal(await evaluate('document.querySelectorAll(".tl-label.on").length'), 3, 'Shift selects the intervening row headers');
|
||||
await evaluate(`document.querySelectorAll('.tl-label.on')[1].dispatchEvent(new MouseEvent('click',{bubbles:true,metaKey:true}))`);
|
||||
await sleep(100);
|
||||
assert.equal(await evaluate('document.querySelectorAll(".tl-label.on").length'), 2, 'Command toggles just one row');
|
||||
await evaluate(`(() => {
|
||||
const k=cljs.core.keyword,v=cljs.core.vector,m=cljs.core.hash_map;
|
||||
let c=cljs.core.get(arthur.footage.store.entry(cljs.core.get(cljs.core.deref(re_frame.db.app_db),k('clip/current'))),k('clip'));
|
||||
c=cljs.core.assoc_in(c,v(k('symbols'),k('main'),k('nodes'),k('a'),k('channels'),v(k('xform'),k('scale'))),
|
||||
arthur.domain.channel.keyed(m(2,v(1,1),4,v(2,2)),k('linear')));
|
||||
const id=arthur.footage.store.install_BANG_(m(k('clip'),c,k('store'),m()),'selection-browser');
|
||||
cljs.core.swap_BANG_(re_frame.db.app_db,db=>cljs.core.assoc(db,k('clip/current'),id,k('ui'),
|
||||
m(k('open'),k('main'),k('expanded'),cljs.core.hash_set(v(k('a'))))));
|
||||
})()`);
|
||||
await sleep(200);
|
||||
await evaluate(`(() => {
|
||||
const tracks=document.querySelector('.tl-tracks');
|
||||
const rows=[...tracks.querySelectorAll('.tl-track')].filter(x=>x.querySelectorAll('button.tl-key').length===2);
|
||||
const first=rows[0], last=rows[1], b=first.getBoundingClientRect(), end=last.getBoundingClientRect();
|
||||
first.dispatchEvent(new PointerEvent('pointerdown',{bubbles:true,pointerId:50,clientX:b.left+1,clientY:b.top+1}));
|
||||
tracks.dispatchEvent(new PointerEvent('pointermove',{bubbles:true,pointerId:50,clientX:b.left+b.width*6/120,clientY:end.bottom-1}));
|
||||
tracks.dispatchEvent(new PointerEvent('pointerup',{bubbles:true,pointerId:50,clientX:b.left+b.width*6/120,clientY:end.bottom-1}));
|
||||
})()`);
|
||||
await sleep(100);
|
||||
assert.equal(await evaluate('document.querySelectorAll("button.tl-key.selected").length'), 4, 'marquee selects keys across both automation rows');
|
||||
await evaluate(`document.querySelector('.tl-tracks').dispatchEvent(new KeyboardEvent('keydown',{key:'ArrowRight',bubbles:true}))`);
|
||||
await sleep(100);
|
||||
assert.deepEqual(await evaluate(`(() => {
|
||||
const k=cljs.core.keyword,v=cljs.core.vector;
|
||||
const c=arthur.footage.store.entry(cljs.core.get(cljs.core.deref(re_frame.db.app_db),k('clip/current')));
|
||||
return ['pos','scale'].map(channel=>cljs.core.clj__GT_js(cljs.core.sort(cljs.core.keys(cljs.core.get_in(c,
|
||||
v(k('clip'),k('symbols'),k('main'),k('nodes'),k('a'),k('channels'),v(k('xform'),k(channel)),k('keys')))))));
|
||||
})()`), [[3,5],[3,5]], 'marquee selection nudges keys in both automations together');
|
||||
await evaluate(`document.querySelector('.tl-tracks').dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}))`);
|
||||
await sleep(100);
|
||||
assert.equal(await evaluate('document.querySelectorAll("button.tl-key.selected").length'), 0);
|
||||
await evaluate(`(() => {
|
||||
const rows=[...document.querySelectorAll('.tl-track')].filter(x=>x.querySelectorAll('button.tl-key').length===2);
|
||||
const a=rows[0].querySelector('button.tl-key'), b=rows[1].querySelectorAll('button.tl-key')[1];
|
||||
for (const [key,shift] of [[a,false],[b,true]]) {
|
||||
const box=key.getBoundingClientRect();
|
||||
for (const type of ['pointerdown','pointerup']) key.dispatchEvent(new PointerEvent(type,{bubbles:true,pointerId:70,clientX:box.left,clientY:box.top,shiftKey:shift}));
|
||||
}
|
||||
})()`);
|
||||
await sleep(100);
|
||||
assert.equal(await evaluate('document.querySelectorAll("button.tl-key.selected").length'), 4, 'Shift-click selects a range across automation rows');
|
||||
await evaluate(`(() => {
|
||||
const key=[...document.querySelectorAll('.tl-track')].filter(x=>x.querySelectorAll('button.tl-key').length===2)[1].querySelectorAll('button.tl-key')[1];
|
||||
const box=key.getBoundingClientRect();
|
||||
for (const type of ['pointerdown','pointerup']) key.dispatchEvent(new PointerEvent(type,{bubbles:true,pointerId:71,clientX:box.left,metaKey:true}));
|
||||
})()`);
|
||||
await sleep(100);
|
||||
assert.equal(await evaluate('document.querySelectorAll("button.tl-key.selected").length'), 3, 'Command-click toggles one channel key');
|
||||
await evaluate(`document.querySelector('.tl-tracks').dispatchEvent(new KeyboardEvent('keydown',{key:'a',metaKey:true,bubbles:true}))`);
|
||||
await sleep(100);
|
||||
assert.equal(await evaluate('document.querySelectorAll("button.tl-key.selected").length'), 4, 'Command-A selects all visible keyframes');
|
||||
assert.equal(errors.length, 0, JSON.stringify(errors));
|
||||
console.log('PASS: passive overview ticks; automation keyframe selection, dragging, marquee, keyboard editing, and undo');
|
||||
} finally {
|
||||
if (ws?.readyState === WebSocket.OPEN) ws.close();
|
||||
chrome.kill('SIGTERM');
|
||||
await Promise.race([
|
||||
new Promise(resolve => chrome.once('exit', resolve)),
|
||||
sleep(2000).then(() => { if (chrome.exitCode === null) chrome.kill('SIGKILL'); }),
|
||||
]);
|
||||
try {
|
||||
rmSync(profile, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOTEMPTY') throw error;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue