Skip to content

Commit 769366c

Browse files
Improve docs, minor fixes
1 parent e8c7b57 commit 769366c

6 files changed

Lines changed: 138 additions & 4 deletions

File tree

__mocks__/lucide-react.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,22 @@
44

55
import type { ReactElement } from 'react';
66

7-
/** @param props - SVG props forwarded from the component. */
7+
/**
8+
* Stub for the Trash2 icon; renders a bare SVG element so tests can locate the icon by test ID.
9+
*
10+
* @param props - SVG props forwarded from the component, including optional className and size.
11+
* @returns A ReactElement SVG element used as a trash icon stub in tests.
12+
*/
813
export function Trash2(props: Readonly<{ className?: string; size?: number }>): ReactElement {
914
return <svg data-testid="trash-icon" {...props} />;
1015
}
1116

12-
/** @param props - SVG props forwarded from the component. */
17+
/**
18+
* Stub for the Info icon; renders a bare SVG element so tests can locate the icon by test ID.
19+
*
20+
* @param props - SVG props forwarded from the component, including optional className and size.
21+
* @returns A ReactElement SVG element used as an info icon stub in tests.
22+
*/
1323
export function Info(props: Readonly<{ className?: string; size?: number }>): ReactElement {
1424
return <svg data-testid="info-icon" {...props} />;
1525
}

