3
const chalk = require('chalk');
4
const util = require('util');
5
const shouldIgnoreConsoleError = require('./shouldIgnoreConsoleError');
6
+const {getTestFlags} = require('./TestFlags');
7
8
if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
9
// Inside the class equivalence tester, we have a custom environment, let's
67
}
68
});
69
69
- ['error', 'warn'].forEach(methodName => {
70
- const unexpectedConsoleCallStacks = [];
70
+ // TODO: Consider consolidating this with `yieldValue`. In both cases, tests
71
+ // should not be allowed to exit without asserting on the entire log.
72
+ const patchConsoleMethod = (methodName, unexpectedConsoleCallStacks) => {
73
const newMethod = function(format, ...args) {
74
// Ignore uncaught errors reported by jsdom
75
// and React addendums because they're too noisy.
89
90
console[methodName] = newMethod;
91
90
- env.beforeEach(() => {
91
- unexpectedConsoleCallStacks.length = 0;
92
- });
92
+ return newMethod;
93
+ };
94
94
- env.afterEach(() => {
95
- if (console[methodName] !== newMethod && !isSpy(console[methodName])) {
96
- throw new Error(
97
- `Test did not tear down console.${methodName} mock properly.`
98
- );
99
- }
95
+ const flushUnexpectedConsoleCalls = (
96
+ mockMethod,
97
+ methodName,
98
+ expectedMatcher,
99
+ unexpectedConsoleCallStacks
100
+ ) => {
101
+ if (console[methodName] !== mockMethod && !isSpy(console[methodName])) {
102
+ throw new Error(
103
+ `Test did not tear down console.${methodName} mock properly.`
104
+ );
105
+ }
106
+ if (unexpectedConsoleCallStacks.length > 0) {
107
+ const messages = unexpectedConsoleCallStacks.map(
108
+ ([stack, message]) =>
109
+ `${chalk.red(message)}\n` +
110
+ `${stack
111
+ .split('\n')
112
+ .map(line => chalk.gray(line))
113
+ .join('\n')}`
114
+ );
115
101
- if (unexpectedConsoleCallStacks.length > 0) {
102
- const messages = unexpectedConsoleCallStacks.map(
103
- ([stack, message]) =>
104
- `${chalk.red(message)}\n` +
105
- `${stack
106
- .split('\n')
107
- .map(line => chalk.gray(line))
108
- .join('\n')}`
109
- );
116
+ const message =
117
+ `Expected test not to call ${chalk.bold(
118
+ `console.${methodName}()`
119
+ )}.\n\n` +
120
+ 'If the warning is expected, test for it explicitly by:\n' +
121
+ `1. Using the ${chalk.bold('.' + expectedMatcher + '()')} ` +
122
+ `matcher, or...\n` +
123
+ `2. Mock it out using ${chalk.bold(
124
+ 'spyOnDev'
125
+ )}(console, '${methodName}') or ${chalk.bold(
126
+ 'spyOnProd'
127
+ )}(console, '${methodName}'), and test that the warning occurs.`;
128
111
- let expectedMatcher;
112
- switch (methodName) {
113
- case 'warn':
114
- expectedMatcher = 'toWarnDev';
115
- break;
116
- case 'error':
117
- expectedMatcher = 'toErrorDev';
118
- break;
119
- default:
120
- throw new Error('No matcher for ' + methodName);
121
- }
122
- const message =
123
- `Expected test not to call ${chalk.bold(
124
- `console.${methodName}()`
125
- )}.\n\n` +
126
- 'If the warning is expected, test for it explicitly by:\n' +
127
- `1. Using the ${chalk.bold('.' + expectedMatcher + '()')} ` +
128
- `matcher, or...\n` +
129
- `2. Mock it out using ${chalk.bold(
130
- 'spyOnDev'
131
- )}(console, '${methodName}') or ${chalk.bold(
132
- 'spyOnProd'
133
- )}(console, '${methodName}'), and test that the warning occurs.`;
129
+ throw new Error(`${message}\n\n${messages.join('\n\n')}`);
130
+ }
131
+ };
132
135
- throw new Error(`${message}\n\n${messages.join('\n\n')}`);
136
- }
137
- });
138
- });
133
+ const unexpectedErrorCallStacks = [];
134
+ const unexpectedWarnCallStacks = [];
135
+
136
+ const errorMethod = patchConsoleMethod('error', unexpectedErrorCallStacks);
137
+ const warnMethod = patchConsoleMethod('warn', unexpectedWarnCallStacks);
138
+
139
+ const flushAllUnexpectedConsoleCalls = () => {
140
+ flushUnexpectedConsoleCalls(
141
+ errorMethod,
142
+ 'error',
143
+ 'toErrorDev',
144
+ unexpectedErrorCallStacks
145
+ );
146
+ flushUnexpectedConsoleCalls(
147
+ warnMethod,
148
+ 'warn',
149
+ 'toWarnDev',
150
+ unexpectedWarnCallStacks
151
+ );
152
+ unexpectedErrorCallStacks.length = 0;
153
+ unexpectedWarnCallStacks.length = 0;
154
+ };
155
+
156
+ const resetAllUnexpectedConsoleCalls = () => {
157
+ unexpectedErrorCallStacks.length = 0;
158
+ unexpectedWarnCallStacks.length = 0;
159
+ };
160
+
161
+ env.beforeEach(resetAllUnexpectedConsoleCalls);
162
+ env.afterEach(flushAllUnexpectedConsoleCalls);
163
164
if (process.env.NODE_ENV === 'production') {
165
// In production, we strip error messages and turn them into codes.
239
global.Error = ErrorProxy;
240
}
241
218
- const expectExperimentalToFail = async callback => {
242
+ const expectTestToFail = async (callback, errorMsg) => {
243
if (callback.length > 0) {
244
throw Error(
221
- 'Experimental test helpers do not support `done` callback. Return a ' +
245
+ 'Gated test helpers do not support the `done` callback. Return a ' +
246
'promise instead.'
247
);
248
}
255
) {
256
await maybePromise;
257
}
258
+ // Flush unexpected console calls inside the test itself, instead of in
259
+ // `afterEach` like we normally do. `afterEach` is too late because if it
260
+ // throws, we won't have captured it.
261
+ flushAllUnexpectedConsoleCalls();
262
} catch (error) {
263
// Failed as expected
264
+ resetAllUnexpectedConsoleCalls();
265
return;
266
}
238
- throw Error(
239
- 'Tests marked experimental are expected to fail, but this one passed.'
240
- );
267
+ throw Error(errorMsg);
268
};
269
270
+ // TODO: Deprecate these helpers in favor of @gate pragma
271
const it = global.it;
272
const fit = global.fit;
273
const xit = global.xit;
276
fit.experimental = it.only.experimental = it.experimental.only = fit;
277
xit.experimental = it.skip.experimental = it.experimental.skip = xit;
278
} else {
279
+ const errorMessage =
280
+ 'Tests marked experimental are expected to fail, but this one passed.';
281
it.experimental = (message, callback) => {
282
it(`[EXPERIMENTAL, SHOULD FAIL] ${message}`, () =>
253
- expectExperimentalToFail(callback));
283
+ expectTestToFail(callback, errorMessage));
284
};
285
fit.experimental = it.only.experimental = it.experimental.only = (
286
message,
287
callback
288
) => {
289
fit(`[EXPERIMENTAL, SHOULD FAIL] ${message}`, () =>
260
- expectExperimentalToFail(callback));
290
+ expectTestToFail(callback, errorMessage));
291
};
292
xit.experimental = it.skip.experimental = it.experimental.skip = (
293
message,
294
callback
295
) => {
296
xit(`[EXPERIMENTAL, SHOULD FAIL] ${message}`, () =>
267
- expectExperimentalToFail(callback));
297
+ expectTestToFail(callback, errorMessage));
298
};
299
}
300
301
+ const gatedErrorMessage = 'Gated test was expected to fail, but it passed.';
302
+ global._test_gate = (gateFn, testName, callback) => {
303
+ let shouldPass;
304
+ try {
305
+ const flags = getTestFlags();
306
+ shouldPass = gateFn(flags);
307
+ } catch (e) {
308
+ test(testName, () => {
309
+ throw e;
310
+ });
311
+ return;
312
+ }
313
+ if (shouldPass) {
314
+ test(testName, callback);
315
+ } else {
316
+ test(`[GATED, SHOULD FAIL] ${testName}`, () =>
317
+ expectTestToFail(callback, gatedErrorMessage));
318
+ }
319
+ };
320
+ global._test_gate_focus = (gateFn, testName, callback) => {
321
+ let shouldPass;
322
+ try {
323
+ const flags = getTestFlags();
324
+ shouldPass = gateFn(flags);
325
+ } catch (e) {
326
+ test.only(testName, () => {
327
+ throw e;
328
+ });
329
+ return;
330
+ }
331
+ if (shouldPass) {
332
+ test.only(testName, callback);
333
+ } else {
334
+ test.only(`[GATED, SHOULD FAIL] ${testName}`, () =>
335
+ expectTestToFail(callback, gatedErrorMessage));
336
+ }
337
+ };
338
+
339
+ // Dynamic version of @gate pragma
340
+ global.gate = fn => {
341
+ const flags = getTestFlags();
342
+ return fn(flags);
343
+ };
344
+
345
require('jasmine-check').install();
346
}