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`.
This commit is contained in:
Ricky
2024-04-03 16:02:04 -04:00
committed by GitHub
parent 20e710aeab
commit a5aedd1e15
7 changed files with 294 additions and 128 deletions

View File

@@ -12,6 +12,7 @@
const React = require('react');
const {startTransition, useDeferredValue} = React;
const chalk = require('chalk');
const ReactNoop = require('react-noop-renderer');
const {
waitFor,
@@ -22,6 +23,11 @@ const {
} = require('internal-test-utils');
const act = require('internal-test-utils').act;
const Scheduler = require('scheduler/unstable_mock');
const {
flushAllUnexpectedConsoleCalls,
resetAllUnexpectedConsoleCalls,
patchConsoleMethods,
} = require('../consoleMock');
describe('ReactInternalTestUtils', () => {
test('waitFor', async () => {
@@ -154,3 +160,144 @@ describe('ReactInternalTestUtils', () => {
assertLog(['A', 'B', 'C']);
});
});
describe('ReactInternalTestUtils console mocks', () => {
beforeEach(() => {
jest.resetAllMocks();
patchConsoleMethods({includeLog: true});
});
afterEach(() => {
resetAllUnexpectedConsoleCalls();
jest.resetAllMocks();
});
describe('console.log', () => {
it('should fail if not asserted', () => {
expect(() => {
console.log('hit');
flushAllUnexpectedConsoleCalls();
}).toThrow(`Expected test not to call ${chalk.bold('console.log()')}.`);
});
// @gate __DEV__
it('should not fail if mocked with spyOnDev', () => {
spyOnDev(console, 'log').mockImplementation(() => {});
expect(() => {
console.log('hit');
flushAllUnexpectedConsoleCalls();
}).not.toThrow();
});
// @gate !__DEV__
it('should not fail if mocked with spyOnProd', () => {
spyOnProd(console, 'log').mockImplementation(() => {});
expect(() => {
console.log('hit');
flushAllUnexpectedConsoleCalls();
}).not.toThrow();
});
it('should not fail if mocked with spyOnDevAndProd', () => {
spyOnDevAndProd(console, 'log').mockImplementation(() => {});
expect(() => {
console.log('hit');
flushAllUnexpectedConsoleCalls();
}).not.toThrow();
});
// @gate __DEV__
it('should not fail with toLogDev', () => {
expect(() => {
console.log('hit');
flushAllUnexpectedConsoleCalls();
}).toLogDev(['hit']);
});
});
describe('console.warn', () => {
it('should fail if not asserted', () => {
expect(() => {
console.warn('hit');
flushAllUnexpectedConsoleCalls();
}).toThrow(`Expected test not to call ${chalk.bold('console.warn()')}.`);
});
// @gate __DEV__
it('should not fail if mocked with spyOnDev', () => {
spyOnDev(console, 'warn').mockImplementation(() => {});
expect(() => {
console.warn('hit');
flushAllUnexpectedConsoleCalls();
}).not.toThrow();
});
// @gate !__DEV__
it('should not fail if mocked with spyOnProd', () => {
spyOnProd(console, 'warn').mockImplementation(() => {});
expect(() => {
console.warn('hit');
flushAllUnexpectedConsoleCalls();
}).not.toThrow();
});
it('should not fail if mocked with spyOnDevAndProd', () => {
spyOnDevAndProd(console, 'warn').mockImplementation(() => {});
expect(() => {
console.warn('hit');
flushAllUnexpectedConsoleCalls();
}).not.toThrow();
});
// @gate __DEV__
it('should not fail with toWarnDev', () => {
expect(() => {
console.warn('hit');
flushAllUnexpectedConsoleCalls();
}).toWarnDev(['hit'], {withoutStack: true});
});
});
describe('console.error', () => {
it('should fail if console.error is not asserted', () => {
expect(() => {
console.error('hit');
flushAllUnexpectedConsoleCalls();
}).toThrow(`Expected test not to call ${chalk.bold('console.error()')}.`);
});
// @gate __DEV__
it('should not fail if mocked with spyOnDev', () => {
spyOnDev(console, 'error').mockImplementation(() => {});
expect(() => {
console.error('hit');
flushAllUnexpectedConsoleCalls();
}).not.toThrow();
});
// @gate !__DEV__
it('should not fail if mocked with spyOnProd', () => {
spyOnProd(console, 'error').mockImplementation(() => {});
expect(() => {
console.error('hit');
flushAllUnexpectedConsoleCalls();
}).not.toThrow();
});
it('should not fail if mocked with spyOnDevAndProd', () => {
spyOnDevAndProd(console, 'error').mockImplementation(() => {});
expect(() => {
console.error('hit');
flushAllUnexpectedConsoleCalls();
}).not.toThrow();
});
// @gate __DEV__
it('should not fail with toErrorDev', () => {
expect(() => {
console.error('hit');
flushAllUnexpectedConsoleCalls();
}).toErrorDev(['hit'], {withoutStack: true});
});
});
});

View File

@@ -0,0 +1,136 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* eslint-disable react-internal/no-production-logging */
const chalk = require('chalk');
const util = require('util');
const shouldIgnoreConsoleError = require('./shouldIgnoreConsoleError');
const shouldIgnoreConsoleWarn = require('./shouldIgnoreConsoleWarn');
const unexpectedErrorCallStacks = [];
const unexpectedWarnCallStacks = [];
const unexpectedLogCallStacks = [];
// TODO: Consider consolidating this with `yieldValue`. In both cases, tests
// should not be allowed to exit without asserting on the entire log.
const patchConsoleMethod = (methodName, unexpectedConsoleCallStacks) => {
const newMethod = function (format, ...args) {
// Ignore uncaught errors reported by jsdom
// and React addendums because they're too noisy.
if (shouldIgnoreConsoleError(format, args)) {
return;
}
// Ignore certain React warnings causing test failures
if (methodName === 'warn' && shouldIgnoreConsoleWarn(format)) {
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.slice(stack.indexOf('\n') + 1),
util.format(format, ...args),
]);
};
console[methodName] = newMethod;
return newMethod;
};
const flushUnexpectedConsoleCalls = (
mockMethod,
methodName,
expectedMatcher,
unexpectedConsoleCallStacks,
) => {
if (
console[methodName] !== mockMethod &&
!jest.isMockFunction(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')}`,
);
const type = methodName === 'log' ? 'log' : 'warning';
const message =
`Expected test not to call ${chalk.bold(
`console.${methodName}()`,
)}.\n\n` +
`If the ${type} 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 ${type} occurs.`;
throw new Error(`${message}\n\n${messages.join('\n\n')}`);
}
};
let errorMethod;
let warnMethod;
let logMethod;
export function patchConsoleMethods({includeLog} = {includeLog: false}) {
errorMethod = patchConsoleMethod('error', unexpectedErrorCallStacks);
warnMethod = patchConsoleMethod('warn', unexpectedWarnCallStacks);
// Only assert console.log isn't called in CI so you can debug tests in DEV.
// The matchers will still work in DEV, so you can assert locally.
if (includeLog) {
logMethod = patchConsoleMethod('log', unexpectedLogCallStacks);
}
}
export function flushAllUnexpectedConsoleCalls() {
flushUnexpectedConsoleCalls(
errorMethod,
'error',
'toErrorDev',
unexpectedErrorCallStacks,
);
flushUnexpectedConsoleCalls(
warnMethod,
'warn',
'toWarnDev',
unexpectedWarnCallStacks,
);
if (logMethod) {
flushUnexpectedConsoleCalls(
logMethod,
'log',
'toLogDev',
unexpectedLogCallStacks,
);
unexpectedLogCallStacks.length = 0;
}
unexpectedErrorCallStacks.length = 0;
unexpectedWarnCallStacks.length = 0;
}
export function resetAllUnexpectedConsoleCalls() {
unexpectedErrorCallStacks.length = 0;
unexpectedWarnCallStacks.length = 0;
if (logMethod) {
unexpectedLogCallStacks.length = 0;
}
}

