main
js 810 lines 29.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 * @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
9 */
10
11 'use strict';
12
13 const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
14
15 let React;
16 let ReactDOM;
17 let ReactDOMClient;
18 let ReactDOMServer;
19 let assertConsoleErrorDev;
20
21 function initModules() {
22 // Reset warning cache.
23 jest.resetModules();
24 React = require('react');
25 ReactDOM = require('react-dom');
26 ReactDOMClient = require('react-dom/client');
27 ReactDOMServer = require('react-dom/server');
28 assertConsoleErrorDev = require('internal-test-utils').assertConsoleErrorDev;
29
30 // Make them available to the helpers.
31 return {
32 ReactDOMClient,
33 ReactDOMServer,
34 };
35 }
36
37 const {resetModules, itRenders, clientCleanRender} =
38 ReactDOMServerIntegrationUtils(initModules);
39
40 describe('ReactDOMServerIntegration', () => {
41 beforeEach(() => {
42 resetModules();
43 });
44 afterEach(() => {
45 // TODO: This is a hack because expectErrors does not restore mock,
46 // however fixing it requires a major refactor to all these tests.
47 if (console.error.mockClear) {
48 console.error.mockRestore();
49 }
50 });
51
52 describe('property to attribute mapping', function () {
53 describe('string properties', function () {
54 itRenders('simple numbers', async render => {
55 const e = await render(<div width={30} />);
56 expect(e.getAttribute('width')).toBe('30');
57 });
58
59 itRenders('simple strings', async render => {
60 const e = await render(<div width={'30'} />);
61 expect(e.getAttribute('width')).toBe('30');
62 });
63
64 itRenders('empty src on img', async render => {
65 const e = await render(<img src="" />, 1);
66 expect(e.getAttribute('src')).toBe(null);
67 });
68
69 itRenders('empty href on anchor', async render => {
70 const e = await render(<a href="" />);
71 expect(e.getAttribute('href')).toBe('');
72 });
73
74 itRenders('empty href on base tags as null', async render => {
75 const e = await render(<base href="" />, 1);
76 expect(e.getAttribute('href')).toBe(null);
77 });
78
79 itRenders('empty href on area tags as null', async render => {
80 const e = await render(
81 <map>
82 <area alt="" href="" />
83 </map>,
84 1,
85 );
86 expect(e.firstChild.getAttribute('href')).toBe(null);
87 });
88
89 itRenders('empty href on link tags as null', async render => {
90 const e = await render(<link rel="stylesheet" href="" />, 1);
91 expect(e.getAttribute('href')).toBe(null);
92 });
93
94 itRenders('no string prop with true value', async render => {
95 const e = await render(<a href={true} />, 1);
96 expect(e.hasAttribute('href')).toBe(false);
97 });
98
99 itRenders('no string prop with false value', async render => {
100 const e = await render(<a href={false} />, 1);
101 expect(e.hasAttribute('href')).toBe(false);
102 });
103
104 itRenders('no string prop with null value', async render => {
105 const e = await render(<div width={null} />);
106 expect(e.hasAttribute('width')).toBe(false);
107 });
108
109 itRenders('no string prop with function value', async render => {
110 const e = await render(<div width={function () {}} />, 1);
111 expect(e.hasAttribute('width')).toBe(false);
112 });
113
114 itRenders('no string prop with symbol value', async render => {
115 const e = await render(<div width={Symbol('foo')} />, 1);
116 expect(e.hasAttribute('width')).toBe(false);
117 });
118 });
119
120 describe('boolean properties', function () {
121 itRenders('boolean prop with true value', async render => {
122 const e = await render(<div hidden={true} />);
123 expect(e.getAttribute('hidden')).toBe('');
124 });
125
126 itRenders('boolean prop with false value', async render => {
127 const e = await render(<div hidden={false} />);
128 expect(e.getAttribute('hidden')).toBe(null);
129 });
130
131 itRenders('boolean prop with self value', async render => {
132 const e = await render(<div hidden="hidden" />);
133 expect(e.getAttribute('hidden')).toBe('');
134 });
135
136 // this does not seem like correct behavior, since hidden="" in HTML indicates
137 // that the boolean property is present. however, it is how the current code
138 // behaves, so the test is included here.
139 itRenders('boolean prop with "" value', async render => {
140 const e = await render(<div hidden="" />);
141 expect(e.getAttribute('hidden')).toBe(null);
142 });
143
144 // this seems like it might mask programmer error, but it's existing behavior.
145 itRenders('boolean prop with string value', async render => {
146 const e = await render(<div hidden="foo" />);
147 expect(e.getAttribute('hidden')).toBe('');
148 });
149
150 // this seems like it might mask programmer error, but it's existing behavior.
151 itRenders('boolean prop with array value', async render => {
152 const e = await render(<div hidden={['foo', 'bar']} />);
153 expect(e.getAttribute('hidden')).toBe('');
154 });
155
156 // this seems like it might mask programmer error, but it's existing behavior.
157 itRenders('boolean prop with object value', async render => {
158 const e = await render(<div hidden={{foo: 'bar'}} />);
159 expect(e.getAttribute('hidden')).toBe('');
160 });
161
162 // this seems like it might mask programmer error, but it's existing behavior.
163 itRenders('boolean prop with non-zero number value', async render => {
164 const e = await render(<div hidden={10} />);
165 expect(e.getAttribute('hidden')).toBe('');
166 });
167
168 // this seems like it might mask programmer error, but it's existing behavior.
169 itRenders('boolean prop with zero value', async render => {
170 const e = await render(<div hidden={0} />);
171 expect(e.getAttribute('hidden')).toBe(null);
172 });
173
174 itRenders('no boolean prop with null value', async render => {
175 const e = await render(<div hidden={null} />);
176 expect(e.hasAttribute('hidden')).toBe(false);
177 });
178
179 itRenders('no boolean prop with function value', async render => {
180 const e = await render(<div hidden={function () {}} />, 1);
181 expect(e.hasAttribute('hidden')).toBe(false);
182 });
183
184 itRenders('no boolean prop with symbol value', async render => {
185 const e = await render(<div hidden={Symbol('foo')} />, 1);
186 expect(e.hasAttribute('hidden')).toBe(false);
187 });
188 });
189
190 describe('credentialless property', function () {
191 itRenders('credentialless prop with true value', async render => {
192 const e = await render(<iframe credentialless={true} />);
193 expect(e.getAttribute('credentialless')).toBe('');
194 });
195
196 itRenders('credentialless prop with false value', async render => {
197 const e = await render(<iframe credentialless={false} />);
198 expect(e.hasAttribute('credentialless')).toBe(false);
199 });
200 });
201
202 describe('download property (combined boolean/string attribute)', function () {
203 itRenders('download prop with true value', async render => {
204 const e = await render(<a download={true} />);
205 expect(e.getAttribute('download')).toBe('');
206 });
207
208 itRenders('download prop with false value', async render => {
209 const e = await render(<a download={false} />);
210 expect(e.getAttribute('download')).toBe(null);
211 });
212
213 itRenders('download prop with string value', async render => {
214 const e = await render(<a download="myfile" />);
215 expect(e.getAttribute('download')).toBe('myfile');
216 });
217
218 itRenders('download prop with string "false" value', async render => {
219 const e = await render(<a download="false" />);
220 expect(e.getAttribute('download')).toBe('false');
221 });
222
223 itRenders('download prop with string "true" value', async render => {
224 const e = await render(<a download={'true'} />);
225 expect(e.getAttribute('download')).toBe('true');
226 });
227
228 itRenders('download prop with number 0 value', async render => {
229 const e = await render(<a download={0} />);
230 expect(e.getAttribute('download')).toBe('0');
231 });
232
233 itRenders('no download prop with null value', async render => {
234 const e = await render(<div download={null} />);
235 expect(e.hasAttribute('download')).toBe(false);
236 });
237
238 itRenders('no download prop with undefined value', async render => {
239 const e = await render(<div download={undefined} />);
240 expect(e.hasAttribute('download')).toBe(false);
241 });
242
243 itRenders('no download prop with function value', async render => {
244 const e = await render(<div download={function () {}} />, 1);
245 expect(e.hasAttribute('download')).toBe(false);
246 });
247
248 itRenders('no download prop with symbol value', async render => {
249 const e = await render(<div download={Symbol('foo')} />, 1);
250 expect(e.hasAttribute('download')).toBe(false);
251 });
252 });
253
254 describe('className property', function () {
255 itRenders('className prop with string value', async render => {
256 const e = await render(<div className="myClassName" />);
257 expect(e.getAttribute('class')).toBe('myClassName');
258 });
259
260 itRenders('className prop with empty string value', async render => {
261 const e = await render(<div className="" />);
262 expect(e.getAttribute('class')).toBe('');
263 });
264
265 itRenders('no className prop with true value', async render => {
266 const e = await render(<div className={true} />, 1);
267 expect(e.hasAttribute('class')).toBe(false);
268 });
269
270 itRenders('no className prop with false value', async render => {
271 const e = await render(<div className={false} />, 1);
272 expect(e.hasAttribute('class')).toBe(false);
273 });
274
275 itRenders('no className prop with null value', async render => {
276 const e = await render(<div className={null} />);
277 expect(e.hasAttribute('className')).toBe(false);
278 });
279
280 itRenders('badly cased className with a warning', async render => {
281 const e = await render(<div classname="test" />, 1);
282 expect(e.hasAttribute('class')).toBe(false);
283 expect(e.hasAttribute('classname')).toBe(true);
284 });
285
286 itRenders(
287 'className prop when given the alias with a warning',
288 async render => {
289 const e = await render(<div class="test" />, 1);
290 expect(e.className).toBe('test');
291 },
292 );
293
294 itRenders(
295 'className prop when given a badly cased alias',
296 async render => {
297 const e = await render(<div cLASs="test" />, 1);
298 expect(e.className).toBe('test');
299 },
300 );
301 });
302
303 describe('htmlFor property', function () {
304 itRenders('htmlFor with string value', async render => {
305 const e = await render(<div htmlFor="myFor" />);
306 expect(e.getAttribute('for')).toBe('myFor');
307 });
308
309 itRenders('no badly cased htmlfor', async render => {
310 const e = await render(<div htmlfor="myFor" />, 1);
311 expect(e.hasAttribute('for')).toBe(false);
312 expect(e.getAttribute('htmlfor')).toBe('myFor');
313 });
314
315 itRenders('htmlFor with an empty string', async render => {
316 const e = await render(<div htmlFor="" />);
317 expect(e.getAttribute('for')).toBe('');
318 });
319
320 itRenders('no htmlFor prop with true value', async render => {
321 const e = await render(<div htmlFor={true} />, 1);
322 expect(e.hasAttribute('for')).toBe(false);
323 });
324
325 itRenders('no htmlFor prop with false value', async render => {
326 const e = await render(<div htmlFor={false} />, 1);
327 expect(e.hasAttribute('for')).toBe(false);
328 });
329
330 itRenders('no htmlFor prop with null value', async render => {
331 const e = await render(<div htmlFor={null} />);
332 expect(e.hasAttribute('htmlFor')).toBe(false);
333 });
334 });
335
336 describe('numeric properties', function () {
337 itRenders(
338 'positive numeric property with positive value',
339 async render => {
340 const e = await render(<input size={2} />);
341 expect(e.getAttribute('size')).toBe('2');
342 },
343 );
344
345 itRenders('numeric property with zero value', async render => {
346 const e = await render(<ol start={0} />);
347 expect(e.getAttribute('start')).toBe('0');
348 });
349
350 itRenders(
351 'no positive numeric property with zero value',
352 async render => {
353 const e = await render(<input size={0} />);
354 expect(e.hasAttribute('size')).toBe(false);
355 },
356 );
357
358 itRenders('no numeric prop with function value', async render => {
359 const e = await render(<ol start={function () {}} />, 1);
360 expect(e.hasAttribute('start')).toBe(false);
361 });
362
363 itRenders('no numeric prop with symbol value', async render => {
364 const e = await render(<ol start={Symbol('foo')} />, 1);
365 expect(e.hasAttribute('start')).toBe(false);
366 });
367
368 itRenders(
369 'no positive numeric prop with function value',
370 async render => {
371 const e = await render(<input size={function () {}} />, 1);
372 expect(e.hasAttribute('size')).toBe(false);
373 },
374 );
375
376 itRenders('no positive numeric prop with symbol value', async render => {
377 const e = await render(<input size={Symbol('foo')} />, 1);
378 expect(e.hasAttribute('size')).toBe(false);
379 });
380 });
381
382 describe('props with special meaning in React', function () {
383 itRenders('no ref attribute', async render => {
384 class RefComponent extends React.Component {
385 render() {
386 return <div ref={React.createRef()} />;
387 }
388 }
389 const e = await render(<RefComponent />);
390 expect(e.getAttribute('ref')).toBe(null);
391 });
392
393 itRenders('no children attribute', async render => {
394 const e = await render(React.createElement('div', {}, 'foo'));
395 expect(e.getAttribute('children')).toBe(null);
396 });
397
398 itRenders('no key attribute', async render => {
399 const e = await render(<div key="foo" />);
400 expect(e.getAttribute('key')).toBe(null);
401 });
402
403 itRenders('no dangerouslySetInnerHTML attribute', async render => {
404 const e = await render(
405 <div dangerouslySetInnerHTML={{__html: '<foo />'}} />,
406 );
407 expect(e.getAttribute('dangerouslySetInnerHTML')).toBe(null);
408 });
409
410 itRenders('no suppressContentEditableWarning attribute', async render => {
411 const e = await render(<div suppressContentEditableWarning={true} />);
412 expect(e.getAttribute('suppressContentEditableWarning')).toBe(null);
413 });
414
415 itRenders('no suppressHydrationWarning attribute', async render => {
416 const e = await render(<span suppressHydrationWarning={true} />);
417 expect(e.getAttribute('suppressHydrationWarning')).toBe(null);
418 });
419 });
420
421 describe('inline styles', function () {
422 itRenders('simple styles', async render => {
423 const e = await render(<div style={{color: 'red', width: '30px'}} />);
424 expect(e.style.color).toBe('red');
425 expect(e.style.width).toBe('30px');
426 });
427
428 itRenders('relevant styles with px', async render => {
429 const e = await render(
430 <div
431 style={{
432 left: 0,
433 margin: 16,
434 opacity: 0.5,
435 padding: '4px',
436 }}
437 />,
438 );
439 expect(e.style.left).toBe('0px');
440 expect(e.style.margin).toBe('16px');
441 expect(e.style.opacity).toBe('0.5');
442 expect(e.style.padding).toBe('4px');
443 });
444
445 itRenders('custom properties', async render => {
446 const e = await render(<div style={{'--foo': 5}} />);
447 expect(e.style.getPropertyValue('--foo')).toBe('5');
448 });
449
450 itRenders('camel cased custom properties', async render => {
451 const e = await render(<div style={{'--someColor': '#000000'}} />);
452 expect(e.style.getPropertyValue('--someColor')).toBe('#000000');
453 });
454
455 itRenders('no undefined styles', async render => {
456 const e = await render(
457 <div style={{color: undefined, width: '30px'}} />,
458 );
459 expect(e.style.color).toBe('');
460 expect(e.style.width).toBe('30px');
461 });
462
463 itRenders('no null styles', async render => {
464 const e = await render(<div style={{color: null, width: '30px'}} />);
465 expect(e.style.color).toBe('');
466 expect(e.style.width).toBe('30px');
467 });
468
469 itRenders('no empty styles', async render => {
470 const e = await render(<div style={{color: null, width: null}} />);
471 expect(e.style.color).toBe('');
472 expect(e.style.width).toBe('');
473 expect(e.hasAttribute('style')).toBe(false);
474 });
475
476 itRenders('unitless-number rules with prefixes', async render => {
477 const {style} = await render(
478 <div
479 style={{
480 lineClamp: 10,
481 // TODO: requires https://github.com/jsdom/cssstyle/pull/112
482 // WebkitLineClamp: 10,
483 // TODO: revisit once cssstyle or jsdom figures out
484 // if they want to support other vendors or not
485 // MozFlexGrow: 10,
486 // msFlexGrow: 10,
487 // msGridRow: 10,
488 // msGridRowEnd: 10,
489 // msGridRowSpan: 10,
490 // msGridRowStart: 10,
491 // msGridColumn: 10,
492 // msGridColumnEnd: 10,
493 // msGridColumnSpan: 10,
494 // msGridColumnStart: 10,
495 }}
496 />,
497 );
498
499 expect(style.lineClamp).toBe('10');
500 // see comment at inline styles above
501 // expect(style.WebkitLineClamp).toBe('10');
502 // expect(style.MozFlexGrow).toBe('10');
503 // jsdom is inconsistent in the style property name
504 // it uses on the client and when processing server markup.
505 // But it should be there either way.
506 //expect(style.MsFlexGrow || style.msFlexGrow).toBe('10');
507 // expect(style.MsGridRow || style.msGridRow).toBe('10');
508 // expect(style.MsGridRowEnd || style.msGridRowEnd).toBe('10');
509 // expect(style.MsGridRowSpan || style.msGridRowSpan).toBe('10');
510 // expect(style.MsGridRowStart || style.msGridRowStart).toBe('10');
511 // expect(style.MsGridColumn || style.msGridColumn).toBe('10');
512 // expect(style.MsGridColumnEnd || style.msGridColumnEnd).toBe('10');
513 // expect(style.MsGridColumnSpan || style.msGridColumnSpan).toBe('10');
514 // expect(style.MsGridColumnStart || style.msGridColumnStart).toBe('10');
515 });
516 });
517
518 describe('aria attributes', function () {
519 itRenders('simple strings', async render => {
520 const e = await render(<div aria-label="hello" />);
521 expect(e.getAttribute('aria-label')).toBe('hello');
522 });
523
524 // this probably is just masking programmer error, but it is existing behavior.
525 itRenders('aria string prop with false value', async render => {
526 const e = await render(<div aria-label={false} />);
527 expect(e.getAttribute('aria-label')).toBe('false');
528 });
529
530 itRenders('no aria prop with null value', async render => {
531 const e = await render(<div aria-label={null} />);
532 expect(e.hasAttribute('aria-label')).toBe(false);
533 });
534
535 itRenders('"aria" attribute with a warning', async render => {
536 // Reserved for future use.
537 const e = await render(<div aria="hello" />, 1);
538 expect(e.getAttribute('aria')).toBe('hello');
539 });
540 });
541
542 describe('cased attributes', function () {
543 itRenders(
544 'badly cased aliased HTML attribute with a warning',
545 async render => {
546 const e = await render(<form acceptcharset="utf-8" />, 1);
547 expect(e.hasAttribute('accept-charset')).toBe(false);
548 expect(e.getAttribute('acceptcharset')).toBe('utf-8');
549 },
550 );
551
552 itRenders('badly cased SVG attribute with a warning', async render => {
553 const e = await render(
554 <svg>
555 <text textlength="10" />
556 </svg>,
557 1,
558 );
559 // The discrepancy is expected as long as we emit a warning
560 // both on the client and the server.
561 if (render === clientCleanRender) {
562 // On the client, "textlength" is treated as a case-sensitive
563 // SVG attribute so the wrong attribute ("textlength") gets set.
564 expect(e.firstChild.getAttribute('textlength')).toBe('10');
565 expect(e.firstChild.hasAttribute('textLength')).toBe(false);
566 } else {
567 // When parsing HTML (including the hydration case), the browser
568 // correctly maps "textlength" to "textLength" SVG attribute.
569 // So it happens to work on the initial render.
570 expect(e.firstChild.getAttribute('textLength')).toBe('10');
571 expect(e.firstChild.hasAttribute('textlength')).toBe(false);
572 }
573 });
574
575 itRenders('no badly cased aliased SVG attribute alias', async render => {
576 const e = await render(
577 <svg>
578 <text strokedasharray="10 10" />
579 </svg>,
580 1,
581 );
582 expect(e.firstChild.hasAttribute('stroke-dasharray')).toBe(false);
583 expect(e.firstChild.getAttribute('strokedasharray')).toBe('10 10');
584 });
585
586 itRenders(
587 'no badly cased original SVG attribute that is aliased',
588 async render => {
589 const e = await render(
590 <svg>
591 <text stroke-dasharray="10 10" />
592 </svg>,
593 1,
594 );
595 expect(e.firstChild.getAttribute('stroke-dasharray')).toBe('10 10');
596 },
597 );
598 });
599
600 describe('unknown attributes', function () {
601 itRenders('unknown attributes', async render => {
602 const e = await render(<div foo="bar" />);
603 expect(e.getAttribute('foo')).toBe('bar');
604 });
605
606 itRenders('unknown data- attributes', async render => {
607 const e = await render(<div data-foo="bar" />);
608 expect(e.getAttribute('data-foo')).toBe('bar');
609 });
610
611 itRenders('badly cased reserved attributes', async render => {
612 const e = await render(<div CHILDREN="5" />, 1);
613 expect(e.getAttribute('CHILDREN')).toBe('5');
614 });
615
616 itRenders('"data" attribute', async render => {
617 // For `<object />` acts as `src`.
618 const e = await render(<object data="hello" />);
619 expect(e.getAttribute('data')).toBe('hello');
620 });
621
622 itRenders('no unknown data- attributes with null value', async render => {
623 const e = await render(<div data-foo={null} />);
624 expect(e.hasAttribute('data-foo')).toBe(false);
625 });
626
627 itRenders('unknown data- attributes with casing', async render => {
628 const e = await render(<div data-fooBar="true" />, 1);
629 expect(e.getAttribute('data-foobar')).toBe('true');
630 });
631
632 itRenders('unknown data- attributes with boolean true', async render => {
633 const e = await render(<div data-foobar={true} />);
634 expect(e.getAttribute('data-foobar')).toBe('true');
635 });
636
637 itRenders('unknown data- attributes with boolean false', async render => {
638 const e = await render(<div data-foobar={false} />);
639 expect(e.getAttribute('data-foobar')).toBe('false');
640 });
641
642 itRenders(
643 'no unknown data- attributes with casing and null value',
644 async render => {
645 const e = await render(<div data-fooBar={null} />, 1);
646 expect(e.hasAttribute('data-foobar')).toBe(false);
647 },
648 );
649
650 itRenders('custom attributes for non-standard elements', async render => {
651 // This test suite generally assumes that we get exactly
652 // the same warnings (or none) for all scenarios including
653 // SSR + innerHTML, hydration, and client-side rendering.
654 // However this particular warning fires only when creating
655 // DOM nodes on the client side. We force it to fire early
656 // so that it gets deduplicated later, and doesn't fail the test.
657 ReactDOM.flushSync(() => {
658 const root = ReactDOMClient.createRoot(document.createElement('div'));
659 root.render(<nonstandard />);
660 });
661 assertConsoleErrorDev([
662 'The tag <nonstandard> is unrecognized in this browser. ' +
663 'If you meant to render a React component, start its name with an uppercase letter.\n' +
664 ' in nonstandard (at **)',
665 ]);
666
667 const e = await render(<nonstandard foo="bar" />);
668 expect(e.getAttribute('foo')).toBe('bar');
669 });
670
671 itRenders('SVG tags with dashes in them', async render => {
672 const e = await render(
673 <svg>
674 <font-face accentHeight={10} />
675 </svg>,
676 );
677 expect(e.firstChild.hasAttribute('accentHeight')).toBe(false);
678 expect(e.firstChild.getAttribute('accent-height')).toBe('10');
679 });
680
681 itRenders('cased custom attributes', async render => {
682 const e = await render(<div fooBar="test" />, 1);
683 expect(e.getAttribute('foobar')).toBe('test');
684 });
685 });
686
687 itRenders('no HTML events', async render => {
688 const e = await render(<div onClick={() => {}} />);
689 expect(e.getAttribute('onClick')).toBe(null);
690 expect(e.getAttribute('onClick')).toBe(null);
691 expect(e.getAttribute('click')).toBe(null);
692 });
693
694 itRenders('no unknown events', async render => {
695 const e = await render(<div onunknownevent='alert("hack")' />, 1);
696 expect(e.getAttribute('onunknownevent')).toBe(null);
697 });
698
699 itRenders('custom attribute named `on`', async render => {
700 const e = await render(<div on="tap:do-something" />);
701 expect(e.getAttribute('on')).toEqual('tap:do-something');
702 });
703 });
704
705 // These tests mostly verify the existing behavior.
706 // It may not always make sense but we can't change it in minors.
707 describe('custom elements', () => {
708 itRenders('class for custom elements', async render => {
709 const e = await render(<div is="custom-element" class="test" />, 0);
710 expect(e.getAttribute('class')).toBe('test');
711 });
712
713 itRenders('className for is elements', async render => {
714 const e = await render(<div is="custom-element" className="test" />, 0);
715 expect(e.getAttribute('className')).toBe(null);
716 expect(e.getAttribute('class')).toBe('test');
717 });
718
719 itRenders('className for custom elements', async render => {
720 const e = await render(<custom-element className="test" />, 0);
721 expect(e.getAttribute('className')).toBe(null);
722 expect(e.getAttribute('class')).toBe('test');
723 });
724
725 itRenders('htmlFor property on is elements', async render => {
726 const e = await render(<div is="custom-element" htmlFor="test" />);
727 expect(e.getAttribute('htmlFor')).toBe(null);
728 expect(e.getAttribute('for')).toBe('test');
729 });
730
731 itRenders('htmlFor attribute on custom elements', async render => {
732 const e = await render(<custom-element htmlFor="test" />);
733 expect(e.getAttribute('htmlFor')).toBe('test');
734 expect(e.getAttribute('for')).toBe(null);
735 });
736
737 itRenders('for attribute on custom elements', async render => {
738 const e = await render(<div is="custom-element" for="test" />);
739 expect(e.getAttribute('htmlFor')).toBe(null);
740 expect(e.getAttribute('for')).toBe('test');
741 });
742
743 itRenders('unknown attributes for custom elements', async render => {
744 const e = await render(<custom-element foo="bar" />);
745 expect(e.getAttribute('foo')).toBe('bar');
746 });
747
748 itRenders('unknown `on*` attributes for custom elements', async render => {
749 const e = await render(<custom-element onunknown="bar" />);
750 expect(e.getAttribute('onunknown')).toBe('bar');
751 });
752
753 itRenders('unknown boolean `true` attributes as strings', async render => {
754 const e = await render(<custom-element foo={true} />);
755 expect(e.getAttribute('foo')).toBe('');
756 });
757
758 itRenders('unknown boolean `false` attributes as strings', async render => {
759 const e = await render(<custom-element foo={false} />);
760 expect(e.getAttribute('foo')).toBe(null);
761 });
762
763 itRenders('new boolean `true` attributes', async render => {
764 const element = await render(<div inert={true} />, 0);
765
766 expect(element.getAttribute('inert')).toBe('');
767 });
768
769 itRenders('new boolean `""` attributes', async render => {
770 const element = await render(
771 <div inert="" />,
772 // Warns since this used to render `inert=""` like `inert={true}`
773 // but now renders it like `inert={false}`.
774 1,
775 );
776
777 expect(element.getAttribute('inert')).toBe(null);
778 });
779
780 itRenders('new boolean `false` attributes', async render => {
781 const element = await render(<div inert={false} />, 0);
782
783 expect(element.getAttribute('inert')).toBe(null);
784 });
785
786 itRenders(
787 'no unknown attributes for custom elements with null value',
788 async render => {
789 const e = await render(<custom-element foo={null} />);
790 expect(e.hasAttribute('foo')).toBe(false);
791 },
792 );
793
794 itRenders(
795 'unknown attributes for custom elements using is',
796 async render => {
797 const e = await render(<div is="custom-element" foo="bar" />);
798 expect(e.getAttribute('foo')).toBe('bar');
799 },
800 );
801
802 itRenders(
803 'no unknown attributes for custom elements using is with null value',
804 async render => {
805 const e = await render(<div is="custom-element" foo={null} />);
806 expect(e.hasAttribute('foo')).toBe(false);
807 },
808 );
809 });
810 });