scripts/jest/setupTests.js
"use strict";
const chalk = require("chalk");
if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
// Inside the class equivalence tester, we have a custom environment, let's
// require that instead.
require("./spec-equivalence-reporter/setupTests.js");
} else {
const env = jasmine.getEnv();
const errorMap = require("./codes.json");
// TODO: Stop using spyOn in all the test since that seem deprecated.
// This is a legacy upgrade path strategy from:
// https://github.com/facebook/jest/blob/v20.0.4/packages/jest-matchers/src/spyMatchers.js#L160
const isSpy = spy => spy.calls && typeof spy.calls.count === "function";
const spyOn = global.spyOn;
const noop = function() {};
// Spying on console methods in production builds can mask errors.
// This is why we added an explicit spyOnDev() helper.
// It's too easy to accidentally use the more familiar spyOn() helper though,
// So we disable it entirely.
// Spying on both dev and prod will require using both spyOnDev() and spyOnProd().
global.spyOn = function() {
throw new Error(
"Do not use spyOn(). " +
"It can accidentally hide unexpected errors in production builds. " +
"Use spyOnDev(), spyOnProd(), or spyOnDevAndProd() instead."
);
};
if (process.env.NODE_ENV === "production") {
global.spyOnDev = noop;
global.spyOnProd = spyOn;
global.spyOnDevAndProd = spyOn;
} else {
global.spyOnDev = spyOn;
global.spyOnProd = noop;
global.spyOnDevAndProd = spyOn;
}
expect.extend(Object.assign({},require("./matchers/toWarnDev")));
// We have a Babel transform that inserts guards against infinite loops.
// If a loop runs for too many iterations, we throw an error and set this
// global variable. The global lets us detect an infinite loop even if
// the actual error object ends up being caught and ignored. An infinite
// loop must always fail the test!
env.beforeEach(() => {
global.infiniteLoopError = null;
});
env.afterEach(() => {
const error = global.infiniteLoopError;
global.infiniteLoopError = null;
if (error) {
throw error;
}
});
["error", "warn"].forEach(methodName => {
const unexpectedConsoleCallStacks = [];
const newMethod = function(message) {
// Capture the call stack now so we can warn about it later.
// The call stack has helpful information for the test author.
// Don't throw yet though b'c it might be accidentally caught and suppressed.
const stack = new Error().stack;
unexpectedConsoleCallStacks.push([
stack.substr(stack.indexOf("\n") + 1),
message,
]);
};
console[methodName] = newMethod;
env.beforeEach(() => {
unexpectedConsoleCallStacks.length = 0;
});
env.afterEach(() => {
if (console[methodName] !== newMethod && !isSpy(console[methodName])) {
throw new Error(
`Test did not tear down console.${methodName} mock properly.`
);
}
if (unexpectedConsoleCallStacks.length > 0) {
const messages = unexpectedConsoleCallStacks.map(
([stack, message]) =>
`${chalk.red(message)}\n` +
`${stack
.split("\n")
.map(line => chalk.gray(line))
.join("\n")}`
);
const message =
`Expected test not to call ${chalk.bold(
`console.${methodName}()`
)}.\n\n` +
"If the warning is expected, test for it explicitly by:\n" +
`1. Using the ${chalk.bold(".toWarnDev()")} / ${chalk.bold(
".toLowPriorityWarnDev()"
)} matchers, or...\n` +
`2. Mock it out using ${chalk.bold("spyOnDev")}(console, '${
methodName
}') or ${chalk.bold("spyOnProd")}(console, '${
methodName
}'), and test that the warning occurs.`;
throw new Error(`${message}\n\n${messages.join("\n\n")}`);
}
});
});
if (process.env.NODE_ENV === "production") {
// In production, we strip error messages and turn them into codes.
// This decodes them back so that the test assertions on them work.
const decodeErrorMessage = function(message) {
if (!message) {
return message;
}
const re = /error-decoder.html\?invariant=(\d+)([^\s]*)/;
const matches = message.match(re);
if (!matches || matches.length !== 3) {
return message;
}
const code = parseInt(matches[1], 10);
const args = matches[2]
.split("&")
.filter(s => s.startsWith("args[]="))
.map(s => s.substr("args[]=".length))
.map(decodeURIComponent);
const format = errorMap[code];
let argIndex = 0;
return format.replace(/%s/g, () => args[argIndex++]);
};
const OriginalError = global.Error;
const ErrorProxy = new Proxy(OriginalError, {
apply(target, thisArg, argumentsList) {
const error = Reflect.apply(target, thisArg, argumentsList);
error.message = decodeErrorMessage(error.message);
return error;
},
construct(target, argumentsList, newTarget) {
const error = Reflect.construct(target, argumentsList, newTarget);
error.message = decodeErrorMessage(error.message);
return error;
},
});
ErrorProxy.OriginalError = OriginalError;
global.Error = ErrorProxy;
}
require("jasmine-check").install();
}