@@ -164,6 +164,9 @@ describe('ModelsScreen', () => {
164164 expect ( RNFS . unlink ) . toHaveBeenCalledWith (
165165 '/path/to/documents/models/local/mockModelFile.bin' ,
166166 ) ;
167+ expect ( modelStore . removeModelByFullPath ) . toHaveBeenCalledWith (
168+ '/path/to/documents/models/local/mockModelFile.bin' ,
169+ ) ;
167170 expect ( RNFS . copyFile ) . toHaveBeenCalled ( ) ;
168171 expect ( modelStore . addLocalModel ) . toHaveBeenCalled ( ) ;
169172 } ) ;
@@ -263,6 +266,132 @@ describe('ModelsScreen', () => {
263266 } ) ;
264267 } ) ;
265268
269+ it ( 'shows a copying snackbar while file is being copied' , async ( ) => {
270+ let resolveCopy ! : ( ) => void ;
271+ ( RNFS . copyFile as jest . Mock ) . mockImplementation (
272+ ( ) =>
273+ new Promise < void > ( resolve => {
274+ resolveCopy = resolve ;
275+ } ) ,
276+ ) ;
277+ ( RNFS . exists as jest . Mock ) . mockImplementation ( async ( path : string ) => {
278+ if ( path . includes ( 'models/local' ) ) {
279+ return false ;
280+ }
281+ return true ;
282+ } ) ;
283+ ( pick as jest . Mock ) . mockResolvedValue ( [
284+ { uri : '/mock/file/path' , name : 'mockModelFile.bin' } ,
285+ ] ) ;
286+
287+ const { getByTestId} = render ( < ModelsScreen /> ) ;
288+
289+ // Open FAB, press local
290+ const fabGroup = getByTestId ( 'fab-group' ) ;
291+ fireEvent . press ( fabGroup ) ;
292+ await waitFor ( ( ) => {
293+ expect (
294+ getByTestId ( 'local-fab' , { includeHiddenElements : true } ) ,
295+ ) . toBeTruthy ( ) ;
296+ } ) ;
297+ await act ( async ( ) => {
298+ fireEvent . press ( getByTestId ( 'local-fab' , { includeHiddenElements : true } ) ) ;
299+ } ) ;
300+
301+ // Snackbar should be visible while copy is pending
302+ await waitFor ( ( ) => {
303+ expect ( getByTestId ( 'copy-model-snackbar' ) ) . toBeTruthy ( ) ;
304+ } ) ;
305+
306+ // Resolve the copy
307+ await act ( async ( ) => {
308+ resolveCopy ( ) ;
309+ } ) ;
310+ } ) ;
311+
312+ it ( 'shows an error alert when copy fails' , async ( ) => {
313+ const alertSpy = jest . spyOn ( Alert , 'alert' ) ;
314+ ( RNFS . copyFile as jest . Mock ) . mockRejectedValue ( new Error ( 'Disk full' ) ) ;
315+ ( RNFS . exists as jest . Mock ) . mockImplementation ( async ( path : string ) => {
316+ if ( path . includes ( 'models/local' ) ) {
317+ return false ;
318+ }
319+ return true ;
320+ } ) ;
321+ ( pick as jest . Mock ) . mockResolvedValue ( [
322+ { uri : '/mock/file/path' , name : 'mockModelFile.bin' } ,
323+ ] ) ;
324+
325+ const { getByTestId} = render ( < ModelsScreen /> ) ;
326+
327+ const fabGroup = getByTestId ( 'fab-group' ) ;
328+ fireEvent . press ( fabGroup ) ;
329+ await waitFor ( ( ) => {
330+ expect (
331+ getByTestId ( 'local-fab' , { includeHiddenElements : true } ) ,
332+ ) . toBeTruthy ( ) ;
333+ } ) ;
334+ await act ( async ( ) => {
335+ fireEvent . press ( getByTestId ( 'local-fab' , { includeHiddenElements : true } ) ) ;
336+ } ) ;
337+
338+ await waitFor ( ( ) => {
339+ expect ( alertSpy ) . toHaveBeenCalledWith ( expect . any ( String ) , 'Disk full' ) ;
340+ } ) ;
341+ } ) ;
342+
343+ it ( 'prevents double-tap by ignoring second add while copy is in progress' , async ( ) => {
344+ // First call: copy never resolves so isCopyingModel stays true
345+ ( RNFS . copyFile as jest . Mock ) . mockImplementation (
346+ ( ) => new Promise < void > ( ( ) => { } ) ,
347+ ) ;
348+ ( RNFS . exists as jest . Mock ) . mockImplementation ( async ( path : string ) => {
349+ if ( path . includes ( 'models/local' ) ) {
350+ return false ;
351+ }
352+ return true ;
353+ } ) ;
354+ ( pick as jest . Mock ) . mockResolvedValue ( [
355+ { uri : '/mock/file/path' , name : 'mockModelFile.bin' } ,
356+ ] ) ;
357+
358+ const { getByTestId} = render ( < ModelsScreen /> ) ;
359+
360+ // Open FAB and press local to start the first copy
361+ const fabGroup = getByTestId ( 'fab-group' ) ;
362+ fireEvent . press ( fabGroup ) ;
363+ await waitFor ( ( ) => {
364+ expect (
365+ getByTestId ( 'local-fab' , { includeHiddenElements : true } ) ,
366+ ) . toBeTruthy ( ) ;
367+ } ) ;
368+ await act ( async ( ) => {
369+ fireEvent . press ( getByTestId ( 'local-fab' , { includeHiddenElements : true } ) ) ;
370+ } ) ;
371+
372+ // Verify first copy started
373+ await waitFor ( ( ) => {
374+ expect ( pick ) . toHaveBeenCalledTimes ( 1 ) ;
375+ } ) ;
376+
377+ // Reset pick mock to detect if it gets called again
378+ ( pick as jest . Mock ) . mockClear ( ) ;
379+
380+ // Try to press local FAB again while copy is in progress
381+ fireEvent . press ( fabGroup ) ;
382+ await waitFor ( ( ) => {
383+ expect (
384+ getByTestId ( 'local-fab' , { includeHiddenElements : true } ) ,
385+ ) . toBeTruthy ( ) ;
386+ } ) ;
387+ await act ( async ( ) => {
388+ fireEvent . press ( getByTestId ( 'local-fab' , { includeHiddenElements : true } ) ) ;
389+ } ) ;
390+
391+ // pick should NOT have been called again — the early return guard blocks it
392+ expect ( pick ) . not . toHaveBeenCalled ( ) ;
393+ } ) ;
394+
266395 // Add tests for model filtering and grouping
267396 describe ( 'Model filtering and grouping' , ( ) => {
268397 beforeEach ( ( ) => {
0 commit comments