2026-10-04 02:05:22 -04:00
// 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' ) ;
2026-10-04 02:14:02 -04:00
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' ) ) ;
let keys = m ( ) ;
for ( let i = 0 ; i < 1000 ; i ++ ) keys = cljs . core . assoc ( keys , i , v ( i , 0 ) ) ;
for ( const channel of [ 'pos' , 'scale' ] ) c = cljs . core . assoc _in ( c , v ( k ( 'symbols' ) , k ( 'main' ) , k ( 'nodes' ) , k ( 'a' ) , k ( 'channels' ) , v ( k ( 'xform' ) , k ( channel ) ) ) , arthur . domain . channel . keyed ( keys , k ( 'linear' ) ) ) ;
c = cljs . core . assoc _in ( c , v ( k ( 'symbols' ) , k ( 'main' ) , k ( 'frames' ) ) , 1200 ) ;
const id = arthur . footage . store . install _BANG _ ( m ( k ( 'clip' ) , c , k ( 'store' ) , m ( ) ) , 'large-selection-browser' ) ;
cljs . core . swap _BANG _ ( re _frame . db . app _db , db => cljs . core . assoc ( db , k ( 'clip/current' ) , id ) ) ;
} ) ( ) ` );
await sleep ( 300 ) ;
assert . equal ( await evaluate ( 'document.querySelectorAll("button.tl-key").length' ) , 2000 ) ;
await evaluate ( ` (() => {
window . shiftCalls = 0 ;
window . originalShifted = arthur . domain . keyframes . shifted ;
arthur . domain . keyframes . shifted = ( ... args ) => { window . shiftCalls ++ ; return window . originalShifted ( ... args ) ; } ;
document . querySelector ( '.tl-tracks' ) . dispatchEvent ( new KeyboardEvent ( 'keydown' , { key : 'a' , metaKey : true , bubbles : true } ) ) ;
} ) ( ) ` );
await sleep ( 150 ) ;
assert . equal ( await evaluate ( 'document.querySelectorAll("button.tl-key.selected").length' ) , 2000 ) ;
assert . equal ( await evaluate ( 'window.shiftCalls' ) , 0 , 'selection performs no group shift calculations per marker' ) ;
await evaluate ( ` (() => {
const key = document . querySelector ( 'button.tl-key' ) , b = key . parentElement . getBoundingClientRect ( ) ;
key . dispatchEvent ( new PointerEvent ( 'pointerdown' , { bubbles : true , pointerId : 90 , clientX : b . left } ) ) ;
key . dispatchEvent ( new PointerEvent ( 'pointermove' , { bubbles : true , pointerId : 90 , clientX : b . left + b . width * 2 / 1200 } ) ) ;
} ) ( ) ` );
await sleep ( 150 ) ;
assert . equal ( await evaluate ( 'window.shiftCalls' ) , 1 , 'large group drag computes shifted positions once for all 2000 markers' ) ;
await evaluate ( ` (() => {
const key = document . querySelector ( 'button.tl-key' ) ;
key . dispatchEvent ( new PointerEvent ( 'pointerup' , { bubbles : true , pointerId : 90 } ) ) ;
arthur . domain . keyframes . shifted = window . originalShifted ;
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' ) ) ) ) ) ) ;
} ) ( ) ` ).then(keys=>assert.equal(keys[0],2,'drop commits before the pointerup handler returns'));
await sleep ( 150 ) ;
assert . equal ( await evaluate ( 'document.querySelectorAll("button.tl-key.selected").length' ) , 2000 , 'drop preserves the entire large selection' ) ;
assert . equal ( await evaluate ( '[...document.querySelectorAll("button.tl-key")].filter(el=>el.style.transform).length' ) , 0 , 'committed keys replace the preview without leftover transforms' ) ;
2026-10-04 02:05:22 -04:00
assert . equal ( errors . length , 0 , JSON . stringify ( errors ) ) ;
2026-10-04 02:14:02 -04:00
console . log ( 'PASS: passive overview ticks; automation editing, group edits, undo, and 2000-key selection performance' ) ;
2026-10-04 02:05:22 -04:00
} 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 ;
}
}