Move console mocks to internal-test-utils (#28710)
Moving this to `internal-test-utils` so I can add helpers in the next PR for: - assertLogDev - assertWarnDev - assertErrorDev Which will be exported from `internal-test-utils`. This isn't strictly necessary, but it makes the factoring nicer, so internal-test-until doesn't need to depend on `scripts/jest`.
Ricky committed
Apr 3, 2024 at 16:02 UTC
a5aedd1e15733311d64ccab3fdd14db630067496
7 files changed
+294
-128
packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js
+147
@@ -12,6 +12,7 @@
12
13
const React = require('react');
14
const {startTransition, useDeferredValue} = React;
15
+const chalk = require('chalk');
16
const ReactNoop = require('react-noop-renderer');
17
const {
18
waitFor,
@@ -22,6 +23,11 @@ const {
23
} = require('internal-test-utils');
24
const act = require('internal-test-utils').act;
25
const Scheduler = require('scheduler/unstable_mock');
26
+const {
27
+ flushAllUnexpectedConsoleCalls,
28
+ resetAllUnexpectedConsoleCalls,
29
+ patchConsoleMethods,
30
+} = require('../consoleMock');
31
32
describe('ReactInternalTestUtils', () => {
33
test('waitFor', async () => {
@@ -154,3 +160,144 @@ describe('ReactInternalTestUtils', () => {
160
assertLog(['A', 'B', 'C']);
161
});
162
});
163
+
164
+describe('ReactInternalTestUtils console mocks', () => {
165
+ beforeEach(() => {
166
+ jest.resetAllMocks();
167
+ patchConsoleMethods({includeLog: true});
168
+ });
169
+
170
+ afterEach(() => {
171
+ resetAllUnexpectedConsoleCalls();
172
+ jest.resetAllMocks();
173
+ });
174
+
175
+ describe('console.log', () => {
176
+ it('should fail if not asserted', () => {
177
+ expect(() => {
178
+ console.log('hit');
179
+ flushAllUnexpectedConsoleCalls();
180
+ }).toThrow(`Expected test not to call ${chalk.bold('console.log()')}.`);
181
+ });
182
+
183
+ // @gate __DEV__
184
+ it('should not fail if mocked with spyOnDev', () => {
185
+ spyOnDev(console, 'log').mockImplementation(() => {});
186
+ expect(() => {
187
+ console.log('hit');
188
+ flushAllUnexpectedConsoleCalls();
189
+ }).not.toThrow();
190
+ });
191
+
192
+ // @gate !__DEV__
193
+ it('should not fail if mocked with spyOnProd', () => {
194
+ spyOnProd(console, 'log').mockImplementation(() => {});
195
+ expect(() => {
196
+ console.log('hit');
197
+ flushAllUnexpectedConsoleCalls();
198
+ }).not.toThrow();
199
+ });
200
+
201
+ it('should not fail if mocked with spyOnDevAndProd', () => {
202
+ spyOnDevAndProd(console, 'log').mockImplementation(() => {});
203
+ expect(() => {
204
+ console.log('hit');
205
+ flushAllUnexpectedConsoleCalls();
206
+ }).not.toThrow();
207
+ });
208
+
209
+ // @gate __DEV__
210
+ it('should not fail with toLogDev', () => {
211
+ expect(() => {
212
+ console.log('hit');
213
+ flushAllUnexpectedConsoleCalls();
214
+ }).toLogDev(['hit']);
215
+ });
216
+ });
217
+
218
+ describe('console.warn', () => {
219
+ it('should fail if not asserted', () => {
220
+ expect(() => {
221
+ console.warn('hit');
222
+ flushAllUnexpectedConsoleCalls();
223
+ }).toThrow(`Expected test not to call ${chalk.bold('console.warn()')}.`);
224
+ });
225
+
226
+ // @gate __DEV__
227
+ it('should not fail if mocked with spyOnDev', () => {
228
+ spyOnDev(console, 'warn').mockImplementation(() => {});
229
+ expect(() => {
230
+ console.warn('hit');
231
+ flushAllUnexpectedConsoleCalls();
232
+ }).not.toThrow();
233
+ });
234
+
235
+ // @gate !__DEV__
236
+ it('should not fail if mocked with spyOnProd', () => {
237
+ spyOnProd(console, 'warn').mockImplementation(() => {});
238
+ expect(() => {
239
+ console.warn('hit');
240
+ flushAllUnexpectedConsoleCalls();
241
+ }).not.toThrow();
242
+ });
243
+
244
+ it('should not fail if mocked with spyOnDevAndProd', () => {
245
+ spyOnDevAndProd(console, 'warn').mockImplementation(() => {});
246
+ expect(() => {
247
+ console.warn('hit');
248
+ flushAllUnexpectedConsoleCalls();
249
+ }).not.toThrow();
250
+ });
251
+
252
+ // @gate __DEV__
253
+ it('should not fail with toWarnDev', () => {
254
+ expect(() => {
255
+ console.warn('hit');
256
+ flushAllUnexpectedConsoleCalls();
257
+ }).toWarnDev(['hit'], {withoutStack: true});
258
+ });
259
+ });
260
+
261
+ describe('console.error', () => {
262
+ it('should fail if console.error is not asserted', () => {
263
+ expect(() => {
264
+ console.error('hit');
265
+ flushAllUnexpectedConsoleCalls();
266
+ }).toThrow(`Expected test not to call ${chalk.bold('console.error()')}.`);
267
+ });
268
+
269
+ // @gate __DEV__
270
+ it('should not fail if mocked with spyOnDev', () => {
271
+ spyOnDev(console, 'error').mockImplementation(() => {});
272
+ expect(() => {
273
+ console.error('hit');
274
+ flushAllUnexpectedConsoleCalls();
275
+ }).not.toThrow();
276
+ });
277
+
278
+ // @gate !__DEV__
279
+ it('should not fail if mocked with spyOnProd', () => {
280
+ spyOnProd(console, 'error').mockImplementation(() => {});
281
+ expect(() => {
282
+ console.error('hit');
283
+ flushAllUnexpectedConsoleCalls();
284
+ }).not.toThrow();
285
+ });
286
+
287
+ it('should not fail if mocked with spyOnDevAndProd', () => {
288
+ spyOnDevAndProd(console, 'error').mockImplementation(() => {});
289
+ expect(() => {
290
+ console.error('hit');
291
+ flushAllUnexpectedConsoleCalls();
292
+ }).not.toThrow();
293
+ });
294
+
295
+ // @gate __DEV__
296
+ it('should not fail with toErrorDev', () => {
297
+ expect(() => {
298
+ console.error('hit');
299
+ flushAllUnexpectedConsoleCalls();
300
+ }).toErrorDev(['hit'], {withoutStack: true});
301
+ });
302
+ });
303
+});
packages/internal-test-utils/consoleMock.js
new
+136
@@ -0,0 +1,136 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+/* eslint-disable react-internal/no-production-logging */
9
+const chalk = require('chalk');
10
+const util = require('util');
11
+const shouldIgnoreConsoleError = require('./shouldIgnoreConsoleError');
12
+const shouldIgnoreConsoleWarn = require('./shouldIgnoreConsoleWarn');
13
+
14
+const unexpectedErrorCallStacks = [];
15
+const unexpectedWarnCallStacks = [];
16
+const unexpectedLogCallStacks = [];
17
+
18
+// TODO: Consider consolidating this with `yieldValue`. In both cases, tests
19
+// should not be allowed to exit without asserting on the entire log.
20
+const patchConsoleMethod = (methodName, unexpectedConsoleCallStacks) => {
21
+ const newMethod = function (format, ...args) {
22
+ // Ignore uncaught errors reported by jsdom
23
+ // and React addendums because they're too noisy.
24
+ if (shouldIgnoreConsoleError(format, args)) {
25
+ return;
26
+ }
27
+
28
+ // Ignore certain React warnings causing test failures
29
+ if (methodName === 'warn' && shouldIgnoreConsoleWarn(format)) {
30
+ return;
31
+ }
32
+
33
+ // Capture the call stack now so we can warn about it later.
34
+ // The call stack has helpful information for the test author.
35
+ // Don't throw yet though b'c it might be accidentally caught and suppressed.
36
+ const stack = new Error().stack;
37
+ unexpectedConsoleCallStacks.push([
38
+ stack.slice(stack.indexOf('\n') + 1),
39
+ util.format(format, ...args),
40
+ ]);
41
+ };
42
+
43
+ console[methodName] = newMethod;
44
+
45
+ return newMethod;
46
+};
47
+
48
+const flushUnexpectedConsoleCalls = (
49
+ mockMethod,
50
+ methodName,
51
+ expectedMatcher,
52
+ unexpectedConsoleCallStacks,
53
+) => {
54
+ if (
55
+ console[methodName] !== mockMethod &&
56
+ !jest.isMockFunction(console[methodName])
57
+ ) {
58
+ // throw new Error(
59
+ // `Test did not tear down console.${methodName} mock properly.`
60
+ // );
61
+ }
62
+ if (unexpectedConsoleCallStacks.length > 0) {
63
+ const messages = unexpectedConsoleCallStacks.map(
64
+ ([stack, message]) =>
65
+ `${chalk.red(message)}\n` +
66
+ `${stack
67
+ .split('\n')
68
+ .map(line => chalk.gray(line))
69
+ .join('\n')}`,
70
+ );
71
+
72
+ const type = methodName === 'log' ? 'log' : 'warning';
73
+ const message =
74
+ `Expected test not to call ${chalk.bold(
75
+ `console.${methodName}()`,
76
+ )}.\n\n` +
77
+ `If the ${type} is expected, test for it explicitly by:\n` +
78
+ `1. Using the ${chalk.bold('.' + expectedMatcher + '()')} ` +
79
+ `matcher, or...\n` +
80
+ `2. Mock it out using ${chalk.bold(
81
+ 'spyOnDev',
82
+ )}(console, '${methodName}') or ${chalk.bold(
83
+ 'spyOnProd',
84
+ )}(console, '${methodName}'), and test that the ${type} occurs.`;
85
+
86
+ throw new Error(`${message}\n\n${messages.join('\n\n')}`);
87
+ }
88
+};
89
+
90
+let errorMethod;
91
+let warnMethod;
92
+let logMethod;
93
+export function patchConsoleMethods({includeLog} = {includeLog: false}) {
94
+ errorMethod = patchConsoleMethod('error', unexpectedErrorCallStacks);
95
+ warnMethod = patchConsoleMethod('warn', unexpectedWarnCallStacks);
96
+
97
+ // Only assert console.log isn't called in CI so you can debug tests in DEV.
98
+ // The matchers will still work in DEV, so you can assert locally.
99
+ if (includeLog) {
100
+ logMethod = patchConsoleMethod('log', unexpectedLogCallStacks);
101
+ }
102
+}
103
+
104
+export function flushAllUnexpectedConsoleCalls() {
105
+ flushUnexpectedConsoleCalls(
106
+ errorMethod,
107
+ 'error',
108
+ 'toErrorDev',
109
+ unexpectedErrorCallStacks,
110
+ );
111
+ flushUnexpectedConsoleCalls(
112
+ warnMethod,
113
+ 'warn',
114
+ 'toWarnDev',
115
+ unexpectedWarnCallStacks,
116
+ );
117
+ if (logMethod) {
118
+ flushUnexpectedConsoleCalls(
119
+ logMethod,
120
+ 'log',
121
+ 'toLogDev',
122
+ unexpectedLogCallStacks,
123
+ );
124
+ unexpectedLogCallStacks.length = 0;
125
+ }
126
+ unexpectedErrorCallStacks.length = 0;
127
+ unexpectedWarnCallStacks.length = 0;
128
+}
129
+
130
+export function resetAllUnexpectedConsoleCalls() {
131
+ unexpectedErrorCallStacks.length = 0;
132
+ unexpectedWarnCallStacks.length = 0;
133
+ if (logMethod) {
134
+ unexpectedLogCallStacks.length = 0;
135
+ }
136
+}
packages/internal-test-utils/shouldIgnoreConsoleError.js
renamed
+2
-2
@@ -19,10 +19,10 @@ module.exports = function shouldIgnoreConsoleError(format, args) {
19
format.indexOf('ReactDOM.render was removed in React 19') !== -1 ||
20
format.indexOf('ReactDOM.hydrate was removed in React 19') !== -1 ||
21
format.indexOf(
22
- 'ReactDOM.render has not been supported since React 18'
22
+ 'ReactDOM.render has not been supported since React 18',
23
) !== -1 ||
24
format.indexOf(
25
- 'ReactDOM.hydrate has not been supported since React 18'
25
+ 'ReactDOM.hydrate has not been supported since React 18',
26
) !== -1
27
) {
28
// We haven't finished migrating our tests to use createRoot.
packages/internal-test-utils/shouldIgnoreConsoleWarn.js
renamed
packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js
+1
-1
@@ -10,7 +10,7 @@
10
'use strict';
11
12
const stream = require('stream');
13
-const shouldIgnoreConsoleError = require('../../../../../scripts/jest/shouldIgnoreConsoleError');
13
+const shouldIgnoreConsoleError = require('internal-test-utils/shouldIgnoreConsoleError');
14
15
module.exports = function (initModules) {
16
let ReactDOM;
scripts/jest/matchers/toWarnDev.js
+1
-1
@@ -2,7 +2,7 @@
2
3
const {diff: jestDiff} = require('jest-diff');
4
const util = require('util');
5
-const shouldIgnoreConsoleError = require('../shouldIgnoreConsoleError');
5
+const shouldIgnoreConsoleError = require('internal-test-utils/shouldIgnoreConsoleError');
6
7
function normalizeCodeLocInfo(str) {
8
if (typeof str !== 'string') {
scripts/jest/setupTests.js
+7
-124
@@ -1,10 +1,11 @@
1
'use strict';
2
3
-const chalk = require('chalk');
4
-const util = require('util');
5
-const shouldIgnoreConsoleError = require('./shouldIgnoreConsoleError');
6
-const shouldIgnoreConsoleWarn = require('./shouldIgnoreConsoleWarn');
3
const {getTestFlags} = require('./TestFlags');
4
+const {
5
+ flushAllUnexpectedConsoleCalls,
6
+ resetAllUnexpectedConsoleCalls,
7
+ patchConsoleMethods,
8
+} = require('internal-test-utils/consoleMock');
9
10
if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
11
// Inside the class equivalence tester, we have a custom environment, let's
@@ -62,126 +63,8 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
63
}
64
});
65
65
- // TODO: Consider consolidating this with `yieldValue`. In both cases, tests
66
- // should not be allowed to exit without asserting on the entire log.
67
- const patchConsoleMethod = (methodName, unexpectedConsoleCallStacks) => {
68
- const newMethod = function (format, ...args) {
69
- // Ignore uncaught errors reported by jsdom
70
- // and React addendums because they're too noisy.
71
- if (shouldIgnoreConsoleError(format, args)) {
72
- return;
73
- }
74
-
75
- // Ignore certain React warnings causing test failures
76
- if (methodName === 'warn' && shouldIgnoreConsoleWarn(format)) {
77
- return;
78
- }
79
-
80
- // Capture the call stack now so we can warn about it later.
81
- // The call stack has helpful information for the test author.
82
- // Don't throw yet though b'c it might be accidentally caught and suppressed.
83
- const stack = new Error().stack;
84
- unexpectedConsoleCallStacks.push([
85
- stack.slice(stack.indexOf('\n') + 1),
86
- util.format(format, ...args),
87
- ]);
88
- };
89
-
90
- console[methodName] = newMethod;
91
-
92
- return newMethod;
93
- };
94
-
95
- const flushUnexpectedConsoleCalls = (
96
- mockMethod,
97
- methodName,
98
- expectedMatcher,
99
- unexpectedConsoleCallStacks
100
- ) => {
101
- if (
102
- console[methodName] !== mockMethod &&
103
- !jest.isMockFunction(console[methodName])
104
- ) {
105
- // throw new Error(
106
- // `Test did not tear down console.${methodName} mock properly.`
107
- // );
108
- }
109
- if (unexpectedConsoleCallStacks.length > 0) {
110
- const messages = unexpectedConsoleCallStacks.map(
111
- ([stack, message]) =>
112
- `${chalk.red(message)}\n` +
113
- `${stack
114
- .split('\n')
115
- .map(line => chalk.gray(line))
116
- .join('\n')}`
117
- );
118
-
119
- const type = methodName === 'log' ? 'log' : 'warning';
120
- const message =
121
- `Expected test not to call ${chalk.bold(
122
- `console.${methodName}()`
123
- )}.\n\n` +
124
- `If the ${type} is expected, test for it explicitly by:\n` +
125
- `1. Using the ${chalk.bold('.' + expectedMatcher + '()')} ` +
126
- `matcher, or...\n` +
127
- `2. Mock it out using ${chalk.bold(
128
- 'spyOnDev'
129
- )}(console, '${methodName}') or ${chalk.bold(
130
- 'spyOnProd'
131
- )}(console, '${methodName}'), and test that the ${type} occurs.`;
132
-
133
- throw new Error(`${message}\n\n${messages.join('\n\n')}`);
134
- }
135
- };
136
-
137
- const unexpectedErrorCallStacks = [];
138
- const unexpectedWarnCallStacks = [];
139
- const unexpectedLogCallStacks = [];
140
-
141
- const errorMethod = patchConsoleMethod('error', unexpectedErrorCallStacks);
142
- const warnMethod = patchConsoleMethod('warn', unexpectedWarnCallStacks);
143
- let logMethod;
144
-
145
- // Only assert console.log isn't called in CI so you can debug tests in DEV.
146
- // The matchers will still work in DEV, so you can assert locally.
147
- if (process.env.CI) {
148
- logMethod = patchConsoleMethod('log', unexpectedLogCallStacks);
149
- }
150
-
151
- const flushAllUnexpectedConsoleCalls = () => {
152
- flushUnexpectedConsoleCalls(
153
- errorMethod,
154
- 'error',
155
- 'toErrorDev',
156
- unexpectedErrorCallStacks
157
- );
158
- flushUnexpectedConsoleCalls(
159
- warnMethod,
160
- 'warn',
161
- 'toWarnDev',
162
- unexpectedWarnCallStacks
163
- );
164
- if (logMethod) {
165
- flushUnexpectedConsoleCalls(
166
- logMethod,
167
- 'log',
168
- 'toLogDev',
169
- unexpectedLogCallStacks
170
- );
171
- unexpectedLogCallStacks.length = 0;
172
- }
173
- unexpectedErrorCallStacks.length = 0;
174
- unexpectedWarnCallStacks.length = 0;
175
- };
176
-
177
- const resetAllUnexpectedConsoleCalls = () => {
178
- unexpectedErrorCallStacks.length = 0;
179
- unexpectedWarnCallStacks.length = 0;
180
- if (logMethod) {
181
- unexpectedLogCallStacks.length = 0;
182
- }
183
- };
184
-
66
+ // Patch the console to assert that all console error/warn/log calls assert.
67
+ patchConsoleMethods({includeLog: !!process.env.CI});
68
beforeEach(resetAllUnexpectedConsoleCalls);
69
afterEach(flushAllUnexpectedConsoleCalls);
70