@@ -4,6 +4,10 @@ import React, { act } from "react";
44import { afterEach , describe , expect , it , vi } from "vitest" ;
55import { usePlayerStore } from "../player/store/playerStore" ;
66import type { TimelineElement } from "../player/store/timelineElement" ;
7+ import {
8+ beginStudioManualEditGesture ,
9+ endStudioManualEditGesture ,
10+ } from "../components/editor/manualEdits" ;
711import { mountReactHarness } from "./domSelectionTestHarness" ;
812import { usePreviewPersistence } from "./usePreviewPersistence" ;
913
@@ -132,3 +136,82 @@ describe("undo that re-runs the top-level script over an element of a nested com
132136 expect ( nwid . getAttribute ( "style" ) ) . toBe ( "left: 40px; width: 337px" ) ;
133137 } ) ;
134138} ) ;
139+
140+ describe ( "undo that lands while a newer gesture holds an element" , ( ) => {
141+ const page = ( left : number ) =>
142+ `<html><body><div id="root" data-composition-id="root"><div id="box" data-hf-id="hf-b" style="left: ${ left } px"></div></div></body></html>` ;
143+ const files = { "index.html" : { previous : page ( 0 ) , restored : page ( 200 ) } } ;
144+
145+ function heldPreview ( held = true ) {
146+ const doc = document . implementation . createHTMLDocument ( "" ) ;
147+ doc . head . innerHTML = `<meta name="hf-scene-parts" content='{"shared":"s1","scenes":{}}'>` ;
148+ doc . body . innerHTML = `<div id="root" data-composition-id="root"><div id="box" data-hf-id="hf-b" style="left: 90px"></div></div>` ;
149+ const box = doc . getElementById ( "box" ) ! ;
150+ if ( held ) beginStudioManualEditGesture ( box , "move" ) ;
151+ const contentWindow = {
152+ __hfForceTimelineRebind : vi . fn ( ) ,
153+ __timelines : { } ,
154+ __player : { getTime : ( ) => 1 , seek : vi . fn ( ) } ,
155+ __hfStudioManualEditsApply : vi . fn ( ) ,
156+ } ;
157+ const iframe = { contentWindow, contentDocument : doc } as unknown as HTMLIFrameElement ;
158+ const reloadPreview = vi . fn ( ) ;
159+ let hook : ReturnType < typeof usePreviewPersistence > | null = null ;
160+ function Harness ( ) {
161+ hook = usePreviewPersistence ( {
162+ showToast : ( ) => { } ,
163+ readOptionalProjectFile : async ( ) => "" ,
164+ writeProjectFile : async ( ) => { } ,
165+ recordEdit : async ( ) => { } ,
166+ previewIframeRef : { current : iframe } ,
167+ activeCompPathRef : { current : "index.html" } ,
168+ reloadPreview,
169+ } ) ;
170+ return null ;
171+ }
172+ mountReactHarness ( < Harness /> ) ;
173+ usePlayerStore . getState ( ) . setSelectedElementId ( "box" ) ;
174+ const sharedPart = ( ) =>
175+ JSON . parse ( doc . querySelector < HTMLMetaElement > ( 'meta[name="hf-scene-parts"]' ) ! . content ) . shared ;
176+ return { box, win : contentWindow , reloadPreview, sharedPart, hook : ( ) => hook ! } ;
177+ }
178+
179+ it ( "leaves the held element where the gesture drew it and reloads the preview once it ends" , async ( ) => {
180+ const { box, win, reloadPreview, sharedPart, hook } = heldPreview ( ) ;
181+
182+ await act ( async ( ) => hook ( ) . syncHistoryPreviewAfterApply ( { paths : [ "index.html" ] , files } ) ) ;
183+
184+ expect ( box . style . left ) . toBe ( "90px" ) ;
185+ expect ( win . __player . seek ) . not . toHaveBeenCalled ( ) ;
186+ expect ( win . __hfStudioManualEditsApply ) . not . toHaveBeenCalled ( ) ;
187+ expect ( reloadPreview ) . not . toHaveBeenCalled ( ) ;
188+ endStudioManualEditGesture ( box ) ;
189+ expect ( reloadPreview ) . toHaveBeenCalledTimes ( 1 ) ;
190+ // The scene swap must load the restored file again, not keep the part it showed before the undo.
191+ expect ( sharedPart ( ) ) . toBe ( "" ) ;
192+ expect ( usePlayerStore . getState ( ) . selectedElementId ) . toBe ( "box" ) ;
193+ } ) ;
194+
195+ it ( "does not paint a predicted undo under the gesture" , ( ) => {
196+ const { box, win, hook } = heldPreview ( ) ;
197+
198+ expect ( hook ( ) . showHistoryRestoreNow ( files ) ) . toBeNull ( ) ;
199+ expect ( box . style . left ) . toBe ( "90px" ) ;
200+ expect ( win . __player . seek ) . not . toHaveBeenCalled ( ) ;
201+ } ) ;
202+
203+ it ( "puts a refused predicted undo back through the preview reload once a gesture took the element" , async ( ) => {
204+ const { box, win, reloadPreview, hook } = heldPreview ( false ) ;
205+ const putBack = hook ( ) . showHistoryRestoreNow ( files ) ! ;
206+ beginStudioManualEditGesture ( box , "move" ) ;
207+ win . __player . seek . mockClear ( ) ;
208+
209+ putBack ( ) ;
210+ await act ( async ( ) => { } ) ;
211+ expect ( reloadPreview ) . not . toHaveBeenCalled ( ) ;
212+ endStudioManualEditGesture ( box ) ;
213+
214+ expect ( reloadPreview ) . toHaveBeenCalledTimes ( 1 ) ;
215+ expect ( win . __player . seek ) . not . toHaveBeenCalled ( ) ;
216+ } ) ;
217+ } ) ;
0 commit comments