@@ -32,6 +32,14 @@ const defaultClientAuth: PublicSessionData = {
3232 userType : 'registered' ,
3333} ;
3434import { mockConfig , mockBuildConfig } from '@/test-utils/config' ;
35+ // ErrorBoundary creates its own isolated i18next instance from errorTranslations —
36+ // it does not use the global i18next singleton, so getTranslation() would return key strings here.
37+ // Importing the JSON directly is the correct source of truth for the translated path assertion.
38+ // The fallback path (no loader data) renders hardcoded inline English strings — no JSON import needed.
39+ import itITTranslations from '@/locales/it-IT/translations.json' ;
40+ const itITRouteError = itITTranslations . routeError ;
41+ import enGBTranslations from '@/locales/en-GB/translations.json' ;
42+ const enGBRouteError = enGBTranslations . routeError ;
3543
3644const mockSite = {
3745 ...mockBuildConfig . app . commerce . sites [ 0 ] ,
@@ -46,42 +54,11 @@ vi.mock('@salesforce/storefront-next-runtime/i18n/client', async () => {
4654 // (no resources pre-loaded, uses backend to fetch translations)
4755 const testInstance = i18next . default . createInstance ( ) ;
4856
49- // Mock the backend to return test translations
5057 const mockBackend = {
5158 type : 'backend' as const ,
5259 init : vi . fn ( ) ,
53- read : vi . fn ( ( _language : string , namespace : string , callback : ( error : any , data : any ) => void ) => {
54- // Return test translations for error namespace
55- if ( namespace === 'error' ) {
56- callback ( null , {
57- defaultTitle : 'Something went wrong' ,
58- goToHomepage : 'Go to Homepage' ,
59- allRightsReserved : 'All rights reserved.' ,
60- '404' : {
61- title : 'Page not found' ,
62- message :
63- "We couldn't find the page you're looking for. It may have been moved or the link might be incorrect." ,
64- secondaryMessage : "Don't worry—you can still explore our collection or head back home." ,
65- details : 'The requested page could not be found.' ,
66- } ,
67- '403' : {
68- title : 'Access restricted' ,
69- message :
70- "You don't have permission to view this page. If you believe this is an error, please contact our support team." ,
71- secondaryMessage : 'In the meantime, feel free to browse our collection.' ,
72- } ,
73- '500' : {
74- title : 'Something went wrong' ,
75- message :
76- "We're sorry, but something unexpected happened on our end. Our team has been notified and is working to fix it." ,
77- secondaryMessage :
78- 'Please try again in a few moments, or browse our shop while we sort things out.' ,
79- } ,
80- } ) ;
81- } else {
82- // Return empty translations for other namespaces
83- callback ( null , { } ) ;
84- }
60+ read : vi . fn ( ( _language : string , _namespace : string , callback : ( error : any , data : any ) => void ) => {
61+ callback ( null , { } ) ;
8562 } ) ,
8663 } ;
8764
@@ -162,6 +139,22 @@ vi.mock('@salesforce/storefront-next-runtime/design/react/core', async (importOr
162139 } ;
163140} ) ;
164141
142+ vi . mock ( 'react-router' , async ( importOriginal ) => {
143+ const actual = await importOriginal < typeof import ( 'react-router' ) > ( ) ;
144+ const realUseRouteLoaderData = actual . useRouteLoaderData ;
145+ return {
146+ ...actual ,
147+ // ErrorBoundary tests render outside a router context; swallow the invariant and return null.
148+ useRouteLoaderData : vi . fn ( ( routeId : string ) => {
149+ try {
150+ return realUseRouteLoaderData ( routeId ) ;
151+ } catch {
152+ return null ;
153+ }
154+ } ) ,
155+ } ;
156+ } ) ;
157+
165158vi . mock ( '@/middlewares/basket.server' , async ( ) => ( {
166159 ...( await vi . importActual ( '@/middlewares/basket.server' ) ) ,
167160 default : vi . fn ( ) ,
@@ -305,6 +298,16 @@ describe('root.tsx', () => {
305298 const stackText = 'Error: Test error with stack' ;
306299
307300 describe ( 'development mode' , ( ) => {
301+ beforeEach ( async ( ) => {
302+ const reactRouter = await import ( 'react-router' ) ;
303+ vi . mocked ( reactRouter . useRouteLoaderData ) . mockReturnValue ( {
304+ errorTranslations : enGBRouteError ,
305+ appConfig : { i18n : { fallbackLng : 'en-GB' } } ,
306+ locale : { id : 'en-GB' } ,
307+ site : mockSite ,
308+ } as any ) ;
309+ } ) ;
310+
308311 it ( 'should render normal error with message' , ( ) => {
309312 const error = new Error ( 'Test error' ) ;
310313 error . stack = stackText ;
@@ -359,7 +362,7 @@ describe('root.tsx', () => {
359362
360363 expect ( getByText ( '500' ) ) . toBeInTheDocument ( ) ;
361364 expect ( getByText ( 'Something went wrong' ) ) . toBeInTheDocument ( ) ;
362- // 500 errors now show friendly translated message instead of statusText
365+ // 500 errors show friendly translated message instead of statusText
363366 expect ( getByText ( / W e ' r e s o r r y , b u t s o m e t h i n g u n e x p e c t e d h a p p e n e d o n o u r e n d / ) ) . toBeInTheDocument ( ) ;
364367 expect ( container . querySelector ( 'pre' ) ) . not . toBeInTheDocument ( ) ;
365368 expect ( container . querySelector ( 'code' ) ) . not . toBeInTheDocument ( ) ;
@@ -369,9 +372,16 @@ describe('root.tsx', () => {
369372 describe ( 'production mode' , ( ) => {
370373 let originalEnv = import . meta. env . DEV ;
371374
372- beforeEach ( ( ) => {
375+ beforeEach ( async ( ) => {
373376 originalEnv = import . meta. env . DEV ;
374377 import . meta. env . DEV = false ;
378+ const reactRouter = await import ( 'react-router' ) ;
379+ vi . mocked ( reactRouter . useRouteLoaderData ) . mockReturnValue ( {
380+ errorTranslations : enGBRouteError ,
381+ appConfig : { i18n : { fallbackLng : 'en-GB' } } ,
382+ locale : { id : 'en-GB' } ,
383+ site : mockSite ,
384+ } as any ) ;
375385 } ) ;
376386
377387 afterEach ( ( ) => {
@@ -421,12 +431,46 @@ describe('root.tsx', () => {
421431
422432 expect ( getByText ( '500' ) ) . toBeInTheDocument ( ) ;
423433 expect ( getByText ( 'Something went wrong' ) ) . toBeInTheDocument ( ) ;
424- // 500 errors now show friendly translated message instead of statusText
434+ // 500 errors show friendly translated message instead of statusText
425435 expect ( getByText ( / W e ' r e s o r r y , b u t s o m e t h i n g u n e x p e c t e d h a p p e n e d o n o u r e n d / ) ) . toBeInTheDocument ( ) ;
426436 expect ( container . querySelector ( 'pre' ) ) . not . toBeInTheDocument ( ) ;
427437 expect ( container . querySelector ( 'code' ) ) . not . toBeInTheDocument ( ) ;
428438 } ) ;
429439 } ) ;
440+
441+ describe ( 'translation source' , ( ) => {
442+ afterEach ( async ( ) => {
443+ // Reset so the mock's try/catch default behaviour is restored for other tests.
444+ const reactRouter = await import ( 'react-router' ) ;
445+ vi . mocked ( reactRouter . useRouteLoaderData ) . mockRestore ( ) ;
446+ } ) ;
447+
448+ it ( 'should use errorTranslations from rootData when available' , async ( ) => {
449+ const reactRouter = await import ( 'react-router' ) ;
450+ vi . mocked ( reactRouter . useRouteLoaderData ) . mockReturnValue ( {
451+ errorTranslations : itITRouteError ,
452+ appConfig : { i18n : { fallbackLng : 'en-GB' } } ,
453+ locale : { id : 'it-IT' } ,
454+ site : mockSite ,
455+ } as any ) ;
456+
457+ const error = { status : 404 , statusText : 'Not Found' , data : { } , internal : false } ;
458+ const { getByText } = render ( < ErrorBoundary error = { error } /> ) ;
459+
460+ expect ( getByText ( '404' ) ) . toBeInTheDocument ( ) ;
461+ expect ( getByText ( itITRouteError [ '404' ] . title ) ) . toBeInTheDocument ( ) ;
462+ } ) ;
463+
464+ it ( 'should render hardcoded English fallback when no errorTranslations are available' , ( ) => {
465+ // useRouteLoaderData returns null (outside router context — default mock behaviour).
466+ // ErrorBoundary skips i18next entirely and renders hardcoded inline English strings.
467+ const error = { status : 404 , statusText : 'Not Found' , data : { } , internal : false } ;
468+ const { getByText } = render ( < ErrorBoundary error = { error } /> ) ;
469+
470+ expect ( getByText ( '404' ) ) . toBeInTheDocument ( ) ;
471+ expect ( getByText ( 'Page not found' ) ) . toBeInTheDocument ( ) ;
472+ } ) ;
473+ } ) ;
430474 } ) ;
431475
432476 describe ( 'App Component' , ( ) => {
0 commit comments