Skip to content

Commit 8b9fa4f

Browse files
Doc/mock audit
1 parent d113425 commit 8b9fa4f

12 files changed

Lines changed: 215 additions & 113 deletions

src/__tests__/components/AnalysisStore.test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,15 @@ describe('useGloss', () => {
166166
it('does not re-render when a different token is glossed', async () => {
167167
let renderCount = 0;
168168

169+
/**
170+
* Renders the current gloss for a token while counting how many times it re-renders, so tests
171+
* can assert that unrelated gloss changes do not cause extra renders.
172+
*
173+
* @param props - Component props.
174+
* @param props.tokenRef - The token whose approved gloss to read via {@link useGloss}.
175+
* @returns A span containing the gloss string.
176+
* @throws When called outside an {@link AnalysisStoreProvider}.
177+
*/
169178
function CountingGlossReader({ tokenRef }: Readonly<{ tokenRef: string }>) {
170179
renderCount += 1;
171180
const gloss = useGloss(tokenRef);

src/__tests__/components/ContinuousView.test.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,28 @@ import { AnalysisStoreProvider } from '../../components/AnalysisStore';
1515

1616
jest.mock('../../components/AnalysisStore', () => ({
1717
__esModule: true,
18+
/**
19+
* Pass-through provider stub that renders children directly, keeping AnalysisStore.tsx out of
20+
* scope.
21+
*
22+
* @param props - Component props.
23+
* @param props.children - Child nodes to render.
24+
* @returns The children unchanged.
25+
*/
1826
AnalysisStoreProvider({ children }: Readonly<{ children: ReactNode; analysisLanguage: string }>) {
1927
return children;
2028
},
29+
/**
30+
* Returns a fixed empty gloss string for any token.
31+
*
32+
* @returns An empty string.
33+
*/
2134
useGloss: () => '',
35+
/**
36+
* Returns a no-op dispatch function.
37+
*
38+
* @returns A function that accepts any arguments and does nothing.
39+
*/
2240
useGlossDispatch: () => () => {},
2341
}));
2442

@@ -44,15 +62,15 @@ jest.mock('../../components/PhraseBox', () => ({
4462
onFocusPhrase,
4563
tokens,
4664
}: Readonly<{
47-
index?: number;
65+
index: number | undefined;
4866
isFocused: boolean;
49-
onFocusPhrase?: (index?: number) => void;
50-
tokens: Token[];
67+
onFocusPhrase: (index?: number) => void;
68+
tokens: (Token & { type: 'word' })[];
5169
}>) => (
5270
<button
5371
data-focus-state={isFocused ? 'focused' : 'default'}
5472
data-phrase-box="true"
55-
onClick={() => onFocusPhrase?.(index)}
73+
onClick={() => onFocusPhrase(index)}
5674
type="button"
5775
>
5876
{tokens.map((t) => (

src/__tests__/components/Interlinearizer.test.tsx

Lines changed: 84 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,44 +4,90 @@
44

55
import type { SerializedVerseRef } from '@sillsdev/scripture';
66
import { act, render, screen } from '@testing-library/react';
7-
import type { Book, Segment } from 'interlinearizer';
7+
import type { Book, ScriptureRef, Segment } from 'interlinearizer';
88
import type { ReactNode } from 'react';
99
import Interlinearizer from '../../components/Interlinearizer';
10+
import type { SegmentDisplayMode } from '../../components/SegmentView';
11+
import { defaultScrRef, GEN_1_1_BOOK } from '../test-helpers';
1012

1113
jest.mock('lucide-react', () => ({
1214
__esModule: true,
15+
/**
16+
* Stub for the LocateFixed icon; renders a minimal SVG so icon-presence assertions work.
17+
*
18+
* @returns An SVG element with `data-testid="locate-fixed-icon"`.
19+
*/
1320
LocateFixed: () => <svg data-testid="locate-fixed-icon" />,
1421
}));
1522

16-
// Store captured props so tests can inspect what Interlinearizer passes down
17-
let capturedContinuousViewProps: Record<string, unknown> = {};
23+
/**
24+
* Props captured from ContinuousView renders so tests can assert on what Interlinearizer passes
25+
* down.
26+
*/
27+
type CapturedContinuousViewProps = {
28+
/** When set, the strip jumps to this phrase index. */
29+
activePhraseIndex: number | undefined;
30+
/** Verse coordinate used to scroll the strip. */
31+
activeVerse: ScriptureRef;
32+
/** The full tokenized book. */
33+
book: Book;
34+
/** Called when the focused phrase index changes. */
35+
onFocusPhraseIndexChange: (index: number) => void;
36+
/** Called when arrow navigation moves focus into a new verse. */
37+
onVerseChange: (verse: ScriptureRef) => void;
38+
};
39+
let capturedContinuousViewProps: CapturedContinuousViewProps | undefined;
1840

41+
/** Props captured from SegmentView renders so tests can assert on what Interlinearizer passes down. */
1942
type CapturedSegmentViewProps = {
43+
/** The segment the component is asked to render. */
2044
segment: Segment;
21-
displayMode?: string;
22-
focusedTokenRef?: string;
23-
isActive?: boolean;
24-
onSelect?: (ref: { book: string; chapter: number; verse: number }, tokenRef?: string) => void;
45+
/** Controls whether tokens are rendered as chips or as raw baseline text. */
46+
displayMode: SegmentDisplayMode;
47+
/** The `Token.ref` string of the currently focused token, if any. */
48+
focusedTokenRef: string | undefined;
49+
/** Whether this segment corresponds to the currently active verse. */
50+
isActive: boolean;
51+
/** Called when the user selects a token. */
52+
onSelect: (ref: ScriptureRef, tokenRef?: string) => void;
2553
};
2654
let capturedSegmentViewPropsList: CapturedSegmentViewProps[] = [];
2755

2856
jest.mock('../../components/AnalysisStore', () => ({
2957
__esModule: true,
58+
/**
59+
* Pass-through provider stub that renders children directly, keeping AnalysisStore.tsx out of
60+
* scope.
61+
*
62+
* @param props - Component props.
63+
* @param props.children - Child nodes to render.
64+
* @returns The children unchanged.
65+
*/
3066
AnalysisStoreProvider({ children }: Readonly<{ children: ReactNode }>) {
3167
return children;
3268
},
69+
/**
70+
* Returns a fixed empty gloss string for any token.
71+
*
72+
* @returns An empty string.
73+
*/
3374
useGloss: () => '',
75+
/**
76+
* Returns a no-op dispatch function.
77+
*
78+
* @returns A function that accepts any arguments and does nothing.
79+
*/
3480
useGlossDispatch: () => () => {},
3581
}));
3682

3783
jest.mock('../../components/ContinuousView', () => ({
3884
__esModule: true,
39-
default: (props: Record<string, unknown>) => {
85+
default: (props: CapturedContinuousViewProps) => {
4086
capturedContinuousViewProps = props;
4187
return (
4288
<div
4389
data-active-phrase-index={
44-
typeof props.activePhraseIndex === 'number' ? String(props.activePhraseIndex) : undefined
90+
props.activePhraseIndex === undefined ? undefined : String(props.activePhraseIndex)
4591
}
4692
data-testid="continuous-view"
4793
/>
@@ -51,6 +97,15 @@ jest.mock('../../components/ContinuousView', () => ({
5197

5298
jest.mock('../../components/SegmentView', () => ({
5399
__esModule: true,
100+
/**
101+
* Named export stub for SegmentView; captures received props and renders a minimal div.
102+
*
103+
* @param props - The props passed by Interlinearizer.
104+
* @param props.segment - The segment being rendered.
105+
* @param props.isActive - Whether this segment is the active verse.
106+
* @param props.rest - Any additional props forwarded from the parent.
107+
* @returns A div with `data-testid="segment-view"` and the segment id.
108+
*/
54109
SegmentView: ({ segment, isActive, ...rest }: CapturedSegmentViewProps) => {
55110
capturedSegmentViewPropsList.push({ segment, isActive, ...rest });
56111
return (
@@ -61,6 +116,15 @@ jest.mock('../../components/SegmentView', () => ({
61116
/>
62117
);
63118
},
119+
/**
120+
* Default export stub for SegmentView; captures received props and renders a minimal div.
121+
*
122+
* @param props - The props passed by Interlinearizer.
123+
* @param props.segment - The segment being rendered.
124+
* @param props.isActive - Whether this segment is the active verse.
125+
* @param props.rest - Any additional props forwarded from the parent.
126+
* @returns A div with `data-testid="segment-view"` and the segment id.
127+
*/
64128
default: ({ segment, isActive, ...rest }: CapturedSegmentViewProps) => {
65129
capturedSegmentViewPropsList.push({ segment, isActive, ...rest });
66130
return (
@@ -73,33 +137,6 @@ jest.mock('../../components/SegmentView', () => ({
73137
},
74138
}));
75139

76-
const defaultScrRef: SerializedVerseRef = { book: 'GEN', chapterNum: 1, verseNum: 1 };
77-
78-
/** Pre-built Book with one GEN 1:1 segment. */
79-
const GEN_1_1_BOOK: Book = {
80-
id: 'GEN',
81-
bookRef: 'GEN',
82-
textVersion: 'v1',
83-
segments: [
84-
{
85-
id: 'GEN 1:1',
86-
startRef: { book: 'GEN', chapter: 1, verse: 1 },
87-
endRef: { book: 'GEN', chapter: 1, verse: 1 },
88-
baselineText: 'In the beginning.',
89-
tokens: [
90-
{
91-
ref: 'GEN 1:1:0',
92-
surfaceText: 'In',
93-
writingSystem: 'en',
94-
type: 'word',
95-
charStart: 0,
96-
charEnd: 2,
97-
},
98-
],
99-
},
100-
],
101-
};
102-
103140
/** Pre-built Book with no segments — used by the no-verse-data test. */
104141
const GEN_EMPTY_BOOK: Book = { id: 'GEN', bookRef: 'GEN', textVersion: 'v1', segments: [] };
105142

@@ -183,7 +220,7 @@ beforeEach(() => {
183220

184221
describe('Interlinearizer', () => {
185222
beforeEach(() => {
186-
capturedContinuousViewProps = {};
223+
capturedContinuousViewProps = undefined;
187224
capturedSegmentViewPropsList = [];
188225
});
189226

@@ -309,6 +346,8 @@ describe('Interlinearizer', () => {
309346
/>,
310347
);
311348

349+
if (!capturedContinuousViewProps)
350+
throw new Error('Expected ContinuousView to have been rendered');
312351
expect(capturedContinuousViewProps.activePhraseIndex).toBe(1);
313352
});
314353

@@ -318,9 +357,9 @@ describe('Interlinearizer', () => {
318357

319358
expect(screen.getByTestId('continuous-view')).toBeInTheDocument();
320359

360+
if (!capturedContinuousViewProps)
361+
throw new Error('Expected ContinuousView to have been rendered');
321362
const { onVerseChange } = capturedContinuousViewProps;
322-
if (typeof onVerseChange !== 'function')
323-
throw new Error('Expected onVerseChange to be a function');
324363

325364
onVerseChange({ book: 'GEN', chapter: 2, verse: 3 });
326365

@@ -334,9 +373,9 @@ describe('Interlinearizer', () => {
334373
continuousScroll: true,
335374
});
336375

376+
if (!capturedContinuousViewProps)
377+
throw new Error('Expected ContinuousView to have been rendered');
337378
const { onFocusPhraseIndexChange } = capturedContinuousViewProps;
338-
if (typeof onFocusPhraseIndexChange !== 'function')
339-
throw new Error('Expected onFocusPhraseIndexChange to be a function');
340379

341380
act(() => {
342381
onFocusPhraseIndexChange(1);
@@ -355,6 +394,8 @@ describe('Interlinearizer', () => {
355394
/>,
356395
);
357396

397+
if (!capturedContinuousViewProps)
398+
throw new Error('Expected ContinuousView to have been rendered');
358399
expect(capturedContinuousViewProps.activePhraseIndex).toBeUndefined();
359400
});
360401

@@ -366,9 +407,9 @@ describe('Interlinearizer', () => {
366407
});
367408

368409
// Simulate ContinuousView reporting that phrase index 1 (GEN 1:2's token) is in view.
410+
if (!capturedContinuousViewProps)
411+
throw new Error('Expected ContinuousView to have been rendered');
369412
const { onFocusPhraseIndexChange } = capturedContinuousViewProps;
370-
if (typeof onFocusPhraseIndexChange !== 'function')
371-
throw new Error('Expected onFocusPhraseIndexChange to be a function');
372413

373414
act(() => {
374415
onFocusPhraseIndexChange(1);

src/__tests__/components/InterlinearizerLoader.test.tsx

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import { useData, useLocalizedStrings, useSetting } from '@papi/frontend/react';
66
import type { SerializedVerseRef } from '@sillsdev/scripture';
77
import { render, screen } from '@testing-library/react';
88
import userEvent from '@testing-library/user-event';
9-
import type { Book } from 'interlinearizer';
9+
import type { Book, Segment } from 'interlinearizer';
1010
import useInterlinearizerBookData from '../../hooks/useInterlinearizerBookData';
1111
import useOptimisticBooleanSetting from '../../hooks/useOptimisticBooleanSetting';
1212
import InterlinearizerLoader from '../../components/InterlinearizerLoader';
13-
import { makeWebViewState } from '../test-helpers';
13+
import { defaultScrRef, GEN_1_1_BOOK, makeWebViewState } from '../test-helpers';
1414

1515
jest.mock('../../hooks/useInterlinearizerBookData');
1616
jest.mock('../../hooks/useOptimisticBooleanSetting');
@@ -48,8 +48,12 @@ jest.mock('../../components/ContinuousView', () => ({
4848
}));
4949

5050
type CapturedInterlinearizerProps = {
51+
book: Book;
52+
chapterSegments: Segment[];
5153
continuousScroll: boolean;
52-
analysisLanguage: string | undefined;
54+
scrRef: SerializedVerseRef;
55+
setScrRef: (newScrRef: SerializedVerseRef) => void;
56+
analysisLanguage: string;
5357
};
5458
let capturedInterlinearizerProps: CapturedInterlinearizerProps | undefined;
5559

@@ -195,33 +199,6 @@ jest.mock('../../components/ProjectModals', () => ({
195199
},
196200
}));
197201

198-
const defaultScrRef: SerializedVerseRef = { book: 'GEN', chapterNum: 1, verseNum: 1 };
199-
200-
/** Pre-built Book with one GEN 1:1 segment. */
201-
const GEN_1_1_BOOK: Book = {
202-
id: 'GEN',
203-
bookRef: 'GEN',
204-
textVersion: 'v1',
205-
segments: [
206-
{
207-
id: 'GEN 1:1',
208-
startRef: { book: 'GEN', chapter: 1, verse: 1 },
209-
endRef: { book: 'GEN', chapter: 1, verse: 1 },
210-
baselineText: 'In the beginning.',
211-
tokens: [
212-
{
213-
ref: 'GEN 1:1:0',
214-
surfaceText: 'In',
215-
writingSystem: 'en',
216-
type: 'word',
217-
charStart: 0,
218-
charEnd: 2,
219-
},
220-
],
221-
},
222-
],
223-
};
224-
225202
/** Returns a `useWebViewScrollGroupScrRef` hook stub fixed to GEN 1:1. */
226203
function makeScrollGroupHook() {
227204
return (): [
@@ -278,6 +255,8 @@ function mockOptimisticSetting(
278255
*
279256
* @param interfaceMode - Value for `platform.interfaceMode`; defaults to `'simple'`.
280257
* @param interfaceLanguage - Value for `platform.interfaceLanguage`; defaults to `[]`.
258+
* @throws {Error} When `useSetting` is called with any key other than `platform.interfaceMode` or
259+
* `platform.interfaceLanguage` (message: `useSetting mock: unexpected key "<key>"`).
281260
*/
282261
function mockSettings(
283262
interfaceMode: 'simple' | 'power' = 'simple',

0 commit comments

Comments
 (0)