@@ -60,8 +60,41 @@ describe('streamServerRenderedReactComponent', () => {
6060 throwSyncError = false ,
6161 throwJsErrors = false ,
6262 throwAsyncError = false ,
63+ componentType = 'reactComponent' ,
6364 } = { } ) => {
64- ComponentRegistry . register ( { TestComponentForStreaming } ) ;
65+ switch ( componentType ) {
66+ case 'reactComponent' :
67+ ComponentRegistry . register ( { TestComponentForStreaming } ) ;
68+ break ;
69+ case 'renderFunction' :
70+ ComponentRegistry . register ( {
71+ TestComponentForStreaming : ( props , _railsContext ) => ( ) => < TestComponentForStreaming { ...props } /> ,
72+ } ) ;
73+ break ;
74+ case 'asyncRenderFunction' :
75+ ComponentRegistry . register ( {
76+ TestComponentForStreaming : ( props , _railsContext ) => ( ) =>
77+ Promise . resolve ( < TestComponentForStreaming { ...props } /> ) ,
78+ } ) ;
79+ break ;
80+ case 'erroneousRenderFunction' :
81+ ComponentRegistry . register ( {
82+ TestComponentForStreaming : ( _props , _railsContext ) => {
83+ // The error happen inside the render function itself not inside the returned React component
84+ throw new Error ( 'Sync Error from render function' ) ;
85+ } ,
86+ } ) ;
87+ break ;
88+ case 'erroneousAsyncRenderFunction' :
89+ ComponentRegistry . register ( {
90+ TestComponentForStreaming : ( _props , _railsContext ) =>
91+ // The error happen inside the render function itself not inside the returned React component
92+ Promise . reject ( new Error ( 'Async Error from render function' ) ) ,
93+ } ) ;
94+ break ;
95+ default :
96+ throw new Error ( `Unknown component type: ${ componentType } ` ) ;
97+ }
6598 const renderResult = streamServerRenderedReactComponent ( {
6699 name : 'TestComponentForStreaming' ,
67100 domNodeId : 'myDomId' ,
@@ -82,7 +115,7 @@ describe('streamServerRenderedReactComponent', () => {
82115 it ( 'streamServerRenderedReactComponent streams the rendered component' , async ( ) => {
83116 const { renderResult, chunks } = setupStreamTest ( ) ;
84117 await new Promise ( ( resolve ) => {
85- renderResult . on ( 'end' , resolve ) ;
118+ renderResult . once ( 'end' , resolve ) ;
86119 } ) ;
87120
88121 expect ( chunks ) . toHaveLength ( 2 ) ;
@@ -101,7 +134,7 @@ describe('streamServerRenderedReactComponent', () => {
101134 const onError = jest . fn ( ) ;
102135 renderResult . on ( 'error' , onError ) ;
103136 await new Promise ( ( resolve ) => {
104- renderResult . on ( 'end' , resolve ) ;
137+ renderResult . once ( 'end' , resolve ) ;
105138 } ) ;
106139
107140 expect ( onError ) . toHaveBeenCalled ( ) ;
@@ -117,7 +150,7 @@ describe('streamServerRenderedReactComponent', () => {
117150 const onError = jest . fn ( ) ;
118151 renderResult . on ( 'error' , onError ) ;
119152 await new Promise ( ( resolve ) => {
120- renderResult . on ( 'end' , resolve ) ;
153+ renderResult . once ( 'end' , resolve ) ;
121154 } ) ;
122155
123156 expect ( onError ) . not . toHaveBeenCalled ( ) ;
@@ -133,17 +166,17 @@ describe('streamServerRenderedReactComponent', () => {
133166 const onError = jest . fn ( ) ;
134167 renderResult . on ( 'error' , onError ) ;
135168 await new Promise ( ( resolve ) => {
136- renderResult . on ( 'end' , resolve ) ;
169+ renderResult . once ( 'end' , resolve ) ;
137170 } ) ;
138171
139172 expect ( onError ) . toHaveBeenCalled ( ) ;
140- expect ( chunks ) . toHaveLength ( 2 ) ;
173+ expect ( chunks . length ) . toBeGreaterThanOrEqual ( 2 ) ;
141174 expect ( chunks [ 0 ] . html ) . toContain ( 'Header In The Shell' ) ;
142175 expect ( chunks [ 0 ] . consoleReplayScript ) . toBe ( '' ) ;
143176 expect ( chunks [ 0 ] . hasErrors ) . toBe ( false ) ;
144177 expect ( chunks [ 0 ] . isShellReady ) . toBe ( true ) ;
145178 // Script that fallbacks the render to client side
146- expect ( chunks [ 1 ] . html ) . toMatch ( / < s c r i p t > [ . \s \S ] * A s y n c E r r o r [ . \s \S ] * < \/ s c r i p t > / ) ;
179+ expect ( chunks [ 1 ] . html ) . toMatch ( / t h e s e r v e r r e n d e r i n g e r r o r e d : \\ n \\ n A s y n c E r r o r / ) ;
147180 expect ( chunks [ 1 ] . consoleReplayScript ) . toBe ( '' ) ;
148181 expect ( chunks [ 1 ] . hasErrors ) . toBe ( true ) ;
149182 expect ( chunks [ 1 ] . isShellReady ) . toBe ( true ) ;
@@ -154,19 +187,149 @@ describe('streamServerRenderedReactComponent', () => {
154187 const onError = jest . fn ( ) ;
155188 renderResult . on ( 'error' , onError ) ;
156189 await new Promise ( ( resolve ) => {
157- renderResult . on ( 'end' , resolve ) ;
190+ renderResult . once ( 'end' , resolve ) ;
158191 } ) ;
159192
160193 expect ( onError ) . not . toHaveBeenCalled ( ) ;
161- expect ( chunks ) . toHaveLength ( 2 ) ;
194+ expect ( chunks . length ) . toBeGreaterThanOrEqual ( 2 ) ;
162195 expect ( chunks [ 0 ] . html ) . toContain ( 'Header In The Shell' ) ;
163196 expect ( chunks [ 0 ] . consoleReplayScript ) . toBe ( '' ) ;
164197 expect ( chunks [ 0 ] . hasErrors ) . toBe ( false ) ;
165198 expect ( chunks [ 0 ] . isShellReady ) . toBe ( true ) ;
166199 // Script that fallbacks the render to client side
167- expect ( chunks [ 1 ] . html ) . toMatch ( / < s c r i p t > [ . \s \S ] * A s y n c E r r o r [ . \s \S ] * < \/ s c r i p t > / ) ;
200+ expect ( chunks [ 1 ] . html ) . toMatch ( / t h e s e r v e r r e n d e r i n g e r r o r e d : \\ n \\ n A s y n c E r r o r / ) ;
168201 expect ( chunks [ 1 ] . consoleReplayScript ) . toBe ( '' ) ;
169202 expect ( chunks [ 1 ] . hasErrors ) . toBe ( true ) ;
170203 expect ( chunks [ 1 ] . isShellReady ) . toBe ( true ) ;
171204 } ) ;
205+
206+ it . each ( [ 'asyncRenderFunction' , 'renderFunction' ] ) (
207+ 'streams a component from a %s that resolves to a React component' ,
208+ async ( componentType ) => {
209+ const { renderResult, chunks } = setupStreamTest ( { componentType } ) ;
210+ await new Promise ( ( resolve ) => {
211+ renderResult . once ( 'end' , resolve ) ;
212+ } ) ;
213+
214+ expect ( chunks ) . toHaveLength ( 2 ) ;
215+ expect ( chunks [ 0 ] . html ) . toContain ( 'Header In The Shell' ) ;
216+ expect ( chunks [ 0 ] . consoleReplayScript ) . toBe ( '' ) ;
217+ expect ( chunks [ 0 ] . hasErrors ) . toBe ( false ) ;
218+ expect ( chunks [ 0 ] . isShellReady ) . toBe ( true ) ;
219+ expect ( chunks [ 1 ] . html ) . toContain ( 'Async Content' ) ;
220+ expect ( chunks [ 1 ] . consoleReplayScript ) . toBe ( '' ) ;
221+ expect ( chunks [ 1 ] . hasErrors ) . toBe ( false ) ;
222+ expect ( chunks [ 1 ] . isShellReady ) . toBe ( true ) ;
223+ } ,
224+ ) ;
225+
226+ it . each ( [ 'asyncRenderFunction' , 'renderFunction' ] ) (
227+ 'handles sync errors in the %s' ,
228+ async ( componentType ) => {
229+ const { renderResult, chunks } = setupStreamTest ( { componentType, throwSyncError : true } ) ;
230+ await new Promise ( ( resolve ) => {
231+ renderResult . once ( 'end' , resolve ) ;
232+ } ) ;
233+
234+ expect ( chunks ) . toHaveLength ( 1 ) ;
235+ expect ( chunks [ 0 ] . html ) . toMatch ( / < p r e > E x c e p t i o n i n r e n d e r i n g [ . \s \S ] * S y n c E r r o r [ . \s \S ] * < \/ p r e > / ) ;
236+ expect ( chunks [ 0 ] . consoleReplayScript ) . toBe ( '' ) ;
237+ expect ( chunks [ 0 ] . hasErrors ) . toBe ( true ) ;
238+ expect ( chunks [ 0 ] . isShellReady ) . toBe ( false ) ;
239+ } ,
240+ ) ;
241+
242+ it . each ( [ 'asyncRenderFunction' , 'renderFunction' ] ) (
243+ 'handles async errors in the %s' ,
244+ async ( componentType ) => {
245+ const { renderResult, chunks } = setupStreamTest ( { componentType, throwAsyncError : true } ) ;
246+ await new Promise ( ( resolve ) => {
247+ renderResult . once ( 'end' , resolve ) ;
248+ } ) ;
249+
250+ expect ( chunks . length ) . toBeGreaterThanOrEqual ( 2 ) ;
251+ expect ( chunks [ 0 ] . html ) . toContain ( 'Header In The Shell' ) ;
252+ expect ( chunks [ 0 ] . consoleReplayScript ) . toBe ( '' ) ;
253+ expect ( chunks [ 0 ] . hasErrors ) . toBe ( false ) ;
254+ expect ( chunks [ 0 ] . isShellReady ) . toBe ( true ) ;
255+ expect ( chunks [ 1 ] . html ) . toMatch ( / t h e s e r v e r r e n d e r i n g e r r o r e d : \\ n \\ n A s y n c E r r o r / ) ;
256+ expect ( chunks [ 1 ] . consoleReplayScript ) . toBe ( '' ) ;
257+ expect ( chunks [ 1 ] . hasErrors ) . toBe ( true ) ;
258+ expect ( chunks [ 1 ] . isShellReady ) . toBe ( true ) ;
259+ } ,
260+ ) ;
261+
262+ it . each ( [ 'erroneousRenderFunction' , 'erroneousAsyncRenderFunction' ] ) (
263+ 'handles error in the %s' ,
264+ async ( componentType ) => {
265+ const { renderResult, chunks } = setupStreamTest ( { componentType } ) ;
266+ await new Promise ( ( resolve ) => {
267+ renderResult . once ( 'end' , resolve ) ;
268+ } ) ;
269+
270+ expect ( chunks ) . toHaveLength ( 1 ) ;
271+ const errorMessage =
272+ componentType === 'erroneousRenderFunction'
273+ ? 'Sync Error from render function'
274+ : 'Async Error from render function' ;
275+ expect ( chunks [ 0 ] . html ) . toMatch (
276+ new RegExp ( `<pre>Exception in rendering[.\\s\\S]*${ errorMessage } [.\\s\\S]*<\\/pre>` ) ,
277+ ) ;
278+ expect ( chunks [ 0 ] . consoleReplayScript ) . toBe ( '' ) ;
279+ expect ( chunks [ 0 ] . hasErrors ) . toBe ( true ) ;
280+ expect ( chunks [ 0 ] . isShellReady ) . toBe ( false ) ;
281+ } ,
282+ ) ;
283+
284+ it . each ( [ 'erroneousRenderFunction' , 'erroneousAsyncRenderFunction' ] ) (
285+ 'emits an error if there is an error in the %s' ,
286+ async ( componentType ) => {
287+ const { renderResult, chunks } = setupStreamTest ( { componentType, throwJsErrors : true } ) ;
288+ const onError = jest . fn ( ) ;
289+ renderResult . on ( 'error' , onError ) ;
290+ await new Promise ( ( resolve ) => {
291+ renderResult . once ( 'end' , resolve ) ;
292+ } ) ;
293+
294+ expect ( chunks ) . toHaveLength ( 1 ) ;
295+ const errorMessage =
296+ componentType === 'erroneousRenderFunction'
297+ ? 'Sync Error from render function'
298+ : 'Async Error from render function' ;
299+ expect ( chunks [ 0 ] . html ) . toMatch (
300+ new RegExp ( `<pre>Exception in rendering[.\\s\\S]*${ errorMessage } [.\\s\\S]*<\\/pre>` ) ,
301+ ) ;
302+ expect ( chunks [ 0 ] . consoleReplayScript ) . toBe ( '' ) ;
303+ expect ( chunks [ 0 ] . hasErrors ) . toBe ( true ) ;
304+ expect ( chunks [ 0 ] . isShellReady ) . toBe ( false ) ;
305+ expect ( onError ) . toHaveBeenCalled ( ) ;
306+ } ,
307+ ) ;
308+
309+ it ( 'streams a string from a Promise that resolves to a string' , async ( ) => {
310+ const StringPromiseComponent = ( ) => Promise . resolve ( '<div>String from Promise</div>' ) ;
311+ ComponentRegistry . register ( { StringPromiseComponent } ) ;
312+
313+ const renderResult = streamServerRenderedReactComponent ( {
314+ name : 'StringPromiseComponent' ,
315+ domNodeId : 'stringPromiseId' ,
316+ trace : false ,
317+ throwJsErrors : false ,
318+ } ) ;
319+
320+ const chunks = [ ] ;
321+ renderResult . on ( 'data' , ( chunk ) => {
322+ const decodedText = new TextDecoder ( ) . decode ( chunk ) ;
323+ chunks . push ( expectStreamChunk ( decodedText ) ) ;
324+ } ) ;
325+
326+ await new Promise ( ( resolve ) => {
327+ renderResult . once ( 'end' , resolve ) ;
328+ } ) ;
329+
330+ // Verify we have at least one chunk and it contains our string
331+ expect ( chunks . length ) . toBeGreaterThan ( 0 ) ;
332+ expect ( chunks [ 0 ] . html ) . toContain ( 'String from Promise' ) ;
333+ expect ( chunks [ 0 ] . hasErrors ) . toBe ( false ) ;
334+ } ) ;
172335} ) ;
0 commit comments