import React from 'react';
import {createElement} from 'glamor/react'; // eslint-disable-line
/* @jsx createElement */import {MultiGrid, AutoSizer} from 'react-virtualized';
import 'react-virtualized/styles.css';
import FileSaver from 'file-saver';
import {
inject as injectErrorOverlay,
uninject as uninjectErrorOverlay,
} from 'react-error-overlay/lib/overlay';
import attributes from './attributes';
const types = [
{name: 'string',
testValue: 'a string',
testDisplayValue: "'a string'",
},{name: 'empty string',
testValue: '',testDisplayValue: "''",
},{name: 'array with string',
testValue: ['string'],
testDisplayValue: "['string']",
},{name: 'empty array',
testValue: [],testDisplayValue: '[]',
},{name: 'object',
testValue: {toString() {
return 'result of toString()';
},},testDisplayValue: "{ toString() { return 'result of toString()'; } }",
},{name: 'numeric string',
testValue: '42',
displayValue: "'42'",
},{name: '-1',
testValue: -1,
},{name: '0',
testValue: 0,
},{name: 'integer',
testValue: 1,
},{name: 'NaN',
testValue: NaN,
},{name: 'float',
testValue: 99.99,
},{name: 'true',
testValue: true,
},{name: 'false',
testValue: false,
},{name: "string 'true'",
testValue: 'true',
displayValue: "'true'",
},{name: "string 'false'",
testValue: 'false',
displayValue: "'false'",
},{name: "string 'on'",
testValue: 'on',
displayValue: "'on'",
},{name: "string 'off'",
testValue: 'off',
displayValue: "'off'",
},{name: 'symbol',
testValue: Symbol('foo'),
testDisplayValue: "Symbol('foo')",
},{name: 'function',
testValue: function f() {},
},{name: 'null',
testValue: null,
},{name: 'undefined',
testValue: undefined,
},];const ALPHABETICAL = 'alphabetical';
const REV_ALPHABETICAL = 'reverse_alphabetical';
const GROUPED_BY_ROW_PATTERN = 'grouped_by_row_pattern';
const ALL = 'all';
const COMPLETE = 'complete';
const INCOMPLETE = 'incomplete';
function getCanonicalizedValue(value) {
switch (typeof value) {
case 'undefined':
return '<undefined>';
case 'object':
if (value === null) {
return '<null>';
}if ('baseVal' in value) {
return getCanonicalizedValue(value.baseVal);
}if (value instanceof SVGLength) {
return '<SVGLength: ' + value.valueAsString + '>';
}if (value instanceof SVGRect) {
return (
'<SVGRect: ' +
[value.x, value.y, value.width, value.height].join(',') +
'>'
);}if (value instanceof SVGPreserveAspectRatio) {
return (
'<SVGPreserveAspectRatio: ' +
value.align +
'/' +
value.meetOrSlice +
'>'
);}if (value instanceof SVGNumber) {
return value.value;
}if (value instanceof SVGMatrix) {
return (
'<SVGMatrix ' +
value.a +
' ' +value.b +
' ' +value.c +
' ' +value.d +
' ' +value.e +
' ' +value.f +
'>'
);}if (value instanceof SVGTransform) {
return (
getCanonicalizedValue(value.matrix) +
'/' +
value.type +
'/' +
value.angle
);}if (typeof value.length === 'number') {
return (
'[' +
Array.from(value)
.map(v => getCanonicalizedValue(v))
.join(', ') +
']'
);}let name = (value.constructor && value.constructor.name) || 'object';
return '<' + name + '>';
case 'function':
return '<function>';
case 'symbol':
return '<symbol>';
case 'number':
return `<number: ${value}>`;
case 'string':
if (value === '') {
return '<empty string>';
}return '"' + value + '"';
case 'boolean':
return `<boolean: ${value}>`;
default:
throw new Error('Switch statement should be exhaustive.');
}}let _didWarn = false;
function warn(str) {
if (str.includes('ReactDOM.render is no longer supported')) {
return;
}_didWarn = true;
}const UNKNOWN_HTML_TAGS = new Set(['keygen', 'time', 'command']);
function getRenderedAttributeValue(
react,renderer,serverRenderer,attribute,type) {const originalConsoleError = console.error;
console.error = warn;
const containerTagName = attribute.containerTagName || 'div';
const tagName = attribute.tagName || 'div';
function createContainer() {
if (containerTagName === 'svg') {
return document.createElementNS('http://www.w3.org/2000/svg', 'svg');
} else if (containerTagName === 'document') {
return document.implementation.createHTMLDocument('');
} else if (containerTagName === 'head') {
return document.implementation.createHTMLDocument('').head;
} else {
return document.createElement(containerTagName);
}}const read = attribute.read;
let testValue = type.testValue;
if (attribute.overrideStringValue !== undefined) {
switch (type.name) {
case 'string':
testValue = attribute.overrideStringValue;
break;
case 'array with string':
testValue = [attribute.overrideStringValue];
break;
default:
break;
}}let baseProps = {
...attribute.extraProps,
};if (attribute.type) {
baseProps.type = attribute.type;
}const props = {
...baseProps,
[attribute.name]: testValue,
};let defaultValue;
let canonicalDefaultValue;
let result;
let canonicalResult;
let ssrResult;
let canonicalSsrResult;
let didWarn;
let didError;
let ssrDidWarn;
let ssrDidError;
_didWarn = false;
try {
let container = createContainer();
renderer.render(react.createElement(tagName, baseProps), container);
defaultValue = read(container.lastChild);
canonicalDefaultValue = getCanonicalizedValue(defaultValue);
container = createContainer();
renderer.render(react.createElement(tagName, props), container);
result = read(container.lastChild);
canonicalResult = getCanonicalizedValue(result);
didWarn = _didWarn;
didError = false;
} catch (error) {
result = null;
didWarn = _didWarn;
didError = true;
}_didWarn = false;
let hasTagMismatch = false;
let hasUnknownElement = false;
try {
let container;
if (containerTagName === 'document') {
const html = serverRenderer.renderToString(
react.createElement(tagName, props)
);container = createContainer();
container.innerHTML = html;
} else if (containerTagName === 'head') {
const html = serverRenderer.renderToString(
react.createElement(tagName, props)
);container = createContainer();
container.innerHTML = html;
} else {
const html = serverRenderer.renderToString(
react.createElement(
containerTagName,
null,
react.createElement(tagName, props)
));const outerContainer = document.createElement('div');
outerContainer.innerHTML = html;
container = outerContainer.firstChild;
}if (
!container.lastChild ||
container.lastChild.tagName.toLowerCase() !== tagName.toLowerCase()
) {hasTagMismatch = true;
}if (
container.lastChild instanceof HTMLUnknownElement &&
!UNKNOWN_HTML_TAGS.has(container.lastChild.tagName.toLowerCase())
) {hasUnknownElement = true;
}ssrResult = read(container.lastChild);
canonicalSsrResult = getCanonicalizedValue(ssrResult);
ssrDidWarn = _didWarn;
ssrDidError = false;
} catch (error) {
ssrResult = null;
ssrDidWarn = _didWarn;
ssrDidError = true;
}console.error = originalConsoleError;
if (hasTagMismatch) {
throw new Error('Tag mismatch. Expected: ' + tagName);
}if (hasUnknownElement) {
throw new Error('Unexpected unknown element: ' + tagName);
}let ssrHasSameBehavior;
let ssrHasSameBehaviorExceptWarnings;
if (didError && ssrDidError) {
ssrHasSameBehavior = true;
} else if (!didError && !ssrDidError) {
if (canonicalResult === canonicalSsrResult) {
ssrHasSameBehaviorExceptWarnings = true;
ssrHasSameBehavior = didWarn === ssrDidWarn;
}ssrHasSameBehavior =
didWarn === ssrDidWarn && canonicalResult === canonicalSsrResult;
} else {
ssrHasSameBehavior = false;
}return {
tagName,
containerTagName,
testValue,
defaultValue,
result,
canonicalResult,
canonicalDefaultValue,
didWarn,
didError,
ssrResult,
canonicalSsrResult,
ssrDidWarn,
ssrDidError,
ssrHasSameBehavior,
ssrHasSameBehaviorExceptWarnings,
};}function prepareState(initGlobals) {
function getRenderedAttributeValues(attribute, type) {
const {
ReactStable,
ReactDOMStable,
ReactDOMServerStable,
ReactNext,
ReactDOMNext,
ReactDOMServerNext,
} = initGlobals(attribute, type);
const reactStableValue = getRenderedAttributeValue(
ReactStable,
ReactDOMStable,
ReactDOMServerStable,
attribute,
type
);const reactNextValue = getRenderedAttributeValue(
ReactNext,
ReactDOMNext,
ReactDOMServerNext,
attribute,
type
);let hasSameBehavior;
if (reactStableValue.didError && reactNextValue.didError) {
hasSameBehavior = true;
} else if (!reactStableValue.didError && !reactNextValue.didError) {
hasSameBehavior =
reactStableValue.didWarn === reactNextValue.didWarn &&
reactStableValue.canonicalResult === reactNextValue.canonicalResult &&
reactStableValue.ssrHasSameBehavior ===
reactNextValue.ssrHasSameBehavior;
} else {
hasSameBehavior = false;
}return {
reactStable: reactStableValue,
reactNext: reactNextValue,
hasSameBehavior,
};}const table = new Map();
const rowPatternHashes = new Map();
// Disable error overlay while testing each attribute
uninjectErrorOverlay();
for (let attribute of attributes) {
const results = new Map();
let hasSameBehaviorForAll = true;
let rowPatternHash = '';
for (let type of types) {
const result = getRenderedAttributeValues(attribute, type);
results.set(type.name, result);
if (!result.hasSameBehavior) {
hasSameBehaviorForAll = false;
}rowPatternHash += [result.reactStable, result.reactNext]
.map(res =>
[res.canonicalResult,
res.canonicalDefaultValue,
res.didWarn,
res.didError,
].join('||')
).join('||');
}const row = {
results,
hasSameBehaviorForAll,
rowPatternHash,
// "Good enough" id that we can store in localStorage
rowIdHash: `${attribute.name} ${attribute.tagName} ${attribute.overrideStringValue}`,
};const rowGroup = rowPatternHashes.get(rowPatternHash) || new Set();
rowGroup.add(row);
rowPatternHashes.set(rowPatternHash, rowGroup);
table.set(attribute, row);
}// Renable error overlay
injectErrorOverlay();
return {
table,
rowPatternHashes,
};}const successColor = 'white';
const warnColor = 'yellow';
const errorColor = 'red';
function RendererResult({
result,canonicalResult,defaultValue,canonicalDefaultValue,didWarn,didError,ssrHasSameBehavior,ssrHasSameBehaviorExceptWarnings,}) {let backgroundColor;
if (didError) {
backgroundColor = errorColor;
} else if (didWarn) {
backgroundColor = warnColor;
} else if (canonicalResult !== canonicalDefaultValue) {
backgroundColor = 'cyan';
} else {
backgroundColor = successColor;
}let style = {
display: 'flex',
alignItems: 'center',
position: 'absolute',
height: '100%',
width: '100%',
backgroundColor,
};if (!ssrHasSameBehavior) {
const color = ssrHasSameBehaviorExceptWarnings ? 'gray' : 'magenta';
style.border = `3px dotted ${color}`;
}return <div css={style}>{canonicalResult}</div>;
}function ResultPopover(props) {return (<precss={{padding: '1em',minWidth: '25em',}}>{JSON.stringify(
{reactStable: props.reactStable,
reactNext: props.reactNext,
hasSameBehavior: props.hasSameBehavior,
},null,2)}</pre>);}class Result extends React.Component {
state = {showInfo: false};onMouseEnter = () => {if (this.timeout) {
clearTimeout(this.timeout);
}this.timeout = setTimeout(() => {
this.setState({showInfo: true});
}, 250);};onMouseLeave = () => {if (this.timeout) {
clearTimeout(this.timeout);
}this.setState({showInfo: false});
};componentWillUnmount() {if (this.timeout) {
clearTimeout(this.interval);
}}render() {const {reactStable, reactNext, hasSameBehavior} = this.props;
const style = {position: 'absolute',width: '100%',height: '100%',};let highlight = null;let popover = null;if (this.state.showInfo) {
highlight = (<divcss={{position: 'absolute',height: '100%',width: '100%',border: '2px solid blue',}}/>);popover = (<divcss={{backgroundColor: 'white',border: '1px solid black',position: 'absolute',top: '100%',zIndex: 999,}}><ResultPopover {...this.props} />
</div>);}if (!hasSameBehavior) {style.border = '4px solid purple';
}return (<divcss={style}onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}>
<div css={{position: 'absolute', width: '50%', height: '100%'}}><RendererResult {...reactStable} />
</div><divcss={{position: 'absolute',width: '50%',left: '50%',height: '100%',}}><RendererResult {...reactNext} />
</div>{highlight}{popover}</div>);}}function ColumnHeader({children}) {return (<divcss={{position: 'absolute',width: '100%',height: '100%',display: 'flex',alignItems: 'center',}}>{children}</div>);}function RowHeader({children, checked, onChange}) {return (<divcss={{position: 'absolute',width: '100%',height: '100%',display: 'flex',alignItems: 'center',}}><input type="checkbox" checked={checked} onChange={onChange} />{children}</div>);}function CellContent(props) {const {columnIndex,rowIndex,attributesInSortedOrder,completedHashes,toggleAttribute,table,} = props;const attribute = attributesInSortedOrder[rowIndex - 1];
const type = types[columnIndex - 1];
if (columnIndex === 0) {if (rowIndex === 0) {return null;}const row = table.get(attribute);
const rowPatternHash = row.rowPatternHash;
return (<RowHeaderchecked={completedHashes.has(rowPatternHash)}
onChange={() => toggleAttribute(rowPatternHash)}>{row.hasSameBehaviorForAll ? (
attribute.name
) : (<b css={{color: 'purple'}}>{attribute.name}</b>
)}</RowHeader>);}if (rowIndex === 0) {return <ColumnHeader>{type.name}</ColumnHeader>;
}const row = table.get(attribute);
const result = row.results.get(type.name);
return <Result {...result} />;
}function saveToLocalStorage(completedHashes) {const str = JSON.stringify([...completedHashes]);
localStorage.setItem('completedHashes', str);
}function restoreFromLocalStorage() {const str = localStorage.getItem('completedHashes');
if (str) {const completedHashes = new Set(JSON.parse(str));
return completedHashes;}return new Set();}const useFastMode = /[?&]fast\b/.test(window.location.href);
class App extends React.Component {
state = {sortOrder: ALPHABETICAL,filter: ALL,completedHashes: restoreFromLocalStorage(),
table: null,rowPatternHashes: null,};
renderCell = ({key, ...props}) => {
return (
<div key={key} style={props.style}>
<CellContent
toggleAttribute={this.toggleAttribute}
completedHashes={this.state.completedHashes}
table={this.state.table}
attributesInSortedOrder={this.attributes}
{...props}
/></div>
);};onUpdateSort = e => {this.setState({sortOrder: e.target.value});
};onUpdateFilter = e => {this.setState({filter: e.target.value});
};toggleAttribute = rowPatternHash => {const completedHashes = new Set(this.state.completedHashes);
if (completedHashes.has(rowPatternHash)) {
completedHashes.delete(rowPatternHash);
} else {completedHashes.add(rowPatternHash);
}this.setState({completedHashes}, () => saveToLocalStorage(completedHashes));
};async componentDidMount() {const sources = {ReactStable: 'https://unpkg.com/react@latest/umd/react.development.js',
ReactDOMStable:
'https://unpkg.com/react-dom@latest/umd/react-dom.development.js',
ReactDOMServerStable:
'https://unpkg.com/react-dom@latest/umd/react-dom-server-legacy.browser.development.js',
ReactNext: '/react.development.js',
ReactDOMNext: '/react-dom.development.js',
ReactDOMServerNext: '/react-dom-server-legacy.browser.development.js',
};const codePromises = Object.values(sources).map(src =>
fetch(src).then(res => res.text())
);const codesByIndex = await Promise.all(codePromises);
const pool = [];
function initGlobals(attribute, type) {
if (useFastMode) {
// Note: this is not giving correct results for warnings.
// But it's much faster.
if (pool[0]) {
return pool[0].globals;
}} else {
document.title = `${attribute.name} (${type.name})`;
}// Creating globals for every single test is too slow.
// However caching them between runs won't work for the same attribute names
// because warnings will be deduplicated. As a result, we only share globals
// between different attribute names.
for (let i = 0; i < pool.length; i++) {
if (!pool[i].testedAttributes.has(attribute.name)) {
pool[i].testedAttributes.add(attribute.name);
return pool[i].globals;
}}let globals = {};
Object.keys(sources).forEach((name, i) => {
eval.call(window, codesByIndex[i]); // eslint-disable-line
globals[name] = window[name.replace(/Stable|Next/g, '')];
});// Cache for future use (for different attributes).
pool.push({
globals,
testedAttributes: new Set([attribute.name]),
});return globals;
}const {table, rowPatternHashes} = prepareState(initGlobals);
document.title = 'Ready';
this.setState({
table,
rowPatternHashes,
});}componentWillUpdate(nextProps, nextState) {
if (
nextState.sortOrder !== this.state.sortOrder ||
nextState.filter !== this.state.filter ||
nextState.completedHashes !== this.state.completedHashes ||
nextState.table !== this.state.table
) {this.attributes = this.getAttributes(
nextState.table,
nextState.rowPatternHashes,
nextState.sortOrder,
nextState.filter,
nextState.completedHashes
);if (this.grid) {
this.grid.forceUpdateGrids();
}}}getAttributes(table, rowPatternHashes, sortOrder, filter, completedHashes) {
// Filter
let filteredAttributes;switch (filter) {
case ALL:
filteredAttributes = attributes.filter(() => true);
break;
case COMPLETE:
filteredAttributes = attributes.filter(attribute => {
const row = table.get(attribute);
return completedHashes.has(row.rowPatternHash);
});break;
case INCOMPLETE:
filteredAttributes = attributes.filter(attribute => {
const row = table.get(attribute);
return !completedHashes.has(row.rowPatternHash);
});break;
default:throw new Error('Switch statement should be exhaustive');
}// Sort
switch (sortOrder) {
case ALPHABETICAL:
return filteredAttributes.sort((attr1, attr2) =>
attr1.name.toLowerCase() < attr2.name.toLowerCase() ? -1 : 1
);case REV_ALPHABETICAL:
return filteredAttributes.sort((attr1, attr2) =>
attr1.name.toLowerCase() < attr2.name.toLowerCase() ? 1 : -1
);case GROUPED_BY_ROW_PATTERN: {
return filteredAttributes.sort((attr1, attr2) => {
const row1 = table.get(attr1);
const row2 = table.get(attr2);
const patternGroup1 = rowPatternHashes.get(row1.rowPatternHash);
const patternGroupSize1 = (patternGroup1 && patternGroup1.size) || 0;
const patternGroup2 = rowPatternHashes.get(row2.rowPatternHash);
const patternGroupSize2 = (patternGroup2 && patternGroup2.size) || 0;
return patternGroupSize2 - patternGroupSize1;
});}default:throw new Error('Switch statement should be exhaustive');
}}handleSaveClick = e => {
e.preventDefault();
if (useFastMode) {
alert(
'Fast mode is not accurate. Please remove ?fast from the query string, and reload.'
);return;
}let log = '';
for (let attribute of attributes) {
log += `## \`${attribute.name}\` (on \`<${
attribute.tagName || 'div'
}>\` inside \`<${attribute.containerTagName || 'div'}>\`)\n`;
log += '| Test Case | Flags | Result |\n';
log += '| --- | --- | --- |\n';
const attributeResults = this.state.table.get(attribute).results;
for (let type of types) {
const {
didError,
didWarn,
canonicalResult,
canonicalDefaultValue,
ssrDidError,
ssrHasSameBehavior,
ssrHasSameBehaviorExceptWarnings,
} = attributeResults.get(type.name).reactNext;
let descriptions = [];
if (canonicalResult === canonicalDefaultValue) {
descriptions.push('initial');
} else {
descriptions.push('changed');
}if (didError) {
descriptions.push('error');
}if (didWarn) {
descriptions.push('warning');
}if (ssrDidError) {
descriptions.push('ssr error');
}if (!ssrHasSameBehavior) {
if (ssrHasSameBehaviorExceptWarnings) {
descriptions.push('ssr warning');
} else {
descriptions.push('ssr mismatch');
}}log +=
`| \`${attribute.name}=(${type.name})\`` +
`| (${descriptions.join(', ')})` +
`| \`${canonicalResult || ''}\` |\n`;
}log += '\n';
}const blob = new Blob([log], {type: 'text/plain;charset=utf-8'});
FileSaver.saveAs(blob, 'AttributeTableSnapshot.md');
};render() {
if (!this.state.table) {
return (
<div>
<h1>Loading...</h1>
{!useFastMode && (<h3>The progress is reported in the window title.</h3>
)}</div>
);
}return (
<div>
<div>
<select value={this.state.sortOrder} onChange={this.onUpdateSort}>
<option value={ALPHABETICAL}>alphabetical</option>
<option value={REV_ALPHABETICAL}>reverse alphabetical</option>
<option value={GROUPED_BY_ROW_PATTERN}>
grouped by row pattern :)
</option>
</select>
<select value={this.state.filter} onChange={this.onUpdateFilter}>
<option value={ALL}>all</option>
<option value={INCOMPLETE}>incomplete</option>
<option value={COMPLETE}>complete</option>
</select>
<button style={{marginLeft: '10px'}} onClick={this.handleSaveClick}>
Save latest results to a file{' '}
<span role="img" aria-label="Save">
💾</span>
</button>
</div>
<AutoSizer disableHeight={true}>{({width}) => (<MultiGridref={input => {this.grid = input;
}}cellRenderer={this.renderCell}
columnWidth={200}
columnCount={1 + types.length}
fixedColumnCount={1}
enableFixedColumnScroll={true}enableFixedRowScroll={true}height={1200}
rowHeight={40}
rowCount={this.attributes.length + 1}
fixedRowCount={1}
width={width}/>)}</AutoSizer>
</div>
);}}export default App;