diff --git a/packages/internal-test-utils/ReactInternalTestUtils.js b/packages/internal-test-utils/ReactInternalTestUtils.js
index c6cb39fd73e08..4d2fa37890850 100644
--- a/packages/internal-test-utils/ReactInternalTestUtils.js
+++ b/packages/internal-test-utils/ReactInternalTestUtils.js
@@ -10,8 +10,14 @@ import {diff} from 'jest-diff';
import {equals} from '@jest/expect-utils';
import enqueueTask from './enqueueTask';
import simulateBrowserEventDispatch from './simulateBrowserEventDispatch';
-
+import {
+ clearLogs,
+ clearWarnings,
+ clearErrors,
+ createLogAssertion,
+} from './consoleMock';
export {act} from './internalAct';
+const {assertConsoleLogsCleared} = require('internal-test-utils/consoleMock');
import {thrownErrors, actingUpdatesScopeDepth} from './internalAct';
@@ -24,6 +30,7 @@ function assertYieldsWereCleared(caller) {
Error.captureStackTrace(error, caller);
throw error;
}
+ assertConsoleLogsCleared();
}
export async function waitForMicrotasks() {
@@ -317,6 +324,22 @@ ${diff(expectedLog, actualLog)}
throw error;
}
+export const assertConsoleLogDev = createLogAssertion(
+ 'log',
+ 'assertConsoleLogDev',
+ clearLogs,
+);
+export const assertConsoleWarnDev = createLogAssertion(
+ 'warn',
+ 'assertConsoleWarnDev',
+ clearWarnings,
+);
+export const assertConsoleErrorDev = createLogAssertion(
+ 'error',
+ 'assertConsoleErrorDev',
+ clearErrors,
+);
+
// Simulates dispatching events, waiting for microtasks in between.
// This matches the browser behavior, which will flush microtasks
// between each event handler. This will allow discrete events to
diff --git a/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js b/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js
index a721dde24acc4..af2a447f49819 100644
--- a/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js
+++ b/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js
@@ -11,6 +11,7 @@
'use strict';
const React = require('react');
+const stripAnsi = require('strip-ansi');
const {startTransition, useDeferredValue} = React;
const chalk = require('chalk');
const ReactNoop = require('react-noop-renderer');
@@ -28,6 +29,11 @@ const {
resetAllUnexpectedConsoleCalls,
patchConsoleMethods,
} = require('../consoleMock');
+const {
+ assertConsoleLogDev,
+ assertConsoleWarnDev,
+ assertConsoleErrorDev,
+} = require('../ReactInternalTestUtils');
describe('ReactInternalTestUtils', () => {
test('waitFor', async () => {
@@ -301,3 +307,2414 @@ describe('ReactInternalTestUtils console mocks', () => {
});
});
});
+
+// Helper method to capture assertion failure.
+const expectToThrowFailure = expectBlock => {
+ let caughtError;
+ try {
+ expectBlock();
+ } catch (error) {
+ caughtError = error;
+ }
+ expect(caughtError).toBeDefined();
+ return stripAnsi(caughtError.message);
+};
+
+// Helper method to capture assertion failure with act.
+const awaitExpectToThrowFailure = async expectBlock => {
+ let caughtError;
+ try {
+ await expectBlock();
+ } catch (error) {
+ caughtError = error;
+ }
+ expect(caughtError).toBeDefined();
+ return stripAnsi(caughtError.message);
+};
+
+describe('ReactInternalTestUtils console assertions', () => {
+ beforeAll(() => {
+ patchConsoleMethods({includeLog: true});
+ });
+
+ describe('assertConsoleLogDev', () => {
+ // @gate __DEV__
+ it('passes for a single log', () => {
+ console.log('Hello');
+ assertConsoleLogDev(['Hello']);
+ });
+
+ // @gate __DEV__
+ it('passes for multiple logs', () => {
+ console.log('Hello');
+ console.log('Good day');
+ console.log('Bye');
+ assertConsoleLogDev(['Hello', 'Good day', 'Bye']);
+ });
+
+ it('fails if act is called without assertConsoleLogDev', async () => {
+ const Yield = ({id}) => {
+ console.log(id);
+ return id;
+ };
+
+ function App() {
+ return (
+
+
+
+
+
+ );
+ }
+
+ const root = ReactNoop.createRoot();
+ await act(() => {
+ root.render();
+ });
+ const message = await awaitExpectToThrowFailure(async () => {
+ await act(() => {
+ root.render();
+ });
+ });
+
+ expect(message).toMatchInlineSnapshot(`
+ "asserConsoleLogsCleared(expected)
+
+ console.log was called without assertConsoleLogDev:
+ + A
+ + B
+ + C
+
+ You must call one of the assertConsoleDev helpers between each act call."
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if first expected log is not included', () => {
+ const message = expectToThrowFailure(() => {
+ console.log('Wow');
+ console.log('Bye');
+ assertConsoleLogDev(['Hi', 'Wow', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleLogDev(expected)
+
+ Unexpected log(s) recorded.
+
+ - Expected logs
+ + Received logs
+
+ - Hi
+ Wow
+ Bye"
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if middle expected log is not included', () => {
+ const message = expectToThrowFailure(() => {
+ console.log('Hi');
+ console.log('Bye');
+ assertConsoleLogDev(['Hi', 'Wow', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleLogDev(expected)
+
+ Unexpected log(s) recorded.
+
+ - Expected logs
+ + Received logs
+
+ Hi
+ - Wow
+ Bye"
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if last expected log is not included', () => {
+ const message = expectToThrowFailure(() => {
+ console.log('Hi');
+ console.log('Wow');
+ assertConsoleLogDev(['Hi', 'Wow', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleLogDev(expected)
+
+ Expected log was not recorded.
+
+ - Expected logs
+ + Received logs
+
+ Hi
+ Wow
+ - Bye"
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if first received log is not included', () => {
+ const message = expectToThrowFailure(() => {
+ console.log('Hi');
+ console.log('Wow');
+ console.log('Bye');
+ assertConsoleLogDev(['Wow', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleLogDev(expected)
+
+ Unexpected log(s) recorded.
+
+ - Expected logs
+ + Received logs
+
+ + Hi
+ Wow
+ Bye"
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if middle received log is not included', () => {
+ const message = expectToThrowFailure(() => {
+ console.log('Hi');
+ console.log('Wow');
+ console.log('Bye');
+ assertConsoleLogDev(['Hi', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleLogDev(expected)
+
+ Unexpected log(s) recorded.
+
+ - Expected logs
+ + Received logs
+
+ Hi
+ + Wow
+ Bye"
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if last received log is not included', () => {
+ const message = expectToThrowFailure(() => {
+ console.log('Hi');
+ console.log('Wow');
+ console.log('Bye');
+ assertConsoleLogDev(['Hi', 'Wow']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleLogDev(expected)
+
+ Unexpected log(s) recorded.
+
+ - Expected logs
+ + Received logs
+
+ Hi
+ Wow
+ + Bye"
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if both expected and received mismatch', () => {
+ const message = expectToThrowFailure(() => {
+ console.log('Hi');
+ console.log('Wow');
+ console.log('Bye');
+ assertConsoleLogDev(['Hi', 'Wow', 'Yikes']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleLogDev(expected)
+
+ Unexpected log(s) recorded.
+
+ - Expected logs
+ + Received logs
+
+ Hi
+ Wow
+ - Yikes
+ + Bye"
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if both expected and received mismatch with multiple lines', () => {
+ const message = expectToThrowFailure(() => {
+ console.log('Hi\nFoo');
+ console.log('Wow\nBar');
+ console.log('Bye\nBaz');
+ assertConsoleLogDev(['Hi\nFoo', 'Wow\nBar', 'Yikes\nFaz']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleLogDev(expected)
+
+ Unexpected log(s) recorded.
+
+ - Expected logs
+ + Received logs
+
+ Hi Foo
+ Wow Bar
+ - Yikes Faz
+ + Bye Baz"
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if local withoutStack passed to assertConsoleLogDev', () => {
+ const message = expectToThrowFailure(() => {
+ console.log('Hello');
+ assertConsoleLogDev([['Hello', {withoutStack: true}]]);
+ });
+
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleLogDev(expected)
+
+ Do not pass withoutStack to assertConsoleLogDev logs, console.log does not have component stacks."
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if global withoutStack passed to assertConsoleLogDev', () => {
+ const message = expectToThrowFailure(() => {
+ console.log('Hello');
+ assertConsoleLogDev(['Hello'], {withoutStack: true});
+ });
+
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleLogDev(expected)
+
+ Do not pass withoutStack to assertConsoleLogDev, console.log does not have component stacks."
+ `);
+
+ assertConsoleLogDev(['Hello']);
+ });
+
+ // @gate __DEV__
+ it('fails if the args is greater than %s argument number', () => {
+ const message = expectToThrowFailure(() => {
+ console.log('Hi %s', 'Sara', 'extra');
+ assertConsoleLogDev(['Hi']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleLogDev(expected)
+
+ Received 2 arguments for a message with 1 placeholders:
+ "Hi %s""
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if the args is greater than %s argument number for multiple logs', () => {
+ const message = expectToThrowFailure(() => {
+ console.log('Hi %s', 'Sara', 'extra');
+ console.log('Bye %s', 'Sara', 'extra');
+ assertConsoleLogDev(['Hi', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleLogDev(expected)
+
+ Received 2 arguments for a message with 1 placeholders:
+ "Hi %s"
+
+ Received 2 arguments for a message with 1 placeholders:
+ "Bye %s""
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if the %s argument number is greater than args', () => {
+ const message = expectToThrowFailure(() => {
+ console.log('Hi %s');
+ assertConsoleLogDev(['Hi']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleLogDev(expected)
+
+ Received 0 arguments for a message with 1 placeholders:
+ "Hi %s""
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if the %s argument number is greater than args for multiple logs', () => {
+ const message = expectToThrowFailure(() => {
+ console.log('Hi %s');
+ console.log('Bye %s');
+ assertConsoleLogDev(['Hi', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleLogDev(expected)
+
+ Received 0 arguments for a message with 1 placeholders:
+ "Hi %s"
+
+ Received 0 arguments for a message with 1 placeholders:
+ "Bye %s""
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if first arg is not an array', () => {
+ const message = expectToThrowFailure(() => {
+ console.log('Hi');
+ console.log('Bye');
+ assertConsoleLogDev('Hi', 'Bye');
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleLogDev(expected)
+
+ Expected messages should be an array of strings but was given type "string"."
+ `);
+
+ assertConsoleLogDev(['Hi', 'Bye']);
+ });
+
+ it('should fail if waitFor is called before asserting', async () => {
+ const Yield = ({id}) => {
+ Scheduler.log(id);
+ return id;
+ };
+
+ const root = ReactNoop.createRoot();
+ startTransition(() => {
+ root.render(
+
+
+
+
+
+ );
+ });
+
+ console.log('Not asserted');
+
+ const message = await awaitExpectToThrowFailure(async () => {
+ await waitFor(['foo', 'bar']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "asserConsoleLogsCleared(expected)
+
+ console.log was called without assertConsoleLogDev:
+ + Not asserted
+
+ You must call one of the assertConsoleDev helpers between each act call."
+ `);
+
+ await waitForAll(['foo', 'bar', 'baz']);
+ });
+
+ test('should fail if waitForThrow is called before asserting', async () => {
+ const Yield = ({id}) => {
+ Scheduler.log(id);
+ return id;
+ };
+
+ function BadRender() {
+ throw new Error('Oh no!');
+ }
+
+ function App() {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ const root = ReactNoop.createRoot();
+ root.render();
+
+ console.log('Not asserted');
+
+ const message = await awaitExpectToThrowFailure(async () => {
+ await waitForThrow('Oh no!');
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "asserConsoleLogsCleared(expected)
+
+ console.log was called without assertConsoleLogDev:
+ + Not asserted
+
+ You must call one of the assertConsoleDev helpers between each act call."
+ `);
+
+ await waitForAll(['A', 'B', 'A', 'B']);
+ });
+
+ test('should fail if waitForPaint is called before asserting', async () => {
+ function App({prop}) {
+ const deferred = useDeferredValue(prop);
+ const text = `Urgent: ${prop}, Deferred: ${deferred}`;
+ Scheduler.log(text);
+ return text;
+ }
+
+ const root = ReactNoop.createRoot();
+ root.render();
+
+ await waitForAll(['Urgent: A, Deferred: A']);
+ expect(root).toMatchRenderedOutput('Urgent: A, Deferred: A');
+
+ // This update will result in two separate paints: an urgent one, and a
+ // deferred one.
+ root.render();
+
+ console.log('Not asserted');
+ const message = await awaitExpectToThrowFailure(async () => {
+ await waitForPaint(['Urgent: B, Deferred: A']);
+ });
+
+ expect(message).toMatchInlineSnapshot(`
+ "asserConsoleLogsCleared(expected)
+
+ console.log was called without assertConsoleLogDev:
+ + Not asserted
+
+ You must call one of the assertConsoleDev helpers between each act call."
+ `);
+
+ await waitForAll(['Urgent: B, Deferred: A', 'Urgent: B, Deferred: B']);
+ });
+
+ it('should fail if waitForAll is called before asserting', async () => {
+ const Yield = ({id}) => {
+ Scheduler.log(id);
+ return id;
+ };
+
+ const root = ReactNoop.createRoot();
+ startTransition(() => {
+ root.render(
+
+
+
+
+
+ );
+ });
+
+ console.log('Not asserted');
+
+ const message = await awaitExpectToThrowFailure(async () => {
+ await waitForAll(['foo', 'bar', 'baz']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "asserConsoleLogsCleared(expected)
+
+ console.log was called without assertConsoleLogDev:
+ + Not asserted
+
+ You must call one of the assertConsoleDev helpers between each act call."
+ `);
+
+ await waitForAll(['foo', 'bar', 'baz']);
+ });
+ it('should fail if toMatchRenderedOutput is called before asserting', async () => {
+ const Yield = ({id}) => {
+ Scheduler.log(id);
+ console.log('Not asserted');
+ return id;
+ };
+
+ const root = ReactNoop.createRoot();
+ startTransition(() => {
+ root.render(
+
+
+
+
+
+ );
+ });
+
+ assertLog([]);
+
+ await waitForAll(['foo', 'bar', 'baz']);
+ const message = expectToThrowFailure(() => {
+ expect(root).toMatchRenderedOutput(foobarbaz
);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "asserConsoleLogsCleared(expected)
+
+ console.log was called without assertConsoleLogDev:
+ + Not asserted
+ + Not asserted
+ + Not asserted
+
+ You must call one of the assertConsoleDev helpers between each act call."
+ `);
+
+ expect(root).toMatchRenderedOutput(foobarbaz
);
+ });
+ });
+
+ describe('assertConsoleWarnDev', () => {
+ // @gate __DEV__
+ it('passes if an warning contains a stack', () => {
+ console.warn('Hello\n in div');
+ assertConsoleWarnDev(['Hello']);
+ });
+
+ // @gate __DEV__
+ it('passes if all warnings contain a stack', () => {
+ console.warn('Hello\n in div');
+ console.warn('Good day\n in div');
+ console.warn('Bye\n in div');
+ assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
+ });
+
+ it('fails if act is called without assertConsoleWarnDev', async () => {
+ const Yield = ({id}) => {
+ console.warn(id);
+ return id;
+ };
+
+ function App() {
+ return (
+
+
+
+
+
+ );
+ }
+
+ const root = ReactNoop.createRoot();
+ await act(() => {
+ root.render();
+ });
+ const message = await awaitExpectToThrowFailure(async () => {
+ await act(() => {
+ root.render();
+ });
+ });
+
+ expect(message).toMatchInlineSnapshot(`
+ "asserConsoleLogsCleared(expected)
+
+ console.warn was called without assertConsoleWarnDev:
+ + A
+ + B
+ + C
+
+ You must call one of the assertConsoleDev helpers between each act call."
+ `);
+ });
+
+ it('fails if act is called without any assertConsoleDev helpers', async () => {
+ const Yield = ({id}) => {
+ console.log(id);
+ console.warn(id);
+ console.error(id);
+ return id;
+ };
+
+ function App() {
+ return (
+
+
+
+
+
+ );
+ }
+
+ const root = ReactNoop.createRoot();
+ await act(() => {
+ root.render();
+ });
+ const message = await awaitExpectToThrowFailure(async () => {
+ await act(() => {
+ root.render();
+ });
+ });
+
+ expect(message).toMatchInlineSnapshot(`
+ "asserConsoleLogsCleared(expected)
+
+ console.log was called without assertConsoleLogDev:
+ + A
+ + B
+ + C
+
+ console.warn was called without assertConsoleWarnDev:
+ + A
+ + B
+ + C
+
+ console.error was called without assertConsoleErrorDev:
+ + A
+ + B
+ + C
+
+ You must call one of the assertConsoleDev helpers between each act call."
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if first expected warning is not included', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Wow \n in div');
+ console.warn('Bye \n in div');
+ assertConsoleWarnDev(['Hi', 'Wow', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Unexpected warning(s) recorded.
+
+ - Expected warnings
+ + Received warnings
+
+ - Hi
+ - Wow
+ - Bye
+ + Wow
+ + Bye "
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if middle expected warning is not included', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hi \n in div');
+ console.warn('Bye \n in div');
+ assertConsoleWarnDev(['Hi', 'Wow', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Unexpected warning(s) recorded.
+
+ - Expected warnings
+ + Received warnings
+
+ - Hi
+ - Wow
+ - Bye
+ + Hi
+ + Bye "
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if last expected warning is not included', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hi \n in div');
+ console.warn('Wow \n in div');
+ assertConsoleWarnDev(['Hi', 'Wow', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Expected warning was not recorded.
+
+ - Expected warnings
+ + Received warnings
+
+ - Hi
+ - Wow
+ - Bye
+ + Hi
+ + Wow "
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if first received warning is not included', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hi \n in div');
+ console.warn('Wow \n in div');
+ console.warn('Bye \n in div');
+ assertConsoleWarnDev(['Wow', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Unexpected warning(s) recorded.
+
+ - Expected warnings
+ + Received warnings
+
+ - Wow
+ - Bye
+ + Hi
+ + Wow
+ + Bye "
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if middle received warning is not included', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hi \n in div');
+ console.warn('Wow \n in div');
+ console.warn('Bye \n in div');
+ assertConsoleWarnDev(['Hi', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Unexpected warning(s) recorded.
+
+ - Expected warnings
+ + Received warnings
+
+ - Hi
+ - Bye
+ + Hi
+ + Wow
+ + Bye "
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if last received warning is not included', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hi \n in div');
+ console.warn('Wow \n in div');
+ console.warn('Bye \n in div');
+ assertConsoleWarnDev(['Hi', 'Wow']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Unexpected warning(s) recorded.
+
+ - Expected warnings
+ + Received warnings
+
+ - Hi
+ - Wow
+ + Hi
+ + Wow
+ + Bye "
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if only warning does not contain a stack', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hello');
+ assertConsoleWarnDev(['Hello']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Missing component stack for:
+ "Hello"
+
+ If this warning should omit a component stack, pass [log, {withoutStack: true}].
+ If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call."
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if first warning does not contain a stack', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hello');
+ console.warn('Good day\n in div');
+ console.warn('Bye\n in div');
+ assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Missing component stack for:
+ "Hello"
+
+ If this warning should omit a component stack, pass [log, {withoutStack: true}].
+ If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call."
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if middle warning does not contain a stack', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hello\n in div');
+ console.warn('Good day');
+ console.warn('Bye\n in div');
+ assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Missing component stack for:
+ "Good day"
+
+ If this warning should omit a component stack, pass [log, {withoutStack: true}].
+ If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call."
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if last warning does not contain a stack', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hello\n in div');
+ console.warn('Good day\n in div');
+ console.warn('Bye');
+ assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Missing component stack for:
+ "Bye"
+
+ If this warning should omit a component stack, pass [log, {withoutStack: true}].
+ If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call."
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if all warnings do not contain a stack', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hello');
+ console.warn('Good day');
+ console.warn('Bye');
+ assertConsoleWarnDev(['Hello', 'Good day', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Missing component stack for:
+ "Hello"
+
+ Missing component stack for:
+ "Good day"
+
+ Missing component stack for:
+ "Bye"
+
+ If this warning should omit a component stack, pass [log, {withoutStack: true}].
+ If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call."
+ `);
+ });
+
+ describe('global withoutStack', () => {
+ // @gate __DEV__
+ it('passes if warnings without stack explicitly opt out', () => {
+ console.warn('Hello');
+ assertConsoleWarnDev(['Hello'], {withoutStack: true});
+
+ console.warn('Hello');
+ console.warn('Good day');
+ console.warn('Bye');
+
+ assertConsoleWarnDev(['Hello', 'Good day', 'Bye'], {
+ withoutStack: true,
+ });
+ });
+
+ // @gate __DEV__
+ it('fails if withoutStack is invalid null value', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hi');
+ assertConsoleWarnDev(['Hi'], {withoutStack: null});
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ The second argument must be {withoutStack: true}.
+
+ Instead received {"withoutStack":null}."
+ `);
+ assertConsoleWarnDev(['Hi'], {withoutStack: true});
+ });
+
+ // @gate __DEV__
+ it('fails if withoutStack is invalid {} value', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hi');
+ assertConsoleWarnDev(['Hi'], {withoutStack: {}});
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ The second argument must be {withoutStack: true}.
+
+ Instead received {"withoutStack":{}}."
+ `);
+ assertConsoleWarnDev(['Hi'], {withoutStack: true});
+ });
+
+ // @gate __DEV__
+ it('fails if withoutStack is invalid string value', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hi');
+ assertConsoleWarnDev(['Hi'], {withoutStack: 'haha'});
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ The second argument must be {withoutStack: true}.
+
+ Instead received {"withoutStack":"haha"}."
+ `);
+ assertConsoleWarnDev(['Hi'], {withoutStack: true});
+ });
+
+ // @gate __DEV__
+ it('fails if only warning is not expected to have a stack, but does', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hello\n in div');
+ assertConsoleWarnDev(['Hello'], {withoutStack: true});
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Unexpected component stack for:
+ "Hello "
+
+ If this warning should include a component stack, remove {withoutStack: true} from this warning.
+ If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call."
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if warnings are not expected to have a stack, but some do', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hello\n in div');
+ console.warn('Good day');
+ console.warn('Bye\n in div');
+ assertConsoleWarnDev(['Hello', 'Good day', 'Bye'], {
+ withoutStack: true,
+ });
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Unexpected component stack for:
+ "Hello "
+
+ Unexpected component stack for:
+ "Bye "
+
+ If this warning should include a component stack, remove {withoutStack: true} from this warning.
+ If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call."
+ `);
+ });
+ });
+ describe('local withoutStack', () => {
+ // @gate __DEV__
+ it('passes when expected withoutStack logs matches the actual logs', () => {
+ console.warn('Hello\n in div');
+ console.warn('Good day');
+ console.warn('Bye\n in div');
+ assertConsoleWarnDev([
+ 'Hello',
+ ['Good day', {withoutStack: true}],
+ 'Bye',
+ ]);
+ });
+
+ // @gate __DEV__
+ it('fails if withoutStack is invalid null value', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hi');
+ assertConsoleWarnDev([['Hi', {withoutStack: null}]]);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Log entries that are arrays must be of the form [string, {withoutStack: true}]
+
+ Instead received [string, {"withoutStack":null}]."
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if withoutStack is invalid {} value', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hi');
+ assertConsoleWarnDev([['Hi', {withoutStack: {}}]]);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Log entries that are arrays must be of the form [string, {withoutStack: true}]
+
+ Instead received [string, {"withoutStack":{}}]."
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if withoutStack is invalid string value', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hi');
+ assertConsoleWarnDev([['Hi', {withoutStack: 'haha'}]]);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Log entries that are arrays must be of the form [string, {withoutStack: true}]
+
+ Instead received [string, {"withoutStack":"haha"}]."
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if withoutStack is invalid number value', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hi');
+ assertConsoleWarnDev([['Hi', {withoutStack: 4}]]);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Log entries that are arrays must be of the form [string, {withoutStack: true}]
+
+ Instead received [string, {"withoutStack":4}]."
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if you forget to wrap local withoutStack in array', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hello\n in div');
+ console.warn('Bye\n in div');
+ assertConsoleWarnDev(['Hello', {withoutStack: true}, 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Did you forget to wrap a log with withoutStack in an array?
+
+ The expected message for assertConsoleWarnDev() must be a string or an array of length 2.
+
+ Instead received {"withoutStack":true}."
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if you wrap in an array unnecessarily', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hello');
+ assertConsoleWarnDev([['Hello']]);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Did you forget to remove the array around the log?
+
+ The expected message for assertConsoleWarnDev() must be a string or an array of length 2, but there's only one item in the array. If this is intentional, remove the extra array."
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if only warning is not expected to have a stack, but does', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hello\n in div');
+ assertConsoleWarnDev([['Hello', {withoutStack: true}]]);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Unexpected component stack for:
+ "Hello "
+
+ If this warning should include a component stack, remove {withoutStack: true} from this warning.
+ If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call."
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if warnings are not expected to have a stack, but some do', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hello\n in div');
+ console.warn('Good day');
+ console.warn('Bye\n in div');
+ assertConsoleWarnDev([
+ [
+ 'Hello',
+ {
+ withoutStack: true,
+ },
+ ],
+ 'Good day',
+ [
+ 'Bye',
+ {
+ withoutStack: true,
+ },
+ ],
+ ]);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Unexpected component stack for:
+ "Hello "
+
+ Unexpected component stack for:
+ "Bye "
+
+ If this warning should include a component stack, remove {withoutStack: true} from this warning.
+ If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call."
+ `);
+ });
+ });
+
+ // @gate __DEV__
+ it('fails if the args is greater than %s argument number', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hi %s', 'Sara', 'extra');
+ assertConsoleWarnDev(['Hi'], {withoutStack: true});
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Received 2 arguments for a message with 1 placeholders:
+ "Hi %s""
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if the args is greater than %s argument number for multiple warnings', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hi %s', 'Sara', 'extra');
+ console.warn('Bye %s', 'Sara', 'extra');
+ assertConsoleWarnDev(['Hi', 'Bye'], {withoutStack: true});
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Received 2 arguments for a message with 1 placeholders:
+ "Hi %s"
+
+ Received 2 arguments for a message with 1 placeholders:
+ "Bye %s""
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if the %s argument number is greater than args', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hi %s');
+ assertConsoleWarnDev(['Hi'], {withoutStack: true});
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Received 0 arguments for a message with 1 placeholders:
+ "Hi %s""
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if the %s argument number is greater than args for multiple warnings', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hi %s');
+ console.warn('Bye %s');
+ assertConsoleWarnDev(['Hi', 'Bye'], {withoutStack: true});
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Received 0 arguments for a message with 1 placeholders:
+ "Hi %s"
+
+ Received 0 arguments for a message with 1 placeholders:
+ "Bye %s""
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if component stack is passed twice', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hi %s%s', '\n in div', '\n in div');
+ assertConsoleWarnDev(['Hi']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Received more than one component stack for a warning:
+ "Hi %s%s""
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if multiple logs pass component stack twice', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hi %s%s', '\n in div', '\n in div');
+ console.warn('Bye %s%s', '\n in div', '\n in div');
+ assertConsoleWarnDev(['Hi', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Received more than one component stack for a warning:
+ "Hi %s%s"
+
+ Received more than one component stack for a warning:
+ "Bye %s%s""
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if multiple strings are passed without an array wrapper for single log', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hi \n in div');
+ console.warn('Bye \n in div');
+ assertConsoleWarnDev('Hi', 'Bye');
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Expected messages should be an array of strings but was given type "string"."
+ `);
+ assertConsoleWarnDev(['Hi', 'Bye']);
+ });
+
+ // @gate __DEV__
+ it('fails if multiple strings are passed without an array wrapper for multiple logs', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hi \n in div');
+ console.warn('Bye \n in div');
+ assertConsoleWarnDev('Hi', 'Bye');
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Expected messages should be an array of strings but was given type "string"."
+ `);
+ assertConsoleWarnDev(['Hi', 'Bye']);
+ });
+
+ // @gate __DEV__
+ it('fails on more than two arguments', () => {
+ const message = expectToThrowFailure(() => {
+ console.warn('Hi \n in div');
+ console.warn('Wow \n in div');
+ console.warn('Bye \n in div');
+ assertConsoleWarnDev('Hi', undefined, 'Bye');
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleWarnDev(expected)
+
+ Expected messages should be an array of strings but was given type "string"."
+ `);
+ assertConsoleWarnDev(['Hi', 'Wow', 'Bye']);
+ });
+
+ it('should fail if waitFor is called before asserting', async () => {
+ const Yield = ({id}) => {
+ Scheduler.log(id);
+ return id;
+ };
+
+ const root = ReactNoop.createRoot();
+ startTransition(() => {
+ root.render(
+
+
+
+
+
+ );
+ });
+
+ console.warn('Not asserted');
+
+ const message = await awaitExpectToThrowFailure(async () => {
+ await waitFor(['foo', 'bar']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "asserConsoleLogsCleared(expected)
+
+ console.warn was called without assertConsoleWarnDev:
+ + Not asserted
+
+ You must call one of the assertConsoleDev helpers between each act call."
+ `);
+
+ await waitForAll(['foo', 'bar', 'baz']);
+ });
+
+ test('should fail if waitForThrow is called before asserting', async () => {
+ const Yield = ({id}) => {
+ Scheduler.log(id);
+ return id;
+ };
+
+ function BadRender() {
+ throw new Error('Oh no!');
+ }
+
+ function App() {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ const root = ReactNoop.createRoot();
+ root.render();
+
+ console.warn('Not asserted');
+
+ const message = await awaitExpectToThrowFailure(async () => {
+ await waitForThrow('Oh no!');
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "asserConsoleLogsCleared(expected)
+
+ console.warn was called without assertConsoleWarnDev:
+ + Not asserted
+
+ You must call one of the assertConsoleDev helpers between each act call."
+ `);
+
+ await waitForAll(['A', 'B', 'A', 'B']);
+ });
+
+ test('should fail if waitForPaint is called before asserting', async () => {
+ function App({prop}) {
+ const deferred = useDeferredValue(prop);
+ const text = `Urgent: ${prop}, Deferred: ${deferred}`;
+ Scheduler.log(text);
+ return text;
+ }
+
+ const root = ReactNoop.createRoot();
+ root.render();
+
+ await waitForAll(['Urgent: A, Deferred: A']);
+ expect(root).toMatchRenderedOutput('Urgent: A, Deferred: A');
+
+ // This update will result in two separate paints: an urgent one, and a
+ // deferred one.
+ root.render();
+
+ console.warn('Not asserted');
+ const message = await awaitExpectToThrowFailure(async () => {
+ await waitForPaint(['Urgent: B, Deferred: A']);
+ });
+
+ expect(message).toMatchInlineSnapshot(`
+ "asserConsoleLogsCleared(expected)
+
+ console.warn was called without assertConsoleWarnDev:
+ + Not asserted
+
+ You must call one of the assertConsoleDev helpers between each act call."
+ `);
+
+ await waitForAll(['Urgent: B, Deferred: A', 'Urgent: B, Deferred: B']);
+ });
+
+ it('should fail if waitForAll is called before asserting', async () => {
+ const Yield = ({id}) => {
+ Scheduler.log(id);
+ return id;
+ };
+
+ const root = ReactNoop.createRoot();
+ startTransition(() => {
+ root.render(
+
+
+
+
+
+ );
+ });
+
+ console.warn('Not asserted');
+
+ const message = await awaitExpectToThrowFailure(async () => {
+ await waitForAll(['foo', 'bar', 'baz']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "asserConsoleLogsCleared(expected)
+
+ console.warn was called without assertConsoleWarnDev:
+ + Not asserted
+
+ You must call one of the assertConsoleDev helpers between each act call."
+ `);
+
+ await waitForAll(['foo', 'bar', 'baz']);
+ });
+ it('should fail if toMatchRenderedOutput is called before asserting', async () => {
+ const Yield = ({id}) => {
+ Scheduler.log(id);
+ console.warn('Not asserted');
+ return id;
+ };
+
+ const root = ReactNoop.createRoot();
+ startTransition(() => {
+ root.render(
+
+
+
+
+
+ );
+ });
+
+ assertLog([]);
+
+ await waitForAll(['foo', 'bar', 'baz']);
+ const message = expectToThrowFailure(() => {
+ expect(root).toMatchRenderedOutput(foobarbaz
);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "asserConsoleLogsCleared(expected)
+
+ console.warn was called without assertConsoleWarnDev:
+ + Not asserted
+ + Not asserted
+ + Not asserted
+
+ You must call one of the assertConsoleDev helpers between each act call."
+ `);
+
+ expect(root).toMatchRenderedOutput(foobarbaz
);
+ });
+ });
+
+ describe('assertConsoleErrorDev', () => {
+ // @gate __DEV__
+ it('passes if an error contains a stack', () => {
+ console.error('Hello\n in div');
+ assertConsoleErrorDev(['Hello']);
+ });
+
+ // @gate __DEV__
+ it('passes if all errors contain a stack', () => {
+ console.error('Hello\n in div');
+ console.error('Good day\n in div');
+ console.error('Bye\n in div');
+ assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
+ });
+
+ it('fails if act is called without assertConsoleErrorDev', async () => {
+ const Yield = ({id}) => {
+ console.error(id);
+ return id;
+ };
+
+ function App() {
+ return (
+
+
+
+
+
+ );
+ }
+
+ const root = ReactNoop.createRoot();
+ await act(() => {
+ root.render();
+ });
+ const message = await awaitExpectToThrowFailure(async () => {
+ await act(() => {
+ root.render();
+ });
+ });
+
+ expect(message).toMatchInlineSnapshot(`
+ "asserConsoleLogsCleared(expected)
+
+ console.error was called without assertConsoleErrorDev:
+ + A
+ + B
+ + C
+
+ You must call one of the assertConsoleDev helpers between each act call."
+ `);
+ });
+
+ it('fails if act is called without any assertConsoleDev helpers', async () => {
+ const Yield = ({id}) => {
+ console.log(id);
+ console.warn(id);
+ console.error(id);
+ return id;
+ };
+
+ function App() {
+ return (
+
+
+
+
+
+ );
+ }
+
+ const root = ReactNoop.createRoot();
+ await act(() => {
+ root.render();
+ });
+ const message = await awaitExpectToThrowFailure(async () => {
+ await act(() => {
+ root.render();
+ });
+ });
+
+ expect(message).toMatchInlineSnapshot(`
+ "asserConsoleLogsCleared(expected)
+
+ console.log was called without assertConsoleLogDev:
+ + A
+ + B
+ + C
+
+ console.warn was called without assertConsoleWarnDev:
+ + A
+ + B
+ + C
+
+ console.error was called without assertConsoleErrorDev:
+ + A
+ + B
+ + C
+
+ You must call one of the assertConsoleDev helpers between each act call."
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if first expected error is not included', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Wow \n in div');
+ console.error('Bye \n in div');
+ assertConsoleErrorDev(['Hi', 'Wow', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Unexpected error(s) recorded.
+
+ - Expected errors
+ + Received errors
+
+ - Hi
+ - Wow
+ - Bye
+ + Wow
+ + Bye "
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if middle expected error is not included', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hi \n in div');
+ console.error('Bye \n in div');
+ assertConsoleErrorDev(['Hi', 'Wow', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Unexpected error(s) recorded.
+
+ - Expected errors
+ + Received errors
+
+ - Hi
+ - Wow
+ - Bye
+ + Hi
+ + Bye "
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if last expected error is not included', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hi \n in div');
+ console.error('Wow \n in div');
+ assertConsoleErrorDev(['Hi', 'Wow', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Expected error was not recorded.
+
+ - Expected errors
+ + Received errors
+
+ - Hi
+ - Wow
+ - Bye
+ + Hi
+ + Wow "
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if first received error is not included', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hi \n in div');
+ console.error('Wow \n in div');
+ console.error('Bye \n in div');
+ assertConsoleErrorDev(['Wow', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Unexpected error(s) recorded.
+
+ - Expected errors
+ + Received errors
+
+ - Wow
+ - Bye
+ + Hi
+ + Wow
+ + Bye "
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if middle received error is not included', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hi \n in div');
+ console.error('Wow \n in div');
+ console.error('Bye \n in div');
+ assertConsoleErrorDev(['Hi', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Unexpected error(s) recorded.
+
+ - Expected errors
+ + Received errors
+
+ - Hi
+ - Bye
+ + Hi
+ + Wow
+ + Bye "
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if last received error is not included', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hi \n in div');
+ console.error('Wow \n in div');
+ console.error('Bye \n in div');
+ assertConsoleErrorDev(['Hi', 'Wow']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Unexpected error(s) recorded.
+
+ - Expected errors
+ + Received errors
+
+ - Hi
+ - Wow
+ + Hi
+ + Wow
+ + Bye "
+ `);
+ });
+ // @gate __DEV__
+ it('fails if only error does not contain a stack', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hello');
+ assertConsoleErrorDev(['Hello']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Missing component stack for:
+ "Hello"
+
+ If this error should omit a component stack, pass [log, {withoutStack: true}].
+ If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if first error does not contain a stack', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hello\n in div');
+ console.error('Good day\n in div');
+ console.error('Bye');
+ assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Missing component stack for:
+ "Bye"
+
+ If this error should omit a component stack, pass [log, {withoutStack: true}].
+ If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
+ `);
+ });
+ // @gate __DEV__
+ it('fails if last error does not contain a stack', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hello');
+ console.error('Good day\n in div');
+ console.error('Bye\n in div');
+ assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Missing component stack for:
+ "Hello"
+
+ If this error should omit a component stack, pass [log, {withoutStack: true}].
+ If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
+ `);
+ });
+ // @gate __DEV__
+ it('fails if middle error does not contain a stack', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hello\n in div');
+ console.error('Good day');
+ console.error('Bye\n in div');
+ assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Missing component stack for:
+ "Good day"
+
+ If this error should omit a component stack, pass [log, {withoutStack: true}].
+ If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
+ `);
+ });
+ // @gate __DEV__
+ it('fails if all errors do not contain a stack', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hello');
+ console.error('Good day');
+ console.error('Bye');
+ assertConsoleErrorDev(['Hello', 'Good day', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Missing component stack for:
+ "Hello"
+
+ Missing component stack for:
+ "Good day"
+
+ Missing component stack for:
+ "Bye"
+
+ If this error should omit a component stack, pass [log, {withoutStack: true}].
+ If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call."
+ `);
+ });
+
+ describe('global withoutStack', () => {
+ // @gate __DEV__
+ it('passes if errors without stack explicitly opt out', () => {
+ console.error('Hello');
+ assertConsoleErrorDev(['Hello'], {withoutStack: true});
+
+ console.error('Hello');
+ console.error('Good day');
+ console.error('Bye');
+
+ assertConsoleErrorDev(['Hello', 'Good day', 'Bye'], {
+ withoutStack: true,
+ });
+ });
+
+ // @gate __DEV__
+ it('fails if withoutStack is invalid null value', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hi');
+ assertConsoleErrorDev(['Hi'], {withoutStack: null});
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ The second argument must be {withoutStack: true}.
+
+ Instead received {"withoutStack":null}."
+ `);
+ assertConsoleErrorDev(['Hi'], {withoutStack: true});
+ });
+
+ // @gate __DEV__
+ it('fails if withoutStack is invalid {} value', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hi');
+ assertConsoleErrorDev(['Hi'], {withoutStack: {}});
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ The second argument must be {withoutStack: true}.
+
+ Instead received {"withoutStack":{}}."
+ `);
+ assertConsoleErrorDev(['Hi'], {withoutStack: true});
+ });
+
+ // @gate __DEV__
+ it('fails if withoutStack is invalid string value', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hi');
+ assertConsoleErrorDev(['Hi'], {withoutStack: 'haha'});
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ The second argument must be {withoutStack: true}.
+
+ Instead received {"withoutStack":"haha"}."
+ `);
+ assertConsoleErrorDev(['Hi'], {withoutStack: true});
+ });
+
+ // @gate __DEV__
+ it('fails if only error is not expected to have a stack, but does', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hello\n in div');
+ assertConsoleErrorDev(['Hello'], {withoutStack: true});
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Unexpected component stack for:
+ "Hello "
+
+ If this error should include a component stack, remove {withoutStack: true} from this error.
+ If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call."
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if errors are not expected to have a stack, but some do', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hello\n in div');
+ console.error('Good day');
+ console.error('Bye\n in div');
+ assertConsoleErrorDev(['Hello', 'Good day', 'Bye'], {
+ withoutStack: true,
+ });
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Unexpected component stack for:
+ "Hello "
+
+ Unexpected component stack for:
+ "Bye "
+
+ If this error should include a component stack, remove {withoutStack: true} from this error.
+ If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call."
+ `);
+ });
+ });
+ describe('local withoutStack', () => {
+ // @gate __DEV__
+ it('passes when expected withoutStack logs matches the actual logs', () => {
+ console.error('Hello\n in div');
+ console.error('Good day');
+ console.error('Bye\n in div');
+ assertConsoleErrorDev([
+ 'Hello',
+ ['Good day', {withoutStack: true}],
+ 'Bye',
+ ]);
+ });
+
+ // @gate __DEV__
+ it('fails if withoutStack is invalid null value', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hi');
+ assertConsoleErrorDev([['Hi', {withoutStack: null}]]);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Log entries that are arrays must be of the form [string, {withoutStack: true}]
+
+ Instead received [string, {"withoutStack":null}]."
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if withoutStack is invalid {} value', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hi');
+ assertConsoleErrorDev([['Hi', {withoutStack: {}}]]);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Log entries that are arrays must be of the form [string, {withoutStack: true}]
+
+ Instead received [string, {"withoutStack":{}}]."
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if withoutStack is invalid string value', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hi');
+ assertConsoleErrorDev([['Hi', {withoutStack: 'haha'}]]);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Log entries that are arrays must be of the form [string, {withoutStack: true}]
+
+ Instead received [string, {"withoutStack":"haha"}]."
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if withoutStack is invalid number value', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hi');
+ assertConsoleErrorDev([['Hi', {withoutStack: 4}]]);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Log entries that are arrays must be of the form [string, {withoutStack: true}]
+
+ Instead received [string, {"withoutStack":4}]."
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if you forget to wrap local withoutStack in array', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hello\n in div');
+ console.error('Bye\n in div');
+ assertConsoleErrorDev(['Hello', {withoutStack: true}, 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Did you forget to wrap a log with withoutStack in an array?
+
+ The expected message for assertConsoleErrorDev() must be a string or an array of length 2.
+
+ Instead received {"withoutStack":true}."
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if you wrap in an array unnecessarily', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hello');
+ assertConsoleErrorDev([['Hello']]);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Did you forget to remove the array around the log?
+
+ The expected message for assertConsoleErrorDev() must be a string or an array of length 2, but there's only one item in the array. If this is intentional, remove the extra array."
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if only error is not expected to have a stack, but does', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hello\n in div');
+ assertConsoleErrorDev([['Hello', {withoutStack: true}]]);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Unexpected component stack for:
+ "Hello "
+
+ If this error should include a component stack, remove {withoutStack: true} from this error.
+ If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call."
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if errors are not expected to have a stack, but some do', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hello\n in div');
+ console.error('Good day');
+ console.error('Bye\n in div');
+ assertConsoleErrorDev([
+ [
+ 'Hello',
+ {
+ withoutStack: true,
+ },
+ ],
+ 'Good day',
+ [
+ 'Bye',
+ {
+ withoutStack: true,
+ },
+ ],
+ ]);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Unexpected component stack for:
+ "Hello "
+
+ Unexpected component stack for:
+ "Bye "
+
+ If this error should include a component stack, remove {withoutStack: true} from this error.
+ If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call."
+ `);
+ });
+ });
+
+ // @gate __DEV__
+ it('fails if the args is greater than %s argument number', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hi %s', 'Sara', 'extra');
+ assertConsoleErrorDev(['Hi'], {withoutStack: true});
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Received 2 arguments for a message with 1 placeholders:
+ "Hi %s""
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if the args is greater than %s argument number for multiple errors', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hi %s', 'Sara', 'extra');
+ console.error('Bye %s', 'Sara', 'extra');
+ assertConsoleErrorDev(['Hi', 'Bye'], {withoutStack: true});
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Received 2 arguments for a message with 1 placeholders:
+ "Hi %s"
+
+ Received 2 arguments for a message with 1 placeholders:
+ "Bye %s""
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if the %s argument number is greater than args', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hi %s');
+ assertConsoleErrorDev(['Hi'], {withoutStack: true});
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Received 0 arguments for a message with 1 placeholders:
+ "Hi %s""
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if the %s argument number is greater than args for multiple errors', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hi %s');
+ console.error('Bye %s');
+ assertConsoleErrorDev(['Hi', 'Bye'], {withoutStack: true});
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Received 0 arguments for a message with 1 placeholders:
+ "Hi %s"
+
+ Received 0 arguments for a message with 1 placeholders:
+ "Bye %s""
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if component stack is passed twice', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hi %s%s', '\n in div', '\n in div');
+ assertConsoleErrorDev(['Hi']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Received more than one component stack for a warning:
+ "Hi %s%s""
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if multiple logs pass component stack twice', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hi %s%s', '\n in div', '\n in div');
+ console.error('Bye %s%s', '\n in div', '\n in div');
+ assertConsoleErrorDev(['Hi', 'Bye']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Received more than one component stack for a warning:
+ "Hi %s%s"
+
+ Received more than one component stack for a warning:
+ "Bye %s%s""
+ `);
+ });
+
+ // @gate __DEV__
+ it('fails if multiple strings are passed without an array wrapper for single log', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hi \n in div');
+ console.error('Bye \n in div');
+ assertConsoleErrorDev('Hi', 'Bye');
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Expected messages should be an array of strings but was given type "string"."
+ `);
+ assertConsoleErrorDev(['Hi', 'Bye']);
+ });
+
+ // @gate __DEV__
+ it('fails if multiple strings are passed without an array wrapper for multiple logs', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hi \n in div');
+ console.error('Bye \n in div');
+ assertConsoleErrorDev('Hi', 'Bye');
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Expected messages should be an array of strings but was given type "string"."
+ `);
+ assertConsoleErrorDev(['Hi', 'Bye']);
+ });
+
+ // @gate __DEV__
+ it('fails on more than two arguments', () => {
+ const message = expectToThrowFailure(() => {
+ console.error('Hi \n in div');
+ console.error('Wow \n in div');
+ console.error('Bye \n in div');
+ assertConsoleErrorDev('Hi', undefined, 'Bye');
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "assertConsoleErrorDev(expected)
+
+ Expected messages should be an array of strings but was given type "string"."
+ `);
+ assertConsoleErrorDev(['Hi', 'Wow', 'Bye']);
+ });
+
+ it('should fail if waitFor is called before asserting', async () => {
+ const Yield = ({id}) => {
+ Scheduler.log(id);
+ return id;
+ };
+
+ const root = ReactNoop.createRoot();
+ startTransition(() => {
+ root.render(
+
+
+
+
+
+ );
+ });
+
+ console.error('Not asserted');
+
+ const message = await awaitExpectToThrowFailure(async () => {
+ await waitFor(['foo', 'bar']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "asserConsoleLogsCleared(expected)
+
+ console.error was called without assertConsoleErrorDev:
+ + Not asserted
+
+ You must call one of the assertConsoleDev helpers between each act call."
+ `);
+
+ await waitForAll(['foo', 'bar', 'baz']);
+ });
+
+ test('should fail if waitForThrow is called before asserting', async () => {
+ const Yield = ({id}) => {
+ Scheduler.log(id);
+ return id;
+ };
+
+ function BadRender() {
+ throw new Error('Oh no!');
+ }
+
+ function App() {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ const root = ReactNoop.createRoot();
+ root.render();
+
+ console.error('Not asserted');
+
+ const message = await awaitExpectToThrowFailure(async () => {
+ await waitForThrow('Oh no!');
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "asserConsoleLogsCleared(expected)
+
+ console.error was called without assertConsoleErrorDev:
+ + Not asserted
+
+ You must call one of the assertConsoleDev helpers between each act call."
+ `);
+
+ await waitForAll(['A', 'B', 'A', 'B']);
+ });
+
+ test('should fail if waitForPaint is called before asserting', async () => {
+ function App({prop}) {
+ const deferred = useDeferredValue(prop);
+ const text = `Urgent: ${prop}, Deferred: ${deferred}`;
+ Scheduler.log(text);
+ return text;
+ }
+
+ const root = ReactNoop.createRoot();
+ root.render();
+
+ await waitForAll(['Urgent: A, Deferred: A']);
+ expect(root).toMatchRenderedOutput('Urgent: A, Deferred: A');
+
+ // This update will result in two separate paints: an urgent one, and a
+ // deferred one.
+ root.render();
+
+ console.error('Not asserted');
+ const message = await awaitExpectToThrowFailure(async () => {
+ await waitForPaint(['Urgent: B, Deferred: A']);
+ });
+
+ expect(message).toMatchInlineSnapshot(`
+ "asserConsoleLogsCleared(expected)
+
+ console.error was called without assertConsoleErrorDev:
+ + Not asserted
+
+ You must call one of the assertConsoleDev helpers between each act call."
+ `);
+
+ await waitForAll(['Urgent: B, Deferred: A', 'Urgent: B, Deferred: B']);
+ });
+
+ it('should fail if waitForAll is called before asserting', async () => {
+ const Yield = ({id}) => {
+ Scheduler.log(id);
+ return id;
+ };
+
+ const root = ReactNoop.createRoot();
+ startTransition(() => {
+ root.render(
+
+
+
+
+
+ );
+ });
+
+ console.error('Not asserted');
+
+ const message = await awaitExpectToThrowFailure(async () => {
+ await waitForAll(['foo', 'bar', 'baz']);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "asserConsoleLogsCleared(expected)
+
+ console.error was called without assertConsoleErrorDev:
+ + Not asserted
+
+ You must call one of the assertConsoleDev helpers between each act call."
+ `);
+
+ await waitForAll(['foo', 'bar', 'baz']);
+ });
+ it('should fail if toMatchRenderedOutput is called before asserting', async () => {
+ const Yield = ({id}) => {
+ Scheduler.log(id);
+ console.error('Not asserted');
+ return id;
+ };
+
+ const root = ReactNoop.createRoot();
+ startTransition(() => {
+ root.render(
+
+
+
+
+
+ );
+ });
+
+ assertLog([]);
+
+ await waitForAll(['foo', 'bar', 'baz']);
+ const message = expectToThrowFailure(() => {
+ expect(root).toMatchRenderedOutput(foobarbaz
);
+ });
+ expect(message).toMatchInlineSnapshot(`
+ "asserConsoleLogsCleared(expected)
+
+ console.error was called without assertConsoleErrorDev:
+ + Not asserted
+ + Not asserted
+ + Not asserted
+
+ You must call one of the assertConsoleDev helpers between each act call."
+ `);
+
+ expect(root).toMatchRenderedOutput(foobarbaz
);
+ });
+ });
+});
diff --git a/packages/internal-test-utils/consoleMock.js b/packages/internal-test-utils/consoleMock.js
index 01e923a43b8c2..4601335f40dbe 100644
--- a/packages/internal-test-utils/consoleMock.js
+++ b/packages/internal-test-utils/consoleMock.js
@@ -10,14 +10,28 @@ const chalk = require('chalk');
const util = require('util');
const shouldIgnoreConsoleError = require('./shouldIgnoreConsoleError');
const shouldIgnoreConsoleWarn = require('./shouldIgnoreConsoleWarn');
+import {diff} from 'jest-diff';
+import {printReceived} from 'jest-matcher-utils';
-const unexpectedErrorCallStacks = [];
-const unexpectedWarnCallStacks = [];
-const unexpectedLogCallStacks = [];
+// Annoying: need to store the log array on the global or it would
+// change reference whenever you call jest.resetModules after patch.
+const loggedErrors = (global.__loggedErrors = global.__loggedErrors || []);
+const loggedWarns = (global.__loggedWarns = global.__loggedWarns || []);
+const loggedLogs = (global.__loggedLogs = global.__loggedLogs || []);
-// 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) => {
+// TODO: delete these after code modding away from toWarnDev.
+const unexpectedErrorCallStacks = (global.__unexpectedErrorCallStacks =
+ global.__unexpectedErrorCallStacks || []);
+const unexpectedWarnCallStacks = (global.__unexpectedWarnCallStacks =
+ global.__unexpectedWarnCallStacks || []);
+const unexpectedLogCallStacks = (global.__unexpectedLogCallStacks =
+ global.__unexpectedLogCallStacks || []);
+
+const patchConsoleMethod = (
+ methodName,
+ unexpectedConsoleCallStacks,
+ logged,
+) => {
const newMethod = function (format, ...args) {
// Ignore uncaught errors reported by jsdom
// and React addendums because they're too noisy.
@@ -38,6 +52,7 @@ const patchConsoleMethod = (methodName, unexpectedConsoleCallStacks) => {
stack.slice(stack.indexOf('\n') + 1),
util.format(format, ...args),
]);
+ logged.push([format, ...args]);
};
console[methodName] = newMethod;
@@ -75,8 +90,7 @@ const flushUnexpectedConsoleCalls = (
`console.${methodName}()`,
)}.\n\n` +
`If the ${type} is expected, test for it explicitly by:\n` +
- `1. Using the ${chalk.bold('.' + expectedMatcher + '()')} ` +
- `matcher, or...\n` +
+ `1. Using ${chalk.bold(expectedMatcher + '()')} or...\n` +
`2. Mock it out using ${chalk.bold(
'spyOnDev',
)}(console, '${methodName}') or ${chalk.bold(
@@ -91,13 +105,21 @@ let errorMethod;
let warnMethod;
let logMethod;
export function patchConsoleMethods({includeLog} = {includeLog: false}) {
- errorMethod = patchConsoleMethod('error', unexpectedErrorCallStacks);
- warnMethod = patchConsoleMethod('warn', unexpectedWarnCallStacks);
+ errorMethod = patchConsoleMethod(
+ 'error',
+ unexpectedErrorCallStacks,
+ loggedErrors,
+ );
+ warnMethod = patchConsoleMethod(
+ 'warn',
+ unexpectedWarnCallStacks,
+ loggedWarns,
+ );
// 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);
+ logMethod = patchConsoleMethod('log', unexpectedLogCallStacks, loggedLogs);
}
}
@@ -105,20 +127,20 @@ export function flushAllUnexpectedConsoleCalls() {
flushUnexpectedConsoleCalls(
errorMethod,
'error',
- 'toErrorDev',
+ 'assertConsoleErrorDev',
unexpectedErrorCallStacks,
);
flushUnexpectedConsoleCalls(
warnMethod,
'warn',
- 'toWarnDev',
+ 'assertConsoleWarnDev',
unexpectedWarnCallStacks,
);
if (logMethod) {
flushUnexpectedConsoleCalls(
logMethod,
'log',
- 'toLogDev',
+ 'assertConsoleLogDev',
unexpectedLogCallStacks,
);
unexpectedLogCallStacks.length = 0;
@@ -128,9 +150,404 @@ export function flushAllUnexpectedConsoleCalls() {
}
export function resetAllUnexpectedConsoleCalls() {
+ loggedErrors.length = 0;
+ loggedWarns.length = 0;
unexpectedErrorCallStacks.length = 0;
unexpectedWarnCallStacks.length = 0;
if (logMethod) {
+ loggedLogs.length = 0;
unexpectedLogCallStacks.length = 0;
}
}
+
+export function clearLogs() {
+ const logs = Array.from(loggedLogs);
+ unexpectedLogCallStacks.length = 0;
+ loggedLogs.length = 0;
+ return logs;
+}
+
+export function clearWarnings() {
+ const warnings = Array.from(loggedWarns);
+ unexpectedWarnCallStacks.length = 0;
+ loggedWarns.length = 0;
+ return warnings;
+}
+
+export function clearErrors() {
+ const errors = Array.from(loggedErrors);
+ unexpectedErrorCallStacks.length = 0;
+ loggedErrors.length = 0;
+ return errors;
+}
+
+export function assertConsoleLogsCleared() {
+ const logs = clearLogs();
+ const warnings = clearWarnings();
+ const errors = clearErrors();
+
+ if (logs.length > 0 || errors.length > 0 || warnings.length > 0) {
+ let message = `${chalk.dim('asserConsoleLogsCleared')}(${chalk.red(
+ 'expected',
+ )})\n`;
+
+ if (logs.length > 0) {
+ message += `\nconsole.log was called without assertConsoleLogDev:\n${diff(
+ '',
+ logs.join('\n'),
+ {
+ omitAnnotationLines: true,
+ },
+ )}\n`;
+ }
+
+ if (warnings.length > 0) {
+ message += `\nconsole.warn was called without assertConsoleWarnDev:\n${diff(
+ '',
+ warnings.join('\n'),
+ {
+ omitAnnotationLines: true,
+ },
+ )}\n`;
+ }
+ if (errors.length > 0) {
+ message += `\nconsole.error was called without assertConsoleErrorDev:\n${diff(
+ '',
+ errors.join('\n'),
+ {
+ omitAnnotationLines: true,
+ },
+ )}\n`;
+ }
+
+ message += `\nYou must call one of the assertConsoleDev helpers between each act call.`;
+
+ const error = Error(message);
+ Error.captureStackTrace(error, assertConsoleLogsCleared);
+ throw error;
+ }
+}
+
+function replaceComponentStack(str) {
+ if (typeof str !== 'string') {
+ return str;
+ }
+ // This special case exists only for the special source location in
+ // ReactElementValidator. That will go away if we remove source locations.
+ str = str.replace(/Check your code at .+?:\d+/g, 'Check your code at **');
+ // V8 format:
+ // at Component (/path/filename.js:123:45)
+ // React format:
+ // in Component (at filename.js:123)
+ return str.replace(/\n +(?:at|in) ([\S]+)[^\n]*.*/, function (m, name) {
+ return chalk.dim(' ');
+ });
+}
+
+const isLikelyAComponentStack = message =>
+ typeof message === 'string' &&
+ (message.indexOf('') > -1 ||
+ message.includes('\n in ') ||
+ message.includes('\n at '));
+
+export function createLogAssertion(
+ consoleMethod,
+ matcherName,
+ clearObservedErrors,
+) {
+ function logName() {
+ switch (consoleMethod) {
+ case 'log':
+ return 'log';
+ case 'error':
+ return 'error';
+ case 'warn':
+ return 'warning';
+ }
+ }
+
+ return function assertConsoleLog(expectedMessages, options = {}) {
+ if (__DEV__) {
+ // eslint-disable-next-line no-inner-declarations
+ function throwFormattedError(message) {
+ const error = new Error(
+ `${chalk.dim(matcherName)}(${chalk.red(
+ 'expected',
+ )})\n\n${message.trim()}`,
+ );
+ Error.captureStackTrace(error, assertConsoleLog);
+ throw error;
+ }
+
+ // Warn about incorrect usage first arg.
+ if (!Array.isArray(expectedMessages)) {
+ throwFormattedError(
+ `Expected messages should be an array of strings ` +
+ `but was given type "${typeof expectedMessages}".`,
+ );
+ }
+
+ // Warn about incorrect usage second arg.
+ if (options != null) {
+ if (typeof options !== 'object' || Array.isArray(options)) {
+ throwFormattedError(
+ `The second argument should be an object. ` +
+ 'Did you forget to wrap the messages into an array?',
+ );
+ }
+ }
+
+ const withoutStack = options.withoutStack;
+
+ // Warn about invalid global withoutStack values.
+ if (consoleMethod === 'log' && withoutStack !== undefined) {
+ throwFormattedError(
+ `Do not pass withoutStack to assertConsoleLogDev, console.log does not have component stacks.`,
+ );
+ } else if (withoutStack !== undefined && withoutStack !== true) {
+ // withoutStack can only have a value true.
+ throwFormattedError(
+ `The second argument must be {withoutStack: true}.` +
+ `\n\nInstead received ${JSON.stringify(options)}.`,
+ );
+ }
+
+ const observedLogs = clearObservedErrors();
+ const receivedLogs = [];
+ const missingExpectedLogs = Array.from(expectedMessages);
+
+ const unexpectedLogs = [];
+ const unexpectedMissingComponentStack = [];
+ const unexpectedIncludingComponentStack = [];
+ const logsMismatchingFormat = [];
+ const logsWithExtraComponentStack = [];
+
+ // Loop over all the observed logs to determine:
+ // - Which expected logs are missing
+ // - Which received logs are unexpected
+ // - Which logs have a component stack
+ // - Which logs have the wrong format
+ // - Which logs have extra stacks
+ for (let index = 0; index < observedLogs.length; index++) {
+ const log = observedLogs[index];
+ const [format, ...args] = log;
+ const message = util.format(format, ...args);
+
+ // Ignore uncaught errors reported by jsdom
+ // and React addendums because they're too noisy.
+ if (shouldIgnoreConsoleError(format, args)) {
+ return;
+ }
+
+ let expectedMessage;
+ let expectedWithoutStack;
+ const expectedMessageOrArray = expectedMessages[index];
+ if (
+ expectedMessageOrArray != null &&
+ Array.isArray(expectedMessageOrArray)
+ ) {
+ // Should be in the local form assert([['log', {withoutStack: true}]])
+
+ // Some validations for common mistakes.
+ if (expectedMessageOrArray.length === 1) {
+ throwFormattedError(
+ `Did you forget to remove the array around the log?` +
+ `\n\nThe expected message for ${matcherName}() must be a string or an array of length 2, but there's only one item in the array. If this is intentional, remove the extra array.`,
+ );
+ } else if (expectedMessageOrArray.length !== 2) {
+ throwFormattedError(
+ `The expected message for ${matcherName}() must be a string or an array of length 2. ` +
+ `Instead received ${expectedMessageOrArray}.`,
+ );
+ } else if (consoleMethod === 'log') {
+ // We don't expect any console.log calls to have a stack.
+ throwFormattedError(
+ `Do not pass withoutStack to assertConsoleLogDev logs, console.log does not have component stacks.`,
+ );
+ }
+
+ // Format is correct, check the values.
+ const currentExpectedMessage = expectedMessageOrArray[0];
+ const currentExpectedOptions = expectedMessageOrArray[1];
+ if (
+ typeof currentExpectedMessage !== 'string' ||
+ typeof currentExpectedOptions !== 'object' ||
+ currentExpectedOptions.withoutStack !== true
+ ) {
+ throwFormattedError(
+ `Log entries that are arrays must be of the form [string, {withoutStack: true}]` +
+ `\n\nInstead received [${typeof currentExpectedMessage}, ${JSON.stringify(
+ currentExpectedOptions,
+ )}].`,
+ );
+ }
+
+ expectedMessage = replaceComponentStack(currentExpectedMessage);
+ expectedWithoutStack = expectedMessageOrArray[1].withoutStack;
+ } else if (typeof expectedMessageOrArray === 'string') {
+ // Should be in the form assert(['log']) or assert(['log'], {withoutStack: true})
+ expectedMessage = replaceComponentStack(expectedMessageOrArray[0]);
+ if (consoleMethod === 'log') {
+ expectedWithoutStack = true;
+ } else {
+ expectedWithoutStack = withoutStack;
+ }
+ } else if (
+ typeof expectedMessageOrArray === 'object' &&
+ expectedMessageOrArray != null &&
+ expectedMessageOrArray.withoutStack != null
+ ) {
+ // Special case for common case of a wrong withoutStack value.
+ throwFormattedError(
+ `Did you forget to wrap a log with withoutStack in an array?` +
+ `\n\nThe expected message for ${matcherName}() must be a string or an array of length 2.` +
+ `\n\nInstead received ${JSON.stringify(expectedMessageOrArray)}.`,
+ );
+ } else if (expectedMessageOrArray != null) {
+ throwFormattedError(
+ `The expected message for ${matcherName}() must be a string or an array of length 2. ` +
+ `Instead received ${JSON.stringify(expectedMessageOrArray)}.`,
+ );
+ }
+
+ const normalizedMessage = replaceComponentStack(message);
+ receivedLogs.push(normalizedMessage);
+
+ // Check the number of %s interpolations.
+ // We'll fail the test if they mismatch.
+ let argIndex = 0;
+ // console.* could have been called with a non-string e.g. `console.error(new Error())`
+ // eslint-disable-next-line react-internal/safe-string-coercion
+ String(format).replace(/%s/g, () => argIndex++);
+ if (argIndex !== args.length) {
+ logsMismatchingFormat.push({
+ format,
+ args,
+ expectedArgCount: argIndex,
+ });
+ }
+
+ // Check for extra component stacks
+ if (
+ args.length >= 2 &&
+ isLikelyAComponentStack(args[args.length - 1]) &&
+ isLikelyAComponentStack(args[args.length - 2])
+ ) {
+ logsWithExtraComponentStack.push({
+ format,
+ });
+ }
+
+ // Main logic to check if log is expected, with the component stack.
+ if (
+ normalizedMessage === expectedMessage ||
+ normalizedMessage.includes(expectedMessage)
+ ) {
+ if (isLikelyAComponentStack(normalizedMessage)) {
+ if (expectedWithoutStack === true) {
+ unexpectedIncludingComponentStack.push(normalizedMessage);
+ }
+ } else if (expectedWithoutStack !== true) {
+ unexpectedMissingComponentStack.push(normalizedMessage);
+ }
+
+ // Found expected log, remove it from missing.
+ missingExpectedLogs.splice(0, 1);
+ } else {
+ unexpectedLogs.push(normalizedMessage);
+ }
+ }
+
+ // Helper for pretty printing diffs consistently.
+ // We inline multi-line logs for better diff printing.
+ // eslint-disable-next-line no-inner-declarations
+ function printDiff() {
+ return `${diff(
+ expectedMessages
+ .map(message => message.replace('\n', ' '))
+ .join('\n'),
+ receivedLogs.map(message => message.replace('\n', ' ')).join('\n'),
+ {
+ aAnnotation: `Expected ${logName()}s`,
+ bAnnotation: `Received ${logName()}s`,
+ },
+ )}`;
+ }
+
+ // Any unexpected warnings should be treated as a failure.
+ if (unexpectedLogs.length > 0) {
+ throwFormattedError(
+ `Unexpected ${logName()}(s) recorded.\n\n${printDiff()}`,
+ );
+ }
+
+ // Any remaining messages indicate a failed expectations.
+ if (missingExpectedLogs.length > 0) {
+ throwFormattedError(
+ `Expected ${logName()} was not recorded.\n\n${printDiff()}`,
+ );
+ }
+
+ // Any logs that include a component stack but shouldn't.
+ if (unexpectedIncludingComponentStack.length > 0) {
+ throwFormattedError(
+ `${unexpectedIncludingComponentStack
+ .map(
+ stack =>
+ `Unexpected component stack for:\n ${printReceived(stack)}`,
+ )
+ .join(
+ '\n\n',
+ )}\n\nIf this ${logName()} should include a component stack, remove {withoutStack: true} from this ${logName()}.` +
+ `\nIf all ${logName()}s should include the component stack, you may need to remove {withoutStack: true} from the ${matcherName} call.`,
+ );
+ }
+
+ // Any logs that are missing a component stack without withoutStack.
+ if (unexpectedMissingComponentStack.length > 0) {
+ throwFormattedError(
+ `${unexpectedMissingComponentStack
+ .map(
+ stack =>
+ `Missing component stack for:\n ${printReceived(stack)}`,
+ )
+ .join(
+ '\n\n',
+ )}\n\nIf this ${logName()} should omit a component stack, pass [log, {withoutStack: true}].` +
+ `\nIf all ${logName()}s should omit the component stack, add {withoutStack: true} to the ${matcherName} call.`,
+ );
+ }
+
+ // Wrong %s formatting is a failure.
+ // This is a common mistake when creating new warnings.
+ if (logsMismatchingFormat.length > 0) {
+ throwFormattedError(
+ logsMismatchingFormat
+ .map(
+ item =>
+ `Received ${item.args.length} arguments for a message with ${
+ item.expectedArgCount
+ } placeholders:\n ${printReceived(item.format)}`,
+ )
+ .join('\n\n'),
+ );
+ }
+
+ // Duplicate component stacks is a failure.
+ // This used to be a common mistake when creating new warnings,
+ // but might not be an issue anymore.
+ if (logsWithExtraComponentStack.length > 0) {
+ throwFormattedError(
+ logsWithExtraComponentStack
+ .map(
+ item =>
+ `Received more than one component stack for a warning:\n ${printReceived(
+ item.format,
+ )}`,
+ )
+ .join('\n\n'),
+ );
+ }
+ }
+ };
+}
diff --git a/packages/internal-test-utils/internalAct.js b/packages/internal-test-utils/internalAct.js
index 8f8667ce0cfd2..22bb92c24fc26 100644
--- a/packages/internal-test-utils/internalAct.js
+++ b/packages/internal-test-utils/internalAct.js
@@ -19,6 +19,7 @@ import type {Thenable} from 'shared/ReactTypes';
import * as Scheduler from 'scheduler/unstable_mock';
import enqueueTask from './enqueueTask';
+import {assertConsoleLogsCleared} from './consoleMock';
import {diff} from 'jest-diff';
export let actingUpdatesScopeDepth: number = 0;
@@ -58,6 +59,10 @@ export async function act(scope: () => Thenable): Thenable {
throw error;
}
+ // We require every `act` call to assert console logs
+ // with one of the assertion helpers. Fails if not empty.
+ assertConsoleLogsCleared();
+
// $FlowFixMe[cannot-resolve-name]: Flow doesn't know about global Jest object
if (!jest.isMockFunction(setTimeout)) {
throw Error(
diff --git a/packages/jest-react/src/JestReact.js b/packages/jest-react/src/JestReact.js
index 21307c8393b9d..4eefc58c85228 100644
--- a/packages/jest-react/src/JestReact.js
+++ b/packages/jest-react/src/JestReact.js
@@ -7,6 +7,7 @@
import {REACT_ELEMENT_TYPE, REACT_FRAGMENT_TYPE} from 'shared/ReactSymbols';
import {disableStringRefs, enableRefAsProp} from 'shared/ReactFeatureFlags';
+const {assertConsoleLogsCleared} = require('internal-test-utils/consoleMock');
import isArray from 'shared/isArray';
@@ -37,6 +38,7 @@ function assertYieldsWereCleared(root) {
Error.captureStackTrace(error, assertYieldsWereCleared);
throw error;
}
+ assertConsoleLogsCleared();
}
function createJSXElementForTestComparison(type, props) {
diff --git a/scripts/jest/matchers/reactTestMatchers.js b/scripts/jest/matchers/reactTestMatchers.js
index 63d03c5d70936..fbe8d00cc2301 100644
--- a/scripts/jest/matchers/reactTestMatchers.js
+++ b/scripts/jest/matchers/reactTestMatchers.js
@@ -1,7 +1,7 @@
'use strict';
const JestReact = require('jest-react');
-
+const {assertConsoleLogsCleared} = require('internal-test-utils/consoleMock');
// TODO: Move to ReactInternalTestUtils
function captureAssertion(fn) {
@@ -29,6 +29,7 @@ function assertYieldsWereCleared(Scheduler, caller) {
Error.captureStackTrace(error, caller);
throw error;
}
+ assertConsoleLogsCleared();
}
function toMatchRenderedOutput(ReactNoop, expectedJSX) {