Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move console mocks to internal-test-utils #28710

Merged
merged 1 commit into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

const React = require('react');
const {startTransition, useDeferredValue} = React;
const chalk = require('chalk');
const ReactNoop = require('react-noop-renderer');
const {
waitFor,
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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});
});
});
});
136 changes: 136 additions & 0 deletions packages/internal-test-utils/consoleMock.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion scripts/jest/matchers/toWarnDev.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
Loading