@@ -10,6 +10,7 @@ import type { Book } from 'interlinearizer';
1010import useInterlinearizerBookData from '../../hooks/useInterlinearizerBookData' ;
1111import useOptimisticBooleanSetting from '../../hooks/useOptimisticBooleanSetting' ;
1212import InterlinearizerLoader from '../../components/InterlinearizerLoader' ;
13+ import { makeWebViewState } from '../test-helpers' ;
1314
1415jest . mock ( '../../hooks/useInterlinearizerBookData' ) ;
1516jest . mock ( '../../hooks/useOptimisticBooleanSetting' ) ;
@@ -220,55 +221,14 @@ const GEN_1_1_BOOK: Book = {
220221 ] ,
221222} ;
222223
223- /**
224- * Returns a `useWebViewScrollGroupScrRef` hook stub bound to the given reference and setter.
225- *
226- * @param scrRef - Scripture reference to expose; defaults to GEN 1:1
227- * @param setScrRef - Setter callback; defaults to a no-op
228- */
229- function makeScrollGroupHook (
230- scrRef : SerializedVerseRef = defaultScrRef ,
231- setScrRef : ( r : SerializedVerseRef ) => void = ( ) => { } ,
232- ) {
224+ /** Returns a `useWebViewScrollGroupScrRef` hook stub fixed to GEN 1:1. */
225+ function makeScrollGroupHook ( ) {
233226 return ( ) : [
234227 SerializedVerseRef ,
235228 ( r : SerializedVerseRef ) => void ,
236229 number | undefined ,
237230 ( id : number | undefined ) => void ,
238- ] => [ scrRef , setScrRef , undefined , ( ) => { } ] ;
239- }
240-
241- /** Typed read/write pair stored per key in {@link makeWebViewState}. */
242- type StateSlot < T > = { get : ( ) => T ; set : ( v : T ) => void } ;
243-
244- /**
245- * Returns a `useWebViewState` hook stub that stores values in typed per-key closures so state
246- * persists across re-renders within the same test without requiring any type assertions.
247- */
248- function makeWebViewState ( ) {
249- // eslint-disable-next-line @typescript-eslint/no-explicit-any
250- const slots = new Map < string , StateSlot < any > > ( ) ;
251- return < T , > ( key : string , defaultValue : T ) : [ T , ( v : T ) => void , ( ) => void ] => {
252- let slot : StateSlot < T > | undefined = slots . get ( key ) ;
253- if ( slot === undefined ) {
254- let stored = defaultValue ;
255- slot = {
256- get : ( ) => stored ,
257- set : ( v ) => {
258- stored = v ;
259- } ,
260- } ;
261- slots . set ( key , slot ) ;
262- }
263- const resolvedSlot = slot ;
264- return [
265- resolvedSlot . get ( ) ,
266- ( v : T ) => resolvedSlot . set ( v ) ,
267- ( ) => {
268- slots . delete ( key ) ;
269- } ,
270- ] ;
271- } ;
231+ ] => [ defaultScrRef , ( ) => { } , undefined , ( ) => { } ] ;
272232}
273233
274234/**
@@ -365,24 +325,7 @@ describe('InterlinearizerLoader', () => {
365325 expect ( screen . getByText ( 'Loading…' ) ) . toBeInTheDocument ( ) ;
366326 } ) ;
367327
368- it ( 'shows an error when no USJ book is available for the project' , ( ) => {
369- mockBookData ( {
370- book : undefined ,
371- bookError : 'No USJ book available for GEN in project test-project-id' ,
372- } ) ;
373- render (
374- < InterlinearizerLoader
375- projectId = { testProjectId }
376- useWebViewScrollGroupScrRef = { makeScrollGroupHook ( ) }
377- useWebViewState = { makeWebViewState ( ) }
378- /> ,
379- ) ;
380-
381- expect ( screen . getByRole ( 'heading' , { name : / e r r o r l o a d i n g b o o k / i } ) ) . toBeInTheDocument ( ) ;
382- expect ( screen . getByText ( / n o u s j b o o k a v a i l a b l e f o r g e n i n p r o j e c t / i) ) . toBeInTheDocument ( ) ;
383- } ) ;
384-
385- it ( 'shows an error heading and message when book data is a PlatformError' , ( ) => {
328+ it ( 'shows an error heading and message when bookError is set' , ( ) => {
386329 mockBookData ( { book : undefined , bookError : 'Project not found' } ) ;
387330 render (
388331 < InterlinearizerLoader
@@ -430,48 +373,6 @@ describe('InterlinearizerLoader', () => {
430373 expect ( screen . getByText ( 'unexpected string error' ) ) . toBeInTheDocument ( ) ;
431374 } ) ;
432375
433- it ( 'passes a book-stable ref to BookUSJ so chapter and verse changes do not re-fetch the book' , ( ) => {
434- // The book-stable ref logic lives in useInterlinearizerBookData, which is tested in its own
435- // test file. This test verifies that InterlinearizerLoader passes scrRef into the hook and that
436- // re-rendering with a new verse does not cause the hook to receive a new book ref.
437- const webViewState = makeWebViewState ( ) ;
438- const { rerender } = render (
439- < InterlinearizerLoader
440- projectId = { testProjectId }
441- useWebViewScrollGroupScrRef = { makeScrollGroupHook ( ) }
442- useWebViewState = { webViewState }
443- /> ,
444- ) ;
445- rerender (
446- < InterlinearizerLoader
447- projectId = { testProjectId }
448- useWebViewScrollGroupScrRef = { makeScrollGroupHook ( {
449- book : 'GEN' ,
450- chapterNum : 2 ,
451- verseNum : 5 ,
452- } ) }
453- useWebViewState = { webViewState }
454- /> ,
455- ) ;
456-
457- const { calls } = jest . mocked ( useInterlinearizerBookData ) . mock ;
458- expect ( calls . length ) . toBeGreaterThanOrEqual ( 2 ) ;
459- calls . forEach ( ( args ) => expect ( args [ 0 ] . projectId ) . toBe ( testProjectId ) ) ;
460- } ) ;
461-
462- it ( 'passes continuousScroll from useOptimisticBooleanSetting to Interlinearizer' , ( ) => {
463- mockOptimisticSetting ( true ) ;
464- render (
465- < InterlinearizerLoader
466- projectId = { testProjectId }
467- useWebViewScrollGroupScrRef = { makeScrollGroupHook ( ) }
468- useWebViewState = { makeWebViewState ( ) }
469- /> ,
470- ) ;
471-
472- expect ( screen . getByTestId ( 'continuous-scroll-toggle' ) ) . toBeInTheDocument ( ) ;
473- } ) ;
474-
475376 it ( 'passes checked and disabled from useOptimisticBooleanSetting to ContinuousScrollToggle' , ( ) => {
476377 mockOptimisticSetting ( true , jest . fn ( ) , true ) ;
477378 render (
@@ -502,22 +403,6 @@ describe('InterlinearizerLoader', () => {
502403 expect ( mockOnChange ) . toHaveBeenCalledWith ( true ) ;
503404 } ) ;
504405
505- it ( 'clicking the continuous scroll toggle calls onChange with the toggled value' , async ( ) => {
506- const mockOnChange = jest . fn ( ) ;
507- mockOptimisticSetting ( true , mockOnChange ) ;
508- render (
509- < InterlinearizerLoader
510- projectId = { testProjectId }
511- useWebViewScrollGroupScrRef = { makeScrollGroupHook ( ) }
512- useWebViewState = { makeWebViewState ( ) }
513- /> ,
514- ) ;
515-
516- await userEvent . click ( screen . getByTestId ( 'continuous-scroll-toggle' ) ) ;
517-
518- expect ( mockOnChange ) . toHaveBeenCalledWith ( false ) ;
519- } ) ;
520-
521406 it ( 'passes continuousScroll=true to Interlinearizer when the setting is true' , ( ) => {
522407 mockOptimisticSetting ( true ) ;
523408 render (
@@ -620,23 +505,6 @@ describe('InterlinearizerLoader', () => {
620505 expect ( screen . getByTestId ( 'create-modal' ) ) . toBeInTheDocument ( ) ;
621506 } ) ;
622507
623- it ( 'closes the create modal and sets the active project when a project is created' , async ( ) => {
624- const state = makeWebViewState ( ) ;
625- render (
626- < InterlinearizerLoader
627- projectId = { testProjectId }
628- useWebViewScrollGroupScrRef = { makeScrollGroupHook ( ) }
629- useWebViewState = { state }
630- /> ,
631- ) ;
632-
633- await userEvent . click ( screen . getByTestId ( 'tab-toolbar-project-menu' ) ) ;
634- await userEvent . click ( screen . getByTestId ( 'select-modal-create-new' ) ) ;
635- await userEvent . click ( screen . getByTestId ( 'create-modal-created' ) ) ;
636-
637- expect ( screen . queryByTestId ( 'create-modal' ) ) . not . toBeInTheDocument ( ) ;
638- } ) ;
639-
640508 it ( 'closes all modals after a project is created from the select modal' , async ( ) => {
641509 render (
642510 < InterlinearizerLoader
@@ -667,6 +535,9 @@ describe('InterlinearizerLoader', () => {
667535 await userEvent . click ( screen . getByTestId ( 'select-modal-select' ) ) ;
668536
669537 expect ( screen . queryByTestId ( 'select-modal' ) ) . not . toBeInTheDocument ( ) ;
538+ // After selection the view-project-info button becomes available, confirming activeProject is set
539+ await userEvent . click ( screen . getByTestId ( 'tab-toolbar-view-project-info' ) ) ;
540+ expect ( screen . getByTestId ( 'metadata-modal' ) ) . toBeInTheDocument ( ) ;
670541 } ) ;
671542
672543 it ( 'opens the metadata modal from the openProjectInfoModal menu item when a project is active' , async ( ) => {
@@ -753,33 +624,6 @@ describe('InterlinearizerLoader', () => {
753624 expect ( screen . queryByTestId ( 'metadata-modal' ) ) . not . toBeInTheDocument ( ) ;
754625 } ) ;
755626
756- it ( 'handles topMenu with no items array without throwing' , ( ) => {
757- jest . mocked ( useData ) . mockReturnValue (
758- new Proxy (
759- { } ,
760- {
761- get : ( ) =>
762- jest
763- . fn ( )
764- . mockReturnValue ( [
765- { topMenu : { label : 'top' } , includeDefaults : true , contextMenu : undefined } ,
766- jest . fn ( ) ,
767- false ,
768- ] ) ,
769- } ,
770- ) ,
771- ) ;
772- render (
773- < InterlinearizerLoader
774- projectId = { testProjectId }
775- useWebViewScrollGroupScrRef = { makeScrollGroupHook ( ) }
776- useWebViewState = { makeWebViewState ( ) }
777- /> ,
778- ) ;
779-
780- expect ( screen . getByTestId ( 'tab-toolbar' ) ) . toBeInTheDocument ( ) ;
781- } ) ;
782-
783627 it ( 'renders without error when useData provides a topMenu with items' , ( ) => {
784628 const mockWebViewMenu = {
785629 topMenu : {
0 commit comments