From 608edcc90a9e1dcee896c6c93cb381d0a61aac37 Mon Sep 17 00:00:00 2001 From: Ricky Date: Thu, 11 Apr 2024 08:19:46 -0400 Subject: [PATCH] [tests] add `assertConsoleDev` helpers (#28732) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Overview **Internal React repo tests only** Depends on https://github.com/facebook/react/pull/28710 Adds three new assertions: - `assertConsoleLogDev` - `assertConsoleWarnDev` - `assertConsoleErrorDev` These will replace this pattern: ```js await expect(async () => { await expect(async () => { await act(() => { root.render() }); }).toThrow(); }).toWarnDev('Warning'); ``` With this: ```js await expect(async () => { await act(() => { root.render() }); }).toThrow(); assertConsoleWarnDev('Warning'); ``` It works similar to our other `assertLog` matchers which clear the log and assert on it, failing the tests if the log is not asserted before the test ends. ## Diffs There are a few improvements I also added including better log diffs and more logging. When there's a failure, the output will look something like: Screenshot 2024-04-03 at 11 50 08 AM Check out the test suite for snapshots of all the failures we may log. --- .../ReactInternalTestUtils.js | 25 +- .../__tests__/ReactInternalTestUtils-test.js | 2417 +++++++++++++++++ packages/internal-test-utils/consoleMock.js | 445 ++- packages/internal-test-utils/internalAct.js | 5 + packages/jest-react/src/JestReact.js | 2 + scripts/jest/matchers/reactTestMatchers.js | 3 +- 6 files changed, 2881 insertions(+), 16 deletions(-) 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) {