main
js 224 lines 6.19 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 const React = require('react');
13 const ReactDOM = require('react-dom');
14 const ReactDOMClient = require('react-dom/client');
15 const assertConsoleErrorDev =
16 require('internal-test-utils').assertConsoleErrorDev;
17
18 function expectWarnings(tags, warnings = []) {
19 tags = [...tags];
20 warnings = [...warnings];
21
22 document.removeChild(document.documentElement);
23 document.appendChild(document.createElement('html'));
24 document.documentElement.innerHTML = '<head></head><body></body>';
25
26 let element = null;
27 const containerTag = tags.shift();
28 let container;
29 switch (containerTag) {
30 case '#document':
31 container = document;
32 break;
33 case 'html':
34 container = document.documentElement;
35 break;
36 case 'body':
37 container = document.body;
38 break;
39 case 'head':
40 container = document.head;
41 break;
42 case 'svg':
43 container = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
44 break;
45 default:
46 container = document.createElement(containerTag);
47 break;
48 }
49
50 while (tags.length) {
51 const Tag = tags.pop();
52 if (Tag === '#text') {
53 element = 'text';
54 } else {
55 element = <Tag>{element}</Tag>;
56 }
57 }
58
59 const root = ReactDOMClient.createRoot(container);
60 ReactDOM.flushSync(() => {
61 root.render(element);
62 });
63 if (warnings.length) {
64 assertConsoleErrorDev(warnings);
65 }
66 root.unmount();
67 }
68
69 describe('validateDOMNesting', () => {
70 it('allows valid nestings', () => {
71 expectWarnings(['table', 'tbody', 'tr', 'td', 'b']);
72 expectWarnings(['body', 'datalist', 'option']);
73 expectWarnings(['div', 'a', 'object', 'a']);
74 expectWarnings(['div', 'p', 'button', 'p']);
75 expectWarnings(['p', 'svg', 'foreignObject', 'p']);
76 expectWarnings(['html', 'body', 'div']);
77
78 // Invalid, but not changed by browser parsing so we allow them
79 expectWarnings(['div', 'ul', 'ul', 'li']);
80 expectWarnings(['div', 'label', 'div']);
81 expectWarnings(['div', 'ul', 'li', 'section', 'li']);
82 expectWarnings(['div', 'ul', 'li', 'dd', 'li']);
83 });
84
85 it('prevents problematic nestings', () => {
86 expectWarnings(
87 ['a', 'a'],
88 [
89 'In HTML, <a> cannot be a descendant of <a>.\n' +
90 'This will cause a hydration error.\n' +
91 ' in a (at **)',
92 ],
93 );
94 expectWarnings(
95 ['form', 'form'],
96 [
97 'In HTML, <form> cannot be a descendant of <form>.\n' +
98 'This will cause a hydration error.\n' +
99 ' in form (at **)',
100 ],
101 );
102 expectWarnings(
103 ['p', 'p'],
104 [
105 'In HTML, <p> cannot be a descendant of <p>.\n' +
106 'This will cause a hydration error.\n' +
107 ' in p (at **)',
108 ],
109 );
110 expectWarnings(
111 ['table', 'tr'],
112 [
113 'In HTML, <tr> cannot be a child of <table>. ' +
114 'Add a <tbody>, <thead> or <tfoot> to your code to match the DOM tree generated by the browser.\n' +
115 'This will cause a hydration error.\n' +
116 ' in tr (at **)',
117 ],
118 );
119 expectWarnings(
120 ['div', 'ul', 'li', 'div', 'li'],
121
122 [
123 'In HTML, <li> cannot be a descendant of <li>.\n' +
124 'This will cause a hydration error.\n' +
125 '\n' +
126 ' <ul>\n' +
127 '> <li>\n' +
128 ' <div>\n' +
129 '> <li>\n' +
130 '\n' +
131 ' in li (at **)',
132 '<li> cannot contain a nested <li>.\nSee this log for the ancestor stack trace.\n' +
133 ' in li (at **)',
134 ],
135 );
136 expectWarnings(
137 ['div', 'html'],
138 [
139 'In HTML, <html> cannot be a child of <div>.\n' +
140 'This will cause a hydration error.\n' +
141 ' in html (at **)',
142 ],
143 );
144 expectWarnings(
145 ['body', 'body'],
146 [
147 'In HTML, <body> cannot be a child of <body>.\n' +
148 'This will cause a hydration error.\n' +
149 ' in body (at **)',
150 ],
151 );
152 expectWarnings(
153 ['head', 'body'],
154 [
155 'In HTML, <body> cannot be a child of <head>.\n' +
156 'This will cause a hydration error.\n' +
157 ' in body (at **)',
158 ],
159 );
160 expectWarnings(
161 ['head', 'head'],
162 [
163 'In HTML, <head> cannot be a child of <head>.\n' +
164 'This will cause a hydration error.\n' +
165 ' in head (at **)',
166 ],
167 );
168 expectWarnings(
169 ['html', 'html'],
170 [
171 'In HTML, <html> cannot be a child of <html>.\n' +
172 'This will cause a hydration error.\n' +
173 ' in html (at **)',
174 ],
175 );
176 expectWarnings(
177 ['body', 'html'],
178 [
179 'In HTML, <html> cannot be a child of <body>.\n' +
180 'This will cause a hydration error.\n' +
181 ' in html (at **)',
182 ],
183 );
184 expectWarnings(
185 ['head', 'html'],
186 [
187 'In HTML, <html> cannot be a child of <head>.\n' +
188 'This will cause a hydration error.\n' +
189 ' in html (at **)',
190 ],
191 );
192 expectWarnings(
193 ['svg', 'foreignObject', 'body', 'p'],
194 [
195 // TODO, this should say "In SVG",
196 'In HTML, <body> cannot be a child of <foreignObject>.\n' +
197 'This will cause a hydration error.\n' +
198 '\n' +
199 '> <foreignObject>\n' +
200 '> <body>\n' +
201 '\n' +
202 ' in body (at **)',
203 ],
204 );
205 });
206
207 it('relaxes the nesting rules at the root when the container is a singleton', () => {
208 expectWarnings(['#document', 'html']);
209 expectWarnings(['#document', 'body']);
210 expectWarnings(['#document', 'head']);
211 expectWarnings(['#document', 'div']);
212 expectWarnings(['#document', 'meta']);
213 expectWarnings(['#document', '#text']);
214 expectWarnings(['html', 'body']);
215 expectWarnings(['html', 'head']);
216 expectWarnings(['html', 'div']);
217 expectWarnings(['html', 'meta']);
218 expectWarnings(['html', '#text']);
219 expectWarnings(['body', 'head']);
220 expectWarnings(['body', 'div']);
221 expectWarnings(['body', 'meta']);
222 expectWarnings(['body', '#text']);
223 });
224 });