@@ -14,6 +14,8 @@ interface PapiBackendTestMock {
1414 __mockOpenWebView : jest . Mock ;
1515 __mockSelectProject : jest . Mock ;
1616 __mockGetOpenWebViewDefinition : jest . Mock ;
17+ __mockOnDidOpenWebView : jest . Mock ;
18+ __mockOnDidCloseWebView : jest . Mock ;
1719 __mockLogger : { debug : jest . Mock ; error : jest . Mock ; info : jest . Mock ; warn : jest . Mock } ;
1820}
1921
@@ -30,6 +32,8 @@ function isPapiBackendTestMock(m: unknown): m is PapiBackendTestMock {
3032 '__mockOpenWebView' in m &&
3133 '__mockSelectProject' in m &&
3234 '__mockGetOpenWebViewDefinition' in m &&
35+ '__mockOnDidOpenWebView' in m &&
36+ '__mockOnDidCloseWebView' in m &&
3337 '__mockLogger' in m
3438 ) ;
3539}
@@ -49,6 +53,8 @@ const {
4953 __mockOpenWebView,
5054 __mockSelectProject,
5155 __mockGetOpenWebViewDefinition,
56+ __mockOnDidOpenWebView,
57+ __mockOnDidCloseWebView,
5258 __mockLogger,
5359} = papiBackendMock ;
5460
@@ -63,6 +69,8 @@ describe('main', () => {
6369 __mockOpenWebView . mockResolvedValue ( 'mock-webview-id' ) ;
6470 __mockSelectProject . mockResolvedValue ( undefined ) ;
6571 __mockGetOpenWebViewDefinition . mockResolvedValue ( undefined ) ;
72+ __mockOnDidOpenWebView . mockReturnValue ( jest . fn ( ) ) ;
73+ __mockOnDidCloseWebView . mockReturnValue ( jest . fn ( ) ) ;
6674 } ) ;
6775
6876 describe ( 'activate' , ( ) => {
@@ -104,12 +112,12 @@ describe('main', () => {
104112 ) ;
105113 } ) ;
106114
107- it ( 'adds all three registrations to the activation context' , async ( ) => {
115+ it ( 'adds all five registrations to the activation context' , async ( ) => {
108116 const context = createTestActivationContext ( ) ;
109117
110118 await activate ( context ) ;
111119
112- expect ( context . registrations . unsubscribers . size ) . toBe ( 3 ) ;
120+ expect ( context . registrations . unsubscribers . size ) . toBe ( 5 ) ;
113121 } ) ;
114122
115123 it ( 'logs activation start and finish' , async ( ) => {
@@ -199,6 +207,24 @@ describe('main', () => {
199207 `${ mainWebViewType } provider received request to provide a ${ savedWebView . webViewType } WebView` ,
200208 ) ;
201209 } ) ;
210+
211+ it ( 'falls back to savedWebView.projectId when options is undefined' , async ( ) => {
212+ const context = createTestActivationContext ( ) ;
213+ await activate ( context ) ;
214+
215+ const rawProvider = jest . mocked ( __mockRegisterWebViewProvider ) . mock . calls [ 0 ] ?. [ 1 ] ;
216+ if ( ! isIWebViewProvider ( rawProvider ) ) throw new Error ( 'Expected registered provider' ) ;
217+ const savedWebView : SavedWebViewDefinition = {
218+ id : 'test-webview-id' ,
219+ webViewType : mainWebViewType ,
220+ projectId : 'saved-project' ,
221+ } ;
222+
223+ // @ts -expect-error -- intentionally passing undefined to test the defensive fallback path
224+ const result = await rawProvider . getWebView ( savedWebView , undefined , 'test-nonce' ) ;
225+
226+ expect ( result ) . toMatchObject ( { projectId : 'saved-project' } ) ;
227+ } ) ;
202228 } ) ;
203229
204230 function isCallable ( f : unknown ) : f is ( ...args : unknown [ ] ) => unknown {
@@ -418,6 +444,149 @@ describe('main', () => {
418444 } ) ;
419445 } ) ;
420446
447+ describe ( 'webview lifecycle event subscriptions' , ( ) => {
448+ type WebViewEvent = ( event : {
449+ webView : { webViewType : string ; id : string ; projectId ?: string } ;
450+ } ) => void ;
451+
452+ it ( 'subscribes to onDidOpenWebView and onDidCloseWebView during activation' , async ( ) => {
453+ const context = createTestActivationContext ( ) ;
454+
455+ await activate ( context ) ;
456+
457+ expect ( __mockOnDidOpenWebView ) . toHaveBeenCalledTimes ( 1 ) ;
458+ expect ( __mockOnDidCloseWebView ) . toHaveBeenCalledTimes ( 1 ) ;
459+ } ) ;
460+
461+ it ( 'populates openWebViewsByProject when a matching webview opens' , async ( ) => {
462+ const context = createTestActivationContext ( ) ;
463+ await activate ( context ) ;
464+ const onOpen : WebViewEvent = __mockOnDidOpenWebView . mock . calls [ 0 ] [ 0 ] ;
465+
466+ onOpen ( { webView : { webViewType : mainWebViewType , id : 'wv-1' , projectId : 'proj-a' } } ) ;
467+
468+ const open = findRegisteredHandler ( 'interlinearizer.open' ) ;
469+ if ( ! open ) throw new Error ( 'Handler not found' ) ;
470+ await open ( 'proj-a' ) ;
471+
472+ expect ( __mockOpenWebView ) . toHaveBeenCalledWith (
473+ mainWebViewType ,
474+ undefined ,
475+ expect . objectContaining ( { existingId : 'wv-1' , projectId : 'proj-a' } ) ,
476+ ) ;
477+ } ) ;
478+
479+ it ( 'ignores onDidOpenWebView events for other webview types' , async ( ) => {
480+ const context = createTestActivationContext ( ) ;
481+ await activate ( context ) ;
482+ const onOpen : WebViewEvent = __mockOnDidOpenWebView . mock . calls [ 0 ] [ 0 ] ;
483+
484+ onOpen ( { webView : { webViewType : 'other.webView' , id : 'wv-x' , projectId : 'proj-x' } } ) ;
485+
486+ const open = findRegisteredHandler ( 'interlinearizer.open' ) ;
487+ if ( ! open ) throw new Error ( 'Handler not found' ) ;
488+ await open ( 'proj-x' ) ;
489+
490+ expect ( __mockOpenWebView ) . toHaveBeenCalledWith (
491+ mainWebViewType ,
492+ undefined ,
493+ expect . objectContaining ( { existingId : undefined } ) ,
494+ ) ;
495+ } ) ;
496+
497+ it ( 'ignores onDidOpenWebView events with no projectId' , async ( ) => {
498+ const context = createTestActivationContext ( ) ;
499+ await activate ( context ) ;
500+ const onOpen : WebViewEvent = __mockOnDidOpenWebView . mock . calls [ 0 ] [ 0 ] ;
501+
502+ onOpen ( { webView : { webViewType : mainWebViewType , id : 'wv-no-project' } } ) ;
503+
504+ const open = findRegisteredHandler ( 'interlinearizer.open' ) ;
505+ if ( ! open ) throw new Error ( 'Handler not found' ) ;
506+ await open ( 'proj-no-project' ) ;
507+
508+ expect ( __mockOpenWebView ) . toHaveBeenCalledWith (
509+ mainWebViewType ,
510+ undefined ,
511+ expect . objectContaining ( { existingId : undefined } ) ,
512+ ) ;
513+ } ) ;
514+
515+ it ( 'removes the entry from the map when the matching webview closes' , async ( ) => {
516+ __mockOpenWebView . mockResolvedValue ( 'wv-close' ) ;
517+ const context = createTestActivationContext ( ) ;
518+ await activate ( context ) ;
519+
520+ const open = findRegisteredHandler ( 'interlinearizer.open' ) ;
521+ if ( ! open ) throw new Error ( 'Handler not found' ) ;
522+ await open ( 'proj-close' ) ;
523+
524+ const onClose : WebViewEvent = __mockOnDidCloseWebView . mock . calls [ 0 ] [ 0 ] ;
525+ onClose ( {
526+ webView : { webViewType : mainWebViewType , id : 'wv-close' , projectId : 'proj-close' } ,
527+ } ) ;
528+
529+ __mockOpenWebView . mockResolvedValue ( 'wv-new' ) ;
530+ await open ( 'proj-close' ) ;
531+
532+ expect ( __mockOpenWebView ) . toHaveBeenLastCalledWith (
533+ mainWebViewType ,
534+ undefined ,
535+ expect . objectContaining ( { existingId : undefined , projectId : 'proj-close' } ) ,
536+ ) ;
537+ } ) ;
538+
539+ it ( 'does not remove the entry when a different webview ID closes for the same project' , async ( ) => {
540+ __mockOpenWebView . mockResolvedValue ( 'wv-current' ) ;
541+ const context = createTestActivationContext ( ) ;
542+ await activate ( context ) ;
543+
544+ const open = findRegisteredHandler ( 'interlinearizer.open' ) ;
545+ if ( ! open ) throw new Error ( 'Handler not found' ) ;
546+ await open ( 'proj-stale' ) ;
547+
548+ const onClose : WebViewEvent = __mockOnDidCloseWebView . mock . calls [ 0 ] [ 0 ] ;
549+ onClose ( {
550+ webView : { webViewType : mainWebViewType , id : 'wv-old' , projectId : 'proj-stale' } ,
551+ } ) ;
552+
553+ await open ( 'proj-stale' ) ;
554+
555+ expect ( __mockOpenWebView ) . toHaveBeenLastCalledWith (
556+ mainWebViewType ,
557+ undefined ,
558+ expect . objectContaining ( { existingId : 'wv-current' , projectId : 'proj-stale' } ) ,
559+ ) ;
560+ } ) ;
561+
562+ it ( 'ignores onDidCloseWebView events for other webview types' , async ( ) => {
563+ __mockOpenWebView . mockResolvedValue ( 'wv-other-type' ) ;
564+ const context = createTestActivationContext ( ) ;
565+ await activate ( context ) ;
566+
567+ const open = findRegisteredHandler ( 'interlinearizer.open' ) ;
568+ if ( ! open ) throw new Error ( 'Handler not found' ) ;
569+ await open ( 'proj-other-type' ) ;
570+
571+ const onClose : WebViewEvent = __mockOnDidCloseWebView . mock . calls [ 0 ] [ 0 ] ;
572+ onClose ( {
573+ webView : {
574+ webViewType : 'other.webView' ,
575+ id : 'wv-other-type' ,
576+ projectId : 'proj-other-type' ,
577+ } ,
578+ } ) ;
579+
580+ await open ( 'proj-other-type' ) ;
581+
582+ expect ( __mockOpenWebView ) . toHaveBeenLastCalledWith (
583+ mainWebViewType ,
584+ undefined ,
585+ expect . objectContaining ( { existingId : 'wv-other-type' , projectId : 'proj-other-type' } ) ,
586+ ) ;
587+ } ) ;
588+ } ) ;
589+
421590 describe ( 'deactivate' , ( ) => {
422591 it ( 'returns true to indicate successful deactivation' , async ( ) => {
423592 const result = await deactivate ( ) ;
0 commit comments