@samitouri / QOS-React-1 / commits / a5aedd1e15

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