Skip to content

Commit 28fda1e

Browse files
Read live PT book via USJ and open interlinearizer from project menu
1 parent bc02e99 commit 28fda1e

13 files changed

Lines changed: 463 additions & 257 deletions

.vscode/settings.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,8 @@
1717
},
1818
"files.eol": "\n",
1919

20-
"javascript.validate.enable": false,
21-
"javascript.format.enable": false,
22-
"typescript.format.enable": false,
20+
"js/ts.validate.enabled": false,
21+
"js/ts.format.enabled": false,
2322

2423
"search.exclude": {
2524
".git": true,

__mocks__/papi-backend.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
*/
55

66
const mockRegisterWebViewProvider = jest.fn().mockResolvedValue({ dispose: jest.fn() });
7-
const mockOpenWebView = jest.fn().mockResolvedValue(undefined);
7+
const mockRegisterCommand = jest.fn().mockResolvedValue({ dispose: jest.fn() });
8+
const mockOpenWebView = jest.fn().mockResolvedValue('mock-webview-id');
9+
const mockSelectProject = jest.fn().mockResolvedValue(undefined);
810
const mockLogger = {
911
debug: jest.fn(),
1012
error: jest.fn(),
@@ -13,6 +15,12 @@ const mockLogger = {
1315
};
1416

1517
const papi = {
18+
commands: {
19+
registerCommand: mockRegisterCommand,
20+
},
21+
dialogs: {
22+
selectProject: mockSelectProject,
23+
},
1624
webViewProviders: {
1725
registerWebViewProvider: mockRegisterWebViewProvider,
1826
},
@@ -24,7 +32,9 @@ const papi = {
2432
const defaultExport = {
2533
...papi,
2634
__mockRegisterWebViewProvider: mockRegisterWebViewProvider,
35+
__mockRegisterCommand: mockRegisterCommand,
2736
__mockOpenWebView: mockOpenWebView,
37+
__mockSelectProject: mockSelectProject,
2838
__mockLogger: mockLogger,
2939
};
3040

@@ -33,9 +43,11 @@ module.exports = {
3343
default: defaultExport,
3444
logger: mockLogger,
3545
__mockRegisterWebViewProvider: mockRegisterWebViewProvider,
46+
__mockRegisterCommand: mockRegisterCommand,
3647
__mockOpenWebView: mockOpenWebView,
48+
__mockSelectProject: mockSelectProject,
3749
__mockLogger: mockLogger,
3850
};
3951

4052
/** Marks this file as a module so top-level const/let are module-scoped; avoids TS "redeclare" when both papi-backend and papi-frontend mocks are in the project (they are used mutually exclusively by Jest). */
41-
export {};
53+
export {};

__mocks__/platform-bible-utils.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,13 @@ class UnsubscriberAsyncList {
5353
}
5454
}
5555

56-
export { UnsubscriberAsyncList };
56+
/** Minimal PlatformError shape matching the real platform-bible-utils type. */
57+
interface PlatformError {
58+
message: string;
59+
isPlatformError: true;
60+
}
61+
62+
const isPlatformError = (value: unknown): value is PlatformError =>
63+
typeof value === 'object' && value !== null && (value as PlatformError).isPlatformError === true;
64+
65+
export { UnsubscriberAsyncList, isPlatformError };
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
{
22
"metadata": {},
33
"localizedStrings": {
4-
"en": {}
4+
"en": {
5+
"%interlinearizer_dialog_open_title%": "Open Interlinearizer",
6+
"%interlinearizer_dialog_open_prompt%": "Choose a project to open in the Interlinearizer:",
7+
"%mainMenu_interlinearizerSubmenu%": "Interlinearizer",
8+
"%mainMenu_openInterlinearizer%": "Open Interlinearizer"
9+
}
510
}
611
}

contributions/menus.json

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,29 @@
11
{
22
"mainMenu": {
33
"columns": {},
4-
"groups": {},
5-
"items": []
4+
"groups": {
5+
"interlinearizer.interlinearizerProjects": {
6+
"order": 1.5,
7+
"isExtensible": true,
8+
"menuItem": "interlinearizer.projectSubmenu"
9+
}
10+
},
11+
"items": [
12+
{
13+
"id": "interlinearizer.projectSubmenu",
14+
"label": "%mainMenu_interlinearizerSubmenu%",
15+
"localizeNotes": "Application main menu > Project > Interlinearizer",
16+
"group": "platform.projectProjects",
17+
"order": 1000.1
18+
},
19+
{
20+
"label": "%mainMenu_openInterlinearizer%",
21+
"localizeNotes": "Application main menu > Project > Interlinearizer > Open Interlinearizer",
22+
"group": "interlinearizer.interlinearizerProjects",
23+
"order": 1,
24+
"command": "interlinearizer.open"
25+
}
26+
]
627
},
728
"defaultWebViewTopMenu": {
829
"columns": {},

jest.config.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@
1010
import type { Config } from 'jest';
1111

1212
const config: Config = {
13-
/** Automatically clear mock calls, instances, contexts and results before every test. */
14-
clearMocks: true,
13+
/**
14+
* Reset mock implementations before every test (superset of clearMocks: also removes
15+
* mockReturnValue/mockImplementation so implementations never leak between tests). Each test must
16+
* set up the implementations it needs, typically in beforeEach.
17+
*/
18+
resetMocks: true,
1519

1620
/**
1721
* Coverage only when run with --coverage (see npm run test:coverage). Omit for faster default

package-lock.json

Lines changed: 13 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 62 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,7 @@
55
import type { WebViewProps } from '@papi/core';
66
import type { SerializedVerseRef } from '@sillsdev/scripture';
77
import { render, screen } from '@testing-library/react';
8-
import { InterlinearXmlParser } from 'parsers/interlinearXmlParser';
9-
10-
/** Mock parser to allow overriding constructor behavior per test. */
11-
jest.mock('parsers/interlinearXmlParser', () => {
12-
const actual = jest.requireActual<typeof import('parsers/interlinearXmlParser')>(
13-
'parsers/interlinearXmlParser',
14-
);
15-
return {
16-
InterlinearXmlParser: jest.fn().mockImplementation(() => new actual.InterlinearXmlParser()),
17-
};
18-
});
8+
import { useProjectData } from '@papi/frontend/react';
199

2010
/**
2111
* Load the WebView module; it assigns the component to globalThis.webViewComponent. This pattern is
@@ -32,94 +22,86 @@ if (!InterlinearizerWebView) throw new Error('webViewComponent not loaded');
3222
/** Minimal SerializedVerseRef for hook mock return. */
3323
const defaultScrRef: SerializedVerseRef = { book: 'GEN', chapterNum: 1, verseNum: 1 };
3424

35-
/** Full WebViewProps for tests; interlinearizer component ignores hooks/update. */
36-
const testWebViewProps: WebViewProps = {
37-
id: 'test-id',
38-
webViewType: 'interlinearizer.mainWebView',
39-
useWebViewState: <T,>(_key: string, defaultValue: T): [T, (v: T) => void, () => void] => [
40-
defaultValue,
41-
() => {},
42-
() => {},
43-
],
44-
useWebViewScrollGroupScrRef: (): [
45-
SerializedVerseRef,
46-
(r: SerializedVerseRef) => void,
47-
number | undefined,
48-
(id: number | undefined) => void,
49-
] => [defaultScrRef, () => {}, undefined, () => {}],
50-
updateWebViewDefinition: () => true,
51-
};
25+
/** Builds a minimal WebViewProps for tests. */
26+
function makeProps(projectId?: string): WebViewProps {
27+
return {
28+
id: 'test-id',
29+
webViewType: 'interlinearizer.mainWebView',
30+
projectId,
31+
useWebViewState: <T,>(_key: string, defaultValue: T): [T, (v: T) => void, () => void] => [
32+
defaultValue,
33+
() => {},
34+
() => {},
35+
],
36+
useWebViewScrollGroupScrRef: (): [
37+
SerializedVerseRef,
38+
(r: SerializedVerseRef) => void,
39+
number | undefined,
40+
(id: number | undefined) => void,
41+
] => [defaultScrRef, () => {}, undefined, () => {}],
42+
updateWebViewDefinition: () => true,
43+
};
44+
}
45+
46+
/** Configures useProjectData to return the given BookUSJ value this render. */
47+
function mockBookData(value: unknown): void {
48+
jest.mocked(useProjectData).mockImplementation(() => ({
49+
BookUSJ: () => [value, jest.fn(), false],
50+
}));
51+
}
5252

5353
describe('InterlinearizerWebView', () => {
54+
beforeEach(() => {
55+
mockBookData(undefined);
56+
});
57+
5458
it('renders the heading "Interlinearizer"', () => {
55-
render(<InterlinearizerWebView {...testWebViewProps} />);
59+
render(<InterlinearizerWebView {...makeProps()} />);
5660

5761
expect(screen.getByRole('heading', { name: /interlinearizer/i })).toBeInTheDocument();
5862
});
5963

60-
it('renders the description mentioning test-data XML', () => {
61-
render(<InterlinearizerWebView {...testWebViewProps} />);
64+
it('shows a prompt to open from a project when no projectId is provided', () => {
65+
render(<InterlinearizerWebView {...makeProps()} />);
6266

63-
expect(
64-
screen.getByText(/raw json of the model parsed from/i, { exact: false }),
65-
).toBeInTheDocument();
66-
expect(screen.getByText(/test-data\/Interlinear_en_MAT\.xml/i)).toBeInTheDocument();
67+
expect(screen.getByText(/open this webview from a paratext project/i)).toBeInTheDocument();
6768
});
6869

69-
it('parses the bundled test XML and displays parsed JSON', () => {
70-
render(<InterlinearizerWebView {...testWebViewProps} />);
70+
it('shows the book and projectId when a project is linked', () => {
71+
mockBookData({
72+
type: 'USJ',
73+
version: '3.1',
74+
content: [{ type: 'book', marker: 'id', code: 'GEN' }],
75+
});
76+
render(<InterlinearizerWebView {...makeProps('test-project-id')} />);
7177

72-
expect(screen.getByText(/parsed interlinear data \(json\)/i)).toBeInTheDocument();
73-
expect(screen.getByText(/"GlossLanguage"/)).toBeInTheDocument();
74-
expect(screen.getByText(/"BookId"/)).toBeInTheDocument();
78+
expect(screen.getByText(/test-project-id/)).toBeInTheDocument();
79+
expect(screen.getByText(/GEN · project/)).toBeInTheDocument();
7580
});
7681

77-
it('displays parsed structure with expected verse data', () => {
78-
render(<InterlinearizerWebView {...testWebViewProps} />);
82+
it('shows Loading when projectId is set but book data has not arrived', () => {
83+
mockBookData(undefined);
84+
render(<InterlinearizerWebView {...makeProps('test-project-id')} />);
7985

80-
expect(screen.getByText(/"en"/)).toBeInTheDocument();
81-
expect(screen.getByText(/"MAT"/)).toBeInTheDocument();
86+
expect(screen.getByText('Loading…')).toBeInTheDocument();
8287
});
8388

84-
it('does not show parse error when XML is valid', () => {
85-
render(<InterlinearizerWebView {...testWebViewProps} />);
89+
it('shows the raw USFM when book data arrives', () => {
90+
mockBookData({
91+
type: 'USJ',
92+
version: '3.1',
93+
content: [{ type: 'book', marker: 'id', code: 'GEN' }],
94+
});
95+
render(<InterlinearizerWebView {...makeProps('test-project-id')} />);
8696

87-
expect(screen.queryByText(/^parse error$/i)).not.toBeInTheDocument();
97+
expect(screen.getByText(/"code": "GEN"/)).toBeInTheDocument();
8898
});
8999

90-
it('displays parse error when parser throws an Error (uses err.message)', () => {
91-
const actual = jest.requireActual<typeof import('../parsers/interlinearXmlParser')>(
92-
'../parsers/interlinearXmlParser',
93-
);
94-
const realInstance = new actual.InterlinearXmlParser();
95-
const throwingParse = (): never => {
96-
throw new Error('Invalid XML structure');
97-
};
98-
Object.defineProperty(realInstance, 'parse', { value: throwingParse, writable: true });
99-
jest.mocked(InterlinearXmlParser).mockImplementationOnce(() => realInstance);
100-
101-
render(<InterlinearizerWebView {...testWebViewProps} />);
102-
103-
expect(screen.getByRole('heading', { name: /^parse error$/i })).toBeInTheDocument();
104-
expect(screen.getByText(/invalid xml structure/i)).toBeInTheDocument();
105-
});
100+
it('shows an error heading and message when book data is a PlatformError', () => {
101+
mockBookData({ isPlatformError: true, message: 'Project not found' });
102+
render(<InterlinearizerWebView {...makeProps('test-project-id')} />);
106103

107-
it('displays parse error when parser throws non-Error (uses String(err))', () => {
108-
const actual = jest.requireActual<typeof import('../parsers/interlinearXmlParser')>(
109-
'../parsers/interlinearXmlParser',
110-
);
111-
const realInstance = new actual.InterlinearXmlParser();
112-
const throwingParse = (): never => {
113-
// Intentionally throw a non-Error to test the String(err) branch in the catch block.
114-
// eslint-disable-next-line no-throw-literal -- testing non-Error handling
115-
throw 'plain string error';
116-
};
117-
Object.defineProperty(realInstance, 'parse', { value: throwingParse, writable: true });
118-
jest.mocked(InterlinearXmlParser).mockImplementationOnce(() => realInstance);
119-
120-
render(<InterlinearizerWebView {...testWebViewProps} />);
121-
122-
expect(screen.getByRole('heading', { name: /^parse error$/i })).toBeInTheDocument();
123-
expect(screen.getByText('plain string error')).toBeInTheDocument();
104+
expect(screen.getByRole('heading', { name: /error loading book/i })).toBeInTheDocument();
105+
expect(screen.getByText(/project not found/i)).toBeInTheDocument();
124106
});
125107
});

0 commit comments

Comments
 (0)