@samitouri / QOS-React / commits / 88a079d5c2

[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