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