View File

@@ -19,10 +19,10 @@ module.exports = function shouldIgnoreConsoleError(format, args) {
format.indexOf('ReactDOM.render was removed in React 19') !== -1 ||
format.indexOf('ReactDOM.hydrate was removed in React 19') !== -1 ||
format.indexOf(
'ReactDOM.render has not been supported since React 18'
'ReactDOM.render has not been supported since React 18',
) !== -1 ||
format.indexOf(
'ReactDOM.hydrate has not been supported since React 18'
'ReactDOM.hydrate has not been supported since React 18',
) !== -1
) {
// We haven't finished migrating our tests to use createRoot.

View File

@@ -10,7 +10,7 @@
'use strict';
const stream = require('stream');
const shouldIgnoreConsoleError = require('../../../../../scripts/jest/shouldIgnoreConsoleError');
const shouldIgnoreConsoleError = require('internal-test-utils/shouldIgnoreConsoleError');
module.exports = function (initModules) {
let ReactDOM;

View File

@@ -2,7 +2,7 @@
const {diff: jestDiff} = require('jest-diff');
const util = require('util');
const shouldIgnoreConsoleError = require('../shouldIgnoreConsoleError');
const shouldIgnoreConsoleError = require('internal-test-utils/shouldIgnoreConsoleError');
function normalizeCodeLocInfo(str) {
if (typeof str !== 'string') {

View File

@@ -1,10 +1,11 @@
'use strict';
const chalk = require('chalk');
const util = require('util');
const shouldIgnoreConsoleError = require('./shouldIgnoreConsoleError');
const shouldIgnoreConsoleWarn = require('./shouldIgnoreConsoleWarn');
const {getTestFlags} = require('./TestFlags');
const {
flushAllUnexpectedConsoleCalls,
resetAllUnexpectedConsoleCalls,
patchConsoleMethods,
} = require('internal-test-utils/consoleMock');
if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
// Inside the class equivalence tester, we have a custom environment, let's
@@ -62,126 +63,8 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
}
});
// TODO: Consider consolidating this with `yieldValue`. In both cases, tests
// should not be allowed to exit without asserting on the entire log.
const patchConsoleMethod = (methodName, unexpectedConsoleCallStacks) => {
const newMethod = function (format, ...args) {
// Ignore uncaught errors reported by jsdom
// and React addendums because they're too noisy.
if (shouldIgnoreConsoleError(format, args)) {
return;
}
// Ignore certain React warnings causing test failures
if (methodName === 'warn' && shouldIgnoreConsoleWarn(format)) {
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.slice(stack.indexOf('\n') + 1),
util.format(format, ...args),
]);
};
console[methodName] = newMethod;
return newMethod;
};
const flushUnexpectedConsoleCalls = (
mockMethod,
methodName,
expectedMatcher,
unexpectedConsoleCallStacks
) => {
if (
console[methodName] !== mockMethod &&
!jest.isMockFunction(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')}`
);
const type = methodName === 'log' ? 'log' : 'warning';
const message =
`Expected test not to call ${chalk.bold(
`console.${methodName}()`
)}.\n\n` +
`If the ${type} 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 ${type} occurs.`;
throw new Error(`${message}\n\n${messages.join('\n\n')}`);
}
};
const unexpectedErrorCallStacks = [];
const unexpectedWarnCallStacks = [];
const unexpectedLogCallStacks = [];
const errorMethod = patchConsoleMethod('error', unexpectedErrorCallStacks);
const warnMethod = patchConsoleMethod('warn', unexpectedWarnCallStacks);
let logMethod;
// Only assert console.log isn't called in CI so you can debug tests in DEV.
// The matchers will still work in DEV, so you can assert locally.
if (process.env.CI) {
logMethod = patchConsoleMethod('log', unexpectedLogCallStacks);
}
const flushAllUnexpectedConsoleCalls = () => {
flushUnexpectedConsoleCalls(
errorMethod,
'error',
'toErrorDev',
unexpectedErrorCallStacks
);
flushUnexpectedConsoleCalls(
warnMethod,
'warn',
'toWarnDev',
unexpectedWarnCallStacks
);
if (logMethod) {
flushUnexpectedConsoleCalls(
logMethod,
'log',
'toLogDev',
unexpectedLogCallStacks
);
unexpectedLogCallStacks.length = 0;
}
unexpectedErrorCallStacks.length = 0;
unexpectedWarnCallStacks.length = 0;
};
const resetAllUnexpectedConsoleCalls = () => {
unexpectedErrorCallStacks.length = 0;
unexpectedWarnCallStacks.length = 0;
if (logMethod) {
unexpectedLogCallStacks.length = 0;
}
};
// Patch the console to assert that all console error/warn/log calls assert.
patchConsoleMethods({includeLog: !!process.env.CI});
beforeEach(resetAllUnexpectedConsoleCalls);
afterEach(flushAllUnexpectedConsoleCalls);