mirror of
https://github.com/facebookarchive/prepack.git
synced 2024-10-26 23:32:02 +03:00
5159b0d832
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
314 lines
9.2 KiB
JavaScript
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;
|