Skip to content

Commit 601a26a

Browse files
Reduce test coverage redundancies, audit docstrings
1 parent 4fd585e commit 601a26a

21 files changed

Lines changed: 588 additions & 364 deletions

__mocks__/papi-frontend-react.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,29 @@
44
*/
55

66
/**
7-
* Mock for `useProjectData`. Returns a `Proxy` whose property accesses each yield a function
8-
* returning `[undefined, jest.fn(), false]`, matching the real hook's `[data, setter, isLoading]`
9-
* tuple without requiring a live data provider.
7+
* Known data-provider method names exposed by this mock. Tests that call an unlisted method will
8+
* receive a descriptive error rather than silently returning `undefined`, which mirrors the real
9+
* PAPI behaviour where requesting an unsupported provider key is a programmer error.
10+
*/
11+
const KNOWN_PROJECT_DATA_METHODS = new Set(['BookUSJ']);
12+
13+
/**
14+
* Mock for `useProjectData`. Returns an object whose known methods each return
15+
* `[undefined, jest.fn(), false]`, matching the real hook's `[data, setter, isLoading]` tuple.
16+
* Accessing an unknown method throws to catch misspelled provider keys in tests.
1017
*/
1118
const useProjectData = jest.fn(() =>
1219
new Proxy(
1320
{},
1421
{
15-
get: () => () => [undefined, jest.fn(), false],
22+
get(_target, prop: string) {
23+
if (KNOWN_PROJECT_DATA_METHODS.has(prop)) {
24+
return () => [undefined, jest.fn(), false];
25+
}
26+
throw new Error(
27+
`useProjectData mock: unknown method "${prop}". Add it to KNOWN_PROJECT_DATA_METHODS if intentional.`,
28+
);
29+
},
1630
},
1731
),
1832
);

src/__tests__/components/ContinuousView.test.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,39 @@
44

55
import { act, render, screen } from '@testing-library/react';
66
import userEvent from '@testing-library/user-event';
7-
import type { Book } from 'interlinearizer';
7+
import type { Book, Token } from 'interlinearizer';
88
import ContinuousView from '../../components/ContinuousView';
99

10+
jest.mock('../../components/PhraseBox', () => ({
11+
__esModule: true,
12+
default: ({
13+
isFocused = false,
14+
index,
15+
onClick,
16+
tokens,
17+
}: {
18+
isFocused?: boolean;
19+
index?: number;
20+
onClick?: (index?: number) => void;
21+
tokens: Token[];
22+
}) => (
23+
<button
24+
data-focus-state={isFocused ? 'focused' : 'default'}
25+
data-phrase-box="true"
26+
onClick={() => onClick?.(index)}
27+
type="button"
28+
>
29+
{tokens.map((t) => t.surfaceText).join(' ')}
30+
</button>
31+
),
32+
}));
33+
34+
jest.mock('../../components/TokenChip', () => ({
35+
__esModule: true,
36+
default: ({ token }: { token: Token }) => <span>{token.surfaceText}</span>,
37+
TokenChip: ({ token }: { token: Token }) => <span>{token.surfaceText}</span>,
38+
}));
39+
1040
// ---------------------------------------------------------------------------
1141
// Test fixtures
1242
// ---------------------------------------------------------------------------

src/__tests__/components/Interlinearizer.test.tsx

Lines changed: 43 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,20 @@
44

55
import type { SerializedVerseRef } from '@sillsdev/scripture';
66
import { render, screen } from '@testing-library/react';
7-
import userEvent from '@testing-library/user-event';
8-
import type { Book } from 'interlinearizer';
7+
import type { Book, Segment } from 'interlinearizer';
98
import Interlinearizer from '../../components/Interlinearizer';
109

11-
// Store captured props so tests can simulate callbacks
10+
// Store captured props so tests can inspect what Interlinearizer passes down
1211
let capturedContinuousViewProps: Record<string, unknown> = {};
1312

