main
js 276 lines 8.27 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 global.TextDecoder = require('util').TextDecoder;
13 global.TextEncoder = require('util').TextEncoder;
14
15 let React;
16 let ReactMarkup;
17
18 function normalizeCodeLocInfo(str) {
19 return (
20 str &&
21 String(str).replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
22 return '\n in ' + name + ' (at **)';
23 })
24 );
25 }
26
27 if (!__EXPERIMENTAL__) {
28 it('should not be built in stable', () => {
29 try {
30 require('react-markup');
31 } catch (x) {
32 return;
33 }
34 throw new Error('Expected react-markup not to exist in stable.');
35 });
36 } else {
37 describe('ReactMarkup', () => {
38 beforeEach(() => {
39 jest.resetModules();
40 // We run in the react-server condition.
41 jest.mock('react', () => require('react/react.react-server'));
42 if (__EXPERIMENTAL__) {
43 jest.mock('react-markup', () =>
44 require('react-markup/react-markup.react-server'),
45 );
46 }
47
48 React = require('react');
49 if (__EXPERIMENTAL__) {
50 ReactMarkup = require('react-markup');
51 } else {
52 try {
53 require('react-markup/react-markup.react-server');
54 } catch (x) {
55 return;
56 }
57 throw new Error('Expected react-markup not to exist in stable.');
58 }
59 });
60
61 it('should be able to render a simple component', async () => {
62 function Component() {
63 // We can't use JSX because that's client-JSX in our tests.
64 return React.createElement('div', null, 'hello world');
65 }
66
67 const html = await ReactMarkup.experimental_renderToHTML(
68 React.createElement(Component),
69 );
70 expect(html).toBe('<div>hello world</div>');
71 });
72
73 it('should be able to render a large string', async () => {
74 function Component() {
75 // We can't use JSX because that's client-JSX in our tests.
76 return React.createElement('div', null, 'hello '.repeat(200) + 'world');
77 }
78
79 const html = await ReactMarkup.experimental_renderToHTML(
80 React.createElement(Component),
81 );
82 expect(html).toBe('<div>' + ('hello '.repeat(200) + 'world') + '</div>');
83 });
84
85 it('should prefix html tags with a doctype', async () => {
86 const html = await ReactMarkup.experimental_renderToHTML(
87 React.createElement(
88 'html',
89 null,
90 React.createElement('body', null, 'hello'),
91 ),
92 );
93 expect(html).toBe(
94 '<!DOCTYPE html><html><head></head><body>hello</body></html>',
95 );
96 });
97
98 it('should error on useState', async () => {
99 function Component() {
100 const [state] = React.useState('hello');
101 // We can't use JSX because that's client-JSX in our tests.
102 return React.createElement('div', null, state);
103 }
104
105 await expect(async () => {
106 await ReactMarkup.experimental_renderToHTML(
107 React.createElement(Component),
108 );
109 }).rejects.toThrow('React.useState is not a function');
110 });
111
112 it('should error on refs passed to host components', async () => {
113 function Component() {
114 const ref = React.createRef();
115 // We can't use JSX because that's client-JSX in our tests.
116 return React.createElement('div', {ref});
117 }
118
119 await expect(async () => {
120 await ReactMarkup.experimental_renderToHTML(
121 React.createElement(Component),
122 );
123 }).rejects.toThrow(
124 'Refs cannot be used in Server Components, nor passed to Client Components.',
125 );
126 });
127
128 it('should error on callbacks passed to event handlers', async () => {
129 function Component() {
130 function onClick() {
131 // This won't be able to be called.
132 }
133 // We can't use JSX because that's client-JSX in our tests.
134 return React.createElement('div', {onClick});
135 }
136
137 await expect(async () => {
138 await ReactMarkup.experimental_renderToHTML(
139 React.createElement(Component),
140 );
141 }).rejects.toThrow(
142 __DEV__
143 ? `Event handlers cannot be passed to Client Component props.\n` +
144 ' <div onClick={function onClick}>\n' +
145 ' ^^^^^^^^^^^^^^^^^^\n' +
146 'If you need interactivity, consider converting part of this to a Client Component.'
147 : `Event handlers cannot be passed to Client Component props.\n` +
148 ' {onClick: function onClick}\n' +
149 ' ^^^^^^^^^^^^^^^^\n' +
150 'If you need interactivity, consider converting part of this to a Client Component.',
151 );
152 });
153
154 it('supports the useId Hook', async () => {
155 function Component() {
156 const firstNameId = React.useId();
157 const lastNameId = React.useId();
158 // We can't use JSX because that's client-JSX in our tests.
159 return React.createElement(
160 'div',
161 null,
162 React.createElement(
163 'h2',
164 {
165 id: firstNameId,
166 },
167 'First',
168 ),
169 React.createElement(
170 'p',
171 {
172 'aria-labelledby': firstNameId,
173 },
174 'Sebastian',
175 ),
176 React.createElement(
177 'h2',
178 {
179 id: lastNameId,
180 },
181 'Last',
182 ),
183 React.createElement(
184 'p',
185 {
186 'aria-labelledby': lastNameId,
187 },
188 'Smith',
189 ),
190 );
191 }
192
193 const html = await ReactMarkup.experimental_renderToHTML(
194 React.createElement(Component),
195 );
196 const container = document.createElement('div');
197 container.innerHTML = html;
198
199 expect(container.getElementsByTagName('h2')[0].id).toBe(
200 container.getElementsByTagName('p')[0].getAttribute('aria-labelledby'),
201 );
202 expect(container.getElementsByTagName('h2')[1].id).toBe(
203 container.getElementsByTagName('p')[1].getAttribute('aria-labelledby'),
204 );
205
206 // It's not the same id between them.
207 expect(container.getElementsByTagName('h2')[0].id).not.toBe(
208 container.getElementsByTagName('p')[1].getAttribute('aria-labelledby'),
209 );
210 });
211
212 it('supports cache', async () => {
213 let counter = 0;
214 const getCount = React.cache(() => {
215 return counter++;
216 });
217 function Component() {
218 const a = getCount();
219 const b = getCount();
220 return React.createElement('div', null, a, b);
221 }
222
223 const html = await ReactMarkup.experimental_renderToHTML(
224 React.createElement(Component),
225 );
226 expect(html).toBe('<div>00</div>');
227 });
228
229 it('can get the component owner stacks for onError in dev', async () => {
230 const thrownError = new Error('hi');
231 const caughtErrors = [];
232
233 function Foo() {
234 return React.createElement(Bar);
235 }
236 function Bar() {
237 return React.createElement('div', null, React.createElement(Baz));
238 }
239 function Baz({unused}) {
240 throw thrownError;
241 }
242
243 await expect(async () => {
244 await ReactMarkup.experimental_renderToHTML(
245 React.createElement('div', null, React.createElement(Foo)),
246 {
247 onError(error, errorInfo) {
248 caughtErrors.push({
249 error: error,
250 parentStack: errorInfo.componentStack,
251 ownerStack: React.captureOwnerStack
252 ? React.captureOwnerStack()
253 : null,
254 });
255 },
256 },
257 );
258 }).rejects.toThrow(thrownError);
259
260 expect(caughtErrors.length).toBe(1);
261 expect(caughtErrors[0].error).toBe(thrownError);
262 expect(normalizeCodeLocInfo(caughtErrors[0].parentStack)).toBe(
263 __DEV__
264 ? '\n in Baz (at **)' +
265 '\n in div (at **)' +
266 '\n in Bar (at **)' +
267 '\n in Foo (at **)' +
268 '\n in div (at **)'
269 : '\n in div (at **)' + '\n in div (at **)',
270 );
271 expect(normalizeCodeLocInfo(caughtErrors[0].ownerStack)).toBe(
272 __DEV__ ? '\n in Bar (at **)' + '\n in Foo (at **)' : null,
273 );
274 });
275 });
276 }