Skip to content

Commit

Permalink
Move console mocks to internal-test-utils (#28710)
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
rickhanlonii committed Apr 3, 2024
1 parent 20e710a commit a5aedd1
Show file tree
Hide file tree
Showing 7 changed files with 294 additions and 128 deletions.
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
File renamed without changes.
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

0 comments on commit a5aedd1

Please sign in to comment.