1. import React from 'react';
    
  2. import {createElement} from 'glamor/react'; // eslint-disable-line
    
  3. /* @jsx createElement */
    
  4. 
    
  5. import {MultiGrid, AutoSizer} from 'react-virtualized';
    
  6. import 'react-virtualized/styles.css';
    
  7. import FileSaver from 'file-saver';
    
  8. 
    
  9. import {
    
  10.   inject as injectErrorOverlay,
    
  11.   uninject as uninjectErrorOverlay,
    
  12. } from 'react-error-overlay/lib/overlay';
    
  13. 
    
  14. import attributes from './attributes';
    
  15. 
    
  16. const types = [
    
  17.   {
    
  18.     name: 'string',
    
  19.     testValue: 'a string',
    
  20.     testDisplayValue: "'a string'",
    
  21.   },
    
  22.   {
    
  23.     name: 'empty string',
    
  24.     testValue: '',
    
  25.     testDisplayValue: "''",
    
  26.   },
    
  27.   {
    
  28.     name: 'array with string',
    
  29.     testValue: ['string'],
    
  30.     testDisplayValue: "['string']",
    
  31.   },
    
  32.   {
    
  33.     name: 'empty array',
    
  34.     testValue: [],
    
  35.     testDisplayValue: '[]',
    
  36.   },
    
  37.   {
    
  38.     name: 'object',
    
  39.     testValue: {
    
  40.       toString() {
    
  41.         return 'result of toString()';
    
  42.       },
    
  43.     },
    
  44.     testDisplayValue: "{ toString() { return 'result of toString()'; } }",
    
  45.   },
    
  46.   {
    
  47.     name: 'numeric string',
    
  48.     testValue: '42',
    
  49.     displayValue: "'42'",
    
  50.   },
    
  51.   {
    
  52.     name: '-1',
    
  53.     testValue: -1,
    
  54.   },
    
  55.   {
    
  56.     name: '0',
    
  57.     testValue: 0,
    
  58.   },
    
  59.   {
    
  60.     name: 'integer',
    
  61.     testValue: 1,
    
  62.   },
    
  63.   {
    
  64.     name: 'NaN',
    
  65.     testValue: NaN,
    
  66.   },
    
  67.   {
    
  68.     name: 'float',
    
  69.     testValue: 99.99,
    
  70.   },
    
  71.   {
    
  72.     name: 'true',
    
  73.     testValue: true,
    
  74.   },
    
  75.   {
    
  76.     name: 'false',
    
  77.     testValue: false,
    
  78.   },
    
  79.   {
    
  80.     name: "string 'true'",
    
  81.     testValue: 'true',
    
  82.     displayValue: "'true'",
    
  83.   },
    
  84.   {
    
  85.     name: "string 'false'",
    
  86.     testValue: 'false',
    
  87.     displayValue: "'false'",
    
  88.   },
    
  89.   {
    
  90.     name: "string 'on'",
    
  91.     testValue: 'on',
    
  92.     displayValue: "'on'",
    
  93.   },
    
  94.   {
    
  95.     name: "string 'off'",
    
  96.     testValue: 'off',
    
  97.     displayValue: "'off'",
    
  98.   },
    
  99.   {
    
  100.     name: 'symbol',
    
  101.     testValue: Symbol('foo'),
    
  102.     testDisplayValue: "Symbol('foo')",
    
  103.   },
    
  104.   {
    
  105.     name: 'function',
    
  106.     testValue: function f() {},
    
  107.   },
    
  108.   {
    
  109.     name: 'null',
    
  110.     testValue: null,
    
  111.   },
    
  112.   {
    
  113.     name: 'undefined',
    
  114.     testValue: undefined,
    
  115.   },
    
  116. ];
    
  117. 
    
  118. const ALPHABETICAL = 'alphabetical';
    
  119. const REV_ALPHABETICAL = 'reverse_alphabetical';
    
  120. const GROUPED_BY_ROW_PATTERN = 'grouped_by_row_pattern';
    
  121. 
    
  122. const ALL = 'all';
    
  123. const COMPLETE = 'complete';
    
  124. const INCOMPLETE = 'incomplete';
    
  125. 
    
  126. function getCanonicalizedValue(value) {
    
  127.   switch (typeof value) {
    
  128.     case 'undefined':
    
  129.       return '<undefined>';
    
  130.     case 'object':
    
  131.       if (value === null) {
    
  132.         return '<null>';
    
  133.       }
    
  134.       if ('baseVal' in value) {
    
  135.         return getCanonicalizedValue(value.baseVal);
    
  136.       }
    
  137.       if (value instanceof SVGLength) {
    
  138.         return '<SVGLength: ' + value.valueAsString + '>';
    
  139.       }
    
  140.       if (value instanceof SVGRect) {
    
  141.         return (
    
  142.           '<SVGRect: ' +
    
  143.           [value.x, value.y, value.width, value.height].join(',') +
    
  144.           '>'
    
  145.         );
    
  146.       }
    
  147.       if (value instanceof SVGPreserveAspectRatio) {
    
  148.         return (
    
  149.           '<SVGPreserveAspectRatio: ' +
    
  150.           value.align +
    
  151.           '/' +
    
  152.           value.meetOrSlice +
    
  153.           '>'
    
  154.         );
    
  155.       }
    
  156.       if (value instanceof SVGNumber) {
    
  157.         return value.value;
    
  158.       }
    
  159.       if (value instanceof SVGMatrix) {
    
  160.         return (
    
  161.           '<SVGMatrix ' +
    
  162.           value.a +
    
  163.           ' ' +
    
  164.           value.b +
    
  165.           ' ' +
    
  166.           value.c +
    
  167.           ' ' +
    
  168.           value.d +
    
  169.           ' ' +
    
  170.           value.e +
    
  171.           ' ' +
    
  172.           value.f +
    
  173.           '>'
    
  174.         );
    
  175.       }
    
  176.       if (value instanceof SVGTransform) {
    
  177.         return (
    
  178.           getCanonicalizedValue(value.matrix) +
    
  179.           '/' +
    
  180.           value.type +
    
  181.           '/' +
    
  182.           value.angle
    
  183.         );
    
  184.       }
    
  185.       if (typeof value.length === 'number') {
    
  186.         return (
    
  187.           '[' +
    
  188.           Array.from(value)
    
  189.             .map(v => getCanonicalizedValue(v))
    
  190.             .join(', ') +
    
  191.           ']'
    
  192.         );
    
  193.       }
    
  194.       let name = (value.constructor && value.constructor.name) || 'object';
    
  195.       return '<' + name + '>';
    
  196.     case 'function':
    
  197.       return '<function>';
    
  198.     case 'symbol':
    
  199.       return '<symbol>';
    
  200.     case 'number':
    
  201.       return `<number: ${value}>`;
    
  202.     case 'string':
    
  203.       if (value === '') {
    
  204.         return '<empty string>';
    
  205.       }
    
  206.       return '"' + value + '"';
    
  207.     case 'boolean':
    
  208.       return `<boolean: ${value}>`;
    
  209.     default:
    
  210.       throw new Error('Switch statement should be exhaustive.');
    
  211.   }
    
  212. }
    
  213. 
    
  214. let _didWarn = false;
    
  215. function warn(str) {
    
  216.   if (str.includes('ReactDOM.render is no longer supported')) {
    
  217.     return;
    
  218.   }
    
  219.   _didWarn = true;
    
  220. }
    
  221. const UNKNOWN_HTML_TAGS = new Set(['keygen', 'time', 'command']);
    
  222. function getRenderedAttributeValue(
    
  223.   react,
    
  224.   renderer,
    
  225.   serverRenderer,
    
  226.   attribute,
    
  227.   type
    
  228. ) {
    
  229.   const originalConsoleError = console.error;
    
  230.   console.error = warn;
    
  231. 
    
  232.   const containerTagName = attribute.containerTagName || 'div';
    
  233.   const tagName = attribute.tagName || 'div';
    
  234. 
    
  235.   function createContainer() {
    
  236.     if (containerTagName === 'svg') {
    
  237.       return document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    
  238.     } else if (containerTagName === 'document') {
    
  239.       return document.implementation.createHTMLDocument('');
    
  240.     } else if (containerTagName === 'head') {
    
  241.       return document.implementation.createHTMLDocument('').head;
    
  242.     } else {
    
  243.       return document.createElement(containerTagName);
    
  244.     }
    
  245.   }
    
  246. 
    
  247.   const read = attribute.read;
    
  248.   let testValue = type.testValue;
    
  249.   if (attribute.overrideStringValue !== undefined) {
    
  250.     switch (type.name) {
    
  251.       case 'string':
    
  252.         testValue = attribute.overrideStringValue;
    
  253.         break;
    
  254.       case 'array with string':
    
  255.         testValue = [attribute.overrideStringValue];
    
  256.         break;
    
  257.       default:
    
  258.         break;
    
  259.     }
    
  260.   }
    
  261.   let baseProps = {
    
  262.     ...attribute.extraProps,
    
  263.   };
    
  264.   if (attribute.type) {
    
  265.     baseProps.type = attribute.type;
    
  266.   }
    
  267.   const props = {
    
  268.     ...baseProps,
    
  269.     [attribute.name]: testValue,
    
  270.   };
    
  271. 
    
  272.   let defaultValue;
    
  273.   let canonicalDefaultValue;
    
  274.   let result;
    
  275.   let canonicalResult;
    
  276.   let ssrResult;
    
  277.   let canonicalSsrResult;
    
  278.   let didWarn;
    
  279.   let didError;
    
  280.   let ssrDidWarn;
    
  281.   let ssrDidError;
    
  282. 
    
  283.   _didWarn = false;
    
  284.   try {
    
  285.     let container = createContainer();
    
  286.     renderer.render(react.createElement(tagName, baseProps), container);
    
  287.     defaultValue = read(container.lastChild);
    
  288.     canonicalDefaultValue = getCanonicalizedValue(defaultValue);
    
  289. 
    
  290.     container = createContainer();
    
  291.     renderer.render(react.createElement(tagName, props), container);
    
  292.     result = read(container.lastChild);
    
  293.     canonicalResult = getCanonicalizedValue(result);
    
  294.     didWarn = _didWarn;
    
  295.     didError = false;
    
  296.   } catch (error) {
    
  297.     result = null;
    
  298.     didWarn = _didWarn;
    
  299.     didError = true;
    
  300.   }
    
  301. 
    
  302.   _didWarn = false;
    
  303.   let hasTagMismatch = false;
    
  304.   let hasUnknownElement = false;
    
  305.   try {
    
  306.     let container;
    
  307.     if (containerTagName === 'document') {
    
  308.       const html = serverRenderer.renderToString(
    
  309.         react.createElement(tagName, props)
    
  310.       );
    
  311.       container = createContainer();
    
  312.       container.innerHTML = html;
    
  313.     } else if (containerTagName === 'head') {
    
  314.       const html = serverRenderer.renderToString(
    
  315.         react.createElement(tagName, props)
    
  316.       );
    
  317.       container = createContainer();
    
  318.       container.innerHTML = html;
    
  319.     } else {
    
  320.       const html = serverRenderer.renderToString(
    
  321.         react.createElement(
    
  322.           containerTagName,
    
  323.           null,
    
  324.           react.createElement(tagName, props)
    
  325.         )
    
  326.       );
    
  327.       const outerContainer = document.createElement('div');
    
  328.       outerContainer.innerHTML = html;
    
  329.       container = outerContainer.firstChild;
    
  330.     }
    
  331. 
    
  332.     if (
    
  333.       !container.lastChild ||
    
  334.       container.lastChild.tagName.toLowerCase() !== tagName.toLowerCase()
    
  335.     ) {
    
  336.       hasTagMismatch = true;
    
  337.     }
    
  338. 
    
  339.     if (
    
  340.       container.lastChild instanceof HTMLUnknownElement &&
    
  341.       !UNKNOWN_HTML_TAGS.has(container.lastChild.tagName.toLowerCase())
    
  342.     ) {
    
  343.       hasUnknownElement = true;
    
  344.     }
    
  345. 
    
  346.     ssrResult = read(container.lastChild);
    
  347.     canonicalSsrResult = getCanonicalizedValue(ssrResult);
    
  348.     ssrDidWarn = _didWarn;
    
  349.     ssrDidError = false;
    
  350.   } catch (error) {
    
  351.     ssrResult = null;
    
  352.     ssrDidWarn = _didWarn;
    
  353.     ssrDidError = true;
    
  354.   }
    
  355. 
    
  356.   console.error = originalConsoleError;
    
  357. 
    
  358.   if (hasTagMismatch) {
    
  359.     throw new Error('Tag mismatch. Expected: ' + tagName);
    
  360.   }
    
  361.   if (hasUnknownElement) {
    
  362.     throw new Error('Unexpected unknown element: ' + tagName);
    
  363.   }
    
  364. 
    
  365.   let ssrHasSameBehavior;
    
  366.   let ssrHasSameBehaviorExceptWarnings;
    
  367.   if (didError && ssrDidError) {
    
  368.     ssrHasSameBehavior = true;
    
  369.   } else if (!didError && !ssrDidError) {
    
  370.     if (canonicalResult === canonicalSsrResult) {
    
  371.       ssrHasSameBehaviorExceptWarnings = true;
    
  372.       ssrHasSameBehavior = didWarn === ssrDidWarn;
    
  373.     }
    
  374.     ssrHasSameBehavior =
    
  375.       didWarn === ssrDidWarn && canonicalResult === canonicalSsrResult;
    
  376.   } else {
    
  377.     ssrHasSameBehavior = false;
    
  378.   }
    
  379. 
    
  380.   return {
    
  381.     tagName,
    
  382.     containerTagName,
    
  383.     testValue,
    
  384.     defaultValue,
    
  385.     result,
    
  386.     canonicalResult,
    
  387.     canonicalDefaultValue,
    
  388.     didWarn,
    
  389.     didError,
    
  390.     ssrResult,
    
  391.     canonicalSsrResult,
    
  392.     ssrDidWarn,
    
  393.     ssrDidError,
    
  394.     ssrHasSameBehavior,
    
  395.     ssrHasSameBehaviorExceptWarnings,
    
  396.   };
    
  397. }
    
  398. 
    
  399. function prepareState(initGlobals) {
    
  400.   function getRenderedAttributeValues(attribute, type) {
    
  401.     const {
    
  402.       ReactStable,
    
  403.       ReactDOMStable,
    
  404.       ReactDOMServerStable,
    
  405.       ReactNext,
    
  406.       ReactDOMNext,
    
  407.       ReactDOMServerNext,
    
  408.     } = initGlobals(attribute, type);
    
  409.     const reactStableValue = getRenderedAttributeValue(
    
  410.       ReactStable,
    
  411.       ReactDOMStable,
    
  412.       ReactDOMServerStable,
    
  413.       attribute,
    
  414.       type
    
  415.     );
    
  416.     const reactNextValue = getRenderedAttributeValue(
    
  417.       ReactNext,
    
  418.       ReactDOMNext,
    
  419.       ReactDOMServerNext,
    
  420.       attribute,
    
  421.       type
    
  422.     );
    
  423. 
    
  424.     let hasSameBehavior;
    
  425.     if (reactStableValue.didError && reactNextValue.didError) {
    
  426.       hasSameBehavior = true;
    
  427.     } else if (!reactStableValue.didError && !reactNextValue.didError) {
    
  428.       hasSameBehavior =
    
  429.         reactStableValue.didWarn === reactNextValue.didWarn &&
    
  430.         reactStableValue.canonicalResult === reactNextValue.canonicalResult &&
    
  431.         reactStableValue.ssrHasSameBehavior ===
    
  432.           reactNextValue.ssrHasSameBehavior;
    
  433.     } else {
    
  434.       hasSameBehavior = false;
    
  435.     }
    
  436. 
    
  437.     return {
    
  438.       reactStable: reactStableValue,
    
  439.       reactNext: reactNextValue,
    
  440.       hasSameBehavior,
    
  441.     };
    
  442.   }
    
  443. 
    
  444.   const table = new Map();
    
  445.   const rowPatternHashes = new Map();
    
  446. 
    
  447.   // Disable error overlay while testing each attribute
    
  448.   uninjectErrorOverlay();
    
  449.   for (let attribute of attributes) {
    
  450.     const results = new Map();
    
  451.     let hasSameBehaviorForAll = true;
    
  452.     let rowPatternHash = '';
    
  453.     for (let type of types) {
    
  454.       const result = getRenderedAttributeValues(attribute, type);
    
  455.       results.set(type.name, result);
    
  456.       if (!result.hasSameBehavior) {
    
  457.         hasSameBehaviorForAll = false;
    
  458.       }
    
  459.       rowPatternHash += [result.reactStable, result.reactNext]
    
  460.         .map(res =>
    
  461.           [
    
  462.             res.canonicalResult,
    
  463.             res.canonicalDefaultValue,
    
  464.             res.didWarn,
    
  465.             res.didError,
    
  466.           ].join('||')
    
  467.         )
    
  468.         .join('||');
    
  469.     }
    
  470.     const row = {
    
  471.       results,
    
  472.       hasSameBehaviorForAll,
    
  473.       rowPatternHash,
    
  474.       // "Good enough" id that we can store in localStorage
    
  475.       rowIdHash: `${attribute.name} ${attribute.tagName} ${attribute.overrideStringValue}`,
    
  476.     };
    
  477.     const rowGroup = rowPatternHashes.get(rowPatternHash) || new Set();
    
  478.     rowGroup.add(row);
    
  479.     rowPatternHashes.set(rowPatternHash, rowGroup);
    
  480.     table.set(attribute, row);
    
  481.   }
    
  482. 
    
  483.   // Renable error overlay
    
  484.   injectErrorOverlay();
    
  485. 
    
  486.   return {
    
  487.     table,
    
  488.     rowPatternHashes,
    
  489.   };
    
  490. }
    
  491. 
    
  492. const successColor = 'white';
    
  493. const warnColor = 'yellow';
    
  494. const errorColor = 'red';
    
  495. 
    
  496. function RendererResult({
    
  497.   result,
    
  498.   canonicalResult,
    
  499.   defaultValue,
    
  500.   canonicalDefaultValue,
    
  501.   didWarn,
    
  502.   didError,
    
  503.   ssrHasSameBehavior,
    
  504.   ssrHasSameBehaviorExceptWarnings,
    
  505. }) {
    
  506.   let backgroundColor;
    
  507.   if (didError) {
    
  508.     backgroundColor = errorColor;
    
  509.   } else if (didWarn) {
    
  510.     backgroundColor = warnColor;
    
  511.   } else if (canonicalResult !== canonicalDefaultValue) {
    
  512.     backgroundColor = 'cyan';
    
  513.   } else {
    
  514.     backgroundColor = successColor;
    
  515.   }
    
  516. 
    
  517.   let style = {
    
  518.     display: 'flex',
    
  519.     alignItems: 'center',
    
  520.     position: 'absolute',
    
  521.     height: '100%',
    
  522.     width: '100%',
    
  523.     backgroundColor,
    
  524.   };
    
  525. 
    
  526.   if (!ssrHasSameBehavior) {
    
  527.     const color = ssrHasSameBehaviorExceptWarnings ? 'gray' : 'magenta';
    
  528.     style.border = `3px dotted ${color}`;
    
  529.   }
    
  530. 
    
  531.   return <div css={style}>{canonicalResult}</div>;
    
  532. }
    
  533. 
    
  534. function ResultPopover(props) {
    
  535.   return (
    
  536.     <pre
    
  537.       css={{
    
  538.         padding: '1em',
    
  539.         minWidth: '25em',
    
  540.       }}>
    
  541.       {JSON.stringify(
    
  542.         {
    
  543.           reactStable: props.reactStable,
    
  544.           reactNext: props.reactNext,
    
  545.           hasSameBehavior: props.hasSameBehavior,
    
  546.         },
    
  547.         null,
    
  548.         2
    
  549.       )}
    
  550.     </pre>
    
  551.   );
    
  552. }
    
  553. 
    
  554. class Result extends React.Component {
    
  555.   state = {showInfo: false};
    
  556.   onMouseEnter = () => {
    
  557.     if (this.timeout) {
    
  558.       clearTimeout(this.timeout);
    
  559.     }
    
  560.     this.timeout = setTimeout(() => {
    
  561.       this.setState({showInfo: true});
    
  562.     }, 250);
    
  563.   };
    
  564.   onMouseLeave = () => {
    
  565.     if (this.timeout) {
    
  566.       clearTimeout(this.timeout);
    
  567.     }
    
  568.     this.setState({showInfo: false});
    
  569.   };
    
  570. 
    
  571.   componentWillUnmount() {
    
  572.     if (this.timeout) {
    
  573.       clearTimeout(this.interval);
    
  574.     }
    
  575.   }
    
  576. 
    
  577.   render() {
    
  578.     const {reactStable, reactNext, hasSameBehavior} = this.props;
    
  579.     const style = {
    
  580.       position: 'absolute',
    
  581.       width: '100%',
    
  582.       height: '100%',
    
  583.     };
    
  584. 
    
  585.     let highlight = null;
    
  586.     let popover = null;
    
  587.     if (this.state.showInfo) {
    
  588.       highlight = (
    
  589.         <div
    
  590.           css={{
    
  591.             position: 'absolute',
    
  592.             height: '100%',
    
  593.             width: '100%',
    
  594.             border: '2px solid blue',
    
  595.           }}
    
  596.         />
    
  597.       );
    
  598. 
    
  599.       popover = (
    
  600.         <div
    
  601.           css={{
    
  602.             backgroundColor: 'white',
    
  603.             border: '1px solid black',
    
  604.             position: 'absolute',
    
  605.             top: '100%',
    
  606.             zIndex: 999,
    
  607.           }}>
    
  608.           <ResultPopover {...this.props} />
    
  609.         </div>
    
  610.       );
    
  611.     }
    
  612. 
    
  613.     if (!hasSameBehavior) {
    
  614.       style.border = '4px solid purple';
    
  615.     }
    
  616.     return (
    
  617.       <div
    
  618.         css={style}
    
  619.         onMouseEnter={this.onMouseEnter}
    
  620.         onMouseLeave={this.onMouseLeave}>
    
  621.         <div css={{position: 'absolute', width: '50%', height: '100%'}}>
    
  622.           <RendererResult {...reactStable} />
    
  623.         </div>
    
  624.         <div
    
  625.           css={{
    
  626.             position: 'absolute',
    
  627.             width: '50%',
    
  628.             left: '50%',
    
  629.             height: '100%',
    
  630.           }}>
    
  631.           <RendererResult {...reactNext} />
    
  632.         </div>
    
  633.         {highlight}
    
  634.         {popover}
    
  635.       </div>
    
  636.     );
    
  637.   }
    
  638. }
    
  639. 
    
  640. function ColumnHeader({children}) {
    
  641.   return (
    
  642.     <div
    
  643.       css={{
    
  644.         position: 'absolute',
    
  645.         width: '100%',
    
  646.         height: '100%',
    
  647.         display: 'flex',
    
  648.         alignItems: 'center',
    
  649.       }}>
    
  650.       {children}
    
  651.     </div>
    
  652.   );
    
  653. }
    
  654. 
    
  655. function RowHeader({children, checked, onChange}) {
    
  656.   return (
    
  657.     <div
    
  658.       css={{
    
  659.         position: 'absolute',
    
  660.         width: '100%',
    
  661.         height: '100%',
    
  662.         display: 'flex',
    
  663.         alignItems: 'center',
    
  664.       }}>
    
  665.       <input type="checkbox" checked={checked} onChange={onChange} />
    
  666.       {children}
    
  667.     </div>
    
  668.   );
    
  669. }
    
  670. 
    
  671. function CellContent(props) {
    
  672.   const {
    
  673.     columnIndex,
    
  674.     rowIndex,
    
  675.     attributesInSortedOrder,
    
  676.     completedHashes,
    
  677.     toggleAttribute,
    
  678.     table,
    
  679.   } = props;
    
  680.   const attribute = attributesInSortedOrder[rowIndex - 1];
    
  681.   const type = types[columnIndex - 1];
    
  682. 
    
  683.   if (columnIndex === 0) {
    
  684.     if (rowIndex === 0) {
    
  685.       return null;
    
  686.     }
    
  687.     const row = table.get(attribute);
    
  688.     const rowPatternHash = row.rowPatternHash;
    
  689.     return (
    
  690.       <RowHeader
    
  691.         checked={completedHashes.has(rowPatternHash)}
    
  692.         onChange={() => toggleAttribute(rowPatternHash)}>
    
  693.         {row.hasSameBehaviorForAll ? (
    
  694.           attribute.name
    
  695.         ) : (
    
  696.           <b css={{color: 'purple'}}>{attribute.name}</b>
    
  697.         )}
    
  698.       </RowHeader>
    
  699.     );
    
  700.   }
    
  701. 
    
  702.   if (rowIndex === 0) {
    
  703.     return <ColumnHeader>{type.name}</ColumnHeader>;
    
  704.   }
    
  705. 
    
  706.   const row = table.get(attribute);
    
  707.   const result = row.results.get(type.name);
    
  708. 
    
  709.   return <Result {...result} />;
    
  710. }
    
  711. 
    
  712. function saveToLocalStorage(completedHashes) {
    
  713.   const str = JSON.stringify([...completedHashes]);
    
  714.   localStorage.setItem('completedHashes', str);
    
  715. }
    
  716. 
    
  717. function restoreFromLocalStorage() {
    
  718.   const str = localStorage.getItem('completedHashes');
    
  719.   if (str) {
    
  720.     const completedHashes = new Set(JSON.parse(str));
    
  721.     return completedHashes;
    
  722.   }
    
  723.   return new Set();
    
  724. }
    
  725. 
    
  726. const useFastMode = /[?&]fast\b/.test(window.location.href);
    
  727. 
    
  728. class App extends React.Component {
    
  729.   state = {
    
  730.     sortOrder: ALPHABETICAL,
    
  731.     filter: ALL,
    
  732.     completedHashes: restoreFromLocalStorage(),
    
  733.     table: null,
    
  734.     rowPatternHashes: null,
    
  735.   };
    
  736. 
    
  737.   renderCell = ({key, ...props}) => {
    
  738.     return (
    
  739.       <div key={key} style={props.style}>
    
  740.         <CellContent
    
  741.           toggleAttribute={this.toggleAttribute}
    
  742.           completedHashes={this.state.completedHashes}
    
  743.           table={this.state.table}
    
  744.           attributesInSortedOrder={this.attributes}
    
  745.           {...props}
    
  746.         />
    
  747.       </div>
    
  748.     );
    
  749.   };
    
  750. 
    
  751.   onUpdateSort = e => {
    
  752.     this.setState({sortOrder: e.target.value});
    
  753.   };
    
  754. 
    
  755.   onUpdateFilter = e => {
    
  756.     this.setState({filter: e.target.value});
    
  757.   };
    
  758. 
    
  759.   toggleAttribute = rowPatternHash => {
    
  760.     const completedHashes = new Set(this.state.completedHashes);
    
  761.     if (completedHashes.has(rowPatternHash)) {
    
  762.       completedHashes.delete(rowPatternHash);
    
  763.     } else {
    
  764.       completedHashes.add(rowPatternHash);
    
  765.     }
    
  766.     this.setState({completedHashes}, () => saveToLocalStorage(completedHashes));
    
  767.   };
    
  768. 
    
  769.   async componentDidMount() {
    
  770.     const sources = {
    
  771.       ReactStable: 'https://unpkg.com/react@latest/umd/react.development.js',
    
  772.       ReactDOMStable:
    
  773.         'https://unpkg.com/react-dom@latest/umd/react-dom.development.js',
    
  774.       ReactDOMServerStable:
    
  775.         'https://unpkg.com/react-dom@latest/umd/react-dom-server-legacy.browser.development.js',
    
  776.       ReactNext: '/react.development.js',
    
  777.       ReactDOMNext: '/react-dom.development.js',
    
  778.       ReactDOMServerNext: '/react-dom-server-legacy.browser.development.js',
    
  779.     };
    
  780.     const codePromises = Object.values(sources).map(src =>
    
  781.       fetch(src).then(res => res.text())
    
  782.     );
    
  783.     const codesByIndex = await Promise.all(codePromises);
    
  784. 
    
  785.     const pool = [];
    
  786.     function initGlobals(attribute, type) {
    
  787.       if (useFastMode) {
    
  788.         // Note: this is not giving correct results for warnings.
    
  789.         // But it's much faster.
    
  790.         if (pool[0]) {
    
  791.           return pool[0].globals;
    
  792.         }
    
  793.       } else {
    
  794.         document.title = `${attribute.name} (${type.name})`;
    
  795.       }
    
  796. 
    
  797.       // Creating globals for every single test is too slow.
    
  798.       // However caching them between runs won't work for the same attribute names
    
  799.       // because warnings will be deduplicated. As a result, we only share globals
    
  800.       // between different attribute names.
    
  801.       for (let i = 0; i < pool.length; i++) {
    
  802.         if (!pool[i].testedAttributes.has(attribute.name)) {
    
  803.           pool[i].testedAttributes.add(attribute.name);
    
  804.           return pool[i].globals;
    
  805.         }
    
  806.       }
    
  807. 
    
  808.       let globals = {};
    
  809.       Object.keys(sources).forEach((name, i) => {
    
  810.         eval.call(window, codesByIndex[i]); // eslint-disable-line
    
  811.         globals[name] = window[name.replace(/Stable|Next/g, '')];
    
  812.       });
    
  813. 
    
  814.       // Cache for future use (for different attributes).
    
  815.       pool.push({
    
  816.         globals,
    
  817.         testedAttributes: new Set([attribute.name]),
    
  818.       });
    
  819. 
    
  820.       return globals;
    
  821.     }
    
  822. 
    
  823.     const {table, rowPatternHashes} = prepareState(initGlobals);
    
  824.     document.title = 'Ready';
    
  825. 
    
  826.     this.setState({
    
  827.       table,
    
  828.       rowPatternHashes,
    
  829.     });
    
  830.   }
    
  831. 
    
  832.   componentWillUpdate(nextProps, nextState) {
    
  833.     if (
    
  834.       nextState.sortOrder !== this.state.sortOrder ||
    
  835.       nextState.filter !== this.state.filter ||
    
  836.       nextState.completedHashes !== this.state.completedHashes ||
    
  837.       nextState.table !== this.state.table
    
  838.     ) {
    
  839.       this.attributes = this.getAttributes(
    
  840.         nextState.table,
    
  841.         nextState.rowPatternHashes,
    
  842.         nextState.sortOrder,
    
  843.         nextState.filter,
    
  844.         nextState.completedHashes
    
  845.       );
    
  846.       if (this.grid) {
    
  847.         this.grid.forceUpdateGrids();
    
  848.       }
    
  849.     }
    
  850.   }
    
  851. 
    
  852.   getAttributes(table, rowPatternHashes, sortOrder, filter, completedHashes) {
    
  853.     // Filter
    
  854.     let filteredAttributes;
    
  855.     switch (filter) {
    
  856.       case ALL:
    
  857.         filteredAttributes = attributes.filter(() => true);
    
  858.         break;
    
  859.       case COMPLETE:
    
  860.         filteredAttributes = attributes.filter(attribute => {
    
  861.           const row = table.get(attribute);
    
  862.           return completedHashes.has(row.rowPatternHash);
    
  863.         });
    
  864.         break;
    
  865.       case INCOMPLETE:
    
  866.         filteredAttributes = attributes.filter(attribute => {
    
  867.           const row = table.get(attribute);
    
  868.           return !completedHashes.has(row.rowPatternHash);
    
  869.         });
    
  870.         break;
    
  871.       default:
    
  872.         throw new Error('Switch statement should be exhaustive');
    
  873.     }
    
  874. 
    
  875.     // Sort
    
  876.     switch (sortOrder) {
    
  877.       case ALPHABETICAL:
    
  878.         return filteredAttributes.sort((attr1, attr2) =>
    
  879.           attr1.name.toLowerCase() < attr2.name.toLowerCase() ? -1 : 1
    
  880.         );
    
  881.       case REV_ALPHABETICAL:
    
  882.         return filteredAttributes.sort((attr1, attr2) =>
    
  883.           attr1.name.toLowerCase() < attr2.name.toLowerCase() ? 1 : -1
    
  884.         );
    
  885.       case GROUPED_BY_ROW_PATTERN: {
    
  886.         return filteredAttributes.sort((attr1, attr2) => {
    
  887.           const row1 = table.get(attr1);
    
  888.           const row2 = table.get(attr2);
    
  889.           const patternGroup1 = rowPatternHashes.get(row1.rowPatternHash);
    
  890.           const patternGroupSize1 = (patternGroup1 && patternGroup1.size) || 0;
    
  891.           const patternGroup2 = rowPatternHashes.get(row2.rowPatternHash);
    
  892.           const patternGroupSize2 = (patternGroup2 && patternGroup2.size) || 0;
    
  893.           return patternGroupSize2 - patternGroupSize1;
    
  894.         });
    
  895.       }
    
  896.       default:
    
  897.         throw new Error('Switch statement should be exhaustive');
    
  898.     }
    
  899.   }
    
  900. 
    
  901.   handleSaveClick = e => {
    
  902.     e.preventDefault();
    
  903. 
    
  904.     if (useFastMode) {
    
  905.       alert(
    
  906.         'Fast mode is not accurate. Please remove ?fast from the query string, and reload.'
    
  907.       );
    
  908.       return;
    
  909.     }
    
  910. 
    
  911.     let log = '';
    
  912.     for (let attribute of attributes) {
    
  913.       log += `## \`${attribute.name}\` (on \`<${
    
  914.         attribute.tagName || 'div'
    
  915.       }>\` inside \`<${attribute.containerTagName || 'div'}>\`)\n`;
    
  916.       log += '| Test Case | Flags | Result |\n';
    
  917.       log += '| --- | --- | --- |\n';
    
  918. 
    
  919.       const attributeResults = this.state.table.get(attribute).results;
    
  920.       for (let type of types) {
    
  921.         const {
    
  922.           didError,
    
  923.           didWarn,
    
  924.           canonicalResult,
    
  925.           canonicalDefaultValue,
    
  926.           ssrDidError,
    
  927.           ssrHasSameBehavior,
    
  928.           ssrHasSameBehaviorExceptWarnings,
    
  929.         } = attributeResults.get(type.name).reactNext;
    
  930. 
    
  931.         let descriptions = [];
    
  932.         if (canonicalResult === canonicalDefaultValue) {
    
  933.           descriptions.push('initial');
    
  934.         } else {
    
  935.           descriptions.push('changed');
    
  936.         }
    
  937.         if (didError) {
    
  938.           descriptions.push('error');
    
  939.         }
    
  940.         if (didWarn) {
    
  941.           descriptions.push('warning');
    
  942.         }
    
  943.         if (ssrDidError) {
    
  944.           descriptions.push('ssr error');
    
  945.         }
    
  946.         if (!ssrHasSameBehavior) {
    
  947.           if (ssrHasSameBehaviorExceptWarnings) {
    
  948.             descriptions.push('ssr warning');
    
  949.           } else {
    
  950.             descriptions.push('ssr mismatch');
    
  951.           }
    
  952.         }
    
  953.         log +=
    
  954.           `| \`${attribute.name}=(${type.name})\`` +
    
  955.           `| (${descriptions.join(', ')})` +
    
  956.           `| \`${canonicalResult || ''}\` |\n`;
    
  957.       }
    
  958.       log += '\n';
    
  959.     }
    
  960. 
    
  961.     const blob = new Blob([log], {type: 'text/plain;charset=utf-8'});
    
  962.     FileSaver.saveAs(blob, 'AttributeTableSnapshot.md');
    
  963.   };
    
  964. 
    
  965.   render() {
    
  966.     if (!this.state.table) {
    
  967.       return (
    
  968.         <div>
    
  969.           <h1>Loading...</h1>
    
  970.           {!useFastMode && (
    
  971.             <h3>The progress is reported in the window title.</h3>
    
  972.           )}
    
  973.         </div>
    
  974.       );
    
  975.     }
    
  976.     return (
    
  977.       <div>
    
  978.         <div>
    
  979.           <select value={this.state.sortOrder} onChange={this.onUpdateSort}>
    
  980.             <option value={ALPHABETICAL}>alphabetical</option>
    
  981.             <option value={REV_ALPHABETICAL}>reverse alphabetical</option>
    
  982.             <option value={GROUPED_BY_ROW_PATTERN}>
    
  983.               grouped by row pattern :)
    
  984.             </option>
    
  985.           </select>
    
  986.           <select value={this.state.filter} onChange={this.onUpdateFilter}>
    
  987.             <option value={ALL}>all</option>
    
  988.             <option value={INCOMPLETE}>incomplete</option>
    
  989.             <option value={COMPLETE}>complete</option>
    
  990.           </select>
    
  991.           <button style={{marginLeft: '10px'}} onClick={this.handleSaveClick}>
    
  992.             Save latest results to a file{' '}
    
  993.             <span role="img" aria-label="Save">
    
  994.               💾
    
  995.             </span>
    
  996.           </button>
    
  997.         </div>
    
  998.         <AutoSizer disableHeight={true}>
    
  999.           {({width}) => (
    
  1000.             <MultiGrid
    
  1001.               ref={input => {
    
  1002.                 this.grid = input;
    
  1003.               }}
    
  1004.               cellRenderer={this.renderCell}
    
  1005.               columnWidth={200}
    
  1006.               columnCount={1 + types.length}
    
  1007.               fixedColumnCount={1}
    
  1008.               enableFixedColumnScroll={true}
    
  1009.               enableFixedRowScroll={true}
    
  1010.               height={1200}
    
  1011.               rowHeight={40}
    
  1012.               rowCount={this.attributes.length + 1}
    
  1013.               fixedRowCount={1}
    
  1014.               width={width}
    
  1015.             />
    
  1016.           )}
    
  1017.         </AutoSizer>
    
  1018.       </div>
    
  1019.     );
    
  1020.   }
    
  1021. }
    
  1022. 
    
  1023. export default App;