Skip to content

Commit db24eb3

Browse files
Fix/add tests
1 parent a8abdd2 commit db24eb3

3 files changed

Lines changed: 183 additions & 4 deletions

File tree

__mocks__/papi-backend.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ const mockRegisterCommand = jest.fn();
88
const mockOpenWebView = jest.fn();
99
const mockSelectProject = jest.fn();
1010
const mockGetOpenWebViewDefinition = jest.fn();
11+
const mockOnDidOpenWebView = jest.fn();
12+
const mockOnDidCloseWebView = jest.fn();
1113
const mockLogger = {
1214
debug: jest.fn(),
1315
error: jest.fn(),
@@ -28,6 +30,8 @@ const papi = {
2830
webViews: {
2931
openWebView: mockOpenWebView,
3032
getOpenWebViewDefinition: mockGetOpenWebViewDefinition,
33+
onDidOpenWebView: mockOnDidOpenWebView,
34+
onDidCloseWebView: mockOnDidCloseWebView,
3135
},
3236
};
3337

@@ -38,6 +42,8 @@ const defaultExport = {
3842
__mockOpenWebView: mockOpenWebView,
3943
__mockSelectProject: mockSelectProject,
4044
__mockGetOpenWebViewDefinition: mockGetOpenWebViewDefinition,
45+
__mockOnDidOpenWebView: mockOnDidOpenWebView,
46+
__mockOnDidCloseWebView: mockOnDidCloseWebView,
4147
__mockLogger: mockLogger,
4248
};
4349

@@ -50,6 +56,8 @@ module.exports = {
5056
__mockOpenWebView: mockOpenWebView,
5157
__mockSelectProject: mockSelectProject,
5258
__mockGetOpenWebViewDefinition: mockGetOpenWebViewDefinition,
59+
__mockOnDidOpenWebView: mockOnDidOpenWebView,
60+
__mockOnDidCloseWebView: mockOnDidCloseWebView,
5361
__mockLogger: mockLogger,
5462
};
5563

src/__tests__/interlinearizer.web-view.test.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,14 @@ describe('InterlinearizerWebView', () => {
8686
expect(screen.getByText('Loading…')).toBeInTheDocument();
8787
});
8888

89-
it('shows an error when the project does not support the USJ book interface', () => {
89+
it('shows an error when no USJ book is available for the project', () => {
9090
mockBookData(undefined, false);
9191
render(<InterlinearizerWebView {...makeProps('test-project-id')} />);
9292

9393
expect(screen.getByRole('heading', { name: /error loading book/i })).toBeInTheDocument();
94-
expect(screen.getByText(/does not support the USJ book interface/i)).toBeInTheDocument();
94+
expect(
95+
screen.getByText(/no usj book available for gen in project test-project-id/i),
96+
).toBeInTheDocument();
9597
});
9698

9799
it('shows the raw USFM when book data arrives', () => {

src/__tests__/main.test.ts

Lines changed: 171 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)