[attribute-behavior] Track differences between DOM and server renderer (#10575)
* Load ReactDOMServer into attribute table * Highlight differences between DOM and server renderer * Use SSR behavior when comparing behavior across renderers * Use less severe color if SSR differences are only due to warnings * Ensure result node's tagName matches what's expected * Throw on unexpected HTMLUnknownElement
Andrew Clark committed
Aug 30, 2017 at 18:17 UTC
88a079d5c23e1405b072954cb9c730ae1d20f758
3 files changed
+174
-76
fixtures/attribute-behavior/package.json
+2
-1
@@ -10,7 +10,8 @@
10
"react-virtualized": "^9.9.0"
11
},
12
"scripts": {
13
- "prestart": "cp ../../build/dist/{react,react-dom}.development.js public/",
13
+ "prestart":
14
+ "cp ../../build/dist/{react,react-dom,react-dom-server.browser}.development.js public/",
15
"start": "react-scripts start",
16
"build": "react-scripts build",
17
"test": "react-scripts test --env=jsdom",
fixtures/attribute-behavior/src/App.js
+165
-75
@@ -13,7 +13,10 @@ const React = global.React;
13
const {Component} = React;
14
15
const ReactDOM15 = global.ReactDOM15;
16
-const ReactDOM16 = global.ReactDOM;
16
+const ReactDOM16 = global.ReactDOM16;
17
+
18
+const ReactDOMServer15 = global.ReactDOMServer15;
19
+const ReactDOMServer16 = global.ReactDOMServer16;
20
21
const types = [
22
{
@@ -480,7 +483,7 @@ const attributes = [
483
read: getSVGAttribute('color-rendering'),
484
},
485
{name: 'cols', tagName: 'textarea'},
483
- {name: 'colSpan', tagName: 'td'},
486
+ {name: 'colSpan', containerTagName: 'tr', tagName: 'td'},
487
{name: 'content', tagName: 'meta'},
488
{name: 'contentEditable'},
489
{
@@ -863,7 +866,7 @@ const attributes = [
866
{name: 'formMethod', tagName: 'input', overrideStringValue: 'POST'},
867
{name: 'formNoValidate', tagName: 'input'},
868
{name: 'formTarget', tagName: 'input'},
866
- {name: 'frameBorder', tagName: 'frame'},
869
+ {name: 'frameBorder', tagName: 'iframe'},
870
{
871
name: 'from',
872
read: getSVGAttribute('from'),
@@ -987,7 +990,7 @@ const attributes = [
990
// Disabled because it crashes other tests with React 15.
991
// TODO: re-enable when we no longer compare to 15.
992
// {name: 'hasOwnProperty', read: getAttribute('hasOwnProperty')},
990
- {name: 'headers', tagName: 'td'},
993
+ {name: 'headers', containerTagName: 'tr', tagName: 'td'},
994
{name: 'height', tagName: 'img'},
995
{
996
name: 'height',
@@ -1213,8 +1216,8 @@ const attributes = [
1216
{name: 'loop', tagName: 'audio'},
1217
{name: 'low', tagName: 'meter'},
1218
{name: 'manifest', read: getAttribute('manifest')},
1216
- {name: 'marginHeight', tagName: 'frame'},
1217
- {name: 'marginWidth', tagName: 'frame'},
1219
+ {name: 'marginHeight', containerTagName: 'frameset', tagName: 'frame'},
1220
+ {name: 'marginWidth', containerTagName: 'frameset', tagName: 'frame'},
1221
{
1222
name: 'marker-end',
1223
containerTagName: 'svg',
@@ -1647,7 +1650,7 @@ const attributes = [
1650
tagName: 'altGlyph',
1651
},
1652
{name: 'rows', tagName: 'textarea'},
1650
- {name: 'rowSpan', tagName: 'td'},
1653
+ {name: 'rowSpan', containerTagName: 'tr', tagName: 'td'},
1654
{
1655
name: 'rx',
1656
read: getSVGProperty('rx'),
@@ -1673,7 +1676,12 @@ const attributes = [
1676
containerTagName: 'svg',
1677
tagName: 'feDisplacementMap',
1678
},
1676
- {name: 'scope', tagName: 'th', overrideStringValue: 'row'},
1679
+ {
1680
+ name: 'scope',
1681
+ containerTagName: 'tr',
1682
+ tagName: 'th',
1683
+ overrideStringValue: 'row',
1684
+ },
1685
{name: 'scoped', tagName: 'style', read: getAttribute('scoped')},
1686
{name: 'scrolling', tagName: 'iframe', overrideStringValue: 'no'},
1687
{name: 'seamless', tagName: 'iframe', read: getAttribute('seamless')},
@@ -1712,7 +1720,7 @@ const attributes = [
1720
tagName: 'textPath',
1721
overrideStringValue: 'auto',
1722
},
1715
- {name: 'span', tagName: 'col'},
1723
+ {name: 'span', containerTagName: 'colgroup', tagName: 'col'},
1724
{
1725
name: 'specularConstant',
1726
read: getSVGProperty('specularConstant'),
@@ -2212,7 +2220,7 @@ const attributes = [
2220
tagName: 'line',
2221
read: getSVGAttribute('vector-effect'),
2222
},
2215
- {name: 'version', tagName: 'html'},
2223
+ {name: 'version', containerTagName: 'document', tagName: 'html'},
2224
{name: 'version', tagName: 'svg', read: getSVGAttribute('version')},
2225
{
2226
name: 'vert-adv-y',
@@ -2522,89 +2530,163 @@ let _didWarn = false;
2530
function warn(str) {
2531
_didWarn = true;
2532
}
2525
-
2526
-function getRenderedAttributeValue(renderer, attribute, type) {
2527
- _didWarn = false;
2533
+const UNKNOWN_HTML_TAGS = new Set(['keygen', 'time', 'command']);
2534
+function getRenderedAttributeValue(renderer, serverRenderer, attribute, type) {
2535
const originalConsoleError = console.error;
2536
console.error = warn;
2537
2538
const containerTagName = attribute.containerTagName || 'div';
2539
const tagName = attribute.tagName || 'div';
2540
2534
- let container;
2535
- if (containerTagName === 'svg') {
2536
- container = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
2537
- } else {
2538
- container = document.createElement(containerTagName);
2541
+ function createContainer() {
2542
+ if (containerTagName === 'svg') {
2543
+ return document.createElementNS('http://www.w3.org/2000/svg', 'svg');
2544
+ } else if (containerTagName === 'document') {
2545
+ return document.implementation.createHTMLDocument('');
2546
+ } else {
2547
+ return document.createElement(containerTagName);
2548
+ }
2549
}
2550
+ let container = createContainer();
2551
2541
- let testValue;
2542
- let defaultValue;
2543
- let canonicalDefaultValue;
2544
- try {
2545
- const read = attribute.read || getProperty(attribute.name);
2546
-
2547
- testValue = type.testValue;
2548
- if (attribute.overrideStringValue !== undefined) {
2549
- switch (type.name) {
2550
- case 'string':
2551
- testValue = attribute.overrideStringValue;
2552
- break;
2553
- case 'array with string':
2554
- testValue = [attribute.overrideStringValue];
2555
- break;
2556
- default:
2557
- break;
2558
- }
2552
+ const read = attribute.read || getProperty(attribute.name);
2553
+ let testValue = type.testValue;
2554
+ if (attribute.overrideStringValue !== undefined) {
2555
+ switch (type.name) {
2556
+ case 'string':
2557
+ testValue = attribute.overrideStringValue;
2558
+ break;
2559
+ case 'array with string':
2560
+ testValue = [attribute.overrideStringValue];
2561
+ break;
2562
+ default:
2563
+ break;
2564
}
2565
+ }
2566
+ let baseProps = {};
2567
+ if (attribute.type) {
2568
+ baseProps.type = attribute.type;
2569
+ }
2570
+ const props = {
2571
+ ...baseProps,
2572
+ [attribute.name]: testValue,
2573
+ };
2574
2561
- let baseProps = {};
2562
- if (attribute.type) {
2563
- baseProps.type = attribute.type;
2564
- }
2575
+ let defaultValue;
2576
+ let result;
2577
+ let canonicalResult;
2578
+ let ssrResult;
2579
+ let canonicalSsrResult;
2580
+ let didWarn;
2581
+ let didError;
2582
+ let ssrDidWarn;
2583
+ let ssrDidError;
2584
+
2585
+ _didWarn = false;
2586
+ try {
2587
renderer.render(React.createElement(tagName, baseProps), container);
2588
defaultValue = read(container.firstChild);
2567
- canonicalDefaultValue = getCanonicalizedValue(defaultValue);
2568
-
2569
- const props = {
2570
- ...baseProps,
2571
- [attribute.name]: testValue,
2572
- };
2589
renderer.render(React.createElement(tagName, props), container);
2590
+ result = read(container.firstChild);
2591
+ canonicalResult = getCanonicalizedValue(result);
2592
+ didWarn = _didWarn;
2593
+ didError = false;
2594
+ } catch (error) {
2595
+ result = null;
2596
+ didWarn = _didWarn;
2597
+ didError = true;
2598
+ }
2599
2575
- const result = read(container.firstChild);
2576
-
2577
- return {
2578
- tagName,
2579
- containerTagName,
2580
- testValue,
2581
- defaultValue,
2582
- result,
2583
- canonicalResult: getCanonicalizedValue(result),
2584
- canonicalDefaultValue,
2585
- didWarn: _didWarn,
2586
- didError: false,
2587
- };
2600
+ _didWarn = false;
2601
+ let hasTagMismatch = false;
2602
+ let hasUnknownElement = false;
2603
+ try {
2604
+ const html = serverRenderer.renderToString(
2605
+ React.createElement(tagName, props)
2606
+ );
2607
+ container = createContainer();
2608
+ container.innerHTML = html;
2609
+
2610
+ if (
2611
+ !container.lastChild ||
2612
+ container.lastChild.tagName.toLowerCase() !== tagName.toLowerCase()
2613
+ ) {
2614
+ hasTagMismatch = true;
2615
+ }
2616
+
2617
+ if (
2618
+ container.lastChild instanceof HTMLUnknownElement &&
2619
+ !UNKNOWN_HTML_TAGS.has(container.lastChild.tagName.toLowerCase())
2620
+ ) {
2621
+ hasUnknownElement = true;
2622
+ }
2623
+
2624
+ ssrResult = read(container.lastChild);
2625
+ canonicalSsrResult = getCanonicalizedValue(ssrResult);
2626
+ ssrDidWarn = _didWarn;
2627
+ ssrDidError = false;
2628
} catch (error) {
2589
- return {
2590
- tagName,
2591
- containerTagName,
2592
- testValue,
2593
- defaultValue,
2594
- result: null,
2595
- canonicalResult: getCanonicalizedValue(null),
2596
- canonicalDefaultValue,
2597
- didWarn: _didWarn,
2598
- didError: true,
2599
- };
2600
- } finally {
2601
- console.error = originalConsoleError;
2629
+ ssrResult = null;
2630
+ ssrDidWarn = _didWarn;
2631
+ ssrDidError = true;
2632
+ }
2633
+
2634
+ console.error = originalConsoleError;
2635
+
2636
+ if (hasTagMismatch) {
2637
+ throw new Error('Tag mismatch. Expected: ' + tagName);
2638
+ }
2639
+ if (hasUnknownElement) {
2640
+ throw new Error('Unexpected unknown element: ' + tagName);
2641
+ }
2642
+
2643
+ let ssrHasSameBehavior;
2644
+ let ssrHasSameBehaviorExceptWarnings;
2645
+ if (didError && ssrDidError) {
2646
+ ssrHasSameBehavior = true;
2647
+ } else if (!didError && !ssrDidError) {
2648
+ if (canonicalResult === canonicalSsrResult) {
2649
+ ssrHasSameBehaviorExceptWarnings = true;
2650
+ ssrHasSameBehavior = didWarn === ssrDidWarn;
2651
+ }
2652
+ ssrHasSameBehavior =
2653
+ didWarn === ssrDidWarn && canonicalResult === canonicalSsrResult;
2654
+ } else {
2655
+ ssrHasSameBehavior = false;
2656
}
2657
+
2658
+ return {
2659
+ tagName,
2660
+ containerTagName,
2661
+ testValue,
2662
+ defaultValue,
2663
+ result,
2664
+ canonicalResult,
2665
+ canonicalDefaultValue: getCanonicalizedValue(defaultValue),
2666
+ didWarn,
2667
+ didError,
2668
+ ssrResult,
2669
+ canonicalSsrResult,
2670
+ ssrDidWarn,
2671
+ ssrDidError,
2672
+ ssrHasSameBehavior,
2673
+ ssrHasSameBehaviorExceptWarnings,
2674
+ };
2675
}
2676
2677
function getRenderedAttributeValues(attribute, type) {
2606
- const react15Value = getRenderedAttributeValue(ReactDOM15, attribute, type);
2607
- const react16Value = getRenderedAttributeValue(ReactDOM16, attribute, type);
2678
+ const react15Value = getRenderedAttributeValue(
2679
+ ReactDOM15,
2680
+ ReactDOMServer15,
2681
+ attribute,
2682
+ type
2683
+ );
2684
+ const react16Value = getRenderedAttributeValue(
2685
+ ReactDOM16,
2686
+ ReactDOMServer16,
2687
+ attribute,
2688
+ type
2689
+ );
2690
2691
let hasSameBehavior;
2692
if (react15Value.didError && react16Value.didError) {
@@ -2612,7 +2694,8 @@ function getRenderedAttributeValues(attribute, type) {
2694
} else if (!react15Value.didError && !react16Value.didError) {
2695
hasSameBehavior =
2696
react15Value.didWarn === react16Value.didWarn &&
2615
- react15Value.canonicalResult === react16Value.canonicalResult;
2697
+ react15Value.canonicalResult === react16Value.canonicalResult &&
2698
+ react15Value.ssrHasSameBehavior === react16Value.ssrHasSameBehavior;
2699
} else {
2700
hasSameBehavior = false;
2701
}
@@ -2678,6 +2761,8 @@ function RendererResult({
2761
canonicalDefaultValue,
2762
didWarn,
2763
didError,
2764
+ ssrHasSameBehavior,
2765
+ ssrHasSameBehaviorExceptWarnings,
2766
}) {
2767
let backgroundColor;
2768
if (didError) {
@@ -2699,6 +2784,11 @@ function RendererResult({
2784
backgroundColor,
2785
};
2786
2787
+ if (!ssrHasSameBehavior) {
2788
+ const color = ssrHasSameBehaviorExceptWarnings ? 'gray' : 'magenta';
2789
+ style.border = `3px dotted ${color}`;
2790
+ }
2791
+
2792
return <div css={style}>{canonicalResult}</div>;
2793
}
2794
fixtures/attribute-behavior/src/index.js
+7
@@ -4,16 +4,23 @@ import './index.css';
4
(async function foo() {
5
await loadScript('https://unpkg.com/react@15.6.1/dist/react.js');
6
await loadScript('https://unpkg.com/react-dom@15.6.1/dist/react-dom.js');
7
+ await loadScript(
8
+ 'https://unpkg.com/react-dom@15.6.1/dist/react-dom-server.js'
9
+ );
10
window.React15 = window.React;
11
window.ReactDOM15 = window.ReactDOM;
12
+ window.ReactDOMServer15 = window.ReactDOMServer;
13
delete window.React;
14
delete window.ReactDOM;
15
+ delete window.ReactDOMServer;
16
17
await loadScript('/react.development.js');
18
await loadScript('/react-dom.development.js');
19
+ await loadScript('/react-dom-server.browser.development.js');
20
21
const React = (window.React16 = window.React);
22
const ReactDOM = (window.ReactDOM16 = window.ReactDOM);
23
+ window.ReactDOMServer16 = window.ReactDOMServer;
24
25
const App = await import('./App');
26