main
js 230 lines 7.5 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 // Fix JSDOM. setAttribute is supposed to throw on things that can't be implicitly toStringed.
13 const setAttribute = Element.prototype.setAttribute;
14 Element.prototype.setAttribute = function (name, value) {
15 // eslint-disable-next-line react-internal/safe-string-coercion
16 return setAttribute.call(this, name, '' + value);
17 };
18
19 describe('ReactDOM unknown attribute', () => {
20 let React;
21 let ReactDOMClient;
22 let act;
23 let assertConsoleErrorDev;
24
25 beforeEach(() => {
26 jest.resetModules();
27 React = require('react');
28 ReactDOMClient = require('react-dom/client');
29 act = require('internal-test-utils').act;
30 assertConsoleErrorDev =
31 require('internal-test-utils').assertConsoleErrorDev;
32 });
33
34 async function testUnknownAttributeRemoval(givenValue) {
35 const el = document.createElement('div');
36 const root = ReactDOMClient.createRoot(el);
37
38 await act(() => {
39 root.render(<div unknown="something" />);
40 });
41
42 expect(el.firstChild.getAttribute('unknown')).toBe('something');
43
44 await act(() => {
45 root.render(<div unknown={givenValue} />);
46 });
47
48 expect(el.firstChild.hasAttribute('unknown')).toBe(false);
49 }
50
51 async function testUnknownAttributeAssignment(givenValue, expectedDOMValue) {
52 const el = document.createElement('div');
53 const root = ReactDOMClient.createRoot(el);
54
55 await act(() => {
56 root.render(<div unknown="something" />);
57 });
58
59 expect(el.firstChild.getAttribute('unknown')).toBe('something');
60
61 await act(() => {
62 root.render(<div unknown={givenValue} />);
63 });
64
65 expect(el.firstChild.getAttribute('unknown')).toBe(expectedDOMValue);
66 }
67
68 describe('unknown attributes', () => {
69 it('removes values null and undefined', async () => {
70 await testUnknownAttributeRemoval(null);
71 await testUnknownAttributeRemoval(undefined);
72 });
73
74 it('changes values true, false to null, and also warns once', async () => {
75 await testUnknownAttributeAssignment(true, null);
76 assertConsoleErrorDev([
77 'Received `true` for a non-boolean attribute `unknown`.\n\n' +
78 'If you want to write it to the DOM, pass a string instead: ' +
79 'unknown="true" or unknown={value.toString()}.\n' +
80 ' in div (at **)',
81 ]);
82 await testUnknownAttributeAssignment(false, null);
83 });
84
85 it('removes unknown attributes that were rendered but are now missing', async () => {
86 const el = document.createElement('div');
87 const root = ReactDOMClient.createRoot(el);
88
89 await act(() => {
90 root.render(<div unknown="something" />);
91 });
92
93 expect(el.firstChild.getAttribute('unknown')).toBe('something');
94
95 await act(() => {
96 root.render(<div />);
97 });
98
99 expect(el.firstChild.hasAttribute('unknown')).toBe(false);
100 });
101
102 it('removes new boolean props', async () => {
103 const el = document.createElement('div');
104 const root = ReactDOMClient.createRoot(el);
105
106 await act(() => {
107 root.render(<div inert={true} />);
108 });
109
110 expect(el.firstChild.getAttribute('inert')).toBe(true ? '' : null);
111 });
112
113 it('warns once for empty strings in new boolean props', async () => {
114 const el = document.createElement('div');
115 const root = ReactDOMClient.createRoot(el);
116
117 await act(() => {
118 root.render(<div inert="" />);
119 });
120 assertConsoleErrorDev([
121 'Received an empty string for a boolean attribute `inert`. ' +
122 'This will treat the attribute as if it were false. ' +
123 'Either pass `false` to silence this warning, or ' +
124 'pass `true` if you used an empty string in earlier versions of React to indicate this attribute is true.\n' +
125 ' in div (at **)',
126 ]);
127
128 expect(el.firstChild.getAttribute('inert')).toBe(true ? null : '');
129
130 // The warning is only printed once.
131 await act(() => {
132 root.render(<div inert="" />);
133 });
134 });
135
136 it('passes through strings', async () => {
137 await testUnknownAttributeAssignment('a string', 'a string');
138 });
139
140 it('coerces numbers to strings', async () => {
141 await testUnknownAttributeAssignment(0, '0');
142 await testUnknownAttributeAssignment(-1, '-1');
143 await testUnknownAttributeAssignment(42, '42');
144 await testUnknownAttributeAssignment(9000.99, '9000.99');
145 });
146
147 it('coerces NaN to strings and warns', async () => {
148 await testUnknownAttributeAssignment(NaN, 'NaN');
149 assertConsoleErrorDev([
150 'Received NaN for the `unknown` attribute. ' +
151 'If this is expected, cast the value to a string.\n' +
152 ' in div (at **)',
153 ]);
154 });
155
156 it('coerces objects to strings and warns', async () => {
157 const lol = {
158 toString() {
159 return 'lol';
160 },
161 };
162
163 await testUnknownAttributeAssignment({hello: 'world'}, '[object Object]');
164 await testUnknownAttributeAssignment(lol, 'lol');
165 });
166
167 it('throws with Temporal-like objects', async () => {
168 class TemporalLike {
169 valueOf() {
170 // Throwing here is the behavior of ECMAScript "Temporal" date/time API.
171 // See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
172 throw new TypeError('prod message');
173 }
174 toString() {
175 return '2020-01-01';
176 }
177 }
178 const test = () =>
179 testUnknownAttributeAssignment(new TemporalLike(), null);
180
181 await expect(test).rejects.toThrow(new TypeError('prod message'));
182 assertConsoleErrorDev([
183 'The provided `unknown` attribute is an unsupported type TemporalLike.' +
184 ' This value must be coerced to a string before using it here.\n' +
185 ' in div (at **)',
186 ]);
187 });
188
189 it('removes symbols and warns', async () => {
190 await testUnknownAttributeRemoval(Symbol('foo'));
191 assertConsoleErrorDev([
192 'Invalid value for prop `unknown` on <div> tag. Either remove it ' +
193 'from the element, or pass a string or number value to keep it ' +
194 'in the DOM. For details, see https://react.dev/link/attribute-behavior \n' +
195 ' in div (at **)',
196 ]);
197 });
198
199 it('removes functions and warns', async () => {
200 await testUnknownAttributeRemoval(function someFunction() {});
201 assertConsoleErrorDev([
202 'Invalid value for prop `unknown` on <div> tag. Either remove ' +
203 'it from the element, or pass a string or number value to ' +
204 'keep it in the DOM. For details, see ' +
205 'https://react.dev/link/attribute-behavior \n' +
206 ' in div (at **)',
207 ]);
208 });
209
210 it('allows camelCase unknown attributes and warns', async () => {
211 const el = document.createElement('div');
212
213 const root = ReactDOMClient.createRoot(el);
214
215 await act(() => {
216 root.render(<div helloWorld="something" />);
217 });
218 assertConsoleErrorDev([
219 'React does not recognize the `helloWorld` prop on a DOM element. ' +
220 'If you intentionally want it to appear in the DOM as a custom ' +
221 'attribute, spell it as lowercase `helloworld` instead. ' +
222 'If you accidentally passed it from a parent component, remove ' +
223 'it from the DOM element.\n' +
224 ' in div (at **)',
225 ]);
226
227 expect(el.firstChild.getAttribute('helloworld')).toBe('something');
228 });
229 });
230 });