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