44
55import type { SerializedVerseRef } from '@sillsdev/scripture' ;
66import { act , render , screen } from '@testing-library/react' ;
7- import type { Book , Segment } from 'interlinearizer' ;
7+ import type { Book , ScriptureRef , Segment } from 'interlinearizer' ;
88import type { ReactNode } from 'react' ;
99import Interlinearizer from '../../components/Interlinearizer' ;
10+ import type { SegmentDisplayMode } from '../../components/SegmentView' ;
11+ import { defaultScrRef , GEN_1_1_BOOK } from '../test-helpers' ;
1012
1113jest . 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. */
1942type 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} ;
2654let capturedSegmentViewPropsList : CapturedSegmentViewProps [ ] = [ ] ;
2755
2856jest . 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
3783jest . 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
5298jest . 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. */
104141const GEN_EMPTY_BOOK : Book = { id : 'GEN' , bookRef : 'GEN' , textVersion : 'v1' , segments : [ ] } ;
105142
@@ -183,7 +220,7 @@ beforeEach(() => {
183220
184221describe ( '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 ) ;
0 commit comments