@@ -25,7 +25,7 @@ describe('useTranslator', () => {
2525 if ( typeof window !== 'undefined' ) {
2626 ( window as unknown as { Translator ?: unknown } ) . Translator = ( global as unknown as { Translator ?: unknown } ) . Translator ;
2727 }
28- mockAvailability . mockResolvedValue ( 'readily' ) ;
28+ mockAvailability . mockImplementation ( async ( ) => 'readily' ) ;
2929 mockTranslatorCreate . mockResolvedValue ( mockTranslator ) ;
3030 } ) ;
3131
@@ -140,6 +140,14 @@ describe('useTranslator', () => {
140140 expect ( result . current . status ) . toBe ( 'success' ) ;
141141 expect ( result . current . data ) . toBe ( 'Hola mundo' ) ;
142142
143+ mockTranslator . translate . mockImplementation ( ( ) => new Promise ( ( resolve ) => setTimeout ( ( ) => resolve ( 'Hola' ) , 100 ) ) ) ;
144+ act ( ( ) => {
145+ void result . current . translate ( 'Hello world' ) ;
146+ } ) ;
147+
148+ // Wait for the status to change from 'idle'
149+ await waitFor ( ( ) => expect ( result . current . status ) . not . toBe ( 'idle' ) ) ;
150+
143151 act ( ( ) => {
144152 result . current . reset ( ) ;
145153 } ) ;
@@ -262,4 +270,224 @@ describe('useTranslator', () => {
262270 expect ( result . current . status ) . toBe ( 'idle' ) ;
263271 expect ( result . current . error ) . toBeNull ( ) ;
264272 } ) ;
273+
274+ it ( 'should return early if already in a busy state' , async ( ) => {
275+ mockTranslator . translate . mockImplementation ( ( ) => new Promise ( ( resolve ) => setTimeout ( ( ) => resolve ( 'Hola' ) , 100 ) ) ) ;
276+ const { result } = renderHook ( ( ) => useTranslator ( { sourceLanguage : 'en' , targetLanguage : 'es' , streaming : false , warmup : false } ) ) ;
277+
278+ let firstTranslatePromise : Promise < void > | undefined ;
279+ act ( ( ) => {
280+ firstTranslatePromise = result . current . translate ( 'Hello' ) ;
281+ } ) ;
282+
283+ // Wait for the status to change from 'idle'
284+ await waitFor ( ( ) => expect ( result . current . status ) . not . toBe ( 'idle' ) ) ;
285+
286+ const secondTranslatePromise = result . current . translate ( 'World' ) ;
287+ expect ( secondTranslatePromise ) . resolves . toBeUndefined ( ) ;
288+ expect ( mockTranslator . translate ) . toHaveBeenCalledTimes ( 1 ) ;
289+
290+ await act ( async ( ) => {
291+ await firstTranslatePromise ;
292+ } ) ;
293+ } ) ;
294+
295+ it ( 'should call destroy and abort on unmount' , async ( ) => {
296+ mockTranslator . translate . mockResolvedValue ( 'Hola' ) ;
297+ const { result, unmount } = renderHook ( ( ) => useTranslator ( { sourceLanguage : 'en' , targetLanguage : 'es' , streaming : false , warmup : false } ) ) ;
298+
299+ await act ( async ( ) => {
300+ await result . current . translate ( 'Hello' ) ;
301+ } ) ;
302+
303+ unmount ( ) ;
304+ expect ( mockTranslator . destroy ) . toHaveBeenCalled ( ) ;
305+ } ) ;
306+
307+ it ( 'should handle LanguageDetector edge cases in resolveLanguages' , async ( ) => {
308+ vi . stubGlobal ( 'navigator' , {
309+ userActivation : { isActive : false } ,
310+ } ) ;
311+
312+ const mockLanguageDetector = {
313+ detect : vi . fn ( ) ,
314+ destroy : vi . fn ( ) ,
315+ } ;
316+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
317+ const LanguageDetectorConstructor = function ( ) { } as any ;
318+ LanguageDetectorConstructor . availability = vi . fn ( ) . mockResolvedValue ( 'readily' ) ;
319+ LanguageDetectorConstructor . create = vi . fn ( ) . mockResolvedValue ( mockLanguageDetector ) ;
320+ vi . stubGlobal ( 'LanguageDetector' , LanguageDetectorConstructor ) ;
321+
322+ const { result } = renderHook ( ( ) => useTranslator ( { sourceLanguage : 'auto' , targetLanguage : 'en' } ) ) ;
323+
324+ // Case: No user activation
325+ await act ( async ( ) => {
326+ await result . current . translate ( 'Hello' ) ;
327+ } ) ;
328+ expect ( result . current . detectedSourceLanguage ) . toBeUndefined ( ) ; // Falls back to 'en'
329+
330+ // Case: availability 'unavailable'
331+ vi . stubGlobal ( 'navigator' , { userActivation : { isActive : true } } ) ;
332+ LanguageDetectorConstructor . availability . mockResolvedValue ( 'unavailable' ) ;
333+ await act ( async ( ) => {
334+ await result . current . translate ( 'Hello' ) ;
335+ } ) ;
336+ expect ( result . current . detectedSourceLanguage ) . toBeUndefined ( ) ;
337+
338+ // Case: No detection results
339+ LanguageDetectorConstructor . availability . mockResolvedValue ( 'readily' ) ;
340+ mockLanguageDetector . detect . mockResolvedValue ( [ ] ) ;
341+ await act ( async ( ) => {
342+ await result . current . translate ( 'Hello' ) ;
343+ } ) ;
344+ expect ( result . current . detectedSourceLanguage ) . toBeUndefined ( ) ;
345+
346+ // Case: Detected language not supported
347+ mockLanguageDetector . detect . mockResolvedValue ( [ { detectedLanguage : 'xyz' , confidence : 1.0 } ] ) ;
348+ await act ( async ( ) => {
349+ await result . current . translate ( 'Hello' ) ;
350+ } ) ;
351+ expect ( result . current . detectedSourceLanguage ) . toBeUndefined ( ) ;
352+
353+ // Case: LanguageDetector throwing error
354+ LanguageDetectorConstructor . availability . mockRejectedValue ( new Error ( 'Fail' ) ) ;
355+ await act ( async ( ) => {
356+ await result . current . translate ( 'Hello' ) ;
357+ } ) ;
358+ expect ( result . current . detectedSourceLanguage ) . toBeUndefined ( ) ;
359+ } ) ;
360+
361+ it ( 'should fallback to en when user language is not supported' , async ( ) => {
362+ const { getUserLanguage } = await import ( '../utilities/userLanguage' ) ;
363+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
364+ vi . mocked ( getUserLanguage ) . mockReturnValue ( 'xyz' as any ) ;
365+
366+ const { result } = renderHook ( ( ) => useTranslator ( { targetLanguage : 'user' , warmup : false } ) ) ;
367+ await act ( async ( ) => {
368+ await result . current . translate ( 'Hello' ) ;
369+ } ) ;
370+ expect ( result . current . resolvedTargetLanguage ) . toBe ( 'en' ) ;
371+ } ) ;
372+
373+ it ( 'should handle security check when Translator is a base constructor' , async ( ) => {
374+ vi . stubGlobal ( 'Translator' , Object ) ;
375+ const { result } = renderHook ( ( ) => useTranslator ( { sourceLanguage : 'en' , targetLanguage : 'es' , warmup : false } ) ) ;
376+
377+ await act ( async ( ) => {
378+ await result . current . translate ( 'Hello' ) ;
379+ } ) ;
380+
381+ expect ( result . current . status ) . toBe ( 'error' ) ;
382+ expect ( result . current . error ?. message ) . toBe ( 'Translator is not available' ) ;
383+ } ) ;
384+
385+ it ( 'should handle security check when LanguageDetector is a base constructor' , async ( ) => {
386+ vi . stubGlobal ( 'LanguageDetector' , Array ) ;
387+ const { result } = renderHook ( ( ) => useTranslator ( { sourceLanguage : 'auto' , targetLanguage : 'en' , warmup : false } ) ) ;
388+
389+ await act ( async ( ) => {
390+ await result . current . translate ( 'Hello' ) ;
391+ } ) ;
392+
393+ // Should fallback to 'en' without throwing
394+ expect ( result . current . status ) . toBe ( 'success' ) ;
395+ expect ( result . current . detectedSourceLanguage ) . toBeUndefined ( ) ;
396+ } ) ;
397+
398+ it ( 'should handle missing Translator.availability' , async ( ) => {
399+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
400+ const TranslatorConstructor = function ( ) { } as any ;
401+ TranslatorConstructor . create = vi . fn ( ) . mockResolvedValue ( mockTranslator ) ;
402+ vi . stubGlobal ( 'Translator' , TranslatorConstructor ) ;
403+
404+ const { result } = renderHook ( ( ) => useTranslator ( { sourceLanguage : 'en' , targetLanguage : 'es' , warmup : false } ) ) ;
405+ await act ( async ( ) => {
406+ await result . current . translate ( 'Hello' ) ;
407+ } ) ;
408+ expect ( result . current . status ) . toBe ( 'success' ) ;
409+ } ) ;
410+
411+ it ( 'should handle availability "unavailable"' , async ( ) => {
412+ mockAvailability . mockResolvedValue ( 'unavailable' ) ;
413+ const { result } = renderHook ( ( ) => useTranslator ( { sourceLanguage : 'en' , targetLanguage : 'es' , warmup : false } ) ) ;
414+ await act ( async ( ) => {
415+ await result . current . translate ( 'Hello' ) ;
416+ } ) ;
417+ expect ( result . current . status ) . toBe ( 'error' ) ;
418+ expect ( result . current . error ?. message ) . toContain ( 'is not available for' ) ;
419+ } ) ;
420+
421+ it ( 'should handle missing Translator.create' , async ( ) => {
422+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
423+ const TranslatorConstructor = function ( ) { } as any ;
424+ TranslatorConstructor . availability = vi . fn ( ) . mockResolvedValue ( 'readily' ) ;
425+ vi . stubGlobal ( 'Translator' , TranslatorConstructor ) ;
426+
427+ const { result } = renderHook ( ( ) => useTranslator ( { sourceLanguage : 'en' , targetLanguage : 'es' , warmup : false } ) ) ;
428+ await act ( async ( ) => {
429+ await result . current . translate ( 'Hello' ) ;
430+ } ) ;
431+ expect ( result . current . status ) . toBe ( 'error' ) ;
432+ expect ( result . current . error ?. message ) . toBe ( 'Translator.create is not available' ) ;
433+ } ) ;
434+
435+ it ( 'should handle missing LanguageDetector.availability' , async ( ) => {
436+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
437+ const LanguageDetectorConstructor = function ( ) { } as any ;
438+ LanguageDetectorConstructor . create = vi . fn ( ) . mockResolvedValue ( {
439+ detect : vi . fn ( ) . mockResolvedValue ( [ { detectedLanguage : 'fr' , confidence : 1.0 } ] ) ,
440+ destroy : vi . fn ( ) ,
441+ } ) ;
442+ vi . stubGlobal ( 'LanguageDetector' , LanguageDetectorConstructor ) ;
443+
444+ const { result } = renderHook ( ( ) => useTranslator ( { sourceLanguage : 'auto' , targetLanguage : 'en' , warmup : false } ) ) ;
445+ await act ( async ( ) => {
446+ await result . current . translate ( 'Hello' ) ;
447+ } ) ;
448+ expect ( result . current . status ) . toBe ( 'success' ) ;
449+ expect ( result . current . detectedSourceLanguage ) . toBeUndefined ( ) ; // Falls back to 'en' because of missing availability check
450+ } ) ;
451+
452+ it ( 'should handle missing window.Translator' , async ( ) => {
453+ vi . stubGlobal ( 'Translator' , undefined ) ;
454+ const { result } = renderHook ( ( ) => useTranslator ( { sourceLanguage : 'en' , targetLanguage : 'es' , warmup : false } ) ) ;
455+ await act ( async ( ) => {
456+ await result . current . translate ( 'Hello' ) ;
457+ } ) ;
458+ expect ( result . current . status ) . toBe ( 'error' ) ;
459+ expect ( result . current . error ?. message ) . toBe ( 'Translator API not supported in this browser' ) ;
460+ } ) ;
461+
462+ it ( 'should handle non-Error rejection in translate' , async ( ) => {
463+ mockTranslator . translate . mockRejectedValue ( 'String error' ) ;
464+ const { result } = renderHook ( ( ) => useTranslator ( { sourceLanguage : 'en' , targetLanguage : 'es' , streaming : false , warmup : false } ) ) ;
465+
466+ await act ( async ( ) => {
467+ await result . current . translate ( 'Hello' ) ;
468+ } ) ;
469+
470+ expect ( result . current . status ) . toBe ( 'error' ) ;
471+ expect ( result . current . error ?. message ) . toBe ( 'Unknown error during translation' ) ;
472+ } ) ;
473+
474+ it ( 'should handle warmup with auto source language' , async ( ) => {
475+ vi . stubGlobal ( 'navigator' , { userActivation : { isActive : true } } ) ;
476+ renderHook ( ( ) => useTranslator ( { sourceLanguage : 'auto' , warmup : true } ) ) ;
477+ // Wait for resolveLanguages and createTranslator to be called
478+ await waitFor ( ( ) => expect ( mockTranslatorCreate ) . toHaveBeenCalled ( ) ) ;
479+ } ) ;
480+
481+ it ( 'should handle warmup error gracefully' , async ( ) => {
482+ mockAvailability . mockRejectedValue ( new Error ( 'Availability failed' ) ) ;
483+ renderHook ( ( ) => useTranslator ( { warmup : true } ) ) ;
484+ // Warmup error is swallowed but we wait to ensure it finished
485+ await new Promise ( resolve => setTimeout ( resolve , 50 ) ) ;
486+ } ) ;
487+
488+ it ( 'should call abort on unmount even if no translator is active' , ( ) => {
489+ const { unmount } = renderHook ( ( ) => useTranslator ( { warmup : false } ) ) ;
490+ unmount ( ) ;
491+ // No errors should occur
492+ } ) ;
265493} ) ;
0 commit comments