11import { describe , it , expect , vi , beforeEach } from "vitest" ;
2- import { render , screen , waitFor } from "@testing-library/react" ;
2+ import { act , render , screen , waitFor } from "@testing-library/react" ;
33import React from "react" ;
44import { LingoProvider , LingoProviderWrapper } from "./provider" ;
55import { LingoContext } from "./context" ;
@@ -52,10 +52,9 @@ describe("client/provider", () => {
5252 } ) ;
5353
5454 describe ( "LingoProviderWrapper" , ( ) => {
55- it ( "loads dictionary and renders children; returns null while loading" , async ( ) => {
56- const loadDictionary = vi
57- . fn ( )
58- . mockResolvedValue ( { locale : "en" , files : { } } ) ;
55+ it ( "renders nothing while loading by default, then shows children" , async ( ) => {
56+ const deferred = createDeferred < { locale : string ; files : Record < string , unknown > } > ( ) ;
57+ const loadDictionary = vi . fn ( ( ) => deferred . promise ) ;
5958
6059 const Child = ( ) => < div data-testid = "child" > ok</ div > ;
6160
@@ -65,24 +64,91 @@ describe("client/provider", () => {
6564 </ LingoProviderWrapper > ,
6665 ) ;
6766
68- // initially null during loading
67+ // No fallback by default (renders nothing during load)
6968 expect ( container . firstChild ) . toBeNull ( ) ;
7069
70+ await act ( async ( ) => {
71+ deferred . resolve ( { locale : "en" , files : { } } ) ;
72+ await deferred . promise ;
73+ } ) ;
74+
7175 await waitFor ( ( ) => expect ( loadDictionary ) . toHaveBeenCalled ( ) ) ;
7276 const child = await findByTestId ( "child" ) ;
73- expect ( child != null ) . toBe ( true ) ;
77+ expect ( child ) . not . toBeNull ( ) ;
7478 } ) ;
7579
76- it ( "swallows load errors and stays null" , async ( ) => {
77- const loadDictionary = vi . fn ( ) . mockRejectedValue ( new Error ( "boom" ) ) ;
78- const { container } = render (
79- < LingoProviderWrapper loadDictionary = { loadDictionary } >
80+ it ( "supports a custom fallback" , ( ) => {
81+ const loadDictionary = vi . fn ( ( ) => new Promise ( ( ) => { } ) ) ;
82+
83+ render (
84+ < LingoProviderWrapper
85+ loadDictionary = { loadDictionary }
86+ fallback = { < div data-testid = "fallback" > waiting</ div > }
87+ >
8088 < div />
8189 </ LingoProviderWrapper > ,
8290 ) ;
8391
84- await vi . waitFor ( ( ) => expect ( loadDictionary ) . toHaveBeenCalled ( ) ) ;
85- expect ( container . firstChild ) . toBeNull ( ) ;
92+ const fallback = screen . getByTestId ( "fallback" ) ;
93+ expect ( fallback ) . not . toBeNull ( ) ;
94+ expect ( fallback . textContent ) . toBe ( "waiting" ) ;
95+ } ) ;
96+
97+ it ( "propagates load errors to the nearest error boundary" , async ( ) => {
98+ const loadDictionary = vi . fn ( ) . mockRejectedValue ( new Error ( "boom" ) ) ;
99+ const onError = vi . fn ( ) ;
100+ const consoleSpy = vi . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } ) ;
101+
102+ render (
103+ < TestErrorBoundary onError = { onError } >
104+ < LingoProviderWrapper loadDictionary = { loadDictionary } >
105+ < div />
106+ </ LingoProviderWrapper >
107+ </ TestErrorBoundary > ,
108+ ) ;
109+
110+ await waitFor ( ( ) => expect ( onError ) . toHaveBeenCalled ( ) ) ;
111+ expect ( onError . mock . calls [ 0 ] [ 0 ] ) . toBeInstanceOf ( Error ) ;
112+ expect ( onError . mock . calls [ 0 ] [ 0 ] . message ) . toBe ( "boom" ) ;
113+
114+ const errorBoundary = await screen . findByTestId ( "boundary-error" ) ;
115+ expect ( errorBoundary ) . not . toBeNull ( ) ;
116+ expect ( errorBoundary . textContent ) . toBe ( "error" ) ;
117+
118+ consoleSpy . mockRestore ( ) ;
86119 } ) ;
87120 } ) ;
88121} ) ;
122+
123+ function createDeferred < T > ( ) {
124+ let resolve ! : ( value : T | PromiseLike < T > ) => void ;
125+ let reject ! : ( reason ?: unknown ) => void ;
126+ const promise = new Promise < T > ( ( res , rej ) => {
127+ resolve = res ;
128+ reject = rej ;
129+ } ) ;
130+ return { promise, resolve, reject } ;
131+ }
132+
133+ class TestErrorBoundary extends React . Component <
134+ { onError : ( error : Error ) => void ; children : React . ReactNode } ,
135+ { hasError : boolean }
136+ > {
137+ state = { hasError : false } ;
138+
139+ static getDerivedStateFromError ( ) {
140+ return { hasError : true } ;
141+ }
142+
143+ componentDidCatch ( error : Error ) {
144+ this . props . onError ( error ) ;
145+ }
146+
147+ render ( ) {
148+ if ( this . state . hasError ) {
149+ return < div data-testid = "boundary-error" > error</ div > ;
150+ }
151+
152+ return this . props . children ;
153+ }
154+ }
0 commit comments