@@ -54,15 +54,18 @@ describe('SelectInterlinearProjectModal', () => {
5454
5555 it ( 'renders nothing while strings are loading' , ( ) => {
5656 jest . mocked ( useLocalizedStrings ) . mockReturnValue ( [ { } , true ] ) ;
57+ mockSendCommand . mockReturnValue ( new Promise ( ( ) => { } ) ) ;
5758 const { container } = render ( < SelectInterlinearProjectModal { ...defaultProps } /> ) ;
5859 expect ( container . firstChild ) . toBeNull ( ) ;
5960 } ) ;
6061
61- it ( 'renders the modal heading when strings are loaded' , ( ) => {
62+ it ( 'renders the modal heading when strings are loaded' , async ( ) => {
6263 render ( < SelectInterlinearProjectModal { ...defaultProps } /> ) ;
63- expect (
64- screen . getByRole ( 'heading' , { name : / s e l e c t i n t e r l i n e a r p r o j e c t / i } ) ,
65- ) . toBeInTheDocument ( ) ;
64+ await waitFor ( ( ) =>
65+ expect (
66+ screen . getByRole ( 'heading' , { name : / s e l e c t i n t e r l i n e a r p r o j e c t / i } ) ,
67+ ) . toBeInTheDocument ( ) ,
68+ ) ;
6669 } ) ;
6770
6871 it ( 'shows empty-state message when no projects are returned' , async ( ) => {
@@ -181,4 +184,82 @@ describe('SelectInterlinearProjectModal', () => {
181184 await waitFor ( ( ) => expect ( screen . getByText ( 'No projects yet.' ) ) . toBeInTheDocument ( ) ) ;
182185 expect ( screen . queryByRole ( 'listitem' ) ) . not . toBeInTheDocument ( ) ;
183186 } ) ;
187+
188+ it ( 'disables Cancel and Create New while the project list is loading' , async ( ) => {
189+ let resolveLoad : ( v : string ) => void = ( ) => { } ;
190+ mockSendCommand . mockImplementation (
191+ ( ) =>
192+ new Promise ( ( resolve ) => {
193+ resolveLoad = resolve ;
194+ } ) ,
195+ ) ;
196+ render ( < SelectInterlinearProjectModal { ...defaultProps } /> ) ;
197+
198+ expect ( screen . getByRole ( 'button' , { name : / ^ c a n c e l $ / i } ) ) . toBeDisabled ( ) ;
199+ expect ( screen . getByRole ( 'button' , { name : / c r e a t e n e w / i } ) ) . toBeDisabled ( ) ;
200+
201+ resolveLoad ( '[]' ) ;
202+ await waitFor ( ( ) =>
203+ expect ( screen . getByRole ( 'button' , { name : / ^ c a n c e l $ / i } ) ) . not . toBeDisabled ( ) ,
204+ ) ;
205+ } ) ;
206+
207+ it ( 're-enables Cancel and Create New after loading finishes' , async ( ) => {
208+ mockSendCommand . mockResolvedValue ( '[]' ) ;
209+ render ( < SelectInterlinearProjectModal { ...defaultProps } /> ) ;
210+
211+ await waitFor ( ( ) =>
212+ expect ( screen . getByRole ( 'button' , { name : / ^ c a n c e l $ / i } ) ) . not . toBeDisabled ( ) ,
213+ ) ;
214+ expect ( screen . getByRole ( 'button' , { name : / c r e a t e n e w / i } ) ) . not . toBeDisabled ( ) ;
215+ } ) ;
216+
217+ it ( 'clears the project list immediately when a new load begins' , async ( ) => {
218+ mockSendCommand . mockResolvedValue ( JSON . stringify ( [ STUB_PROJECT ] ) ) ;
219+ const { rerender } = render ( < SelectInterlinearProjectModal { ...defaultProps } /> ) ;
220+ await waitFor ( ( ) => expect ( screen . getByText ( 'Unnamed' ) ) . toBeInTheDocument ( ) ) ;
221+
222+ // Hold the second load in-flight so we can assert the list cleared before it resolves.
223+ let resolveSecond : ( v : string ) => void = ( ) => { } ;
224+ mockSendCommand . mockImplementation (
225+ ( ) =>
226+ new Promise ( ( resolve ) => {
227+ resolveSecond = resolve ;
228+ } ) ,
229+ ) ;
230+
231+ rerender ( < SelectInterlinearProjectModal { ...defaultProps } sourceProjectId = "src-proj-2" /> ) ;
232+
233+ await waitFor ( ( ) => expect ( screen . queryByText ( 'Unnamed' ) ) . not . toBeInTheDocument ( ) ) ;
234+ expect ( screen . queryByRole ( 'listitem' ) ) . not . toBeInTheDocument ( ) ;
235+
236+ resolveSecond ( '[]' ) ;
237+ await waitFor ( ( ) =>
238+ expect ( screen . getByRole ( 'button' , { name : / ^ c a n c e l $ / i } ) ) . not . toBeDisabled ( ) ,
239+ ) ;
240+ } ) ;
241+
242+ it ( 'ignores a response that arrives after a newer load has started' , async ( ) => {
243+ let resolveFirst : ( v : string ) => void = ( ) => { } ;
244+ mockSendCommand
245+ . mockImplementationOnce (
246+ ( ) =>
247+ new Promise ( ( resolve ) => {
248+ resolveFirst = resolve ;
249+ } ) ,
250+ )
251+ . mockResolvedValue ( JSON . stringify ( [ STUB_PROJECT_2 ] ) ) ;
252+
253+ const { rerender } = render ( < SelectInterlinearProjectModal { ...defaultProps } /> ) ;
254+
255+ // Start a second load by changing sourceProjectId before the first resolves.
256+ rerender ( < SelectInterlinearProjectModal { ...defaultProps } sourceProjectId = "src-proj-2" /> ) ;
257+ await waitFor ( ( ) => expect ( screen . getByText ( 'French glosses' ) ) . toBeInTheDocument ( ) ) ;
258+
259+ // Now deliver the stale first response — it must not replace the current list.
260+ resolveFirst ( JSON . stringify ( [ STUB_PROJECT ] ) ) ;
261+
262+ await waitFor ( ( ) => expect ( screen . queryByText ( 'Unnamed' ) ) . not . toBeInTheDocument ( ) ) ;
263+ expect ( screen . getByText ( 'French glosses' ) ) . toBeInTheDocument ( ) ;
264+ } ) ;
184265} ) ;
0 commit comments