src/__tests__/components/ContinuousView.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@ function makeSingleTokenBook(): Book {
185185
* A book whose GEN 1:1 segment has word tokens and whose GEN 1:2 segment has only a punctuation
186186
* token (no word tokens). Used to exercise code paths that run when a segment exists in the book
187187
* but contributes nothing to phraseEntries / segmentStartIndex.
188+
*
189+
* @returns A two-segment Book where the second segment has no word tokens.
188190
*/
189191
function makeMixedBook(): Book {
190192
return {

src/__tests__/components/InterlinearizerLoader.test.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,48 @@ type MockProject = {
7171

7272
const testProjectId = 'test-project-id';
7373

74+
jest.mock('../../hooks/useInterlinearizerBookData');
75+
jest.mock('../../hooks/useOptimisticBooleanSetting');
76+
77+
jest.mock('../../components/ContinuousView', () => ({
78+
__esModule: true,
79+
default: () => <div data-testid="continuous-view" />,
80+
}));
81+
82+
jest.mock('../../components/Interlinearizer', () => ({
83+
__esModule: true,
84+
default: () => <div data-testid="interlinearizer" />,
85+
}));
86+
87+
jest.mock('../../components/ContinuousScrollToggle', () => ({
88+
__esModule: true,
89+
default: ({
90+
checked,
91+
disabled,
92+
onCheckedChange,
93+
}: {
94+
checked: boolean;
95+
disabled: boolean;
96+
label: string;
97+
onCheckedChange: (v: boolean) => void;
98+
}) => (
99+
<input
100+
type="checkbox"
101+
data-testid="continuous-scroll-toggle"
102+
checked={checked}
103+
disabled={disabled}
104+
onChange={(e) => {
105+
if (!disabled) onCheckedChange(e.target.checked);
106+
}}
107+
/>
108+
),
109+
}));
110+
111+
jest.mock('../../components/ScriptureNavControls', () => ({
112+
__esModule: true,
113+
default: () => <div data-testid="scripture-nav-controls" />,
114+
}));
115+
74116
const STUB_ACTIVE_PROJECT: MockProject = {
75117
id: 'proj-1',
76118
createdAt: '2026-01-01T00:00:00Z',
@@ -81,6 +123,19 @@ const STUB_ACTIVE_PROJECT: MockProject = {
81123

82124
jest.mock('../../components/ProjectModals', () => ({
83125
__esModule: true,
126+
/**
127+
* Minimal ProjectModals stand-in that drives modal state and active-project state through the
128+
* same `useWebViewState` hook the real component uses, so tests can assert on state transitions
129+
* without mounting the full modal tree.
130+
*
131+
* @param modal - Current modal identifier controlling which stub panel is rendered.
132+
* @param setModal - Callback to transition to a different modal state.
133+
* @param activeProject - The currently active interlinear project, or undefined when none is
134+
* selected.
135+
* @param useWebViewState - Injected hook used to read and write persisted WebView state; must
136+
* support the `'activeProject'` key.
137+
* @returns A JSX element containing the stub modal panels keyed by `modal`.
138+
*/
84139
default: function StubProjectModals({
85140
modal,
86141
setModal,

src/__tests__/main.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ interface PapiBackendTestMock {
2626
/**
2727
* Type guard for the mocked @papi/backend default export. Allows destructuring mocks without type
2828
* assertions.
29+
*
30+
* @param m - The value to test, typically the default export of the mocked module.
31+
* @returns `true` if `m` exposes all expected `__mock*` properties.
2932
*/
3033
function isPapiBackendTestMock(m: unknown): m is PapiBackendTestMock {
3134
return (
@@ -74,6 +77,12 @@ type WebViewProvider = {
7477
): Promise<WebViewDefinition | undefined>;
7578
};
7679

80+
/**
81+
* Type guard that narrows an unknown value to a {@link WebViewProvider}.
82+
*
83+
* @param x - The value to test.
84+
* @returns `true` if `x` has a callable `getWebView` method.
85+
*/
7786
function isWebViewProvider(x: unknown): x is WebViewProvider {
7887
return !!x && typeof x === 'object' && 'getWebView' in x && typeof x.getWebView === 'function';
7988
}
@@ -85,12 +94,26 @@ function getRegisteredProvider(): WebViewProvider {
8594
return raw;
8695
}
8796

97+
/**
98+
* Finds the handler registered for `commandName` in the most recent `activate()` call.
99+
*
100+
* @param commandName - The fully-qualified command name to look up.
101+
* @returns The registered handler function, or `undefined` if none was registered for that name.
102+
*/
88103
function findRegisteredHandler(commandName: string): ((...args: unknown[]) => unknown) | undefined {
89104
const call = jest.mocked(__mockRegisterCommand).mock.calls.find((c) => c[0] === commandName);
90105
const rawHandler: unknown = call?.[1];
91106
return isCallable(rawHandler) ? rawHandler : undefined;
92107
}
93108

109+
/**
110+
* Activates the extension and returns a typed wrapper around the `interlinearizer.openForWebView`
111+
* command handler.
112+
*
113+
* @returns A function that invokes the handler with an optional WebView ID and resolves to the
114+
* opened WebView ID string, or `undefined` if the handler returns a non-string.
115+
* @throws If the handler was not registered during `activate()`.
116+
*/
94117
async function getOpenForWebViewHandler(): Promise<
95118
(webViewId?: string) => Promise<string | undefined>
96119
> {
@@ -128,6 +151,14 @@ function getCloseWebViewCallback(): (event: { webView: SavedWebViewDefinition })
128151
return (event) => cb(event);
129152
}
130153

154+
/**
155+
* Activates the extension and returns a typed wrapper around the `interlinearizer.createProject`
156+
* command handler.
157+
*
158+
* @returns A function that invokes the handler with a source project ID and analysis languages,
159+
* resolving to the JSON-stringified project or `undefined` if the handler returns a non-string.
160+
* @throws If the handler was not registered during `activate()`.
161+
*/
131162
async function getCreateProjectHandler(): Promise<
132163
(sourceProjectId: string, analysisLanguages: string[]) => Promise<string | undefined>
133164
> {
@@ -144,6 +175,14 @@ async function getCreateProjectHandler(): Promise<
144175
};
145176
}
146177

178+
/**
179+
* Activates the extension and returns a typed wrapper around the `interlinearizer.deleteProject`
180+
* command handler.
181+
*
182+
* @returns A function that invokes the handler with a project UUID and resolves when deletion
183+
* completes.
184+
* @throws If the handler was not registered during `activate()`.
185+
*/
147186
async function getDeleteProjectHandler(): Promise<(id: string) => Promise<void>> {
148187
const context = createTestActivationContext();
149188
await activate(context);
@@ -154,6 +193,14 @@ async function getDeleteProjectHandler(): Promise<(id: string) => Promise<void>>
154193
};
155194
}
156195

196+
/**
197+
* Activates the extension and returns a typed wrapper around the
198+
* `interlinearizer.getProjectsForSource` command handler.
199+
*
200+
* @returns A function that invokes the handler with a source project ID and resolves to the
201+
* JSON-stringified project array (falls back to `'[]'` if the handler returns a non-string).
202+
* @throws If the handler was not registered during `activate()`.
203+
*/
157204
async function getProjectsForSourceHandler(): Promise<
158205
(sourceProjectId: string) => Promise<string>
159206
> {
@@ -167,6 +214,15 @@ async function getProjectsForSourceHandler(): Promise<
167214
};
168215
}
169216

217+
/**
218+
* Activates the extension and returns a typed wrapper around the
219+
* `interlinearizer.updateProjectMetadata` command handler.
220+
*
221+
* @returns A function that invokes the handler with a project UUID and updated fields, resolving to
222+
* the JSON-stringified updated project or `undefined` if the project was not found or the handler
223+
* returns a non-string.
224+
* @throws If the handler was not registered during `activate()`.
225+
*/
170226
async function getUpdateProjectMetadataHandler(): Promise<
171227
(
172228
id: string,

src/main.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ export async function activate(context: ExecutionActivationContext): Promise<voi
336336
result: {
337337
name: 'return value',
338338
summary:
339-
'JSON-stringified InterlinearProject for the new project, or undefined if storage failed',
339+
'JSON-stringified InterlinearProject for the new project; rejects (throws) on storage failure',
340340
schema: { type: 'string' },
341341
},
342342
},
@@ -409,7 +409,7 @@ export async function activate(context: ExecutionActivationContext): Promise<voi
409409
result: {
410410
name: 'return value',
411411
summary:
412-
'JSON-stringified updated InterlinearProject, or undefined if not found or if a storage failure occurred',
412+
'JSON-stringified updated InterlinearProject, or undefined if no project with that ID exists; rejects (throws) on storage failure',
413413
schema: { type: 'string' },
414414
},
415415
},

src/types/interlinearizer.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ declare module 'papi-shared-types' {
1818
* Opens the Interlinearizer for the project associated with the given WebView ID. Called from
1919
* WebView context menus, which pass the tab's WebView ID as the argument. Falls back to a
2020
* project picker dialog if the WebView has no project or no ID is given.
21+
*
22+
* @param webViewId - ID of the WebView tab whose associated project should be opened; when
23+
* omitted or when the WebView has no linked project, a project-picker dialog is shown
24+
* instead.
25+
* @returns A promise that resolves to the opened WebView ID, or `undefined` if the user
26+
* dismissed the picker without selecting a project.
2127
*/
2228
'interlinearizer.openForWebView': (webViewId?: string) => Promise<string | undefined>;
2329

@@ -54,6 +60,11 @@ declare module 'papi-shared-types' {
5460
*
5561
* @param sourceProjectId Platform.Bible project ID of the source text to query.
5662
* @returns A JSON string containing an `InterlinearProject[]`; `"[]"` when none exist.
63+
* @throws {SyntaxError} If the project-IDs index or any stored project record contains invalid
64+
* JSON.
65+
* @throws If `papi.storage.readUserData` rejects for a reason other than the file not existing
66+
* (propagated from the storage layer). Callers can use this to distinguish a storage outage
67+
* from a legitimately empty list.
5768
*/
5869
'interlinearizer.getProjectsForSource': (sourceProjectId: string) => Promise<string>;
5970

0 commit comments

Comments
 (0)