main
js 216 lines 5.41 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 */
9
10 'use strict';
11
12 let React;
13 let ReactDOMServer;
14 let Suspense;
15
16 describe('ReactDOMServerFB', () => {
17 beforeEach(() => {
18 jest.resetModules();
19 React = require('react');
20 ReactDOMServer = require('../ReactDOMServerFB');
21 Suspense = React.Suspense;
22 });
23
24 const theError = new Error('This is an error');
25 function Throw() {
26 throw theError;
27 }
28 const theInfinitePromise = new Promise(() => {});
29 function InfiniteSuspend() {
30 throw theInfinitePromise;
31 }
32
33 function readResult(stream) {
34 let result = '';
35 while (!ReactDOMServer.hasFinished(stream)) {
36 result += ReactDOMServer.renderNextChunk(stream);
37 }
38 return result;
39 }
40
41 it('should be able to render basic HTML', async () => {
42 const stream = ReactDOMServer.renderToStream(<div>hello world</div>, {
43 onError(x) {
44 console.error(x);
45 },
46 });
47 const result = readResult(stream);
48 expect(result).toMatchInlineSnapshot(`"<div>hello world</div>"`);
49 });
50
51 it('should emit bootstrap script src at the end', () => {
52 const stream = ReactDOMServer.renderToStream(<div>hello world</div>, {
53 bootstrapScriptContent: 'INIT();',
54 bootstrapScripts: ['init.js'],
55 bootstrapModules: ['init.mjs'],
56 onError(x) {
57 console.error(x);
58 },
59 });
60 const result = readResult(stream);
61 expect(result).toMatchInlineSnapshot(
62 `"<link rel="preload" as="script" fetchPriority="low" href="init.js"/><link rel="modulepreload" fetchPriority="low" href="init.mjs"/><div>hello world</div><script id="_R_">INIT();</script><script src="init.js" async=""></script><script type="module" src="init.mjs" async=""></script>"`,
63 );
64 });
65
66 it('emits all HTML as one unit if we wait until the end to start', async () => {
67 let hasLoaded = false;
68 let resolve;
69 const promise = new Promise(r => (resolve = r));
70 function Wait() {
71 if (!hasLoaded) {
72 throw promise;
73 }
74 return 'Done';
75 }
76 const stream = ReactDOMServer.renderToStream(
77 <div>
78 <Suspense fallback="Loading">
79 <Wait />
80 </Suspense>
81 </div>,
82 {
83 onError(x) {
84 console.error(x);
85 },
86 },
87 );
88 await jest.runAllTimers();
89 // Resolve the loading.
90 hasLoaded = true;
91 await resolve();
92
93 await jest.runAllTimers();
94
95 const result = readResult(stream);
96 expect(result).toMatchInlineSnapshot(`"<div><!--$-->Done<!--/$--></div>"`);
97 });
98
99 it('should throw an error when an error is thrown at the root', () => {
100 const reportedErrors = [];
101 const stream = ReactDOMServer.renderToStream(
102 <div>
103 <Throw />
104 </div>,
105 {
106 onError(x) {
107 reportedErrors.push(x);
108 },
109 },
110 );
111
112 let caughtError = null;
113 let result = '';
114 try {
115 result = readResult(stream);
116 } catch (x) {
117 caughtError = x;
118 }
119 expect(caughtError).toBe(theError);
120 expect(result).toBe('');
121 expect(reportedErrors).toEqual([theError]);
122 });
123
124 it('should throw an error when an error is thrown inside a fallback', () => {
125 const reportedErrors = [];
126 const stream = ReactDOMServer.renderToStream(
127 <div>
128 <Suspense fallback={<Throw />}>
129 <InfiniteSuspend />
130 </Suspense>
131 </div>,
132 {
133 onError(x) {
134 reportedErrors.push(x);
135 },
136 },
137 );
138
139 let caughtError = null;
140 let result = '';
141 try {
142 result = readResult(stream);
143 } catch (x) {
144 caughtError = x;
145 }
146 expect(caughtError).toBe(theError);
147 expect(result).toBe('');
148 expect(reportedErrors).toEqual([theError]);
149 });
150
151 it('should not throw an error when an error is thrown inside suspense boundary', async () => {
152 const reportedErrors = [];
153 const stream = ReactDOMServer.renderToStream(
154 <div>
155 <Suspense fallback={<div>Loading</div>}>
156 <Throw />
157 </Suspense>
158 </div>,
159 {
160 onError(x) {
161 reportedErrors.push(x);
162 },
163 },
164 );
165
166 const result = readResult(stream);
167 expect(result).toContain('Loading');
168 expect(reportedErrors).toEqual([theError]);
169 });
170
171 it('should be able to complete by aborting even if the promise never resolves', () => {
172 const errors = [];
173 const stream = ReactDOMServer.renderToStream(
174 <div>
175 <Suspense fallback={<div>Loading</div>}>
176 <InfiniteSuspend />
177 </Suspense>
178 </div>,
179 {
180 onError(x) {
181 errors.push(x.message);
182 },
183 },
184 );
185
186 const partial = ReactDOMServer.renderNextChunk(stream);
187 expect(partial).toContain('Loading');
188
189 ReactDOMServer.abortStream(stream);
190
191 const remaining = readResult(stream);
192 expect(remaining).toEqual('');
193
194 expect(errors).toEqual([
195 'The render was aborted by the server without a reason.',
196 ]);
197 });
198
199 it('should allow setting an abort reason', () => {
200 const errors = [];
201 const stream = ReactDOMServer.renderToStream(
202 <div>
203 <Suspense fallback={<div>Loading</div>}>
204 <InfiniteSuspend />
205 </Suspense>
206 </div>,
207 {
208 onError(error) {
209 errors.push(error);
210 },
211 },
212 );
213 ReactDOMServer.abortStream(stream, theError);
214 expect(errors).toEqual([theError]);
215 });
216 });