Skip to content

Commit d253d71

Browse files
rickhanloniiAndyPengc12
authored andcommitted
Move console mocks to internal-test-utils (facebook#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`.
1 parent cf9a4f1 commit d253d71

File tree

7 files changed

+294
-128
lines changed

7 files changed

+294
-128
lines changed

packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
const React = require('react');
1414
const {startTransition, useDeferredValue} = React;
15+
const chalk = require('chalk');
1516
const ReactNoop = require('react-noop-renderer');
1617
const {
1718
waitFor,
@@ -22,6 +23,11 @@ const {
2223
} = require('internal-test-utils');
2324
const act = require('internal-test-utils').act;
2425
const Scheduler = require('scheduler/unstable_mock');
26+
const {
27+
flushAllUnexpectedConsoleCalls,
28+
resetAllUnexpectedConsoleCalls,
29+
patchConsoleMethods,
30+
} = require('../consoleMock');
2531

2632
describe('ReactInternalTestUtils', () => {
2733
test('waitFor', async () => {
@@ -154,3 +160,144 @@ describe('ReactInternalTestUtils', () => {
154160
assertLog(['A', 'B', 'C']);
155161
});
156162
});
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+
});
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

scripts/jest/shouldIgnoreConsoleError.js renamed to packages/internal-test-utils/shouldIgnoreConsoleError.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ module.exports = function shouldIgnoreConsoleError(format, args) {
1919
format.indexOf('ReactDOM.render was removed in React 19') !== -1 ||
2020
format.indexOf('ReactDOM.hydrate was removed in React 19') !== -1 ||
2121
format.indexOf(
22-
'ReactDOM.render has not been supported since React 18'
22+
'ReactDOM.render has not been supported since React 18',
2323
) !== -1 ||
2424
format.indexOf(
25-
'ReactDOM.hydrate has not been supported since React 18'
25+
'ReactDOM.hydrate has not been supported since React 18',
2626
) !== -1
2727
) {
2828
// We haven't finished migrating our tests to use createRoot.

packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
'use strict';
1111

1212
const stream = require('stream');
13-
const shouldIgnoreConsoleError = require('../../../../../scripts/jest/shouldIgnoreConsoleError');
13+
const shouldIgnoreConsoleError = require('internal-test-utils/shouldIgnoreConsoleError');
1414

1515
module.exports = function (initModules) {
1616
let ReactDOM;

scripts/jest/matchers/toWarnDev.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
const {diff: jestDiff} = require('jest-diff');
44
const util = require('util');
5-
const shouldIgnoreConsoleError = require('../shouldIgnoreConsoleError');
5+
const shouldIgnoreConsoleError = require('internal-test-utils/shouldIgnoreConsoleError');
66

77
function normalizeCodeLocInfo(str) {
88
if (typeof str !== 'string') {

0 commit comments

Comments
 (0)