13+
type CapturedSegmentViewProps = {
14+
segment: Segment;
15+
displayMode?: string;
16+
isActive?: boolean;
17+
onClick?: (ref: { book: string; chapter: number; verse: number }) => void;
18+
};
19+
let capturedSegmentViewPropsList: CapturedSegmentViewProps[] = [];
20+
1421
jest.mock('../../components/ContinuousView', () => ({
1522
__esModule: true,
1623
default: (props: Record<string, unknown>) => {
@@ -19,6 +26,18 @@ jest.mock('../../components/ContinuousView', () => ({
1926
},
2027
}));
2128

29+
jest.mock('../../components/SegmentView', () => ({
30+
__esModule: true,
31+
SegmentView: ({ segment, ...rest }: CapturedSegmentViewProps) => {
32+
capturedSegmentViewPropsList.push({ segment, ...rest });
33+
return <div data-testid="segment-view" data-segment-id={segment.id} />;
34+
},
35+
default: ({ segment, ...rest }: CapturedSegmentViewProps) => {
36+
capturedSegmentViewPropsList.push({ segment, ...rest });
37+
return <div data-testid="segment-view" data-segment-id={segment.id} />;
38+
},
39+
}));
40+
2241
const defaultScrRef: SerializedVerseRef = { book: 'GEN', chapterNum: 1, verseNum: 1 };
2342

2443
/** Pre-built Book with one GEN 1:1 segment. */
@@ -90,31 +109,6 @@ const GEN_1_MULTI_BOOK: Book = {
90109
],
91110
};
92111

93-
/** Book with a non-word (punctuation) token — exercises the non-word chip branch. */
94-
const GEN_1_1_PUNCTUATION_BOOK: Book = {
95-
id: 'GEN',
96-
bookRef: 'GEN',
97-
textVersion: 'v1',
98-
segments: [
99-
{
100-
id: 'GEN 1:1',
101-
startRef: { book: 'GEN', chapter: 1, verse: 1 },
102-
endRef: { book: 'GEN', chapter: 1, verse: 1 },
103-
baselineText: '.',
104-
tokens: [
105-
{
106-
id: 'GEN 1:1:0',
107-
surfaceText: '.',
108-
writingSystem: 'en',
109-
type: 'punctuation',
110-
charStart: 0,
111-
charEnd: 1,
112-
},
113-
],
114-
},
115-
],
116-
};
117-
118112
function renderInterlinearizer({
119113
book = GEN_1_1_BOOK,
120114
bookSegments = GEN_1_1_BOOK.segments,
@@ -142,12 +136,13 @@ function renderInterlinearizer({
142136
describe('Interlinearizer', () => {
143137
beforeEach(() => {
144138
capturedContinuousViewProps = {};
139+
capturedSegmentViewPropsList = [];
145140
});
146141

147-
it('renders token chips when the tokenized book has a segment for the current reference', () => {
142+
it('renders a SegmentView when the tokenized book has a segment for the current reference', () => {
148143
renderInterlinearizer();
149144

150-
expect(screen.getByText('In')).toBeInTheDocument();
145+
expect(screen.getAllByTestId('segment-view')).toHaveLength(1);
151146
});
152147

153148
it('shows a no-verse message when the tokenized book has no segments at all', () => {
@@ -156,56 +151,48 @@ describe('Interlinearizer', () => {
156151
expect(screen.getByText(/no verse data for gen 1\./i)).toBeInTheDocument();
157152
});
158153

159-
it('renders all segments in the current chapter', () => {
154+
it('renders a SegmentView for every segment in the current chapter', () => {
160155
renderInterlinearizer({ bookSegments: GEN_1_MULTI_BOOK.segments });
161156

162-
expect(screen.getByText('In')).toBeInTheDocument();
163-
expect(screen.getByText('And')).toBeInTheDocument();
157+
expect(screen.getAllByTestId('segment-view')).toHaveLength(2);
158+
expect(capturedSegmentViewPropsList[0].segment.id).toBe('GEN 1:1');
159+
expect(capturedSegmentViewPropsList[1].segment.id).toBe('GEN 1:2');
164160
});
165161

166-
it('highlights only the segment matching the current verse', () => {
167-
const { container } = renderInterlinearizer({ bookSegments: GEN_1_MULTI_BOOK.segments });
162+
it('passes isActive=true only to the segment matching the current verse', () => {
163+
renderInterlinearizer({ bookSegments: GEN_1_MULTI_BOOK.segments });
168164

169-
// defaultScrRef is GEN 1:1, so verse 1 is active
170-
const activeSegments = container.querySelectorAll('button[aria-current="true"]');
171-
expect(activeSegments).toHaveLength(1);
165+
// defaultScrRef is GEN 1:1
166+
expect(capturedSegmentViewPropsList[0].isActive).toBe(true);
167+
expect(capturedSegmentViewPropsList[1].isActive).toBeFalsy();
172168
});
173169

174-
it('shows all chapter segments when navigating to a title reference (verse 0)', () => {
170+
it('renders all segments when navigating to a title reference (verse 0)', () => {
175171
const titleRef: SerializedVerseRef = { book: 'GEN', chapterNum: 1, verseNum: 0 };
176172
renderInterlinearizer({ bookSegments: GEN_1_MULTI_BOOK.segments, scrRef: titleRef });
177173

178-
expect(screen.getByText('In')).toBeInTheDocument();
179-
expect(screen.getByText('And')).toBeInTheDocument();
180-
});
181-
182-
it('renders non-word tokens as muted chips', () => {
183-
renderInterlinearizer({ bookSegments: GEN_1_1_PUNCTUATION_BOOK.segments });
184-
185-
expect(screen.getByText('.')).toBeInTheDocument();
174+
expect(screen.getAllByTestId('segment-view')).toHaveLength(2);
186175
});
187176

188-
it('calls setScrRef with the segment ref when a verse box is clicked', async () => {
177+
it('calls setScrRef with the segment ref when a verse box is clicked', () => {
189178
const mockSetScrRef = jest.fn();
190179
renderInterlinearizer({ bookSegments: GEN_1_MULTI_BOOK.segments, setScrRef: mockSetScrRef });
191180

192-
await userEvent.click(screen.getByText('And'));
181+
capturedSegmentViewPropsList[1].onClick?.({ book: 'GEN', chapter: 1, verse: 2 });
193182

194183
expect(mockSetScrRef).toHaveBeenCalledWith({ book: 'GEN', chapterNum: 1, verseNum: 2 });
195184
});
196185

197-
it('renders segments in baseline-text mode when continuousScroll is true', () => {
186+
it('passes displayMode="baseline-text" to SegmentView when continuousScroll is true', () => {
198187
renderInterlinearizer({ continuousScroll: true });
199188

200-
expect(screen.getByText('In the beginning.')).toBeInTheDocument();
201-
expect(screen.queryByText('In')).not.toBeInTheDocument();
189+
expect(capturedSegmentViewPropsList[0].displayMode).toBe('baseline-text');
202190
});
203191

204-
it('renders all chapter segments in baseline-text mode when continuousScroll is true', () => {
192+
it('passes displayMode="baseline-text" to all SegmentViews when continuousScroll is true', () => {
205193
renderInterlinearizer({ bookSegments: GEN_1_MULTI_BOOK.segments, continuousScroll: true });
206194

207-
expect(screen.getByText('In the beginning.')).toBeInTheDocument();
208-
expect(screen.getByText('And the earth.')).toBeInTheDocument();
195+
capturedSegmentViewPropsList.forEach((p) => expect(p.displayMode).toBe('baseline-text'));
209196
});
210197

211198
it('renders ContinuousView when continuousScroll is true', () => {
@@ -228,7 +215,7 @@ describe('Interlinearizer', () => {
228215

229216
const continuousView = screen.getByTestId('continuous-view');
230217
const allElements = Array.from(
231-
container.querySelectorAll('[data-testid="continuous-view"], button[aria-current]'),
218+
container.querySelectorAll('[data-testid="continuous-view"], [data-testid="segment-view"]'),
232219
);
233220
expect(allElements[0]).toBe(continuousView);
234221
});

0 commit comments

Comments
 (0)