@@ -3030,13 +3030,76 @@ describe("initSandboxRuntimeModular", () => {
30303030 expect ( seekTimes . length ) . toBeGreaterThan ( beforePlaying ) ;
30313031 player ?. pause ( ) ;
30323032
3033- // (3) Paused + marker cleared (drop/cancel) → the per-frame re- seek resumes .
3033+ // (3) Paused + marker cleared (drop/cancel) → one reconciliation seek runs .
30343034 document . getElementById ( "dragged" ) ?. removeAttribute ( "data-hf-studio-manual-edit-gesture" ) ;
30353035 const beforeResume = seekTimes . length ;
30363036 raf . step ( 16 ) ;
30373037 expect ( seekTimes . length ) . toBeGreaterThan ( beforeResume ) ;
30383038 } ) ;
30393039
3040+ it ( "does not re-seek an unchanged paused timeline on every animation frame" , ( ) => {
3041+ const raf = createManualRaf ( ) ;
3042+ vi . spyOn ( performance , "now" ) . mockImplementation ( ( ) => raf . now ( ) ) ;
3043+ window . requestAnimationFrame = raf . requestAnimationFrame as typeof window . requestAnimationFrame ;
3044+ window . cancelAnimationFrame = raf . cancelAnimationFrame as typeof window . cancelAnimationFrame ;
3045+
3046+ const seekTimes : number [ ] = [ ] ;
3047+ const tl = createMockTimeline ( 5 ) ;
3048+ const origTotalTime = tl . totalTime ;
3049+ tl . totalTime = ( ( time : number , ...rest : unknown [ ] ) => {
3050+ seekTimes . push ( time ) ;
3051+ ( origTotalTime as Function ) . call ( tl , time , ...rest ) ;
3052+ } ) as RuntimeTimelineLike [ "totalTime" ] ;
3053+
3054+ document . body . innerHTML = `
3055+ <div data-composition-id="root" data-duration="5" data-width="1920" data-height="1080"></div>
3056+ ` ;
3057+ window . __timelines = { root : tl } ;
3058+ initSandboxRuntimeModular ( ) ;
3059+
3060+ // The first transport frame reconciles the initial timeline at the paused playhead.
3061+ raf . step ( 16 ) ;
3062+ const afterInitialFrame = seekTimes . length ;
3063+ expect ( afterInitialFrame ) . toBeGreaterThan ( 0 ) ;
3064+
3065+ // No time or timeline change means there is no new frame to render.
3066+ raf . step ( 16 ) ;
3067+ raf . step ( 16 ) ;
3068+ raf . step ( 16 ) ;
3069+ expect ( seekTimes . length ) . toBe ( afterInitialFrame ) ;
3070+
3071+ // An explicit paused seek still renders immediately, then settles again after the transport
3072+ // records the new playhead on its next frame.
3073+ window . __player ?. seek ( 2 ) ;
3074+ expect ( seekTimes . some ( ( time ) => time === 2 ) ) . toBe ( true ) ;
3075+ raf . step ( 16 ) ;
3076+ const afterPausedSeek = seekTimes . length ;
3077+ raf . step ( 16 ) ;
3078+ expect ( seekTimes . length ) . toBe ( afterPausedSeek ) ;
3079+
3080+ // A runtime-data rebuild can replace the timeline without moving the paused playhead. The
3081+ // identity check must render that new object once instead of treating it as the old frame.
3082+ const replacementSeekTimes : number [ ] = [ ] ;
3083+ const replacement = createMockTimeline ( 5 ) ;
3084+ const replacementTotalTime = replacement . totalTime ;
3085+ replacement . totalTime = ( ( time : number , ...rest : unknown [ ] ) => {
3086+ replacementSeekTimes . push ( time ) ;
3087+ ( replacementTotalTime as Function ) . call ( replacement , time , ...rest ) ;
3088+ } ) as RuntimeTimelineLike [ "totalTime" ] ;
3089+ window . __timelines = { root : replacement } ;
3090+ window . __hfForceTimelineRebind ?.( ) ;
3091+ raf . step ( 16 ) ;
3092+ expect ( replacementSeekTimes . length ) . toBeGreaterThan ( 0 ) ;
3093+ const afterReplacementFrame = replacementSeekTimes . length ;
3094+ raf . step ( 16 ) ;
3095+ expect ( replacementSeekTimes . length ) . toBe ( afterReplacementFrame ) ;
3096+
3097+ // Playback still traverses the timeline every frame.
3098+ window . __player ?. play ( ) ;
3099+ raf . step ( 16 ) ;
3100+ expect ( replacementSeekTimes . length ) . toBeGreaterThan ( afterReplacementFrame ) ;
3101+ } ) ;
3102+
30403103 it ( "redraws animated grading from the transport clock only during playback" , ( ) => {
30413104 const raf = createManualRaf ( ) ;
30423105 vi . spyOn ( performance , "now" ) . mockImplementation ( ( ) => raf . now ( ) ) ;
0 commit comments