44
55import type { WebViewProps } from '@papi/core' ;
66import type { SerializedVerseRef } from '@sillsdev/scripture' ;
7- import { fireEvent , render , screen } from '@testing-library/react' ;
7+ import { act , fireEvent , render , screen , waitFor } from '@testing-library/react' ;
88import type { InterlinearData } from 'paratext-9-types' ;
99
1010/** Stub InterlinearData returned by the mocked parser. Matches shape the WebView displays. */
@@ -23,7 +23,7 @@ const stubInterlinearization = {
2323} ;
2424
2525const mockParse = jest . fn ( ) . mockReturnValue ( stubInterlinearData ) ;
26- const mockConvert = jest . fn ( ) . mockReturnValue ( stubInterlinearization ) ;
26+ const mockConvert = jest . fn ( ) . mockResolvedValue ( stubInterlinearization ) ;
2727
2828/** Stub analyses map for Analyses view (ID → Analysis). */
2929const stubAnalysesMap = new Map ( [
@@ -88,24 +88,40 @@ const testWebViewProps: WebViewProps = {
8888 updateWebViewDefinition : ( ) => true ,
8989} ;
9090
91+ /**
92+ * Renders the WebView and waits for the mount effect's async conversion to settle inside act(). The
93+ * component calls convertParatext9ToInterlinearization(parsed) in useEffect; when the promise
94+ * resolves it calls setInterlinearization. Without waiting, that update runs after the test and
95+ * triggers "An update to ... was not wrapped in act(...)". This helper flushes the async work so
96+ * all state updates are wrapped.
97+ */
98+ async function renderWebView ( ) : Promise < ReturnType < typeof render > > {
99+ return act ( async ( ) => {
100+ const result = render ( < InterlinearizerWebView { ...testWebViewProps } /> ) ;
101+ await Promise . resolve ( ) ;
102+ await Promise . resolve ( ) ;
103+ return result ;
104+ } ) ;
105+ }
106+
91107describe ( 'InterlinearizerWebView' , ( ) => {
92- it ( 'renders the heading "Interlinearizer"' , ( ) => {
93- render ( < InterlinearizerWebView { ... testWebViewProps } /> ) ;
108+ it ( 'renders the heading "Interlinearizer"' , async ( ) => {
109+ await renderWebView ( ) ;
94110
95111 expect ( screen . getByRole ( 'heading' , { name : / i n t e r l i n e a r i z e r / i } ) ) . toBeInTheDocument ( ) ;
96112 } ) ;
97113
98- it ( 'renders the description mentioning test-data XML' , ( ) => {
99- render ( < InterlinearizerWebView { ... testWebViewProps } /> ) ;
114+ it ( 'renders the description mentioning test-data XML' , async ( ) => {
115+ await renderWebView ( ) ;
100116
101117 expect (
102118 screen . getByText ( / r a w j s o n o f t h e m o d e l p a r s e d f r o m / i, { exact : false } ) ,
103119 ) . toBeInTheDocument ( ) ;
104120 expect ( screen . getByText ( / t e s t - d a t a \/ I n t e r l i n e a r _ e n _ M A T \. x m l / i) ) . toBeInTheDocument ( ) ;
105121 } ) ;
106122
107- it ( 'renders the JSON view mode switch (InterlinearData / Interlinearization / Analyses)' , ( ) => {
108- render ( < InterlinearizerWebView { ... testWebViewProps } /> ) ;
123+ it ( 'renders the JSON view mode switch (InterlinearData / Interlinearization / Analyses)' , async ( ) => {
124+ await renderWebView ( ) ;
109125
110126 const radiogroup = screen . getByRole ( 'radiogroup' , { name : / j s o n v i e w m o d e / i } ) ;
111127 expect ( radiogroup ) . toBeInTheDocument ( ) ;
@@ -115,64 +131,67 @@ describe('InterlinearizerWebView', () => {
115131 expect ( screen . getByText ( / v i e w j s o n a s : / i) ) . toBeInTheDocument ( ) ;
116132 } ) ;
117133
118- it ( 'displays InterlinearData JSON by default when parser returns data' , ( ) => {
119- render ( < InterlinearizerWebView { ... testWebViewProps } /> ) ;
134+ it ( 'displays InterlinearData JSON by default when parser returns data' , async ( ) => {
135+ await renderWebView ( ) ;
120136
121137 expect ( screen . getByText ( / ^ I n t e r l i n e a r D a t a \( J S O N \) : $ / ) ) . toBeInTheDocument ( ) ;
122138 expect ( screen . getByText ( / g l o s s L a n g u a g e / i) ) . toBeInTheDocument ( ) ;
123139 expect ( screen . getByText ( / b o o k I d / i) ) . toBeInTheDocument ( ) ;
124140 } ) ;
125141
126- it ( 'displays parsed structure including glossLanguage and bookId values' , ( ) => {
127- render ( < InterlinearizerWebView { ... testWebViewProps } /> ) ;
142+ it ( 'displays parsed structure including glossLanguage and bookId values' , async ( ) => {
143+ await renderWebView ( ) ;
128144
129145 expect ( screen . getByText ( / " e n " / ) ) . toBeInTheDocument ( ) ;
130146 expect ( screen . getByText ( / " M A T " / ) ) . toBeInTheDocument ( ) ;
131147 } ) ;
132148
133- it ( 'does not show parse error when parser succeeds' , ( ) => {
134- render ( < InterlinearizerWebView { ... testWebViewProps } /> ) ;
149+ it ( 'does not show parse error when parser succeeds' , async ( ) => {
150+ await renderWebView ( ) ;
135151
136152 expect ( screen . queryByText ( / ^ p a r s e e r r o r $ / i) ) . not . toBeInTheDocument ( ) ;
137153 } ) ;
138154
139- it ( 'displays parse error when parser throws an Error (uses err.message)' , ( ) => {
155+ it ( 'displays parse error when parser throws an Error (uses err.message)' , async ( ) => {
140156 mockParse . mockImplementationOnce ( ( ) => {
141157 throw new Error ( 'Invalid XML structure' ) ;
142158 } ) ;
143159
144- render ( < InterlinearizerWebView { ... testWebViewProps } /> ) ;
160+ await renderWebView ( ) ;
145161
146162 expect ( screen . getByRole ( 'heading' , { name : / ^ p a r s e e r r o r $ / i } ) ) . toBeInTheDocument ( ) ;
147163 expect ( screen . getByText ( / i n v a l i d x m l s t r u c t u r e / i) ) . toBeInTheDocument ( ) ;
148164 } ) ;
149165
150- it ( 'switching to Interlinearization shows converted model JSON' , ( ) => {
151- render ( < InterlinearizerWebView { ... testWebViewProps } /> ) ;
166+ it ( 'switching to Interlinearization shows converted model JSON' , async ( ) => {
167+ await renderWebView ( ) ;
152168
153169 fireEvent . click ( screen . getByRole ( 'radio' , { name : / ^ i n t e r l i n e a r i z a t i o n $ / i } ) ) ;
154170
155171 expect ( screen . getByText ( / ^ I n t e r l i n e a r i z a t i o n \( J S O N \) : $ / ) ) . toBeInTheDocument ( ) ;
156- expect ( screen . getByText ( / a n a l y s i s L a n g u a g e s / i) ) . toBeInTheDocument ( ) ;
157- expect ( screen . getByText ( / s o u r c e W r i t i n g S y s t e m / i) ) . toBeInTheDocument ( ) ;
158- expect ( screen . getByText ( / s e g m e n t s / i) ) . toBeInTheDocument ( ) ;
172+ await waitFor ( ( ) => {
173+ expect ( screen . getByText ( / a n a l y s i s L a n g u a g e s / i) ) . toBeInTheDocument ( ) ;
174+ expect ( screen . getByText ( / s o u r c e W r i t i n g S y s t e m / i) ) . toBeInTheDocument ( ) ;
175+ expect ( screen . getByText ( / s e g m e n t s / i) ) . toBeInTheDocument ( ) ;
176+ } ) ;
159177 } ) ;
160178
161- it ( 'switching back to InterlinearData shows PT9 structure JSON' , ( ) => {
162- render ( < InterlinearizerWebView { ... testWebViewProps } /> ) ;
179+ it ( 'switching back to InterlinearData shows PT9 structure JSON' , async ( ) => {
180+ await renderWebView ( ) ;
163181
164182 fireEvent . click ( screen . getByRole ( 'radio' , { name : / ^ i n t e r l i n e a r i z a t i o n $ / i } ) ) ;
165- expect ( screen . getByText ( / ^ I n t e r l i n e a r i z a t i o n \( J S O N \) : $ / ) ) . toBeInTheDocument ( ) ;
166-
183+ await waitFor ( ( ) => {
184+ expect ( screen . getByText ( / ^ I n t e r l i n e a r i z a t i o n \( J S O N \) : $ / ) ) . toBeInTheDocument ( ) ;
185+ } ) ;
167186 fireEvent . click ( screen . getByRole ( 'radio' , { name : / ^ i n t e r l i n e a r d a t a $ / i } ) ) ;
168187
169188 expect ( screen . getByText ( / ^ I n t e r l i n e a r D a t a \( J S O N \) : $ / ) ) . toBeInTheDocument ( ) ;
170189 expect ( screen . getByText ( / g l o s s L a n g u a g e / i) ) . toBeInTheDocument ( ) ;
171190 expect ( screen . getByText ( / b o o k I d / i) ) . toBeInTheDocument ( ) ;
172191 } ) ;
173192
174- it ( 'switching to Analyses shows analysis map JSON from test data' , ( ) => {
175- render ( < InterlinearizerWebView { ... testWebViewProps } /> ) ;
193+ it ( 'switching to Analyses shows analysis map JSON from test data' , async ( ) => {
194+ await renderWebView ( ) ;
176195
177196 fireEvent . click ( screen . getByRole ( 'radio' , { name : / ^ a n a l y s e s $ / i } ) ) ;
178197
@@ -183,28 +202,45 @@ describe('InterlinearizerWebView', () => {
183202 expect ( screen . getByText ( / p a r a t e x t - 9 / i) ) . toBeInTheDocument ( ) ;
184203 } ) ;
185204
186- it ( 'renders empty JSON pre when jsonToShow is undefined (converter returns undefined)' , ( ) => {
187- mockConvert . mockReturnValueOnce ( undefined ) ;
205+ it ( 'renders empty JSON pre when jsonToShow is undefined (converter returns undefined)' , async ( ) => {
206+ mockConvert . mockResolvedValueOnce ( undefined ) ;
188207
189- const { container } = render ( < InterlinearizerWebView { ... testWebViewProps } /> ) ;
208+ const { container } = await renderWebView ( ) ;
190209 fireEvent . click ( screen . getByRole ( 'radio' , { name : / ^ i n t e r l i n e a r i z a t i o n $ / i } ) ) ;
210+ await waitFor ( ( ) => {
211+ expect ( container . querySelector ( 'pre' ) ) . toBeInTheDocument ( ) ;
212+ } ) ;
191213
192214 const jsonPre = container . querySelector ( 'pre' ) ;
193215 expect ( jsonPre ) . toBeInTheDocument ( ) ;
194216 expect ( jsonPre ) . toBeEmptyDOMElement ( ) ;
195217 expect ( jsonPre ) . not . toHaveTextContent ( 'undefined' ) ;
196218 } ) ;
197219
198- it ( 'displays parse error when parser throws non-Error (uses String(err))' , ( ) => {
220+ it ( 'displays parse error when parser throws non-Error (uses String(err))' , async ( ) => {
199221 mockParse . mockImplementationOnce ( ( ) => {
200222 // Intentionally throw a non-Error to test the String(err) branch in the catch block.
201223 // eslint-disable-next-line no-throw-literal -- testing non-Error handling
202224 throw 'plain string error' ;
203225 } ) ;
204226
205- render ( < InterlinearizerWebView { ... testWebViewProps } /> ) ;
227+ await renderWebView ( ) ;
206228
207229 expect ( screen . getByRole ( 'heading' , { name : / ^ p a r s e e r r o r $ / i } ) ) . toBeInTheDocument ( ) ;
208230 expect ( screen . getByText ( 'plain string error' ) ) . toBeInTheDocument ( ) ;
209231 } ) ;
232+
233+ it ( 'sets interlinearization to undefined when converter rejects' , async ( ) => {
234+ mockConvert . mockRejectedValueOnce ( new Error ( 'Conversion failed' ) ) ;
235+
236+ const { container } = await renderWebView ( ) ;
237+ fireEvent . click ( screen . getByRole ( 'radio' , { name : / ^ i n t e r l i n e a r i z a t i o n $ / i } ) ) ;
238+ await waitFor ( ( ) => {
239+ expect ( container . querySelector ( 'pre' ) ) . toBeInTheDocument ( ) ;
240+ } ) ;
241+
242+ const jsonPre = container . querySelector ( 'pre' ) ;
243+ expect ( jsonPre ) . toBeInTheDocument ( ) ;
244+ expect ( jsonPre ) . toBeEmptyDOMElement ( ) ;
245+ } ) ;
210246} ) ;
0 commit comments