react/scripts/jest/setupTests.js

273 lines
9.0 KiB
JavaScript

'use strict';
const chalk = require('chalk');
const util = require('util');
const shouldIgnoreConsoleError = require('./shouldIgnoreConsoleError');
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('../error-codes/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({
...require('./matchers/interactionTracingMatchers'),
...require('./matchers/profilerMatchers'),
...require('./matchers/toWarnDev'),
...require('./matchers/reactTestMatchers'),
});
// 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(format, ...args) {
// Ignore uncaught errors reported by jsdom
// and React addendums because they're too noisy.
if (methodName === 'error' && shouldIgnoreConsoleError(format, args)) {
return;
}
// 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),
util.format(format, ...args),
]);
};
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')}`
);
let expectedMatcher;
switch (methodName) {
case 'warn':
expectedMatcher = 'toWarnDev';
break;
case 'error':
expectedMatcher = 'toErrorDev';
break;
default:
throw new Error('No matcher for ' + methodName);
}
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('.' + expectedMatcher + '()')} ` +
`matcher, 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.
// 1. `ErrorProxy` decodes error messages at Error construction time and
// also proxies error instances with `proxyErrorInstance`.
// 2. `proxyErrorInstance` decodes error messages when the `message`
// property is changed.
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;
// V8's Error.captureStackTrace (used in Jest) fails if the error object is
// a Proxy, so we need to pass it the unproxied instance.
const originalErrorInstances = new WeakMap();
const captureStackTrace = function(error, ...args) {
return OriginalError.captureStackTrace.call(
this,
originalErrorInstances.get(error) ||
// Sometimes this wrapper receives an already-unproxied instance.
error,
...args
);
};
const proxyErrorInstance = error => {
const proxy = new Proxy(error, {
set(target, key, value, receiver) {
if (key === 'message') {
return Reflect.set(
target,
key,
decodeErrorMessage(value),
receiver
);
}
return Reflect.set(target, key, value, receiver);
},
});
originalErrorInstances.set(proxy, error);
return proxy;
};
const ErrorProxy = new Proxy(OriginalError, {
apply(target, thisArg, argumentsList) {
const error = Reflect.apply(target, thisArg, argumentsList);
error.message = decodeErrorMessage(error.message);
return proxyErrorInstance(error);
},
construct(target, argumentsList, newTarget) {
const error = Reflect.construct(target, argumentsList, newTarget);
error.message = decodeErrorMessage(error.message);
return proxyErrorInstance(error);
},
get(target, key, receiver) {
if (key === 'captureStackTrace') {
return captureStackTrace;
}
return Reflect.get(target, key, receiver);
},
});
ErrorProxy.OriginalError = OriginalError;
global.Error = ErrorProxy;
}
const expectExperimentalToFail = async callback => {
if (callback.length > 0) {
throw Error(
'Experimental test helpers do not support `done` callback. Return a ' +
'promise instead.'
);
}
try {
const maybePromise = callback();
if (
maybePromise !== undefined &&
maybePromise !== null &&
typeof maybePromise.then === 'function'
) {
await maybePromise;
}
} catch (error) {
// Failed as expected
return;
}
throw Error(
'Tests marked experimental are expected to fail, but this one passed.'
);
};
const it = global.it;
const fit = global.fit;
const xit = global.xit;
if (__EXPERIMENTAL__) {
it.experimental = it;
fit.experimental = it.only.experimental = it.experimental.only = fit;
xit.experimental = it.skip.experimental = it.experimental.skip = xit;
} else {
it.experimental = (message, callback) => {
it(`[EXPERIMENTAL, SHOULD FAIL] ${message}`, () =>
expectExperimentalToFail(callback));
};
fit.experimental = it.only.experimental = it.experimental.only = (
message,
callback
) => {
fit(`[EXPERIMENTAL, SHOULD FAIL] ${message}`, () =>
expectExperimentalToFail(callback));
};
xit.experimental = it.skip.experimental = it.experimental.skip = (
message,
callback
) => {
xit(`[EXPERIMENTAL, SHOULD FAIL] ${message}`, () =>
expectExperimentalToFail(callback));
};
}
require('jasmine-check').install();
}