@@ -7,10 +7,15 @@ import type { DomEditSelection } from "../components/editor/domEditing";
77import { usePlayerStore } from "../player/store/playerStore" ;
88import { useAppHotkeys } from "./useAppHotkeys" ;
99import { mountReactHarness } from "./domSelectionTestHarness" ;
10+ import { trackStudioEvent } from "../utils/studioTelemetry" ;
11+
12+ vi . mock ( "../utils/studioTelemetry" , ( ) => ( { trackStudioEvent : vi . fn ( ) } ) ) ;
1013
1114( globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT : boolean } ) . IS_REACT_ACT_ENVIRONMENT = true ;
1215
1316const domDelete = vi . fn ( async ( ) => undefined ) ;
17+ const historyUndo = vi . fn ( async ( ) => ( { ok : false } ) ) ;
18+ const historyRedo = vi . fn ( async ( ) => ( { ok : false } ) ) ;
1419let root : Root | null = null ;
1520let sync : ( ( iframe : HTMLIFrameElement | null ) => void ) | null = null ;
1621
@@ -35,8 +40,8 @@ function Harness() {
3540 domEditSelectionRef : selectionRef ,
3641 clearDomSelectionRef : useRef < ( ) => void > ( ( ) => undefined ) ,
3742 editHistory : {
38- undo : vi . fn ( async ( ) => ( { ok : false } ) ) ,
39- redo : vi . fn ( async ( ) => ( { ok : false } ) ) ,
43+ undo : historyUndo ,
44+ redo : historyRedo ,
4045 state : { undo : [ ] , redo : [ ] } ,
4146 } ,
4247 readOptionalProjectFile : vi . fn ( async ( ) => "" ) ,
@@ -62,6 +67,9 @@ afterEach(() => {
6267 document . body . innerHTML = "" ;
6368 usePlayerStore . getState ( ) . reset ( ) ;
6469 domDelete . mockClear ( ) ;
70+ historyUndo . mockClear ( ) ;
71+ historyRedo . mockClear ( ) ;
72+ vi . mocked ( trackStudioEvent ) . mockClear ( ) ;
6573} ) ;
6674
6775describe ( "preview iframe hotkey forwarding" , ( ) => {
@@ -87,4 +95,61 @@ describe("preview iframe hotkey forwarding", () => {
8795
8896 expect ( domDelete ) . toHaveBeenCalledTimes ( 1 ) ;
8997 } ) ;
98+
99+ function mountWithPreview ( ) : Window & typeof globalThis {
100+ root = mountReactHarness ( < Harness /> ) ;
101+ const iframe = document . createElement ( "iframe" ) ;
102+ document . body . append ( iframe ) ;
103+ act ( ( ) => sync ?.( iframe ) ) ;
104+ const inner = iframe . contentWindow as ( Window & typeof globalThis ) | null ;
105+ if ( ! inner ) throw new Error ( "expected an iframe window" ) ;
106+ return inner ;
107+ }
108+
109+ async function press ( inner : Window & typeof globalThis , init : KeyboardEventInit ) {
110+ await act ( async ( ) => {
111+ inner . document . body . dispatchEvent (
112+ new inner . KeyboardEvent ( "keydown" , { ...init , bubbles : true } ) ,
113+ ) ;
114+ } ) ;
115+ }
116+
117+ it ( "takes one tracked undo step per Cmd+Z pressed inside the preview" , async ( ) => {
118+ await press ( mountWithPreview ( ) , { key : "z" , ctrlKey : true } ) ;
119+
120+ expect ( historyUndo ) . toHaveBeenCalledTimes ( 1 ) ;
121+ expect ( trackStudioEvent ) . toHaveBeenCalledTimes ( 1 ) ;
122+ expect ( trackStudioEvent ) . toHaveBeenCalledWith ( "keyboard_shortcut" , { action : "undo" } ) ;
123+ } ) ;
124+
125+ it . each ( [
126+ [ "Shift+Cmd+Z" , { key : "Z" , metaKey : true , shiftKey : true } ] ,
127+ [ "Ctrl+Y" , { key : "y" , ctrlKey : true } ] ,
128+ ] ) ( "takes one tracked redo step per %s pressed inside the preview" , async ( _ , init ) => {
129+ await press ( mountWithPreview ( ) , init ) ;
130+
131+ expect ( historyRedo ) . toHaveBeenCalledTimes ( 1 ) ;
132+ expect ( trackStudioEvent ) . toHaveBeenCalledTimes ( 1 ) ;
133+ expect ( trackStudioEvent ) . toHaveBeenCalledWith ( "keyboard_shortcut" , { action : "redo" } ) ;
134+ } ) ;
135+
136+ it ( "stops handling preview keys once the app unmounts" , async ( ) => {
137+ root = mountReactHarness ( < Harness /> ) ;
138+ const iframe = document . createElement ( "iframe" ) ;
139+ document . body . append ( iframe ) ;
140+ const inner = iframe . contentWindow as ( Window & typeof globalThis ) | null ;
141+ if ( ! inner ) throw new Error ( "expected an iframe window" ) ;
142+ const add = vi . spyOn ( inner , "addEventListener" ) ;
143+ const remove = vi . spyOn ( inner , "removeEventListener" ) ;
144+ act ( ( ) => sync ?.( iframe ) ) ;
145+ act ( ( ) => root ?. unmount ( ) ) ;
146+ root = null ;
147+
148+ await press ( inner , { key : "z" , ctrlKey : true } ) ;
149+
150+ expect ( historyUndo ) . not . toHaveBeenCalled ( ) ;
151+ // happy-dom removes a listener whatever its capture flag; a browser does not, so pin the flag.
152+ expect ( add ) . toHaveBeenCalled ( ) ;
153+ expect ( remove . mock . calls ) . toEqual ( add . mock . calls ) ;
154+ } ) ;
90155} ) ;
0 commit comments