prepack/test/react/setupReactTests.js
Dan Abramov 5159b0d832 Make React tests fast (#2187)
Summary:
Currently we have a single giant file with all tests, and a giant snapshot. This is both slow, and hard to work with and iterate on.

In this PR I will refactor our test setup.

- [x] Split it up into multiple files (gets the test running from 45s to 27s)
- [x] Run Prettier on test files
- [x] Split tests further for better performance
- [x] Make it possible to run one test file
- [x] Fix the issue with double test re-runs in watch mode on changes in the test file
- [x] Refactor error handling
- [x] Run Prettier on fixtures
- [x] Add a fast mode with `yarn test-react-fast <Filename>`
- [x] Fix double reruns on failure

Potential followups:
- [x] Figure out why test interruption broke (need https://github.com/facebook/jest/issues/6599 and https://github.com/facebook/jest/issues/6598 fixed)
- [x] Revisit weird things like `this['React']` assignment with a funny comment in every test
Closes https://github.com/facebook/prepack/pull/2187

Differential Revision: D8713639

Pulled By: gaearon

fbshipit-source-id: 5edbfa4e61610ecafff17c0e5e7f84d44cd51168
2018-07-02 11:25:58 -07:00

314 lines
9.2 KiB
JavaScript

/**
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
/* @flow */
let fs = require("fs");
let path = require("path");
let { prepackSources } = require("../../lib/prepack-node.js");
let babel = require("babel-core");
let React = require("react");
let ReactDOM = require("react-dom");
let ReactDOMServer = require("react-dom/server");
let PropTypes = require("prop-types");
let ReactRelay = require("react-relay");
let ReactTestRenderer = require("react-test-renderer");
let { mergeAdjacentJSONTextNodes } = require("../../lib/utils/json.js");
/* eslint-disable no-undef */
const { expect } = global;
// Patch console.error to reduce the noise
let originalConsoleError = global.console.error;
let excludeErrorsContaining = [
"Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null",
"Consider adding an error boundary to your tree to customize error handling behavior.",
"Warning:",
];
global.console.error = function(...args) {
let text = args[0];
if (typeof text === "string") {
for (let excludeError of excludeErrorsContaining) {
if (text.indexOf(excludeError) !== -1) {
return;
}
}
}
originalConsoleError.apply(this, args);
};
function cxShim(...args) {
let classNames = [];
for (let arg of args) {
if (typeof arg === "string") {
classNames.push(arg);
} else if (typeof arg === "object" && arg !== null) {
let keys = Object.keys(arg);
for (let key of keys) {
if (arg[key]) {
classNames.push(key);
}
}
}
}
return classNames.join(" ");
}
global.cx = cxShim;
function MockURI(url) {
this.url = url;
}
MockURI.prototype.addQueryData = function() {
this.url += "&queryData";
return this;
};
MockURI.prototype.makeString = function() {
return this.url;
};
function setupReactTests() {
function compileSourceWithPrepack(
source: string,
useJSXOutput: boolean,
diagnosticLog: mixed[],
shouldRecover: (errorCode: string) => boolean
): {|
compiledSource: string,
statistics: Object,
|} {
let code = `(function() {
// For some reason, the serializer fails if React is not
// reachable from a global in tests. This field isn't used.
// TODO: figure out why this is necessary in tests.
window.___unused_React = require('react');
${source}
})()`;
let prepackOptions = {
errorHandler: diag => {
diagnosticLog.push(diag);
if (diag.severity !== "Warning" && diag.severity !== "Information") {
if (shouldRecover(diag.errorCode)) {
return "Recover";
}
return "Fail";
}
return "Recover";
},
compatibility: "fb-www",
internalDebug: true,
serialize: true,
uniqueSuffix: "",
maxStackDepth: 100,
reactEnabled: true,
reactOutput: useJSXOutput ? "jsx" : "create-element",
reactOptimizeNestedFunctions: true,
inlineExpressions: true,
invariantLevel: 0,
stripFlow: true,
};
const serialized = prepackSources([{ filePath: "", fileContents: code, sourceMapContents: "" }], prepackOptions);
if (serialized == null || serialized.reactStatistics == null) {
throw new Error("React test runner failed during serialization");
}
return {
compiledSource: serialized.code,
statistics: serialized.reactStatistics,
};
}
function transpileSource(source) {
return babel.transform(source, {
presets: ["babel-preset-react"],
plugins: ["transform-object-rest-spread"],
}).code;
}
function runSource(source) {
let transformedSource = `
// Inject React since compiled JSX would reference it.
let React = require('react');
(function() {
${transpileSource(source)}
})();
`;
/* eslint-disable no-new-func */
let fn = new Function("require", "module", transformedSource);
let moduleShim = { exports: null };
let requireShim = name => {
switch (name) {
case "React":
case "react":
return React;
case "react-dom":
case "ReactDOM":
return ReactDOM;
case "react-dom/server":
case "ReactDOMServer":
return ReactDOMServer;
case "PropTypes":
case "prop-types":
return PropTypes;
case "RelayModern":
return ReactRelay;
case "cx":
return cxShim;
case "FBEnvironment":
return {};
case "URI":
return MockURI;
default:
throw new Error(`Unrecognized import: "${name}".`);
}
};
try {
// $FlowFixMe flow doesn't new Function
fn(requireShim, moduleShim);
} catch (e) {
console.error(transformedSource);
throw e;
}
return moduleShim.exports;
}
function stubReactRelay(f: Function) {
let oldReactRelay = ReactRelay;
ReactRelay = {
QueryRenderer(props) {
return props.render({ props: {}, error: null });
},
createFragmentContainer() {
return null;
},
graphql() {
return null;
},
};
try {
return f();
} finally {
ReactRelay = oldReactRelay;
}
}
function runTestWithOptions(source, useJSXOutput, options, snapshotName) {
let {
firstRenderOnly = false,
// By default, we recover from PP0025 even though it's technically unsafe.
// We do the same in debug-fb-www script.
shouldRecover = errorCode => errorCode === "PP0025",
expectReconcilerError = false,
expectedCreateElementCalls,
data,
} = options;
let diagnosticLog = [];
let compiledSource, statistics;
try {
({ compiledSource, statistics } = compileSourceWithPrepack(source, useJSXOutput, diagnosticLog, shouldRecover));
} catch (err) {
if (err.__isReconcilerFatalError && expectReconcilerError) {
expect(err.message).toMatchSnapshot(snapshotName);
return;
}
diagnosticLog.forEach(diag => {
console.error(diag);
});
throw err;
}
let totalElementCount = 0;
let originalCreateElement = React.createElement;
// $FlowFixMe: intentional for this test
React.createElement = (...args) => {
totalElementCount++;
return originalCreateElement(...args);
};
try {
expect(statistics).toMatchSnapshot(snapshotName);
let A = runSource(source);
let B = runSource(compiledSource);
expect(typeof A).toBe(typeof B);
if (typeof A !== "function") {
// Test without exports just verifies that the file compiles.
return;
}
let config = {
createNodeMock(x) {
return x;
},
};
let rendererA = ReactTestRenderer.create(null, config);
let rendererB = ReactTestRenderer.create(null, config);
if (A == null || B == null) {
throw new Error("React test runner issue");
}
// Use the original version of the test in case transforming messes it up.
let { getTrials: getTrialsA, independent } = A;
let { getTrials: getTrialsB } = B;
// Run tests that assert the rendered output matches.
let resultA = getTrialsA(rendererA, A, data);
let resultB = independent ? getTrialsB(rendererB, B, data) : getTrialsA(rendererB, B, data);
// The test has returned many values for us to check
for (let i = 0; i < resultA.length; i++) {
let [nameA, valueA] = resultA[i];
let [nameB, valueB] = resultB[i];
if (typeof valueA === "string" && typeof valueB === "string") {
expect(valueA).toBe(valueB);
} else {
expect(mergeAdjacentJSONTextNodes(valueB, firstRenderOnly)).toEqual(
mergeAdjacentJSONTextNodes(valueA, firstRenderOnly)
);
}
expect(nameB).toEqual(nameA);
}
} finally {
// $FlowFixMe: intentional for this test
React.createElement = originalCreateElement;
}
if (typeof expectedCreateElementCalls === "number") {
// TODO: it would be nice to check original and prepacked ones separately.
expect(totalElementCount).toBe(expectedCreateElementCalls);
}
}
type TestOptions = {
firstRenderOnly?: boolean,
data?: mixed,
expectReconcilerError?: boolean,
expectedCreateElementCalls?: number,
shouldRecover?: (errorCode: string) => boolean,
};
function runTest(fixturePath: string, options: TestOptions = {}) {
let source = fs.readFileSync(fixturePath).toString();
// Run tests that don't need the transform first so they can fail early.
runTestWithOptions(source, false, options, "(createElement => createElement)");
if (process.env.SKIP_REACT_JSX_TESTS !== "true") {
runTestWithOptions(source, true, options, "(createElement => JSX)");
let jsxSource = transpileSource(source);
runTestWithOptions(jsxSource, false, options, "(JSX => createElement)");
runTestWithOptions(jsxSource, true, options, "(JSX => JSX)");
}
}
return {
runTest,
stubReactRelay,
};
}
module.exports = setupReactTests;