@@ -293,7 +293,7 @@ const createCommentsStoreWithFloatingGetter = () => {
293293
294294const mountComponent = async (
295295 superdocStub ,
296- { surfaceManager = null , superdocStore = null , commentsStore = null } = { } ,
296+ { surfaceManager = null , superdocStore = null , commentsStore = null , attachTo = null } = { } ,
297297) => {
298298 superdocStoreStub = superdocStore ?? buildSuperdocStore ( ) ;
299299 commentsStoreStub = commentsStore ?? buildCommentsStore ( ) ;
@@ -303,6 +303,7 @@ const mountComponent = async (
303303 const component = ( await import ( './SuperDoc.vue' ) ) . default ;
304304
305305 return mount ( component , {
306+ ...( attachTo ? { attachTo } : { } ) ,
306307 global : {
307308 components : {
308309 SuperEditor : SuperEditorStub ,
@@ -335,6 +336,7 @@ const mountComponent = async (
335336
336337const createSuperdocStub = ( ) => {
337338 const toolbar = { config : { aiApiKey : 'abc' } , setActiveEditor : vi . fn ( ) , updateToolbarState : vi . fn ( ) } ;
339+ const runtimeMap = new Map ( ) ;
338340 return {
339341 config : {
340342 modules : { comments : { } , ai : { } , toolbar : { } , pdf : { } } ,
@@ -354,6 +356,13 @@ const createSuperdocStub = () => {
354356 broadcastPdfDocumentReady : vi . fn ( ) ,
355357 broadcastSidebarToggle : vi . fn ( ) ,
356358 setActiveEditor : vi . fn ( ) ,
359+ registerEditorRuntime : vi . fn ( ( runtime ) => {
360+ if ( runtime ?. id ) runtimeMap . set ( runtime . id , runtime ) ;
361+ } ) ,
362+ unregisterEditorRuntime : vi . fn ( ( runtimeId ) => runtimeMap . delete ( runtimeId ) ) ,
363+ setActiveRuntime : vi . fn ( ) ,
364+ getActiveRuntime : vi . fn ( ( ) => null ) ,
365+ activateRuntimeFromEventTarget : vi . fn ( ( ) => false ) ,
357366 lockSuperdoc : vi . fn ( ) ,
358367 emit : vi . fn ( ) ,
359368 listeners : vi . fn ( ) ,
@@ -362,6 +371,32 @@ const createSuperdocStub = () => {
362371 } ;
363372} ;
364373
374+ const createRuntimeEditorMock = ( documentId = 'doc-1' ) => ( {
375+ options : { documentId } ,
376+ editorVersion : 1 ,
377+ state : {
378+ doc : { textBetween : vi . fn ( ( ) => '' ) } ,
379+ selection : { from : 0 , to : 0 , empty : true } ,
380+ } ,
381+ commands : {
382+ insertContent : vi . fn ( ( ) => true ) ,
383+ } ,
384+ view : { focus : vi . fn ( ) } ,
385+ focus : vi . fn ( ) ,
386+ on : vi . fn ( ) ,
387+ off : vi . fn ( ) ,
388+ exportDocx : vi . fn ( async ( ) => new ArrayBuffer ( 0 ) ) ,
389+ } ) ;
390+
391+ const createPresentationEditorMock = ( ) => ( {
392+ focus : vi . fn ( ) ,
393+ setZoom : vi . fn ( ) ,
394+ setContextMenuDisabled : vi . fn ( ) ,
395+ on : vi . fn ( ) ,
396+ off : vi . fn ( ) ,
397+ getCommentBounds : vi . fn ( ( ) => ( { } ) ) ,
398+ } ) ;
399+
365400const createFloatingCommentsSchema = ( ) =>
366401 new Schema ( {
367402 nodes : {
@@ -861,6 +896,141 @@ describe('SuperDoc.vue', () => {
861896 expect ( removeEventListenerSpy ) . toHaveBeenCalledWith ( 'keydown' , expect . any ( Function ) , true ) ;
862897 } ) ;
863898
899+ it ( 'routes product focus/pointer hits through activateRuntimeFromEventTarget and cleans up on unmount' , async ( ) => {
900+ const superdocStub = createSuperdocStub ( ) ;
901+ const wrapper = await mountComponent ( superdocStub , { attachTo : document . body } ) ;
902+ await nextTick ( ) ;
903+
904+ const subDocument = wrapper . element . querySelector ( '.superdoc__sub-document' ) ;
905+ expect ( subDocument ) . not . toBeNull ( ) ;
906+ const target = document . createElement ( 'span' ) ;
907+ subDocument . appendChild ( target ) ;
908+
909+ // Real product DOM events inside the marked runtime root activate the owning
910+ // runtime through the shell helper — no painter inspection or dispatch here.
911+ target . dispatchEvent ( new FocusEvent ( 'focusin' , { bubbles : true } ) ) ;
912+ expect ( superdocStub . activateRuntimeFromEventTarget ) . toHaveBeenCalledWith ( target , 'focusin' ) ;
913+
914+ target . dispatchEvent ( new Event ( 'pointerdown' , { bubbles : true } ) ) ;
915+ expect ( superdocStub . activateRuntimeFromEventTarget ) . toHaveBeenCalledWith ( target , 'pointerdown' ) ;
916+
917+ target . dispatchEvent ( new Event ( 'mousedown' , { bubbles : true } ) ) ;
918+ expect ( superdocStub . activateRuntimeFromEventTarget ) . toHaveBeenCalledWith ( target , 'mousedown' ) ;
919+
920+ const callsBeforeUnmount = superdocStub . activateRuntimeFromEventTarget . mock . calls . length ;
921+
922+ wrapper . unmount ( ) ;
923+
924+ // After unmount the capture listeners are gone: further hits do not route.
925+ document . body . dispatchEvent ( new FocusEvent ( 'focusin' , { bubbles : true } ) ) ;
926+ document . body . dispatchEvent ( new Event ( 'pointerdown' , { bubbles : true } ) ) ;
927+ expect ( superdocStub . activateRuntimeFromEventTarget . mock . calls . length ) . toBe ( callsBeforeUnmount ) ;
928+ } ) ;
929+
930+ it ( 'runtime hit routing outside any marked root delegates to the registry no-op (does not throw)' , async ( ) => {
931+ const superdocStub = createSuperdocStub ( ) ;
932+ const wrapper = await mountComponent ( superdocStub , { attachTo : document . body } ) ;
933+ await nextTick ( ) ;
934+
935+ // An event outside the document area still routes to the helper, which
936+ // resolves no owning runtime and is a safe no-op (returns false).
937+ document . body . dispatchEvent ( new Event ( 'pointerdown' , { bubbles : true } ) ) ;
938+ expect ( superdocStub . activateRuntimeFromEventTarget ) . toHaveBeenCalledWith ( document . body , 'pointerdown' ) ;
939+
940+ wrapper . unmount ( ) ;
941+ } ) ;
942+
943+ it ( 'skips v1 runtime registration when the document host root is unavailable' , async ( ) => {
944+ const superdocStub = createSuperdocStub ( ) ;
945+ const warnSpy = vi . spyOn ( console , 'warn' ) . mockImplementation ( ( ) => { } ) ;
946+ const wrapper = await mountComponent ( superdocStub ) ;
947+ await nextTick ( ) ;
948+
949+ const doc = superdocStoreStub . documents . value [ 0 ] ;
950+ wrapper . vm . $ . setupState . setSubDocumentRoot ( doc , null ) ;
951+
952+ const options = wrapper . findComponent ( SuperEditorStub ) . props ( 'options' ) ;
953+ options . onCreate ( { editor : createRuntimeEditorMock ( 'doc-1' ) } ) ;
954+
955+ expect ( warnSpy ) . toHaveBeenCalledWith (
956+ '[SuperDoc] v1 runtime host root unavailable; skipping runtime registration for' ,
957+ 'doc-1' ,
958+ ) ;
959+ expect ( superdocStub . registerEditorRuntime ) . not . toHaveBeenCalled ( ) ;
960+
961+ wrapper . unmount ( ) ;
962+ } ) ;
963+
964+ it ( 'registers a v1 runtime, attaches the presentation editor, and activates it on focus' , async ( ) => {
965+ const superdocStub = createSuperdocStub ( ) ;
966+ const wrapper = await mountComponent ( superdocStub ) ;
967+ await nextTick ( ) ;
968+
969+ const editor = createRuntimeEditorMock ( 'doc-1' ) ;
970+ const presentationEditor = createPresentationEditorMock ( ) ;
971+ const editorComponent = wrapper . findComponent ( SuperEditorStub ) ;
972+ const options = editorComponent . props ( 'options' ) ;
973+ superdocStoreStub . documents . value [ 0 ] . setPresentationEditor = vi . fn ( ) ;
974+
975+ options . onCreate ( { editor } ) ;
976+ const runtime = superdocStub . registerEditorRuntime . mock . calls . at ( - 1 ) [ 0 ] ;
977+
978+ expect ( runtime . documentId ) . toBe ( 'doc-1' ) ;
979+ expect ( superdocStub . setActiveRuntime ) . toHaveBeenLastCalledWith ( runtime . id , 'v1-editor-create' ) ;
980+
981+ editorComponent . vm . $emit ( 'editor-ready' , { editor, presentationEditor } ) ;
982+ await nextTick ( ) ;
983+ expect ( runtime . getSnapshot ( ) . state ) . toBe ( 'editing-ready' ) ;
984+ expect ( presentationEditor . on ) . toHaveBeenCalledWith ( 'paginationUpdate' , expect . any ( Function ) ) ;
985+ expect ( presentationEditor . setContextMenuDisabled ) . toHaveBeenCalledWith ( false ) ;
986+
987+ superdocStub . setActiveRuntime . mockClear ( ) ;
988+ options . onFocus ( { editor } ) ;
989+ expect ( superdocStub . setActiveRuntime ) . toHaveBeenCalledWith ( runtime . id , 'v1-editor-focus' ) ;
990+
991+ runtime . dispose ( ) ;
992+ expect ( superdocStub . unregisterEditorRuntime ) . toHaveBeenCalledWith ( runtime . id ) ;
993+
994+ wrapper . unmount ( ) ;
995+ } ) ;
996+
997+ it ( 'disposes an existing v1 runtime before registering a replacement for the same document' , async ( ) => {
998+ const superdocStub = createSuperdocStub ( ) ;
999+ const wrapper = await mountComponent ( superdocStub ) ;
1000+ await nextTick ( ) ;
1001+
1002+ const options = wrapper . findComponent ( SuperEditorStub ) . props ( 'options' ) ;
1003+ options . onCreate ( { editor : createRuntimeEditorMock ( 'doc-1' ) } ) ;
1004+ const firstRuntime = superdocStub . registerEditorRuntime . mock . calls . at ( - 1 ) [ 0 ] ;
1005+ const disposeSpy = vi . spyOn ( firstRuntime , 'dispose' ) ;
1006+
1007+ options . onCreate ( { editor : createRuntimeEditorMock ( 'doc-1' ) } ) ;
1008+ const secondRuntime = superdocStub . registerEditorRuntime . mock . calls . at ( - 1 ) [ 0 ] ;
1009+
1010+ expect ( disposeSpy ) . toHaveBeenCalledTimes ( 1 ) ;
1011+ expect ( superdocStub . unregisterEditorRuntime ) . toHaveBeenCalledWith ( firstRuntime . id ) ;
1012+ expect ( superdocStub . registerEditorRuntime ) . toHaveBeenCalledTimes ( 2 ) ;
1013+ expect ( secondRuntime . id ) . not . toBe ( firstRuntime . id ) ;
1014+
1015+ wrapper . unmount ( ) ;
1016+ } ) ;
1017+
1018+ it ( 'disposes registered v1 runtimes on component unmount' , async ( ) => {
1019+ const superdocStub = createSuperdocStub ( ) ;
1020+ const wrapper = await mountComponent ( superdocStub ) ;
1021+ await nextTick ( ) ;
1022+
1023+ const options = wrapper . findComponent ( SuperEditorStub ) . props ( 'options' ) ;
1024+ options . onCreate ( { editor : createRuntimeEditorMock ( 'doc-1' ) } ) ;
1025+ const runtime = superdocStub . registerEditorRuntime . mock . calls . at ( - 1 ) [ 0 ] ;
1026+ const disposeSpy = vi . spyOn ( runtime , 'dispose' ) ;
1027+
1028+ wrapper . unmount ( ) ;
1029+
1030+ expect ( disposeSpy ) . toHaveBeenCalledTimes ( 1 ) ;
1031+ expect ( superdocStub . unregisterEditorRuntime ) . toHaveBeenCalledWith ( runtime . id ) ;
1032+ } ) ;
1033+
8641034 it ( 'forwards configured passwords to SuperEditor options' , async ( ) => {
8651035 const superdocStub = createSuperdocStub ( ) ;
8661036 superdocStub . config . password = 'top-secret' ;
0 commit comments