1
-/**
2
- * Copyright (c) 2013-present, Facebook, Inc.
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 ExecutionEnvironment;
13
-let PropTypes;
14
-let React;
15
-let ReactDOM;
16
-let ReactDOMServer;
17
-let ReactTestUtils;
18
-
19
-const stream = require('stream');
20
-
21
-const TEXT_NODE_TYPE = 3;
22
-
23
-// Helper functions for rendering tests
24
-// ====================================
25
-
26
-// promisified version of ReactDOM.render()
27
-function asyncReactDOMRender(reactElement, domElement, forceHydrate) {
28
- return new Promise(resolve => {
29
- if (forceHydrate) {
30
- ReactDOM.hydrate(reactElement, domElement);
31
- } else {
32
- ReactDOM.render(reactElement, domElement);
33
- }
34
- // We can't use the callback for resolution because that will not catch
35
- // errors. They're thrown.
36
- resolve();
37
- });
38
-}
39
-// performs fn asynchronously and expects count errors logged to console.error.
40
-// will fail the test if the count of errors logged is not equal to count.
41
-async function expectErrors(fn, count) {
42
- if (console.error.calls && console.error.calls.reset) {
43
- console.error.calls.reset();
44
- } else {
45
- spyOnDev(console, 'error');
46
- }
47
-
48
- const result = await fn();
49
- if (
50
- console.error.calls &&
51
- console.error.calls.count() !== count &&
52
- console.error.calls.count() !== 0
53
- ) {
54
- console.log(
55
- `We expected ${
56
- count
57
- } warning(s), but saw ${console.error.calls.count()} warning(s).`,
58
- );
59
- if (console.error.calls.count() > 0) {
60
- console.log(`We saw these warnings:`);
61
- for (var i = 0; i < console.error.calls.count(); i++) {
62
- console.log(console.error.calls.argsFor(i)[0]);
63
- }
64
- }
65
- }
66
- if (__DEV__) {
67
- expect(console.error.calls.count()).toBe(count);
68
- }
69
- return result;
70
-}
71
-
72
-// renders the reactElement into domElement, and expects a certain number of errors.
73
-// returns a Promise that resolves when the render is complete.
74
-function renderIntoDom(reactElement, domElement, forceHydrate, errorCount = 0) {
75
- return expectErrors(async () => {
76
- ExecutionEnvironment.canUseDOM = true;
77
- await asyncReactDOMRender(reactElement, domElement, forceHydrate);
78
- ExecutionEnvironment.canUseDOM = false;
79
- return domElement.firstChild;
80
- }, errorCount);
81
-}
82
-
83
-async function renderIntoString(reactElement, errorCount = 0) {
84
- return await expectErrors(
85
- () =>
86
- new Promise(resolve =>
87
- resolve(ReactDOMServer.renderToString(reactElement)),
88
- ),
89
- errorCount,
90
- );
91
-}
92
-
93
-// Renders text using SSR and then stuffs it into a DOM node; returns the DOM
94
-// element that corresponds with the reactElement.
95
-// Does not render on client or perform client-side revival.
96
-async function serverRender(reactElement, errorCount = 0) {
97
- const markup = await renderIntoString(reactElement, errorCount);
98
- var domElement = document.createElement('div');
99
- domElement.innerHTML = markup;
100
- return domElement.firstChild;
101
-}
102
-
103
-// this just drains a readable piped into it to a string, which can be accessed
104
-// via .buffer.
105
-class DrainWritable extends stream.Writable {
106
- constructor(options) {
107
- super(options);
108
- this.buffer = '';
109
- }
110
-
111
- _write(chunk, encoding, cb) {
112
- this.buffer += chunk;
113
- cb();
114
- }
115
-}
116
-
117
-async function renderIntoStream(reactElement, errorCount = 0) {
118
- return await expectErrors(
119
- () =>
120
- new Promise(resolve => {
121
- let writable = new DrainWritable();
122
- ReactDOMServer.renderToNodeStream(reactElement).pipe(writable);
123
- writable.on('finish', () => resolve(writable.buffer));
124
- }),
125
- errorCount,
126
- );
127
-}
128
-
129
-// Renders text using node stream SSR and then stuffs it into a DOM node;
130
-// returns the DOM element that corresponds with the reactElement.
131
-// Does not render on client or perform client-side revival.
132
-async function streamRender(reactElement, errorCount = 0) {
133
- const markup = await renderIntoStream(reactElement, errorCount);
134
- var domElement = document.createElement('div');
135
- domElement.innerHTML = markup;
136
- return domElement.firstChild;
137
-}
138
-
139
-const clientCleanRender = (element, errorCount = 0) => {
140
- const div = document.createElement('div');
141
- return renderIntoDom(element, div, false, errorCount);
142
-};
143
-
144
-const clientRenderOnServerString = async (element, errorCount = 0) => {
145
- const markup = await renderIntoString(element, errorCount);
146
- resetModules();
147
-
148
- var domElement = document.createElement('div');
149
- domElement.innerHTML = markup;
150
- let serverNode = domElement.firstChild;
151
-
152
- const firstClientNode = await renderIntoDom(
153
- element,
154
- domElement,
155
- true,
156
- errorCount,
157
- );
158
- let clientNode = firstClientNode;
159
-
160
- // Make sure all top level nodes match up
161
- while (serverNode || clientNode) {
162
- expect(serverNode != null).toBe(true);
163
- expect(clientNode != null).toBe(true);
164
- expect(clientNode.nodeType).toBe(serverNode.nodeType);
165
- // Assert that the DOM element hasn't been replaced.
166
- // Note that we cannot use expect(serverNode).toBe(clientNode) because
167
- // of jest bug #1772.
168
- expect(serverNode === clientNode).toBe(true);
169
- serverNode = serverNode.nextSibling;
170
- clientNode = clientNode.nextSibling;
171
- }
172
- return firstClientNode;
173
-};
174
-
175
-function BadMarkupExpected() {}
176
-
177
-const clientRenderOnBadMarkup = async (element, errorCount = 0) => {
178
- // First we render the top of bad mark up.
179
- var domElement = document.createElement('div');
180
- domElement.innerHTML =
181
- '<div id="badIdWhichWillCauseMismatch" data-reactroot="" data-reactid="1"></div>';
182
- await renderIntoDom(element, domElement, true, errorCount + 1);
183
-
184
- // This gives us the resulting text content.
185
- var hydratedTextContent = domElement.textContent;
186
-
187
- // Next we render the element into a clean DOM node client side.
188
- const cleanDomElement = document.createElement('div');
189
- ExecutionEnvironment.canUseDOM = true;
190
- await asyncReactDOMRender(element, cleanDomElement, true);
191
- ExecutionEnvironment.canUseDOM = false;
192
- // This gives us the expected text content.
193
- const cleanTextContent = cleanDomElement.textContent;
194
-
195
- // The only guarantee is that text content has been patched up if needed.
196
- expect(hydratedTextContent).toBe(cleanTextContent);
197
-
198
- // Abort any further expects. All bets are off at this point.
199
- throw new BadMarkupExpected();
200
-};
201
-
202
-// runs a DOM rendering test as four different tests, with four different rendering
203
-// scenarios:
204
-// -- render to string on server
205
-// -- render on client without any server markup "clean client render"
206
-// -- render on client on top of good server-generated string markup
207
-// -- render on client on top of bad server-generated markup
208
-//
209
-// testFn is a test that has one arg, which is a render function. the render
210
-// function takes in a ReactElement and an optional expected error count and
211
-// returns a promise of a DOM Element.
212
-//
213
-// You should only perform tests that examine the DOM of the results of
214
-// render; you should not depend on the interactivity of the returned DOM element,
215
-// as that will not work in the server string scenario.
216
-function itRenders(desc, testFn) {
217
- it(`renders ${desc} with server string render`, () => testFn(serverRender));
218
- it(`renders ${desc} with server stream render`, () => testFn(streamRender));
219
- itClientRenders(desc, testFn);
220
-}
221
-
222
-// run testFn in three different rendering scenarios:
223
-// -- render on client without any server markup "clean client render"
224
-// -- render on client on top of good server-generated string markup
225
-// -- render on client on top of bad server-generated markup
226
-//
227
-// testFn is a test that has one arg, which is a render function. the render
228
-// function takes in a ReactElement and an optional expected error count and
229
-// returns a promise of a DOM Element.
230
-//
231
-// Since all of the renders in this function are on the client, you can test interactivity,
232
-// unlike with itRenders.
233
-function itClientRenders(desc, testFn) {
234
- it(`renders ${desc} with clean client render`, () =>
235
- testFn(clientCleanRender));
236
- it(`renders ${desc} with client render on top of good server markup`, () =>
237
- testFn(clientRenderOnServerString));
238
- it(`renders ${
239
- desc
240
- } with client render on top of bad server markup`, async () => {
241
- try {
242
- await testFn(clientRenderOnBadMarkup);
243
- } catch (x) {
244
- // We expect this to trigger the BadMarkupExpected rejection.
245
- if (!(x instanceof BadMarkupExpected)) {
246
- // If not, rethrow.
247
- throw x;
248
- }
249
- }
250
- });
251
-}
252
-
253
-function itThrows(desc, testFn, partialMessage) {
254
- it(`throws ${desc}`, () => {
255
- return testFn().then(
256
- () => expect(false).toBe('The promise resolved and should not have.'),
257
- err => {
258
- expect(err).toBeInstanceOf(Error);
259
- expect(err.message).toContain(partialMessage);
260
- },
261
- );
262
- });
263
-}
264
-
265
-function itThrowsWhenRendering(desc, testFn, partialMessage) {
266
- itThrows(
267
- `when rendering ${desc} with server string render`,
268
- () => testFn(serverRender),
269
- partialMessage,
270
- );
271
- itThrows(
272
- `when rendering ${desc} with clean client render`,
273
- () => testFn(clientCleanRender),
274
- partialMessage,
275
- );
276
-
277
- // we subtract one from the warning count here because the throw means that it won't
278
- // get the usual markup mismatch warning.
279
- itThrows(
280
- `when rendering ${desc} with client render on top of bad server markup`,
281
- () =>
282
- testFn((element, warningCount = 0) =>
283
- clientRenderOnBadMarkup(element, warningCount - 1),
284
- ),
285
- partialMessage,
286
- );
287
-}
288
-
289
-// renders serverElement to a string, sticks it into a DOM element, and then
290
-// tries to render clientElement on top of it. shouldMatch is a boolean
291
-// telling whether we should expect the markup to match or not.
292
-async function testMarkupMatch(serverElement, clientElement, shouldMatch) {
293
- const domElement = await serverRender(serverElement);
294
- resetModules();
295
- return renderIntoDom(
296
- clientElement,
297
- domElement.parentNode,
298
- true,
299
- shouldMatch ? 0 : 1,
300
- );
301
-}
302
-
303
-// expects that rendering clientElement on top of a server-rendered
304
-// serverElement does NOT raise a markup mismatch warning.
305
-function expectMarkupMatch(serverElement, clientElement) {
306
- return testMarkupMatch(serverElement, clientElement, true);
307
-}
308
-
309
-// expects that rendering clientElement on top of a server-rendered
310
-// serverElement DOES raise a markup mismatch warning.
311
-function expectMarkupMismatch(serverElement, clientElement) {
312
- return testMarkupMatch(serverElement, clientElement, false);
313
-}
314
-
315
-// When there is a test that renders on server and then on client and expects a logged
316
-// error, you want to see the error show up both on server and client. Unfortunately,
317
-// React refuses to issue the same error twice to avoid clogging up the console.
318
-// To get around this, we must reload React modules in between server and client render.
319
-function resetModules() {
320
- // First, reset the modules to load the client renderer.
321
- jest.resetModuleRegistry();
322
-
323
- // TODO: can we express this test with only public API?
324
- ExecutionEnvironment = require('fbjs/lib/ExecutionEnvironment');
325
- require('shared/ReactFeatureFlags').enableReactFragment = true;
326
-
327
- PropTypes = require('prop-types');
328
- React = require('react');
329
- ReactDOM = require('react-dom');
330
- ReactTestUtils = require('react-dom/test-utils');
331
-
332
- // Now we reset the modules again to load the server renderer.
333
- // Resetting is important because we want to avoid any shared state
334
- // influencing the tests.
335
- jest.resetModuleRegistry();
336
- require('shared/ReactFeatureFlags').enableReactFragment = true;
337
- ReactDOMServer = require('react-dom/server');
338
-}
339
-
340
-describe('ReactDOMServerIntegration', () => {
341
- beforeEach(() => {
342
- resetModules();
343
-
344
- ExecutionEnvironment.canUseDOM = false;
345
- });
346
-
347
- describe('basic rendering', function() {
348
- itRenders('a blank div', async render => {
349
- const e = await render(<div />);
350
- expect(e.tagName).toBe('DIV');
351
- });
352
-
353
- itRenders('a self-closing tag', async render => {
354
- const e = await render(<br />);
355
- expect(e.tagName).toBe('BR');
356
- });
357
-
358
- itRenders('a self-closing tag as a child', async render => {
359
- const e = await render(
360
- <div>
361
- <br />
362
- </div>,
363
- );
364
- expect(e.childNodes.length).toBe(1);
365
- expect(e.firstChild.tagName).toBe('BR');
366
- });
367
-
368
- itRenders('a string', async render => {
369
- let e = await render('Hello');
370
- expect(e.nodeType).toBe(3);
371
- expect(e.nodeValue).toMatch('Hello');
372
- });
373
-
374
- itRenders('a number', async render => {
375
- let e = await render(42);
376
- expect(e.nodeType).toBe(3);
377
- expect(e.nodeValue).toMatch('42');
378
- });
379
-
380
- itRenders('an array with one child', async render => {
381
- let e = await render([<div key={1}>text1</div>]);
382
- let parent = e.parentNode;
383
- expect(parent.childNodes[0].tagName).toBe('DIV');
384
- });
385
-
386
- itRenders('an array with several children', async render => {
387
- let Header = props => {
388
- return <p>header</p>;
389
- };
390
- let Footer = props => {
391
- return [<h2 key={1}>footer</h2>, <h3 key={2}>about</h3>];
392
- };
393
- let e = await render([
394
- <div key={1}>text1</div>,
395
- <span key={2}>text2</span>,
396
- <Header key={3} />,
397
- <Footer key={4} />,
398
- ]);
399
- let parent = e.parentNode;
400
- expect(parent.childNodes[0].tagName).toBe('DIV');
401
- expect(parent.childNodes[1].tagName).toBe('SPAN');
402
- expect(parent.childNodes[2].tagName).toBe('P');
403
- expect(parent.childNodes[3].tagName).toBe('H2');
404
- expect(parent.childNodes[4].tagName).toBe('H3');
405
- });
406
-
407
- itRenders('a nested array', async render => {
408
- let e = await render([
409
- [<div key={1}>text1</div>],
410
- <span key={1}>text2</span>,
411
- [[[null, <p key={1} />], false]],
412
- ]);
413
- let parent = e.parentNode;
414
- expect(parent.childNodes[0].tagName).toBe('DIV');
415
- expect(parent.childNodes[1].tagName).toBe('SPAN');
416
- expect(parent.childNodes[2].tagName).toBe('P');
417
- });
418
-
419
- itRenders('a fragment with one child', async render => {
420
- let e = await render(
421
- <React.Fragment>
422
- <div>text1</div>
423
- </React.Fragment>,
424
- );
425
- let parent = e.parentNode;
426
- expect(parent.childNodes[0].tagName).toBe('DIV');
427
- });
428
-
429
- itRenders('a fragment with several children', async render => {
430
- let Header = props => {
431
- return <p>header</p>;
432
- };
433
- let Footer = props => {
434
- return (
435
- <React.Fragment>
436
- <h2>footer</h2>
437
- <h3>about</h3>
438
- </React.Fragment>
439
- );
440
- };
441
- let e = await render(
442
- <React.Fragment>
443
- <div>text1</div>
444
- <span>text2</span>
445
- <Header />
446
- <Footer />
447
- </React.Fragment>,
448
- );
449
- let parent = e.parentNode;
450
- expect(parent.childNodes[0].tagName).toBe('DIV');
451
- expect(parent.childNodes[1].tagName).toBe('SPAN');
452
- expect(parent.childNodes[2].tagName).toBe('P');
453
- expect(parent.childNodes[3].tagName).toBe('H2');
454
- expect(parent.childNodes[4].tagName).toBe('H3');
455
- });
456
-
457
- itRenders('a nested fragment', async render => {
458
- let e = await render(
459
- <React.Fragment>
460
- <React.Fragment>
461
- <div>text1</div>
462
- </React.Fragment>
463
- <span>text2</span>
464
- <React.Fragment>
465
- <React.Fragment>
466
- <React.Fragment>
467
- {null}
468
- <p />
469
- </React.Fragment>
470
- {false}
471
- </React.Fragment>
472
- </React.Fragment>
473
- </React.Fragment>,
474
- );
475
- let parent = e.parentNode;
476
- expect(parent.childNodes[0].tagName).toBe('DIV');
477
- expect(parent.childNodes[1].tagName).toBe('SPAN');
478
- expect(parent.childNodes[2].tagName).toBe('P');
479
- });
480
-
481
- itRenders('an iterable', async render => {
482
- const threeDivIterable = {
483
- '@@iterator': function() {
484
- var i = 0;
485
- return {
486
- next: function() {
487
- if (i++ < 3) {
488
- return {value: <div key={i} />, done: false};
489
- } else {
490
- return {value: undefined, done: true};
491
- }
492
- },
493
- };
494
- },
495
- };
496
- let e = await render(threeDivIterable);
497
- let parent = e.parentNode;
498
- expect(parent.childNodes.length).toBe(3);
499
- expect(parent.childNodes[0].tagName).toBe('DIV');
500
- expect(parent.childNodes[1].tagName).toBe('DIV');
501
- expect(parent.childNodes[2].tagName).toBe('DIV');
502
- });
503
-
504
- itRenders('emptyish values', async render => {
505
- let e = await render(0);
506
- expect(e.nodeType).toBe(TEXT_NODE_TYPE);
507
- expect(e.nodeValue).toMatch('0');
508
-
509
- // Empty string is special because client renders a node
510
- // but server returns empty HTML. So we compare parent text.
511
- expect((await render(<div>{''}</div>)).textContent).toBe('');
512
-
513
- expect(await render(<React.Fragment />)).toBe(null);
514
- expect(await render([])).toBe(null);
515
- expect(await render(false)).toBe(null);
516
- expect(await render(true)).toBe(null);
517
- expect(await render(undefined)).toBe(null);
518
- expect(await render([[[false]], undefined])).toBe(null);
519
- });
520
- });
521
-
522
- describe('property to attribute mapping', function() {
523
- describe('string properties', function() {
524
- itRenders('simple numbers', async render => {
525
- const e = await render(<div width={30} />);
526
- expect(e.getAttribute('width')).toBe('30');
527
- });
528
-
529
- itRenders('simple strings', async render => {
530
- const e = await render(<div width={'30'} />);
531
- expect(e.getAttribute('width')).toBe('30');
532
- });
533
-
534
- itRenders('no string prop with true value', async render => {
535
- const e = await render(<a href={true} />, 1);
536
- expect(e.hasAttribute('href')).toBe(false);
537
- });
538
-
539
- itRenders('no string prop with false value', async render => {
540
- const e = await render(<a href={false} />, 1);
541
- expect(e.hasAttribute('href')).toBe(false);
542
- });
543
-
544
- itRenders('no string prop with null value', async render => {
545
- const e = await render(<div width={null} />);
546
- expect(e.hasAttribute('width')).toBe(false);
547
- });
548
- });
549
-
550
- describe('boolean properties', function() {
551
- itRenders('boolean prop with true value', async render => {
552
- const e = await render(<div hidden={true} />);
553
- expect(e.getAttribute('hidden')).toBe('');
554
- });
555
-
556
- itRenders('boolean prop with false value', async render => {
557
- const e = await render(<div hidden={false} />);
558
- expect(e.getAttribute('hidden')).toBe(null);
559
- });
560
-
561
- itRenders('boolean prop with self value', async render => {
562
- const e = await render(<div hidden="hidden" />);
563
- expect(e.getAttribute('hidden')).toBe('');
564
- });
565
-
566
- // this does not seem like correct behavior, since hidden="" in HTML indicates
567
- // that the boolean property is present. however, it is how the current code
568
- // behaves, so the test is included here.
569
- itRenders('boolean prop with "" value', async render => {
570
- const e = await render(<div hidden="" />);
571
- expect(e.getAttribute('hidden')).toBe(null);
572
- });
573
-
574
- // this seems like it might mask programmer error, but it's existing behavior.
575
- itRenders('boolean prop with string value', async render => {
576
- const e = await render(<div hidden="foo" />);
577
- expect(e.getAttribute('hidden')).toBe('');
578
- });
579
-
580
- // this seems like it might mask programmer error, but it's existing behavior.
581
- itRenders('boolean prop with array value', async render => {
582
- const e = await render(<div hidden={['foo', 'bar']} />);
583
- expect(e.getAttribute('hidden')).toBe('');
584
- });
585
-
586
- // this seems like it might mask programmer error, but it's existing behavior.
587
- itRenders('boolean prop with object value', async render => {
588
- const e = await render(<div hidden={{foo: 'bar'}} />);
589
- expect(e.getAttribute('hidden')).toBe('');
590
- });
591
-
592
- // this seems like it might mask programmer error, but it's existing behavior.
593
- itRenders('boolean prop with non-zero number value', async render => {
594
- const e = await render(<div hidden={10} />);
595
- expect(e.getAttribute('hidden')).toBe('');
596
- });
597
-
598
- // this seems like it might mask programmer error, but it's existing behavior.
599
- itRenders('boolean prop with zero value', async render => {
600
- const e = await render(<div hidden={0} />);
601
- expect(e.getAttribute('hidden')).toBe(null);
602
- });
603
-
604
- itRenders('no boolean prop with null value', async render => {
605
- const e = await render(<div hidden={null} />);
606
- expect(e.hasAttribute('hidden')).toBe(false);
607
- });
608
- });
609
-
610
- describe('download property (combined boolean/string attribute)', function() {
611
- itRenders('download prop with true value', async render => {
612
- const e = await render(<a download={true} />);
613
- expect(e.getAttribute('download')).toBe('');
614
- });
615
-
616
- itRenders('download prop with false value', async render => {
617
- const e = await render(<a download={false} />);
618
- expect(e.getAttribute('download')).toBe(null);
619
- });
620
-
621
- itRenders('download prop with string value', async render => {
622
- const e = await render(<a download="myfile" />);
623
- expect(e.getAttribute('download')).toBe('myfile');
624
- });
625
-
626
- itRenders('download prop with string "false" value', async render => {
627
- const e = await render(<a download="false" />);
628
- expect(e.getAttribute('download')).toBe('false');
629
- });
630
-
631
- itRenders('download prop with string "true" value', async render => {
632
- const e = await render(<a download={'true'} />);
633
- expect(e.getAttribute('download')).toBe('true');
634
- });
635
-
636
- itRenders('download prop with number 0 value', async render => {
637
- const e = await render(<a download={0} />);
638
- expect(e.getAttribute('download')).toBe('0');
639
- });
640
-
641
- itRenders('no download prop with null value', async render => {
642
- const e = await render(<div download={null} />);
643
- expect(e.hasAttribute('download')).toBe(false);
644
- });
645
-
646
- itRenders('no download prop with undefined value', async render => {
647
- const e = await render(<div download={undefined} />);
648
- expect(e.hasAttribute('download')).toBe(false);
649
- });
650
- });
651
-
652
- describe('className property', function() {
653
- itRenders('className prop with string value', async render => {
654
- const e = await render(<div className="myClassName" />);
655
- expect(e.getAttribute('class')).toBe('myClassName');
656
- });
657
-
658
- itRenders('className prop with empty string value', async render => {
659
- const e = await render(<div className="" />);
660
- expect(e.getAttribute('class')).toBe('');
661
- });
662
-
663
- itRenders('no className prop with true value', async render => {
664
- const e = await render(<div className={true} />, 1);
665
- expect(e.hasAttribute('class')).toBe(false);
666
- });
667
-
668
- itRenders('no className prop with false value', async render => {
669
- const e = await render(<div className={false} />, 1);
670
- expect(e.hasAttribute('class')).toBe(false);
671
- });
672
-
673
- itRenders('no className prop with null value', async render => {
674
- const e = await render(<div className={null} />);
675
- expect(e.hasAttribute('className')).toBe(false);
676
- });
677
-
678
- itRenders('badly cased className with a warning', async render => {
679
- const e = await render(<div classname="test" />, 1);
680
- expect(e.hasAttribute('class')).toBe(false);
681
- expect(e.hasAttribute('classname')).toBe(true);
682
- });
683
-
684
- itRenders(
685
- 'className prop when given the alias with a warning',
686
- async render => {
687
- const e = await render(<div class="test" />, 1);
688
- expect(e.className).toBe('test');
689
- },
690
- );
691
-
692
- itRenders(
693
- 'className prop when given a badly cased alias',
694
- async render => {
695
- const e = await render(<div cLASs="test" />, 1);
696
- expect(e.className).toBe('test');
697
- },
698
- );
699
-
700
- itRenders('class for custom elements', async render => {
701
- const e = await render(<div is="custom-element" class="test" />, 0);
702
- expect(e.getAttribute('class')).toBe('test');
703
- });
704
-
705
- itRenders('className for custom elements', async render => {
706
- const e = await render(<div is="custom-element" className="test" />, 0);
707
- expect(e.getAttribute('className')).toBe('test');
708
- });
709
- });
710
-
711
- describe('htmlFor property', function() {
712
- itRenders('htmlFor with string value', async render => {
713
- const e = await render(<div htmlFor="myFor" />);
714
- expect(e.getAttribute('for')).toBe('myFor');
715
- });
716
-
717
- itRenders('no badly cased htmlfor', async render => {
718
- const e = await render(<div htmlfor="myFor" />, 1);
719
- expect(e.hasAttribute('for')).toBe(false);
720
- expect(e.getAttribute('htmlfor')).toBe('myFor');
721
- });
722
-
723
- itRenders('htmlFor with an empty string', async render => {
724
- const e = await render(<div htmlFor="" />);
725
- expect(e.getAttribute('for')).toBe('');
726
- });
727
-
728
- itRenders('no htmlFor prop with true value', async render => {
729
- const e = await render(<div htmlFor={true} />, 1);
730
- expect(e.hasAttribute('for')).toBe(false);
731
- });
732
-
733
- itRenders('no htmlFor prop with false value', async render => {
734
- const e = await render(<div htmlFor={false} />, 1);
735
- expect(e.hasAttribute('for')).toBe(false);
736
- });
737
-
738
- itRenders('no htmlFor prop with null value', async render => {
739
- const e = await render(<div htmlFor={null} />);
740
- expect(e.hasAttribute('htmlFor')).toBe(false);
741
- });
742
-
743
- itRenders('htmlFor attribute on custom elements', async render => {
744
- const e = await render(<div is="custom-element" htmlFor="test" />);
745
- expect(e.getAttribute('htmlFor')).toBe('test');
746
- });
747
-
748
- itRenders('for attribute on custom elements', async render => {
749
- const e = await render(<div is="custom-element" for="test" />);
750
- expect(e.getAttribute('for')).toBe('test');
751
- });
752
- });
753
-
754
- describe('numeric properties', function() {
755
- itRenders(
756
- 'positive numeric property with positive value',
757
- async render => {
758
- const e = await render(<input size={2} />);
759
- expect(e.getAttribute('size')).toBe('2');
760
- },
761
- );
762
-
763
- itRenders(
764
- 'no positive numeric property with zero value',
765
- async render => {
766
- const e = await render(<input size={0} />);
767
- expect(e.hasAttribute('size')).toBe(false);
768
- },
769
- );
770
-
771
- itRenders('numeric property with zero value', async render => {
772
- const e = await render(<ol start={0} />);
773
- expect(e.getAttribute('start')).toBe('0');
774
- });
775
- });
776
-
777
- describe('props with special meaning in React', function() {
778
- itRenders('no ref attribute', async render => {
779
- class RefComponent extends React.Component {
780
- render() {
781
- return <div ref="foo" />;
782
- }
783
- }
784
- const e = await render(<RefComponent />);
785
- expect(e.getAttribute('ref')).toBe(null);
786
- });
787
-
788
- itRenders('no children attribute', async render => {
789
- const e = await render(React.createElement('div', {}, 'foo'));
790
- expect(e.getAttribute('children')).toBe(null);
791
- });
792
-
793
- itRenders('no key attribute', async render => {
794
- const e = await render(<div key="foo" />);
795
- expect(e.getAttribute('key')).toBe(null);
796
- });
797
-
798
- itRenders('no dangerouslySetInnerHTML attribute', async render => {
799
- const e = await render(
800
- <div dangerouslySetInnerHTML={{__html: '<foo />'}} />,
801
- );
802
- expect(e.getAttribute('dangerouslySetInnerHTML')).toBe(null);
803
- });
804
-
805
- itRenders('no suppressContentEditableWarning attribute', async render => {
806
- const e = await render(<div suppressContentEditableWarning={true} />);
807
- expect(e.getAttribute('suppressContentEditableWarning')).toBe(null);
808
- });
809
-
810
- itRenders('no suppressHydrationWarning attribute', async render => {
811
- const e = await render(<span suppressHydrationWarning={true} />);
812
- expect(e.getAttribute('suppressHydrationWarning')).toBe(null);
813
- });
814
- });
815
-
816
- describe('inline styles', function() {
817
- itRenders('simple styles', async render => {
818
- const e = await render(<div style={{color: 'red', width: '30px'}} />);
819
- expect(e.style.color).toBe('red');
820
- expect(e.style.width).toBe('30px');
821
- });
822
-
823
- itRenders('relevant styles with px', async render => {
824
- const e = await render(
825
- <div
826
- style={{
827
- left: 0,
828
- margin: 16,
829
- opacity: 0.5,
830
- padding: '4px',
831
- }}
832
- />,
833
- );
834
- expect(e.style.left).toBe('0px');
835
- expect(e.style.margin).toBe('16px');
836
- expect(e.style.opacity).toBe('0.5');
837
- expect(e.style.padding).toBe('4px');
838
- });
839
-
840
- itRenders('custom properties', async render => {
841
- const e = await render(<div style={{'--foo': 5}} />);
842
- // This seems like an odd way computed properties are exposed in jsdom.
843
- // In a real browser we'd read it with e.style.getPropertyValue('--foo')
844
- expect(e.style.Foo).toBe('5');
845
- });
846
-
847
- itRenders('no undefined styles', async render => {
848
- const e = await render(
849
- <div style={{color: undefined, width: '30px'}} />,
850
- );
851
- expect(e.style.color).toBe('');
852
- expect(e.style.width).toBe('30px');
853
- });
854
-
855
- itRenders('no null styles', async render => {
856
- const e = await render(<div style={{color: null, width: '30px'}} />);
857
- expect(e.style.color).toBe('');
858
- expect(e.style.width).toBe('30px');
859
- });
860
-
861
- itRenders('no empty styles', async render => {
862
- const e = await render(<div style={{color: null, width: null}} />);
863
- expect(e.style.color).toBe('');
864
- expect(e.style.width).toBe('');
865
- expect(e.hasAttribute('style')).toBe(false);
866
- });
867
-
868
- itRenders('unitless-number rules with prefixes', async render => {
869
- const {style} = await render(
870
- <div
871
- style={{
872
- lineClamp: 10,
873
- WebkitLineClamp: 10,
874
- MozFlexGrow: 10,
875
- msFlexGrow: 10,
876
- msGridRow: 10,
877
- msGridRowEnd: 10,
878
- msGridRowSpan: 10,
879
- msGridRowStart: 10,
880
- msGridColumn: 10,
881
- msGridColumnEnd: 10,
882
- msGridColumnSpan: 10,
883
- msGridColumnStart: 10,
884
- }}
885
- />,
886
- );
887
-
888
- expect(style.lineClamp).toBe('10');
889
- expect(style.WebkitLineClamp).toBe('10');
890
- expect(style.MozFlexGrow).toBe('10');
891
- // jsdom is inconsistent in the style property name
892
- // it uses on the client and when processing server markup.
893
- // But it should be there either way.
894
- expect(style.MsFlexGrow || style.msFlexGrow).toBe('10');
895
- expect(style.MsGridRow || style.msGridRow).toBe('10');
896
- expect(style.MsGridRowEnd || style.msGridRowEnd).toBe('10');
897
- expect(style.MsGridRowSpan || style.msGridRowSpan).toBe('10');
898
- expect(style.MsGridRowStart || style.msGridRowStart).toBe('10');
899
- expect(style.MsGridColumn || style.msGridColumn).toBe('10');
900
- expect(style.MsGridColumnEnd || style.msGridColumnEnd).toBe('10');
901
- expect(style.MsGridColumnSpan || style.msGridColumnSpan).toBe('10');
902
- expect(style.MsGridColumnStart || style.msGridColumnStart).toBe('10');
903
- });
904
- });
905
-
906
- describe('aria attributes', function() {
907
- itRenders('simple strings', async render => {
908
- const e = await render(<div aria-label="hello" />);
909
- expect(e.getAttribute('aria-label')).toBe('hello');
910
- });
911
-
912
- // this probably is just masking programmer error, but it is existing behavior.
913
- itRenders('aria string prop with false value', async render => {
914
- const e = await render(<div aria-label={false} />);
915
- expect(e.getAttribute('aria-label')).toBe('false');
916
- });
917
-
918
- itRenders('no aria prop with null value', async render => {
919
- const e = await render(<div aria-label={null} />);
920
- expect(e.hasAttribute('aria-label')).toBe(false);
921
- });
922
-
923
- itRenders('"aria" attribute with a warning', async render => {
924
- // Reserved for future use.
925
- const e = await render(<div aria="hello" />, 1);
926
- expect(e.getAttribute('aria')).toBe('hello');
927
- });
928
- });
929
-
930
- describe('cased attributes', function() {
931
- itRenders(
932
- 'badly cased aliased HTML attribute with a warning',
933
- async render => {
934
- const e = await render(<meta httpequiv="refresh" />, 1);
935
- expect(e.hasAttribute('http-equiv')).toBe(false);
936
- expect(e.getAttribute('httpequiv')).toBe('refresh');
937
- },
938
- );
939
-
940
- itRenders('badly cased SVG attribute with a warning', async render => {
941
- const e = await render(<text textlength="10" />, 1);
942
- expect(e.getAttribute('textLength')).toBe('10');
943
- });
944
-
945
- itRenders('no badly cased aliased SVG attribute alias', async render => {
946
- const e = await render(<text strokedasharray="10 10" />, 1);
947
- expect(e.hasAttribute('stroke-dasharray')).toBe(false);
948
- expect(e.getAttribute('strokedasharray')).toBe('10 10');
949
- });
950
-
951
- itRenders(
952
- 'no badly cased original SVG attribute that is aliased',
953
- async render => {
954
- const e = await render(<text stroke-dasharray="10 10" />, 1);
955
- expect(e.getAttribute('stroke-dasharray')).toBe('10 10');
956
- },
957
- );
958
- });
959
-
960
- describe('unknown attributes', function() {
961
- itRenders('unknown attributes', async render => {
962
- const e = await render(<div foo="bar" />);
963
- expect(e.getAttribute('foo')).toBe('bar');
964
- });
965
-
966
- itRenders('unknown data- attributes', async render => {
967
- const e = await render(<div data-foo="bar" />);
968
- expect(e.getAttribute('data-foo')).toBe('bar');
969
- });
970
-
971
- itRenders('badly cased reserved attributes', async render => {
972
- const e = await render(<div CHILDREN="5" />, 1);
973
- expect(e.getAttribute('CHILDREN')).toBe('5');
974
- });
975
-
976
- itRenders('"data" attribute', async render => {
977
- // For `<object />` acts as `src`.
978
- const e = await render(<object data="hello" />);
979
- expect(e.getAttribute('data')).toBe('hello');
980
- });
981
-
982
- itRenders('no unknown data- attributes with null value', async render => {
983
- const e = await render(<div data-foo={null} />);
984
- expect(e.hasAttribute('data-foo')).toBe(false);
985
- });
986
-
987
- itRenders('unknown data- attributes with casing', async render => {
988
- const e = await render(<div data-fooBar="true" />, 1);
989
- expect(e.getAttribute('data-foobar')).toBe('true');
990
- });
991
-
992
- itRenders('unknown data- attributes with boolean true', async render => {
993
- const e = await render(<div data-foobar={true} />);
994
- expect(e.getAttribute('data-foobar')).toBe('true');
995
- });
996
-
997
- itRenders('unknown data- attributes with boolean false', async render => {
998
- const e = await render(<div data-foobar={false} />);
999
- expect(e.getAttribute('data-foobar')).toBe('false');
1000
- });
1001
-
1002
- itRenders(
1003
- 'no unknown data- attributes with casing and null value',
1004
- async render => {
1005
- const e = await render(<div data-fooBar={null} />, 1);
1006
- expect(e.hasAttribute('data-foobar')).toBe(false);
1007
- },
1008
- );
1009
-
1010
- itRenders('custom attributes for non-standard elements', async render => {
1011
- const e = await render(<nonstandard foo="bar" />);
1012
- expect(e.getAttribute('foo')).toBe('bar');
1013
- });
1014
-
1015
- itRenders('unknown attributes for custom elements', async render => {
1016
- const e = await render(<custom-element foo="bar" />);
1017
- expect(e.getAttribute('foo')).toBe('bar');
1018
- });
1019
-
1020
- itRenders(
1021
- 'no unknown attributes for custom elements with null value',
1022
- async render => {
1023
- const e = await render(<custom-element foo={null} />);
1024
- expect(e.hasAttribute('foo')).toBe(false);
1025
- },
1026
- );
1027
-
1028
- itRenders(
1029
- 'unknown attributes for custom elements using is',
1030
- async render => {
1031
- const e = await render(<div is="custom-element" foo="bar" />);
1032
- expect(e.getAttribute('foo')).toBe('bar');
1033
- },
1034
- );
1035
-
1036
- itRenders(
1037
- 'no unknown attributes for custom elements using is with null value',
1038
- async render => {
1039
- const e = await render(<div is="custom-element" foo={null} />);
1040
- expect(e.hasAttribute('foo')).toBe(false);
1041
- },
1042
- );
1043
-
1044
- itRenders('SVG tags with dashes in them', async render => {
1045
- const e = await render(
1046
- <svg>
1047
- <font-face accentHeight={10} />
1048
- </svg>,
1049
- );
1050
- expect(e.firstChild.hasAttribute('accentHeight')).toBe(false);
1051
- expect(e.firstChild.getAttribute('accent-height')).toBe('10');
1052
- });
1053
-
1054
- itRenders('cased custom attributes', async render => {
1055
- const e = await render(<div fooBar="test" />, 1);
1056
- expect(e.getAttribute('foobar')).toBe('test');
1057
- });
1058
- });
1059
-
1060
- itRenders('no HTML events', async render => {
1061
- const e = await render(<div onClick={() => {}} />);
1062
- expect(e.getAttribute('onClick')).toBe(null);
1063
- expect(e.getAttribute('onClick')).toBe(null);
1064
- expect(e.getAttribute('click')).toBe(null);
1065
- });
1066
-
1067
- itRenders('no unknown events', async render => {
1068
- const e = await render(
1069
- <div onunknownevent="alert("hack")" />,
1070
- 1,
1071
- );
1072
- expect(e.getAttribute('onunknownevent')).toBe(null);
1073
- });
1074
-
1075
- itRenders('custom attribute named `on`', async render => {
1076
- const e = await render(<div on="tap:do-something" />);
1077
- expect(e.getAttribute('on')).toEqual('tap:do-something');
1078
- });
1079
- });
1080
-
1081
- describe('elements and children', function() {
1082
- function expectNode(node, type, value) {
1083
- expect(node).not.toBe(null);
1084
- expect(node.nodeType).toBe(type);
1085
- expect(node.nodeValue).toMatch(value);
1086
- }
1087
-
1088
- function expectTextNode(node, text) {
1089
- expectNode(node, TEXT_NODE_TYPE, text);
1090
- }
1091
-
1092
- describe('text children', function() {
1093
- itRenders('a div with text', async render => {
1094
- const e = await render(<div>Text</div>);
1095
- expect(e.tagName).toBe('DIV');
1096
- expect(e.childNodes.length).toBe(1);
1097
- expectNode(e.firstChild, TEXT_NODE_TYPE, 'Text');
1098
- });
1099
-
1100
- itRenders('a div with text with flanking whitespace', async render => {
1101
- // prettier-ignore
1102
- const e = await render(<div> Text </div>);
1103
- expect(e.childNodes.length).toBe(1);
1104
- expectNode(e.childNodes[0], TEXT_NODE_TYPE, ' Text ');
1105
- });
1106
-
1107
- itRenders('a div with an empty text child', async render => {
1108
- const e = await render(<div>{''}</div>);
1109
- expect(e.childNodes.length).toBe(0);
1110
- });
1111
-
1112
- itRenders('a div with multiple empty text children', async render => {
1113
- const e = await render(
1114
- <div>
1115
- {''}
1116
- {''}
1117
- {''}
1118
- </div>,
1119
- );
1120
- if (render === serverRender || render === streamRender) {
1121
- // For plain server markup result we should have no text nodes if
1122
- // they're all empty.
1123
- expect(e.childNodes.length).toBe(0);
1124
- expect(e.textContent).toBe('');
1125
- } else {
1126
- expect(e.childNodes.length).toBe(3);
1127
- expectTextNode(e.childNodes[0], '');
1128
- expectTextNode(e.childNodes[1], '');
1129
- expectTextNode(e.childNodes[2], '');
1130
- }
1131
- });
1132
-
1133
- itRenders('a div with multiple whitespace children', async render => {
1134
- // prettier-ignore
1135
- const e = await render(<div>{' '}{' '}{' '}</div>);
1136
- if (
1137
- render === serverRender ||
1138
- render === clientRenderOnServerString ||
1139
- render === streamRender
1140
- ) {
1141
- // For plain server markup result we have comments between.
1142
- // If we're able to hydrate, they remain.
1143
- expect(e.childNodes.length).toBe(5);
1144
- expectTextNode(e.childNodes[0], ' ');
1145
- expectTextNode(e.childNodes[2], ' ');
1146
- expectTextNode(e.childNodes[4], ' ');
1147
- } else {
1148
- expect(e.childNodes.length).toBe(3);
1149
- expectTextNode(e.childNodes[0], ' ');
1150
- expectTextNode(e.childNodes[1], ' ');
1151
- expectTextNode(e.childNodes[2], ' ');
1152
- }
1153
- });
1154
-
1155
- itRenders('a div with text sibling to a node', async render => {
1156
- const e = await render(
1157
- <div>
1158
- Text<span>More Text</span>
1159
- </div>,
1160
- );
1161
- let spanNode;
1162
- expect(e.childNodes.length).toBe(2);
1163
- spanNode = e.childNodes[1];
1164
- expectTextNode(e.childNodes[0], 'Text');
1165
- expect(spanNode.tagName).toBe('SPAN');
1166
- expect(spanNode.childNodes.length).toBe(1);
1167
- expectNode(spanNode.firstChild, TEXT_NODE_TYPE, 'More Text');
1168
- });
1169
-
1170
- itRenders('a non-standard element with text', async render => {
1171
- const e = await render(<nonstandard>Text</nonstandard>);
1172
- expect(e.tagName).toBe('NONSTANDARD');
1173
- expect(e.childNodes.length).toBe(1);
1174
- expectNode(e.firstChild, TEXT_NODE_TYPE, 'Text');
1175
- });
1176
-
1177
- itRenders('a custom element with text', async render => {
1178
- const e = await render(<custom-element>Text</custom-element>);
1179
- expect(e.tagName).toBe('CUSTOM-ELEMENT');
1180
- expect(e.childNodes.length).toBe(1);
1181
- expectNode(e.firstChild, TEXT_NODE_TYPE, 'Text');
1182
- });
1183
-
1184
- itRenders('a leading blank child with a text sibling', async render => {
1185
- const e = await render(<div>{''}foo</div>);
1186
- if (render === serverRender || render === streamRender) {
1187
- expect(e.childNodes.length).toBe(1);
1188
- expectTextNode(e.childNodes[0], 'foo');
1189
- } else {
1190
- expect(e.childNodes.length).toBe(2);
1191
- expectTextNode(e.childNodes[0], '');
1192
- expectTextNode(e.childNodes[1], 'foo');
1193
- }
1194
- });
1195
-
1196
- itRenders('a trailing blank child with a text sibling', async render => {
1197
- const e = await render(<div>foo{''}</div>);
1198
- // with Fiber, there are just two text nodes.
1199
- if (render === serverRender || render === streamRender) {
1200
- expect(e.childNodes.length).toBe(1);
1201
- expectTextNode(e.childNodes[0], 'foo');
1202
- } else {
1203
- expect(e.childNodes.length).toBe(2);
1204
- expectTextNode(e.childNodes[0], 'foo');
1205
- expectTextNode(e.childNodes[1], '');
1206
- }
1207
- });
1208
-
1209
- itRenders('an element with two text children', async render => {
1210
- const e = await render(
1211
- <div>
1212
- {'foo'}
1213
- {'bar'}
1214
- </div>,
1215
- );
1216
- if (
1217
- render === serverRender ||
1218
- render === clientRenderOnServerString ||
1219
- render === streamRender
1220
- ) {
1221
- // In the server render output there's a comment between them.
1222
- expect(e.childNodes.length).toBe(3);
1223
- expectTextNode(e.childNodes[0], 'foo');
1224
- expectTextNode(e.childNodes[2], 'bar');
1225
- } else {
1226
- expect(e.childNodes.length).toBe(2);
1227
- expectTextNode(e.childNodes[0], 'foo');
1228
- expectTextNode(e.childNodes[1], 'bar');
1229
- }
1230
- });
1231
-
1232
- itRenders(
1233
- 'a component returning text node between two text nodes',
1234
- async render => {
1235
- const B = () => 'b';
1236
- const e = await render(
1237
- <div>
1238
- {'a'}
1239
- <B />
1240
- {'c'}
1241
- </div>,
1242
- );
1243
- if (
1244
- render === serverRender ||
1245
- render === clientRenderOnServerString ||
1246
- render === streamRender
1247
- ) {
1248
- // In the server render output there's a comment between them.
1249
- expect(e.childNodes.length).toBe(5);
1250
- expectTextNode(e.childNodes[0], 'a');
1251
- expectTextNode(e.childNodes[2], 'b');
1252
- expectTextNode(e.childNodes[4], 'c');
1253
- } else {
1254
- expect(e.childNodes.length).toBe(3);
1255
- expectTextNode(e.childNodes[0], 'a');
1256
- expectTextNode(e.childNodes[1], 'b');
1257
- expectTextNode(e.childNodes[2], 'c');
1258
- }
1259
- },
1260
- );
1261
-
1262
- itRenders('a tree with sibling host and text nodes', async render => {
1263
- class X extends React.Component {
1264
- render() {
1265
- return [null, [<Y key="1" />], false];
1266
- }
1267
- }
1268
-
1269
- function Y() {
1270
- return [<Z key="1" />, ['c']];
1271
- }
1272
-
1273
- function Z() {
1274
- return null;
1275
- }
1276
-
1277
- const e = await render(
1278
- <div>
1279
- {[['a'], 'b']}
1280
- <div>
1281
- <X key="1" />
1282
- d
1283
- </div>
1284
- e
1285
- </div>,
1286
- );
1287
- if (
1288
- render === serverRender ||
1289
- render === clientRenderOnServerString ||
1290
- render === streamRender
1291
- ) {
1292
- // In the server render output there's comments between text nodes.
1293
- expect(e.childNodes.length).toBe(5);
1294
- expectTextNode(e.childNodes[0], 'a');
1295
- expectTextNode(e.childNodes[2], 'b');
1296
- expect(e.childNodes[3].childNodes.length).toBe(3);
1297
- expectTextNode(e.childNodes[3].childNodes[0], 'c');
1298
- expectTextNode(e.childNodes[3].childNodes[2], 'd');
1299
- expectTextNode(e.childNodes[4], 'e');
1300
- } else {
1301
- expect(e.childNodes.length).toBe(4);
1302
- expectTextNode(e.childNodes[0], 'a');
1303
- expectTextNode(e.childNodes[1], 'b');
1304
- expect(e.childNodes[2].childNodes.length).toBe(2);
1305
- expectTextNode(e.childNodes[2].childNodes[0], 'c');
1306
- expectTextNode(e.childNodes[2].childNodes[1], 'd');
1307
- expectTextNode(e.childNodes[3], 'e');
1308
- }
1309
- });
1310
- });
1311
-
1312
- describe('number children', function() {
1313
- itRenders('a number as single child', async render => {
1314
- const e = await render(<div>{3}</div>);
1315
- expect(e.textContent).toBe('3');
1316
- });
1317
-
1318
- // zero is falsey, so it could look like no children if the code isn't careful.
1319
- itRenders('zero as single child', async render => {
1320
- const e = await render(<div>{0}</div>);
1321
- expect(e.textContent).toBe('0');
1322
- });
1323
-
1324
- itRenders('an element with number and text children', async render => {
1325
- const e = await render(
1326
- <div>
1327
- {'foo'}
1328
- {40}
1329
- </div>,
1330
- );
1331
- // with Fiber, there are just two text nodes.
1332
- if (
1333
- render === serverRender ||
1334
- render === clientRenderOnServerString ||
1335
- render === streamRender
1336
- ) {
1337
- // In the server markup there's a comment between.
1338
- expect(e.childNodes.length).toBe(3);
1339
- expectTextNode(e.childNodes[0], 'foo');
1340
- expectTextNode(e.childNodes[2], '40');
1341
- } else {
1342
- expect(e.childNodes.length).toBe(2);
1343
- expectTextNode(e.childNodes[0], 'foo');
1344
- expectTextNode(e.childNodes[1], '40');
1345
- }
1346
- });
1347
- });
1348
-
1349
- describe('null, false, and undefined children', function() {
1350
- itRenders('null single child as blank', async render => {
1351
- const e = await render(<div>{null}</div>);
1352
- expect(e.childNodes.length).toBe(0);
1353
- });
1354
-
1355
- itRenders('false single child as blank', async render => {
1356
- const e = await render(<div>{false}</div>);
1357
- expect(e.childNodes.length).toBe(0);
1358
- });
1359
-
1360
- itRenders('undefined single child as blank', async render => {
1361
- const e = await render(<div>{undefined}</div>);
1362
- expect(e.childNodes.length).toBe(0);
1363
- });
1364
-
1365
- itRenders('a null component children as empty', async render => {
1366
- const NullComponent = () => null;
1367
- const e = await render(
1368
- <div>
1369
- <NullComponent />
1370
- </div>,
1371
- );
1372
- expect(e.childNodes.length).toBe(0);
1373
- });
1374
-
1375
- itRenders('null children as blank', async render => {
1376
- const e = await render(<div>{null}foo</div>);
1377
- expect(e.childNodes.length).toBe(1);
1378
- expectTextNode(e.childNodes[0], 'foo');
1379
- });
1380
-
1381
- itRenders('false children as blank', async render => {
1382
- const e = await render(<div>{false}foo</div>);
1383
- expect(e.childNodes.length).toBe(1);
1384
- expectTextNode(e.childNodes[0], 'foo');
1385
- });
1386
-
1387
- itRenders('null and false children together as blank', async render => {
1388
- const e = await render(
1389
- <div>
1390
- {false}
1391
- {null}foo{null}
1392
- {false}
1393
- </div>,
1394
- );
1395
- expect(e.childNodes.length).toBe(1);
1396
- expectTextNode(e.childNodes[0], 'foo');
1397
- });
1398
-
1399
- itRenders('only null and false children as blank', async render => {
1400
- const e = await render(
1401
- <div>
1402
- {false}
1403
- {null}
1404
- {null}
1405
- {false}
1406
- </div>,
1407
- );
1408
- expect(e.childNodes.length).toBe(0);
1409
- });
1410
- });
1411
-
1412
- describe('elements with implicit namespaces', function() {
1413
- itRenders('an svg element', async render => {
1414
- const e = await render(<svg />);
1415
- expect(e.childNodes.length).toBe(0);
1416
- expect(e.tagName).toBe('svg');
1417
- expect(e.namespaceURI).toBe('http://www.w3.org/2000/svg');
1418
- });
1419
-
1420
- itRenders('svg child element with an attribute', async render => {
1421
- let e = await render(<svg viewBox="0 0 0 0" />);
1422
- expect(e.childNodes.length).toBe(0);
1423
- expect(e.tagName).toBe('svg');
1424
- expect(e.namespaceURI).toBe('http://www.w3.org/2000/svg');
1425
- expect(e.getAttribute('viewBox')).toBe('0 0 0 0');
1426
- });
1427
-
1428
- itRenders(
1429
- 'svg child element with a namespace attribute',
1430
- async render => {
1431
- let e = await render(
1432
- <svg>
1433
- <image xlinkHref="http://i.imgur.com/w7GCRPb.png" />
1434
- </svg>,
1435
- );
1436
- e = e.firstChild;
1437
- expect(e.childNodes.length).toBe(0);
1438
- expect(e.tagName).toBe('image');
1439
- expect(e.namespaceURI).toBe('http://www.w3.org/2000/svg');
1440
- expect(e.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe(
1441
- 'http://i.imgur.com/w7GCRPb.png',
1442
- );
1443
- },
1444
- );
1445
-
1446
- itRenders('svg child element with a badly cased alias', async render => {
1447
- let e = await render(
1448
- <svg>
1449
- <image xlinkhref="http://i.imgur.com/w7GCRPb.png" />
1450
- </svg>,
1451
- 1,
1452
- );
1453
- e = e.firstChild;
1454
- expect(e.hasAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe(
1455
- false,
1456
- );
1457
- expect(e.getAttribute('xlinkhref')).toBe(
1458
- 'http://i.imgur.com/w7GCRPb.png',
1459
- );
1460
- });
1461
-
1462
- itRenders('svg element with a tabIndex attribute', async render => {
1463
- let e = await render(<svg tabIndex="1" />);
1464
- expect(e.tabIndex).toBe(1);
1465
- });
1466
-
1467
- itRenders(
1468
- 'svg element with a badly cased tabIndex attribute',
1469
- async render => {
1470
- let e = await render(<svg tabindex="1" />, 1);
1471
- expect(e.tabIndex).toBe(1);
1472
- },
1473
- );
1474
-
1475
- itRenders('svg element with a mixed case name', async render => {
1476
- let e = await render(
1477
- <svg>
1478
- <filter>
1479
- <feMorphology />
1480
- </filter>
1481
- </svg>,
1482
- );
1483
- e = e.firstChild.firstChild;
1484
- expect(e.childNodes.length).toBe(0);
1485
- expect(e.tagName).toBe('feMorphology');
1486
- expect(e.namespaceURI).toBe('http://www.w3.org/2000/svg');
1487
- });
1488
-
1489
- itRenders('a math element', async render => {
1490
- const e = await render(<math />);
1491
- expect(e.childNodes.length).toBe(0);
1492
- expect(e.tagName).toBe('math');
1493
- expect(e.namespaceURI).toBe('http://www.w3.org/1998/Math/MathML');
1494
- });
1495
- });
1496
- // specially wrapped components
1497
- // (see the big switch near the beginning ofReactDOMComponent.mountComponent)
1498
- itRenders('an img', async render => {
1499
- const e = await render(<img />);
1500
- expect(e.childNodes.length).toBe(0);
1501
- expect(e.nextSibling).toBe(null);
1502
- expect(e.tagName).toBe('IMG');
1503
- });
1504
-
1505
- itRenders('a button', async render => {
1506
- const e = await render(<button />);
1507
- expect(e.childNodes.length).toBe(0);
1508
- expect(e.nextSibling).toBe(null);
1509
- expect(e.tagName).toBe('BUTTON');
1510
- });
1511
-
1512
- itRenders('a div with dangerouslySetInnerHTML', async render => {
1513
- const e = await render(
1514
- <div dangerouslySetInnerHTML={{__html: "<span id='child'/>"}} />,
1515
- );
1516
- expect(e.childNodes.length).toBe(1);
1517
- expect(e.firstChild.tagName).toBe('SPAN');
1518
- expect(e.firstChild.getAttribute('id')).toBe('child');
1519
- expect(e.firstChild.childNodes.length).toBe(0);
1520
- });
1521
-
1522
- describe('newline-eating elements', function() {
1523
- itRenders(
1524
- 'a newline-eating tag with content not starting with \\n',
1525
- async render => {
1526
- const e = await render(<pre>Hello</pre>);
1527
- expect(e.textContent).toBe('Hello');
1528
- },
1529
- );
1530
- itRenders(
1531
- 'a newline-eating tag with content starting with \\n',
1532
- async render => {
1533
- const e = await render(<pre>{'\nHello'}</pre>);
1534
- expect(e.textContent).toBe('\nHello');
1535
- },
1536
- );
1537
- itRenders('a normal tag with content starting with \\n', async render => {
1538
- const e = await render(<div>{'\nHello'}</div>);
1539
- expect(e.textContent).toBe('\nHello');
1540
- });
1541
- });
1542
-
1543
- describe('different component implementations', function() {
1544
- function checkFooDiv(e) {
1545
- expect(e.childNodes.length).toBe(1);
1546
- expectNode(e.firstChild, TEXT_NODE_TYPE, 'foo');
1547
- }
1548
-
1549
- itRenders('stateless components', async render => {
1550
- const StatelessComponent = () => <div>foo</div>;
1551
- checkFooDiv(await render(<StatelessComponent />));
1552
- });
1553
-
1554
- itRenders('ES6 class components', async render => {
1555
- class ClassComponent extends React.Component {
1556
- render() {
1557
- return <div>foo</div>;
1558
- }
1559
- }
1560
- checkFooDiv(await render(<ClassComponent />));
1561
- });
1562
-
1563
- itRenders('factory components', async render => {
1564
- const FactoryComponent = () => {
1565
- return {
1566
- render: function() {
1567
- return <div>foo</div>;
1568
- },
1569
- };
1570
- };
1571
- checkFooDiv(await render(<FactoryComponent />));
1572
- });
1573
- });
1574
-
1575
- describe('component hierarchies', async function() {
1576
- itRenders('single child hierarchies of components', async render => {
1577
- const Component = props => <div>{props.children}</div>;
1578
- let e = await render(
1579
- <Component>
1580
- <Component>
1581
- <Component>
1582
- <Component />
1583
- </Component>
1584
- </Component>
1585
- </Component>,
1586
- );
1587
- for (var i = 0; i < 3; i++) {
1588
- expect(e.tagName).toBe('DIV');
1589
- expect(e.childNodes.length).toBe(1);
1590
- e = e.firstChild;
1591
- }
1592
- expect(e.tagName).toBe('DIV');
1593
- expect(e.childNodes.length).toBe(0);
1594
- });
1595
-
1596
- itRenders('multi-child hierarchies of components', async render => {
1597
- const Component = props => <div>{props.children}</div>;
1598
- const e = await render(
1599
- <Component>
1600
- <Component>
1601
- <Component />
1602
- <Component />
1603
- </Component>
1604
- <Component>
1605
- <Component />
1606
- <Component />
1607
- </Component>
1608
- </Component>,
1609
- );
1610
- expect(e.tagName).toBe('DIV');
1611
- expect(e.childNodes.length).toBe(2);
1612
- for (var i = 0; i < 2; i++) {
1613
- var child = e.childNodes[i];
1614
- expect(child.tagName).toBe('DIV');
1615
- expect(child.childNodes.length).toBe(2);
1616
- for (var j = 0; j < 2; j++) {
1617
- var grandchild = child.childNodes[j];
1618
- expect(grandchild.tagName).toBe('DIV');
1619
- expect(grandchild.childNodes.length).toBe(0);
1620
- }
1621
- }
1622
- });
1623
-
1624
- itRenders('a div with a child', async render => {
1625
- const e = await render(
1626
- <div id="parent">
1627
- <div id="child" />
1628
- </div>,
1629
- );
1630
- expect(e.id).toBe('parent');
1631
- expect(e.childNodes.length).toBe(1);
1632
- expect(e.childNodes[0].id).toBe('child');
1633
- expect(e.childNodes[0].childNodes.length).toBe(0);
1634
- });
1635
-
1636
- itRenders('a div with multiple children', async render => {
1637
- const e = await render(
1638
- <div id="parent">
1639
- <div id="child1" />
1640
- <div id="child2" />
1641
- </div>,
1642
- );
1643
- expect(e.id).toBe('parent');
1644
- expect(e.childNodes.length).toBe(2);
1645
- expect(e.childNodes[0].id).toBe('child1');
1646
- expect(e.childNodes[0].childNodes.length).toBe(0);
1647
- expect(e.childNodes[1].id).toBe('child2');
1648
- expect(e.childNodes[1].childNodes.length).toBe(0);
1649
- });
1650
-
1651
- itRenders(
1652
- 'a div with multiple children separated by whitespace',
1653
- async render => {
1654
- const e = await render(
1655
- <div id="parent">
1656
- <div id="child1" /> <div id="child2" />
1657
- </div>,
1658
- );
1659
- expect(e.id).toBe('parent');
1660
- let child1, child2, textNode;
1661
- expect(e.childNodes.length).toBe(3);
1662
- child1 = e.childNodes[0];
1663
- textNode = e.childNodes[1];
1664
- child2 = e.childNodes[2];
1665
- expect(child1.id).toBe('child1');
1666
- expect(child1.childNodes.length).toBe(0);
1667
- expectTextNode(textNode, ' ');
1668
- expect(child2.id).toBe('child2');
1669
- expect(child2.childNodes.length).toBe(0);
1670
- },
1671
- );
1672
-
1673
- itRenders(
1674
- 'a div with a single child surrounded by whitespace',
1675
- async render => {
1676
- // prettier-ignore
1677
- const e = await render(<div id="parent"> <div id="child" /> </div>); // eslint-disable-line no-multi-spaces
1678
- let textNode1, child, textNode2;
1679
- expect(e.childNodes.length).toBe(3);
1680
- textNode1 = e.childNodes[0];
1681
- child = e.childNodes[1];
1682
- textNode2 = e.childNodes[2];
1683
- expect(e.id).toBe('parent');
1684
- expectTextNode(textNode1, ' ');
1685
- expect(child.id).toBe('child');
1686
- expect(child.childNodes.length).toBe(0);
1687
- expectTextNode(textNode2, ' ');
1688
- },
1689
- );
1690
-
1691
- itRenders('a composite with multiple children', async render => {
1692
- const Component = props => props.children;
1693
- const e = await render(
1694
- <Component>{['a', 'b', [undefined], [[false, 'c']]]}</Component>,
1695
- );
1696
-
1697
- let parent = e.parentNode;
1698
- if (
1699
- render === serverRender ||
1700
- render === clientRenderOnServerString ||
1701
- render === streamRender
1702
- ) {
1703
- // For plain server markup result we have comments between.
1704
- // If we're able to hydrate, they remain.
1705
- expect(parent.childNodes.length).toBe(5);
1706
- expectTextNode(parent.childNodes[0], 'a');
1707
- expectTextNode(parent.childNodes[2], 'b');
1708
- expectTextNode(parent.childNodes[4], 'c');
1709
- } else {
1710
- expect(parent.childNodes.length).toBe(3);
1711
- expectTextNode(parent.childNodes[0], 'a');
1712
- expectTextNode(parent.childNodes[1], 'b');
1713
- expectTextNode(parent.childNodes[2], 'c');
1714
- }
1715
- });
1716
- });
1717
-
1718
- describe('escaping >, <, and &', function() {
1719
- itRenders('>,<, and & as single child', async render => {
1720
- const e = await render(<div>{'<span>Text"</span>'}</div>);
1721
- expect(e.childNodes.length).toBe(1);
1722
- expectNode(e.firstChild, TEXT_NODE_TYPE, '<span>Text"</span>');
1723
- });
1724
-
1725
- itRenders('>,<, and & as multiple children', async render => {
1726
- const e = await render(
1727
- <div>
1728
- {'<span>Text1"</span>'}
1729
- {'<span>Text2"</span>'}
1730
- </div>,
1731
- );
1732
- if (
1733
- render === serverRender ||
1734
- render === clientRenderOnServerString ||
1735
- render === streamRender
1736
- ) {
1737
- expect(e.childNodes.length).toBe(3);
1738
- expectTextNode(e.childNodes[0], '<span>Text1"</span>');
1739
- expectTextNode(e.childNodes[2], '<span>Text2"</span>');
1740
- } else {
1741
- expect(e.childNodes.length).toBe(2);
1742
- expectTextNode(e.childNodes[0], '<span>Text1"</span>');
1743
- expectTextNode(e.childNodes[1], '<span>Text2"</span>');
1744
- }
1745
- });
1746
- });
1747
-
1748
- describe('carriage return and null character', () => {
1749
- // HTML parsing normalizes CR and CRLF to LF.
1750
- // It also ignores null character.
1751
- // https://www.w3.org/TR/html5/single-page.html#preprocessing-the-input-stream
1752
- // If we have a mismatch, it might be caused by that (and should not be reported).
1753
- // We won't be patching up in this case as that matches our past behavior.
1754
-
1755
- itRenders(
1756
- 'an element with one text child with special characters',
1757
- async render => {
1758
- const e = await render(<div>{'foo\rbar\r\nbaz\nqux\u0000'}</div>);
1759
- if (render === serverRender || render === streamRender) {
1760
- expect(e.childNodes.length).toBe(1);
1761
- // Everything becomes LF when parsed from server HTML.
1762
- // Null character is ignored.
1763
- expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\nbar\nbaz\nqux');
1764
- } else {
1765
- expect(e.childNodes.length).toBe(1);
1766
- // Client rendering (or hydration) uses JS value with CR.
1767
- // Null character stays.
1768
- expectNode(
1769
- e.childNodes[0],
1770
- TEXT_NODE_TYPE,
1771
- 'foo\rbar\r\nbaz\nqux\u0000',
1772
- );
1773
- }
1774
- },
1775
- );
1776
-
1777
- itRenders(
1778
- 'an element with two text children with special characters',
1779
- async render => {
1780
- const e = await render(
1781
- <div>
1782
- {'foo\rbar'}
1783
- {'\r\nbaz\nqux\u0000'}
1784
- </div>,
1785
- );
1786
- if (render === serverRender || render === streamRender) {
1787
- // We have three nodes because there is a comment between them.
1788
- expect(e.childNodes.length).toBe(3);
1789
- // Everything becomes LF when parsed from server HTML.
1790
- // Null character is ignored.
1791
- expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\nbar');
1792
- expectNode(e.childNodes[2], TEXT_NODE_TYPE, '\nbaz\nqux');
1793
- } else if (render === clientRenderOnServerString) {
1794
- // We have three nodes because there is a comment between them.
1795
- expect(e.childNodes.length).toBe(3);
1796
- // Hydration uses JS value with CR and null character.
1797
- expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\rbar');
1798
- expectNode(e.childNodes[2], TEXT_NODE_TYPE, '\r\nbaz\nqux\u0000');
1799
- } else {
1800
- expect(e.childNodes.length).toBe(2);
1801
- // Client rendering uses JS value with CR and null character.
1802
- expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\rbar');
1803
- expectNode(e.childNodes[1], TEXT_NODE_TYPE, '\r\nbaz\nqux\u0000');
1804
- }
1805
- },
1806
- );
1807
-
1808
- itRenders(
1809
- 'an element with an attribute value with special characters',
1810
- async render => {
1811
- const e = await render(<a title={'foo\rbar\r\nbaz\nqux\u0000'} />);
1812
- if (
1813
- render === serverRender ||
1814
- render === streamRender ||
1815
- render === clientRenderOnServerString
1816
- ) {
1817
- // Everything becomes LF when parsed from server HTML.
1818
- // Null character in an attribute becomes the replacement character.
1819
- // Hydration also ends up with LF because we don't patch up attributes.
1820
- expect(e.title).toBe('foo\nbar\nbaz\nqux\uFFFD');
1821
- } else {
1822
- // Client rendering uses JS value with CR and null character.
1823
- expect(e.title).toBe('foo\rbar\r\nbaz\nqux\u0000');
1824
- }
1825
- },
1826
- );
1827
- });
1828
-
1829
- describe('components that throw errors', function() {
1830
- itThrowsWhenRendering(
1831
- 'a function returning undefined',
1832
- async render => {
1833
- const UndefinedComponent = () => undefined;
1834
- await render(<UndefinedComponent />, 1);
1835
- },
1836
- 'UndefinedComponent(...): Nothing was returned from render. ' +
1837
- 'This usually means a return statement is missing. Or, to ' +
1838
- 'render nothing, return null.',
1839
- );
1840
-
1841
- itThrowsWhenRendering(
1842
- 'a class returning undefined',
1843
- async render => {
1844
- class UndefinedComponent extends React.Component {
1845
- render() {
1846
- return undefined;
1847
- }
1848
- }
1849
- await render(<UndefinedComponent />, 1);
1850
- },
1851
- 'UndefinedComponent(...): Nothing was returned from render. ' +
1852
- 'This usually means a return statement is missing. Or, to ' +
1853
- 'render nothing, return null.',
1854
- );
1855
-
1856
- itThrowsWhenRendering(
1857
- 'a function returning an object',
1858
- async render => {
1859
- const ObjectComponent = () => ({x: 123});
1860
- await render(<ObjectComponent />, 1);
1861
- },
1862
- 'Objects are not valid as a React child (found: object with keys {x}).' +
1863
- (__DEV__
1864
- ? ' If you meant to render a collection of children, use ' +
1865
- 'an array instead.'
1866
- : ''),
1867
- );
1868
-
1869
- itThrowsWhenRendering(
1870
- 'a class returning an object',
1871
- async render => {
1872
- class ObjectComponent extends React.Component {
1873
- render() {
1874
- return {x: 123};
1875
- }
1876
- }
1877
- await render(<ObjectComponent />, 1);
1878
- },
1879
- 'Objects are not valid as a React child (found: object with keys {x}).' +
1880
- (__DEV__
1881
- ? ' If you meant to render a collection of children, use ' +
1882
- 'an array instead.'
1883
- : ''),
1884
- );
1885
-
1886
- itThrowsWhenRendering(
1887
- 'top-level object',
1888
- async render => {
1889
- await render({x: 123});
1890
- },
1891
- 'Objects are not valid as a React child (found: object with keys {x}).' +
1892
- (__DEV__
1893
- ? ' If you meant to render a collection of children, use ' +
1894
- 'an array instead.'
1895
- : ''),
1896
- );
1897
- });
1898
- });
1899
-
1900
- describe('form controls', function() {
1901
- describe('inputs', function() {
1902
- itRenders('an input with a value and an onChange', async render => {
1903
- const e = await render(<input value="foo" onChange={() => {}} />);
1904
- expect(e.value).toBe('foo');
1905
- });
1906
-
1907
- itRenders('an input with a value and readOnly', async render => {
1908
- const e = await render(<input value="foo" readOnly={true} />);
1909
- expect(e.value).toBe('foo');
1910
- });
1911
-
1912
- itRenders(
1913
- 'an input with a value and no onChange/readOnly',
1914
- async render => {
1915
- // this configuration should raise a dev warning that value without
1916
- // onChange or readOnly is a mistake.
1917
- const e = await render(<input value="foo" />, 1);
1918
- expect(e.value).toBe('foo');
1919
- expect(e.getAttribute('value')).toBe('foo');
1920
- },
1921
- );
1922
-
1923
- itRenders('an input with a defaultValue', async render => {
1924
- const e = await render(<input defaultValue="foo" />);
1925
- expect(e.value).toBe('foo');
1926
- expect(e.getAttribute('value')).toBe('foo');
1927
- expect(e.getAttribute('defaultValue')).toBe(null);
1928
- });
1929
-
1930
- itRenders('an input value overriding defaultValue', async render => {
1931
- const e = await render(
1932
- <input value="foo" defaultValue="bar" readOnly={true} />,
1933
- 1,
1934
- );
1935
- expect(e.value).toBe('foo');
1936
- expect(e.getAttribute('value')).toBe('foo');
1937
- expect(e.getAttribute('defaultValue')).toBe(null);
1938
- });
1939
-
1940
- itRenders(
1941
- 'an input value overriding defaultValue no matter the prop order',
1942
- async render => {
1943
- const e = await render(
1944
- <input defaultValue="bar" value="foo" readOnly={true} />,
1945
- 1,
1946
- );
1947
- expect(e.value).toBe('foo');
1948
- expect(e.getAttribute('value')).toBe('foo');
1949
- expect(e.getAttribute('defaultValue')).toBe(null);
1950
- },
1951
- );
1952
- });
1953
-
1954
- describe('checkboxes', function() {
1955
- itRenders('a checkbox that is checked with an onChange', async render => {
1956
- const e = await render(
1957
- <input type="checkbox" checked={true} onChange={() => {}} />,
1958
- );
1959
- expect(e.checked).toBe(true);
1960
- });
1961
-
1962
- itRenders('a checkbox that is checked with readOnly', async render => {
1963
- const e = await render(
1964
- <input type="checkbox" checked={true} readOnly={true} />,
1965
- );
1966
- expect(e.checked).toBe(true);
1967
- });
1968
-
1969
- itRenders(
1970
- 'a checkbox that is checked and no onChange/readOnly',
1971
- async render => {
1972
- // this configuration should raise a dev warning that checked without
1973
- // onChange or readOnly is a mistake.
1974
- const e = await render(<input type="checkbox" checked={true} />, 1);
1975
- expect(e.checked).toBe(true);
1976
- },
1977
- );
1978
-
1979
- itRenders('a checkbox with defaultChecked', async render => {
1980
- const e = await render(<input type="checkbox" defaultChecked={true} />);
1981
- expect(e.checked).toBe(true);
1982
- expect(e.getAttribute('defaultChecked')).toBe(null);
1983
- });
1984
-
1985
- itRenders(
1986
- 'a checkbox checked overriding defaultChecked',
1987
- async render => {
1988
- const e = await render(
1989
- <input
1990
- type="checkbox"
1991
- checked={true}
1992
- defaultChecked={false}
1993
- readOnly={true}
1994
- />,
1995
- 1,
1996
- );
1997
- expect(e.checked).toBe(true);
1998
- expect(e.getAttribute('defaultChecked')).toBe(null);
1999
- },
2000
- );
2001
-
2002
- itRenders(
2003
- 'a checkbox checked overriding defaultChecked no matter the prop order',
2004
- async render => {
2005
- const e = await render(
2006
- <input
2007
- type="checkbox"
2008
- defaultChecked={false}
2009
- checked={true}
2010
- readOnly={true}
2011
- />,
2012
- 1,
2013
- );
2014
- expect(e.checked).toBe(true);
2015
- expect(e.getAttribute('defaultChecked')).toBe(null);
2016
- },
2017
- );
2018
- });
2019
-
2020
- describe('textareas', function() {
2021
- // textareas
2022
- // ---------
2023
- itRenders('a textarea with a value and an onChange', async render => {
2024
- const e = await render(<textarea value="foo" onChange={() => {}} />);
2025
- // textarea DOM elements don't have a value **attribute**, the text is
2026
- // a child of the element and accessible via the .value **property**.
2027
- expect(e.getAttribute('value')).toBe(null);
2028
- expect(e.value).toBe('foo');
2029
- });
2030
-
2031
- itRenders('a textarea with a value and readOnly', async render => {
2032
- const e = await render(<textarea value="foo" readOnly={true} />);
2033
- // textarea DOM elements don't have a value **attribute**, the text is
2034
- // a child of the element and accessible via the .value **property**.
2035
- expect(e.getAttribute('value')).toBe(null);
2036
- expect(e.value).toBe('foo');
2037
- });
2038
-
2039
- itRenders(
2040
- 'a textarea with a value and no onChange/readOnly',
2041
- async render => {
2042
- // this configuration should raise a dev warning that value without
2043
- // onChange or readOnly is a mistake.
2044
- const e = await render(<textarea value="foo" />, 1);
2045
- expect(e.getAttribute('value')).toBe(null);
2046
- expect(e.value).toBe('foo');
2047
- },
2048
- );
2049
-
2050
- itRenders('a textarea with a defaultValue', async render => {
2051
- const e = await render(<textarea defaultValue="foo" />);
2052
- expect(e.getAttribute('value')).toBe(null);
2053
- expect(e.getAttribute('defaultValue')).toBe(null);
2054
- expect(e.value).toBe('foo');
2055
- });
2056
-
2057
- itRenders('a textarea value overriding defaultValue', async render => {
2058
- const e = await render(
2059
- <textarea value="foo" defaultValue="bar" readOnly={true} />,
2060
- 1,
2061
- );
2062
- expect(e.getAttribute('value')).toBe(null);
2063
- expect(e.getAttribute('defaultValue')).toBe(null);
2064
- expect(e.value).toBe('foo');
2065
- });
2066
-
2067
- itRenders(
2068
- 'a textarea value overriding defaultValue no matter the prop order',
2069
- async render => {
2070
- const e = await render(
2071
- <textarea defaultValue="bar" value="foo" readOnly={true} />,
2072
- 1,
2073
- );
2074
- expect(e.getAttribute('value')).toBe(null);
2075
- expect(e.getAttribute('defaultValue')).toBe(null);
2076
- expect(e.value).toBe('foo');
2077
- },
2078
- );
2079
- });
2080
-
2081
- describe('selects', function() {
2082
- var options;
2083
- beforeEach(function() {
2084
- options = [
2085
- <option key={1} value="foo" id="foo">
2086
- Foo
2087
- </option>,
2088
- <option key={2} value="bar" id="bar">
2089
- Bar
2090
- </option>,
2091
- <option key={3} value="baz" id="baz">
2092
- Baz
2093
- </option>,
2094
- ];
2095
- });
2096
-
2097
- // a helper function to test the selected value of a <select> element.
2098
- // takes in a <select> DOM element (element) and a value or array of
2099
- // values that should be selected (selected).
2100
- const expectSelectValue = (element, selected) => {
2101
- if (!Array.isArray(selected)) {
2102
- selected = [selected];
2103
- }
2104
- // the select DOM element shouldn't ever have a value or defaultValue
2105
- // attribute; that is not how select values are expressed in the DOM.
2106
- expect(element.getAttribute('value')).toBe(null);
2107
- expect(element.getAttribute('defaultValue')).toBe(null);
2108
-
2109
- ['foo', 'bar', 'baz'].forEach(value => {
2110
- const expectedValue = selected.indexOf(value) !== -1;
2111
- var option = element.querySelector(`#${value}`);
2112
- expect(option.selected).toBe(expectedValue);
2113
- });
2114
- };
2115
-
2116
- itRenders('a select with a value and an onChange', async render => {
2117
- const e = await render(
2118
- <select value="bar" onChange={() => {}}>
2119
- {options}
2120
- </select>,
2121
- );
2122
- expectSelectValue(e, 'bar');
2123
- });
2124
-
2125
- itRenders('a select with a value and readOnly', async render => {
2126
- const e = await render(
2127
- <select value="bar" readOnly={true}>
2128
- {options}
2129
- </select>,
2130
- );
2131
- expectSelectValue(e, 'bar');
2132
- });
2133
-
2134
- itRenders(
2135
- 'a select with a multiple values and an onChange',
2136
- async render => {
2137
- const e = await render(
2138
- <select value={['bar', 'baz']} multiple={true} onChange={() => {}}>
2139
- {options}
2140
- </select>,
2141
- );
2142
- expectSelectValue(e, ['bar', 'baz']);
2143
- },
2144
- );
2145
-
2146
- itRenders(
2147
- 'a select with a multiple values and readOnly',
2148
- async render => {
2149
- const e = await render(
2150
- <select value={['bar', 'baz']} multiple={true} readOnly={true}>
2151
- {options}
2152
- </select>,
2153
- );
2154
- expectSelectValue(e, ['bar', 'baz']);
2155
- },
2156
- );
2157
-
2158
- itRenders(
2159
- 'a select with a value and no onChange/readOnly',
2160
- async render => {
2161
- // this configuration should raise a dev warning that value without
2162
- // onChange or readOnly is a mistake.
2163
- const e = await render(<select value="bar">{options}</select>, 1);
2164
- expectSelectValue(e, 'bar');
2165
- },
2166
- );
2167
-
2168
- itRenders('a select with a defaultValue', async render => {
2169
- const e = await render(<select defaultValue="bar">{options}</select>);
2170
- expectSelectValue(e, 'bar');
2171
- });
2172
-
2173
- itRenders('a select value overriding defaultValue', async render => {
2174
- const e = await render(
2175
- <select value="bar" defaultValue="baz" readOnly={true}>
2176
- {options}
2177
- </select>,
2178
- 1,
2179
- );
2180
- expectSelectValue(e, 'bar');
2181
- });
2182
-
2183
- itRenders(
2184
- 'a select value overriding defaultValue no matter the prop order',
2185
- async render => {
2186
- const e = await render(
2187
- <select defaultValue="baz" value="bar" readOnly={true}>
2188
- {options}
2189
- </select>,
2190
- 1,
2191
- );
2192
- expectSelectValue(e, 'bar');
2193
- },
2194
- );
2195
- });
2196
-
2197
- describe('user interaction', function() {
2198
- let ControlledInput,
2199
- ControlledTextArea,
2200
- ControlledCheckbox,
2201
- ControlledSelect;
2202
- beforeEach(() => {
2203
- ControlledInput = class extends React.Component {
2204
- constructor() {
2205
- super();
2206
- this.state = {value: 'Hello'};
2207
- }
2208
- handleChange(event) {
2209
- if (this.props.onChange) {
2210
- this.props.onChange(event);
2211
- }
2212
- this.setState({value: event.target.value});
2213
- }
2214
- render() {
2215
- return (
2216
- <input
2217
- value={this.state.value}
2218
- onChange={this.handleChange.bind(this)}
2219
- />
2220
- );
2221
- }
2222
- };
2223
- ControlledTextArea = class extends React.Component {
2224
- constructor() {
2225
- super();
2226
- this.state = {value: 'Hello'};
2227
- }
2228
- handleChange(event) {
2229
- if (this.props.onChange) {
2230
- this.props.onChange(event);
2231
- }
2232
- this.setState({value: event.target.value});
2233
- }
2234
- render() {
2235
- return (
2236
- <textarea
2237
- value={this.state.value}
2238
- onChange={this.handleChange.bind(this)}
2239
- />
2240
- );
2241
- }
2242
- };
2243
- ControlledCheckbox = class extends React.Component {
2244
- constructor() {
2245
- super();
2246
- this.state = {value: true};
2247
- }
2248
- handleChange(event) {
2249
- if (this.props.onChange) {
2250
- this.props.onChange(event);
2251
- }
2252
- this.setState({value: event.target.checked});
2253
- }
2254
- render() {
2255
- return (
2256
- <input
2257
- type="checkbox"
2258
- checked={this.state.value}
2259
- onChange={this.handleChange.bind(this)}
2260
- />
2261
- );
2262
- }
2263
- };
2264
- ControlledSelect = class extends React.Component {
2265
- constructor() {
2266
- super();
2267
- this.state = {value: 'Hello'};
2268
- }
2269
- handleChange(event) {
2270
- if (this.props.onChange) {
2271
- this.props.onChange(event);
2272
- }
2273
- this.setState({value: event.target.value});
2274
- }
2275
- render() {
2276
- return (
2277
- <select
2278
- value={this.state.value}
2279
- onChange={this.handleChange.bind(this)}>
2280
- <option key="1" value="Hello">
2281
- Hello
2282
- </option>
2283
- <option key="2" value="Goodbye">
2284
- Goodbye
2285
- </option>
2286
- </select>
2287
- );
2288
- }
2289
- };
2290
- });
2291
-
2292
- describe('user interaction with controlled inputs', function() {
2293
- itClientRenders('a controlled text input', async render => {
2294
- let changeCount = 0;
2295
- const e = await render(
2296
- <ControlledInput onChange={() => changeCount++} />,
2297
- );
2298
- expect(changeCount).toBe(0);
2299
- expect(e.value).toBe('Hello');
2300
-
2301
- // simulate a user typing.
2302
- e.value = 'Goodbye';
2303
- ReactTestUtils.Simulate.change(e);
2304
-
2305
- expect(changeCount).toBe(1);
2306
- expect(e.value).toBe('Goodbye');
2307
- });
2308
-
2309
- itClientRenders('a controlled textarea', async render => {
2310
- let changeCount = 0;
2311
- const e = await render(
2312
- <ControlledTextArea onChange={() => changeCount++} />,
2313
- );
2314
- expect(changeCount).toBe(0);
2315
- expect(e.value).toBe('Hello');
2316
-
2317
- // simulate a user typing.
2318
- e.value = 'Goodbye';
2319
- ReactTestUtils.Simulate.change(e);
2320
-
2321
- expect(changeCount).toBe(1);
2322
- expect(e.value).toBe('Goodbye');
2323
- });
2324
-
2325
- itClientRenders('a controlled checkbox', async render => {
2326
- let changeCount = 0;
2327
- const e = await render(
2328
- <ControlledCheckbox onChange={() => changeCount++} />,
2329
- );
2330
- expect(changeCount).toBe(0);
2331
- expect(e.checked).toBe(true);
2332
-
2333
- // simulate a user typing.
2334
- e.checked = false;
2335
- ReactTestUtils.Simulate.change(e);
2336
-
2337
- expect(changeCount).toBe(1);
2338
- expect(e.checked).toBe(false);
2339
- });
2340
-
2341
- itClientRenders('a controlled select', async render => {
2342
- let changeCount = 0;
2343
- const e = await render(
2344
- <ControlledSelect onChange={() => changeCount++} />,
2345
- );
2346
- expect(changeCount).toBe(0);
2347
- expect(e.value).toBe('Hello');
2348
-
2349
- // simulate a user typing.
2350
- e.value = 'Goodbye';
2351
- ReactTestUtils.Simulate.change(e);
2352
-
2353
- expect(changeCount).toBe(1);
2354
- expect(e.value).toBe('Goodbye');
2355
- });
2356
- });
2357
-
2358
- describe('user interaction with inputs before client render', function() {
2359
- // renders the element and changes the value **before** the client
2360
- // code has a chance to render; this simulates what happens when a
2361
- // user starts to interact with a server-rendered form before
2362
- // ReactDOM.render is called. the client render should NOT blow away
2363
- // the changes the user has made.
2364
- const testUserInteractionBeforeClientRender = async (
2365
- element,
2366
- initialValue = 'Hello',
2367
- changedValue = 'Goodbye',
2368
- valueKey = 'value',
2369
- ) => {
2370
- const field = await serverRender(element);
2371
- expect(field[valueKey]).toBe(initialValue);
2372
-
2373
- // simulate a user typing in the field **before** client-side reconnect happens.
2374
- field[valueKey] = changedValue;
2375
-
2376
- resetModules();
2377
- // client render on top of the server markup.
2378
- const clientField = await renderIntoDom(
2379
- element,
2380
- field.parentNode,
2381
- true,
2382
- );
2383
- // verify that the input field was not replaced.
2384
- // Note that we cannot use expect(clientField).toBe(field) because
2385
- // of jest bug #1772
2386
- expect(clientField === field).toBe(true);
2387
- // confirm that the client render has not changed what the user typed.
2388
- expect(clientField[valueKey]).toBe(changedValue);
2389
- };
2390
-
2391
- it('should not blow away user-entered text on successful reconnect to an uncontrolled input', () =>
2392
- testUserInteractionBeforeClientRender(
2393
- <input defaultValue="Hello" />,
2394
- ));
2395
-
2396
- it('should not blow away user-entered text on successful reconnect to a controlled input', async () => {
2397
- let changeCount = 0;
2398
- await testUserInteractionBeforeClientRender(
2399
- <ControlledInput onChange={() => changeCount++} />,
2400
- );
2401
- // note that there's a strong argument to be made that the DOM revival
2402
- // algorithm should notice that the user has changed the value and fire
2403
- // an onChange. however, it does not now, so that's what this tests.
2404
- expect(changeCount).toBe(0);
2405
- });
2406
-
2407
- it('should not blow away user-entered text on successful reconnect to an uncontrolled checkbox', () =>
2408
- testUserInteractionBeforeClientRender(
2409
- <input type="checkbox" defaultChecked={true} />,
2410
- true,
2411
- false,
2412
- 'checked',
2413
- ));
2414
-
2415
- it('should not blow away user-entered text on successful reconnect to a controlled checkbox', async () => {
2416
- let changeCount = 0;
2417
- await testUserInteractionBeforeClientRender(
2418
- <ControlledCheckbox onChange={() => changeCount++} />,
2419
- true,
2420
- false,
2421
- 'checked',
2422
- );
2423
- expect(changeCount).toBe(0);
2424
- });
2425
-
2426
- // skipping this test because React 15 does the wrong thing. it blows
2427
- // away the user's typing in the textarea.
2428
- xit('should not blow away user-entered text on successful reconnect to an uncontrolled textarea', () =>
2429
- testUserInteractionBeforeClientRender(
2430
- <textarea defaultValue="Hello" />,
2431
- ));
2432
-
2433
- // skipping this test because React 15 does the wrong thing. it blows
2434
- // away the user's typing in the textarea.
2435
- xit('should not blow away user-entered text on successful reconnect to a controlled textarea', async () => {
2436
- let changeCount = 0;
2437
- await testUserInteractionBeforeClientRender(
2438
- <ControlledTextArea onChange={() => changeCount++} />,
2439
- );
2440
- expect(changeCount).toBe(0);
2441
- });
2442
-
2443
- it('should not blow away user-selected value on successful reconnect to an uncontrolled select', () =>
2444
- testUserInteractionBeforeClientRender(
2445
- <select defaultValue="Hello">
2446
- <option key="1" value="Hello">
2447
- Hello
2448
- </option>
2449
- <option key="2" value="Goodbye">
2450
- Goodbye
2451
- </option>
2452
- </select>,
2453
- ));
2454
-
2455
- it('should not blow away user-selected value on successful reconnect to an controlled select', async () => {
2456
- let changeCount = 0;
2457
- await testUserInteractionBeforeClientRender(
2458
- <ControlledSelect onChange={() => changeCount++} />,
2459
- );
2460
- expect(changeCount).toBe(0);
2461
- });
2462
- });
2463
- });
2464
- });
2465
-
2466
- describe('context', function() {
2467
- let PurpleContext, RedContext;
2468
- beforeEach(() => {
2469
- class Parent extends React.Component {
2470
- getChildContext() {
2471
- return {text: this.props.text};
2472
- }
2473
- render() {
2474
- return this.props.children;
2475
- }
2476
- }
2477
- Parent.childContextTypes = {text: PropTypes.string};
2478
-
2479
- PurpleContext = props => <Parent text="purple">{props.children}</Parent>;
2480
- RedContext = props => <Parent text="red">{props.children}</Parent>;
2481
- });
2482
-
2483
- itRenders('class child with context', async render => {
2484
- class ClassChildWithContext extends React.Component {
2485
- render() {
2486
- return <div>{this.context.text}</div>;
2487
- }
2488
- }
2489
- ClassChildWithContext.contextTypes = {text: PropTypes.string};
2490
-
2491
- const e = await render(
2492
- <PurpleContext>
2493
- <ClassChildWithContext />
2494
- </PurpleContext>,
2495
- );
2496
- expect(e.textContent).toBe('purple');
2497
- });
2498
-
2499
- itRenders('stateless child with context', async render => {
2500
- function StatelessChildWithContext(props, context) {
2501
- return <div>{context.text}</div>;
2502
- }
2503
- StatelessChildWithContext.contextTypes = {text: PropTypes.string};
2504
-
2505
- const e = await render(
2506
- <PurpleContext>
2507
- <StatelessChildWithContext />
2508
- </PurpleContext>,
2509
- );
2510
- expect(e.textContent).toBe('purple');
2511
- });
2512
-
2513
- itRenders('class child without context', async render => {
2514
- class ClassChildWithoutContext extends React.Component {
2515
- render() {
2516
- // this should render blank; context isn't passed to this component.
2517
- return <div>{this.context.text}</div>;
2518
- }
2519
- }
2520
-
2521
- const e = await render(
2522
- <PurpleContext>
2523
- <ClassChildWithoutContext />
2524
- </PurpleContext>,
2525
- );
2526
- expect(e.textContent).toBe('');
2527
- });
2528
-
2529
- itRenders('stateless child without context', async render => {
2530
- function StatelessChildWithoutContext(props, context) {
2531
- // this should render blank; context isn't passed to this component.
2532
- return <div>{context.text}</div>;
2533
- }
2534
-
2535
- const e = await render(
2536
- <PurpleContext>
2537
- <StatelessChildWithoutContext />
2538
- </PurpleContext>,
2539
- );
2540
- expect(e.textContent).toBe('');
2541
- });
2542
-
2543
- itRenders('class child with wrong context', async render => {
2544
- class ClassChildWithWrongContext extends React.Component {
2545
- render() {
2546
- // this should render blank; context.text isn't passed to this component.
2547
- return <div id="classWrongChild">{this.context.text}</div>;
2548
- }
2549
- }
2550
- ClassChildWithWrongContext.contextTypes = {foo: PropTypes.string};
2551
-
2552
- const e = await render(
2553
- <PurpleContext>
2554
- <ClassChildWithWrongContext />
2555
- </PurpleContext>,
2556
- );
2557
- expect(e.textContent).toBe('');
2558
- });
2559
-
2560
- itRenders('stateless child with wrong context', async render => {
2561
- function StatelessChildWithWrongContext(props, context) {
2562
- // this should render blank; context.text isn't passed to this component.
2563
- return <div id="statelessWrongChild">{context.text}</div>;
2564
- }
2565
- StatelessChildWithWrongContext.contextTypes = {
2566
- foo: PropTypes.string,
2567
- };
2568
-
2569
- const e = await render(
2570
- <PurpleContext>
2571
- <StatelessChildWithWrongContext />
2572
- </PurpleContext>,
2573
- );
2574
- expect(e.textContent).toBe('');
2575
- });
2576
-
2577
- itRenders('with context passed through to a grandchild', async render => {
2578
- function Grandchild(props, context) {
2579
- return <div>{context.text}</div>;
2580
- }
2581
- Grandchild.contextTypes = {text: PropTypes.string};
2582
-
2583
- const Child = props => <Grandchild />;
2584
-
2585
- const e = await render(
2586
- <PurpleContext>
2587
- <Child />
2588
- </PurpleContext>,
2589
- );
2590
- expect(e.textContent).toBe('purple');
2591
- });
2592
-
2593
- itRenders('a child context overriding a parent context', async render => {
2594
- const Grandchild = (props, context) => {
2595
- return <div>{context.text}</div>;
2596
- };
2597
- Grandchild.contextTypes = {text: PropTypes.string};
2598
-
2599
- const e = await render(
2600
- <PurpleContext>
2601
- <RedContext>
2602
- <Grandchild />
2603
- </RedContext>
2604
- </PurpleContext>,
2605
- );
2606
- expect(e.textContent).toBe('red');
2607
- });
2608
-
2609
- itRenders('a child context merged with a parent context', async render => {
2610
- class Parent extends React.Component {
2611
- getChildContext() {
2612
- return {text1: 'purple'};
2613
- }
2614
- render() {
2615
- return <Child />;
2616
- }
2617
- }
2618
- Parent.childContextTypes = {text1: PropTypes.string};
2619
-
2620
- class Child extends React.Component {
2621
- getChildContext() {
2622
- return {text2: 'red'};
2623
- }
2624
- render() {
2625
- return <Grandchild />;
2626
- }
2627
- }
2628
- Child.childContextTypes = {text2: PropTypes.string};
2629
-
2630
- const Grandchild = (props, context) => {
2631
- return (
2632
- <div>
2633
- <div id="first">{context.text1}</div>
2634
- <div id="second">{context.text2}</div>
2635
- </div>
2636
- );
2637
- };
2638
- Grandchild.contextTypes = {
2639
- text1: PropTypes.string,
2640
- text2: PropTypes.string,
2641
- };
2642
-
2643
- const e = await render(<Parent />);
2644
- expect(e.querySelector('#first').textContent).toBe('purple');
2645
- expect(e.querySelector('#second').textContent).toBe('red');
2646
- });
2647
-
2648
- itRenders(
2649
- 'with a call to componentWillMount before getChildContext',
2650
- async render => {
2651
- class WillMountContext extends React.Component {
2652
- getChildContext() {
2653
- return {text: this.state.text};
2654
- }
2655
- componentWillMount() {
2656
- this.setState({text: 'foo'});
2657
- }
2658
- render() {
2659
- return <Child />;
2660
- }
2661
- }
2662
- WillMountContext.childContextTypes = {text: PropTypes.string};
2663
-
2664
- const Child = (props, context) => {
2665
- return <div>{context.text}</div>;
2666
- };
2667
- Child.contextTypes = {text: PropTypes.string};
2668
-
2669
- const e = await render(<WillMountContext />);
2670
- expect(e.textContent).toBe('foo');
2671
- },
2672
- );
2673
-
2674
- itRenders(
2675
- 'if getChildContext exists but childContextTypes is missing with a warning',
2676
- async render => {
2677
- function HopefulChild(props, context) {
2678
- return context.foo || 'nope';
2679
- }
2680
- HopefulChild.contextTypes = {
2681
- foo: PropTypes.string,
2682
- };
2683
- class ForgetfulParent extends React.Component {
2684
- render() {
2685
- return <HopefulChild />;
2686
- }
2687
- getChildContext() {
2688
- return {foo: 'bar'};
2689
- }
2690
- }
2691
- const e = await render(<ForgetfulParent />, 1);
2692
- expect(e.textContent).toBe('nope');
2693
- },
2694
- );
2695
-
2696
- itThrowsWhenRendering(
2697
- 'if getChildContext returns a value not in childContextTypes',
2698
- render => {
2699
- class MyComponent extends React.Component {
2700
- render() {
2701
- return <div />;
2702
- }
2703
- getChildContext() {
2704
- return {value1: 'foo', value2: 'bar'};
2705
- }
2706
- }
2707
- MyComponent.childContextTypes = {value1: PropTypes.string};
2708
- return render(<MyComponent />);
2709
- },
2710
- 'MyComponent.getChildContext(): key "value2" is not defined in childContextTypes.',
2711
- );
2712
- });
2713
-
2714
- describe('refs', function() {
2715
- it('should not run ref code on server', async () => {
2716
- let refCount = 0;
2717
- class RefsComponent extends React.Component {
2718
- render() {
2719
- return <div ref={e => refCount++} />;
2720
- }
2721
- }
2722
- await expectMarkupMatch(<RefsComponent />, <div />);
2723
- expect(refCount).toBe(0);
2724
- });
2725
-
2726
- it('should run ref code on client', async () => {
2727
- let refCount = 0;
2728
- class RefsComponent extends React.Component {
2729
- render() {
2730
- return <div ref={e => refCount++} />;
2731
- }
2732
- }
2733
- await expectMarkupMatch(<div />, <RefsComponent />);
2734
- expect(refCount).toBe(1);
2735
- });
2736
-
2737
- it('should send the correct element to ref functions on client', async () => {
2738
- let refElement = null;
2739
- class RefsComponent extends React.Component {
2740
- render() {
2741
- return <div ref={e => (refElement = e)} />;
2742
- }
2743
- }
2744
- const e = await clientRenderOnServerString(<RefsComponent />);
2745
- expect(refElement).not.toBe(null);
2746
- expect(refElement).toBe(e);
2747
- });
2748
-
2749
- it('should have string refs on client when rendered over server markup', async () => {
2750
- class RefsComponent extends React.Component {
2751
- render() {
2752
- return <div ref="myDiv" />;
2753
- }
2754
- }
2755
-
2756
- const markup = ReactDOMServer.renderToString(<RefsComponent />);
2757
- const root = document.createElement('div');
2758
- root.innerHTML = markup;
2759
- let component = null;
2760
- resetModules();
2761
- await asyncReactDOMRender(
2762
- <RefsComponent ref={e => (component = e)} />,
2763
- root,
2764
- true,
2765
- );
2766
- expect(component.refs.myDiv).toBe(root.firstChild);
2767
- });
2768
- });
2769
-
2770
- describe('reconnecting to server markup', function() {
2771
- var EmptyComponent;
2772
- beforeEach(() => {
2773
- EmptyComponent = class extends React.Component {
2774
- render() {
2775
- return null;
2776
- }
2777
- };
2778
- });
2779
-
2780
- describe('elements', function() {
2781
- describe('reconnecting different component implementations', function() {
2782
- let ES6ClassComponent, PureComponent, bareElement;
2783
- beforeEach(() => {
2784
- // try each type of component on client and server.
2785
- ES6ClassComponent = class extends React.Component {
2786
- render() {
2787
- return <div id={this.props.id} />;
2788
- }
2789
- };
2790
- PureComponent = props => <div id={props.id} />;
2791
- bareElement = <div id="foobarbaz" />;
2792
- });
2793
-
2794
- it('should reconnect ES6 Class to ES6 Class', () =>
2795
- expectMarkupMatch(
2796
- <ES6ClassComponent id="foobarbaz" />,
2797
- <ES6ClassComponent id="foobarbaz" />,
2798
- ));
2799
-
2800
- it('should reconnect Pure Component to ES6 Class', () =>
2801
- expectMarkupMatch(
2802
- <ES6ClassComponent id="foobarbaz" />,
2803
- <PureComponent id="foobarbaz" />,
2804
- ));
2805
-
2806
- it('should reconnect Bare Element to ES6 Class', () =>
2807
- expectMarkupMatch(<ES6ClassComponent id="foobarbaz" />, bareElement));
2808
-
2809
- it('should reconnect ES6 Class to Pure Component', () =>
2810
- expectMarkupMatch(
2811
- <PureComponent id="foobarbaz" />,
2812
- <ES6ClassComponent id="foobarbaz" />,
2813
- ));
2814
-
2815
- it('should reconnect Pure Component to Pure Component', () =>
2816
- expectMarkupMatch(
2817
- <PureComponent id="foobarbaz" />,
2818
- <PureComponent id="foobarbaz" />,
2819
- ));
2820
-
2821
- it('should reconnect Bare Element to Pure Component', () =>
2822
- expectMarkupMatch(<PureComponent id="foobarbaz" />, bareElement));
2823
-
2824
- it('should reconnect ES6 Class to Bare Element', () =>
2825
- expectMarkupMatch(bareElement, <ES6ClassComponent id="foobarbaz" />));
2826
-
2827
- it('should reconnect Pure Component to Bare Element', () =>
2828
- expectMarkupMatch(bareElement, <PureComponent id="foobarbaz" />));
2829
-
2830
- it('should reconnect Bare Element to Bare Element', () =>
2831
- expectMarkupMatch(bareElement, bareElement));
2832
- });
2833
-
2834
- it('should error reconnecting different element types', () =>
2835
- expectMarkupMismatch(<div />, <span />));
2836
-
2837
- it('should error reconnecting fewer root children', () =>
2838
- expectMarkupMismatch(<span key="a" />, [
2839
- <span key="a" />,
2840
- <span key="b" />,
2841
- ]));
2842
-
2843
- it('should error reconnecting missing attributes', () =>
2844
- expectMarkupMismatch(<div id="foo" />, <div />));
2845
-
2846
- it('should error reconnecting added attributes', () =>
2847
- expectMarkupMismatch(<div />, <div id="foo" />));
2848
-
2849
- it('should error reconnecting different attribute values', () =>
2850
- expectMarkupMismatch(<div id="foo" />, <div id="bar" />));
2851
-
2852
- it('can explicitly ignore errors reconnecting different element types of children', () =>
2853
- expectMarkupMatch(
2854
- <div>
2855
- <div />
2856
- </div>,
2857
- <div suppressHydrationWarning={true}>
2858
- <span />
2859
- </div>,
2860
- ));
2861
-
2862
- it('can explicitly ignore errors reconnecting missing attributes', () =>
2863
- expectMarkupMatch(
2864
- <div id="foo" />,
2865
- <div suppressHydrationWarning={true} />,
2866
- ));
2867
-
2868
- it('can explicitly ignore errors reconnecting added attributes', () =>
2869
- expectMarkupMatch(
2870
- <div />,
2871
- <div id="foo" suppressHydrationWarning={true} />,
2872
- ));
2873
-
2874
- it('can explicitly ignore errors reconnecting different attribute values', () =>
2875
- expectMarkupMatch(
2876
- <div id="foo" />,
2877
- <div id="bar" suppressHydrationWarning={true} />,
2878
- ));
2879
-
2880
- it('can not deeply ignore errors reconnecting different attribute values', () =>
2881
- expectMarkupMismatch(
2882
- <div>
2883
- <div id="foo" />
2884
- </div>,
2885
- <div suppressHydrationWarning={true}>
2886
- <div id="bar" />
2887
- </div>,
2888
- ));
2889
- });
2890
-
2891
- describe('inline styles', function() {
2892
- it('should error reconnecting missing style attribute', () =>
2893
- expectMarkupMismatch(<div style={{width: '1px'}} />, <div />));
2894
-
2895
- it('should error reconnecting added style attribute', () =>
2896
- expectMarkupMismatch(<div />, <div style={{width: '1px'}} />));
2897
-
2898
- it('should error reconnecting empty style attribute', () =>
2899
- expectMarkupMismatch(
2900
- <div style={{width: '1px'}} />,
2901
- <div style={{}} />,
2902
- ));
2903
-
2904
- it('should error reconnecting added style values', () =>
2905
- expectMarkupMismatch(
2906
- <div style={{}} />,
2907
- <div style={{width: '1px'}} />,
2908
- ));
2909
-
2910
- it('should error reconnecting different style values', () =>
2911
- expectMarkupMismatch(
2912
- <div style={{width: '1px'}} />,
2913
- <div style={{width: '2px'}} />,
2914
- ));
2915
-
2916
- it('should reconnect number and string versions of a number', () =>
2917
- expectMarkupMatch(
2918
- <div style={{width: '1px', height: 2}} />,
2919
- <div style={{width: 1, height: '2px'}} />,
2920
- ));
2921
-
2922
- it('should error reconnecting reordered style values', () =>
2923
- expectMarkupMismatch(
2924
- <div style={{width: '1px', fontSize: '2px'}} />,
2925
- <div style={{fontSize: '2px', width: '1px'}} />,
2926
- ));
2927
-
2928
- it('can explicitly ignore errors reconnecting added style values', () =>
2929
- expectMarkupMatch(
2930
- <div style={{}} />,
2931
- <div style={{width: '1px'}} suppressHydrationWarning={true} />,
2932
- ));
2933
-
2934
- it('can explicitly ignore reconnecting different style values', () =>
2935
- expectMarkupMatch(
2936
- <div style={{width: '1px'}} />,
2937
- <div style={{width: '2px'}} suppressHydrationWarning={true} />,
2938
- ));
2939
- });
2940
-
2941
- describe('text nodes', function() {
2942
- it('should error reconnecting different text', () =>
2943
- expectMarkupMismatch(<div>Text</div>, <div>Other Text</div>));
2944
-
2945
- it('should reconnect a div with a number and string version of number', () =>
2946
- expectMarkupMatch(<div>{2}</div>, <div>2</div>));
2947
-
2948
- it('should error reconnecting different numbers', () =>
2949
- expectMarkupMismatch(<div>{2}</div>, <div>{3}</div>));
2950
-
2951
- it('should error reconnecting different number from text', () =>
2952
- expectMarkupMismatch(<div>{2}</div>, <div>3</div>));
2953
-
2954
- it('should error reconnecting different text in two code blocks', () =>
2955
- expectMarkupMismatch(
2956
- <div>
2957
- {'Text1'}
2958
- {'Text2'}
2959
- </div>,
2960
- <div>
2961
- {'Text1'}
2962
- {'Text3'}
2963
- </div>,
2964
- ));
2965
-
2966
- it('can explicitly ignore reconnecting different text', () =>
2967
- expectMarkupMatch(
2968
- <div>Text</div>,
2969
- <div suppressHydrationWarning={true}>Other Text</div>,
2970
- ));
2971
-
2972
- it('can explicitly ignore reconnecting different text in two code blocks', () =>
2973
- expectMarkupMatch(
2974
- <div suppressHydrationWarning={true}>
2975
- {'Text1'}
2976
- {'Text2'}
2977
- </div>,
2978
- <div suppressHydrationWarning={true}>
2979
- {'Text1'}
2980
- {'Text3'}
2981
- </div>,
2982
- ));
2983
- });
2984
-
2985
- describe('element trees and children', function() {
2986
- it('should error reconnecting missing children', () =>
2987
- expectMarkupMismatch(
2988
- <div>
2989
- <div />
2990
- </div>,
2991
- <div />,
2992
- ));
2993
-
2994
- it('should error reconnecting added children', () =>
2995
- expectMarkupMismatch(
2996
- <div />,
2997
- <div>
2998
- <div />
2999
- </div>,
3000
- ));
3001
-
3002
- it('should error reconnecting more children', () =>
3003
- expectMarkupMismatch(
3004
- <div>
3005
- <div />
3006
- </div>,
3007
- <div>
3008
- <div />
3009
- <div />
3010
- </div>,
3011
- ));
3012
-
3013
- it('should error reconnecting fewer children', () =>
3014
- expectMarkupMismatch(
3015
- <div>
3016
- <div />
3017
- <div />
3018
- </div>,
3019
- <div>
3020
- <div />
3021
- </div>,
3022
- ));
3023
-
3024
- it('should error reconnecting reordered children', () =>
3025
- expectMarkupMismatch(
3026
- <div>
3027
- <div />
3028
- <span />
3029
- </div>,
3030
- <div>
3031
- <span />
3032
- <div />
3033
- </div>,
3034
- ));
3035
-
3036
- it('should error reconnecting a div with children separated by whitespace on the client', () =>
3037
- expectMarkupMismatch(
3038
- <div id="parent">
3039
- <div id="child1" />
3040
- <div id="child2" />
3041
- </div>,
3042
- // prettier-ignore
3043
- <div id="parent"><div id="child1" /> <div id="child2" /></div>, // eslint-disable-line no-multi-spaces
3044
- ));
3045
-
3046
- it('should error reconnecting a div with children separated by different whitespace on the server', () =>
3047
- expectMarkupMismatch(
3048
- // prettier-ignore
3049
- <div id="parent"><div id="child1" /> <div id="child2" /></div>, // eslint-disable-line no-multi-spaces
3050
- <div id="parent">
3051
- <div id="child1" />
3052
- <div id="child2" />
3053
- </div>,
3054
- ));
3055
-
3056
- it('should error reconnecting a div with children separated by different whitespace', () =>
3057
- expectMarkupMismatch(
3058
- <div id="parent">
3059
- <div id="child1" /> <div id="child2" />
3060
- </div>,
3061
- // prettier-ignore
3062
- <div id="parent"><div id="child1" /> <div id="child2" /></div>, // eslint-disable-line no-multi-spaces
3063
- ));
3064
-
3065
- it('can distinguish an empty component from a dom node', () =>
3066
- expectMarkupMismatch(
3067
- <div>
3068
- <span />
3069
- </div>,
3070
- <div>
3071
- <EmptyComponent />
3072
- </div>,
3073
- ));
3074
-
3075
- it('can distinguish an empty component from an empty text component', () =>
3076
- expectMarkupMatch(
3077
- <div>
3078
- <EmptyComponent />
3079
- </div>,
3080
- <div>{''}</div>,
3081
- ));
3082
-
3083
- it('can explicitly ignore reconnecting more children', () =>
3084
- expectMarkupMatch(
3085
- <div>
3086
- <div />
3087
- </div>,
3088
- <div suppressHydrationWarning={true}>
3089
- <div />
3090
- <div />
3091
- </div>,
3092
- ));
3093
-
3094
- it('can explicitly ignore reconnecting fewer children', () =>
3095
- expectMarkupMatch(
3096
- <div>
3097
- <div />
3098
- <div />
3099
- </div>,
3100
- <div suppressHydrationWarning={true}>
3101
- <div />
3102
- </div>,
3103
- ));
3104
-
3105
- it('can explicitly ignore reconnecting reordered children', () =>
3106
- expectMarkupMatch(
3107
- <div suppressHydrationWarning={true}>
3108
- <div />
3109
- <span />
3110
- </div>,
3111
- <div suppressHydrationWarning={true}>
3112
- <span />
3113
- <div />
3114
- </div>,
3115
- ));
3116
-
3117
- it('can not deeply ignore reconnecting reordered children', () =>
3118
- expectMarkupMismatch(
3119
- <div suppressHydrationWarning={true}>
3120
- <div>
3121
- <div />
3122
- <span />
3123
- </div>
3124
- </div>,
3125
- <div suppressHydrationWarning={true}>
3126
- <div>
3127
- <span />
3128
- <div />
3129
- </div>
3130
- </div>,
3131
- ));
3132
- });
3133
-
3134
- // Markup Mismatches: misc
3135
- it('should error reconnecting a div with different dangerouslySetInnerHTML', () =>
3136
- expectMarkupMismatch(
3137
- <div dangerouslySetInnerHTML={{__html: "<span id='child1'/>"}} />,
3138
- <div dangerouslySetInnerHTML={{__html: "<span id='child2'/>"}} />,
3139
- ));
3140
-
3141
- it('can explicitly ignore reconnecting a div with different dangerouslySetInnerHTML', () =>
3142
- expectMarkupMatch(
3143
- <div dangerouslySetInnerHTML={{__html: "<span id='child1'/>"}} />,
3144
- <div
3145
- dangerouslySetInnerHTML={{__html: "<span id='child2'/>"}}
3146
- suppressHydrationWarning={true}
3147
- />,
3148
- ));
3149
- });
3150
-});