From a40a5e568c6fc2bff037715e0bd26ce445a334a7 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Mon, 1 Apr 2024 16:47:04 -0500 Subject: [PATCH 1/7] Move console mocks to internal-test-utils --- packages/internal-test-utils/consoleMock.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/internal-test-utils/consoleMock.js b/packages/internal-test-utils/consoleMock.js index 01e923a43b8c2..bfd0502ed8dfa 100644 --- a/packages/internal-test-utils/consoleMock.js +++ b/packages/internal-test-utils/consoleMock.js @@ -15,8 +15,6 @@ const unexpectedErrorCallStacks = []; const unexpectedWarnCallStacks = []; const unexpectedLogCallStacks = []; -// TODO: Consider consolidating this with `yieldValue`. In both cases, tests -// should not be allowed to exit without asserting on the entire log. const patchConsoleMethod = (methodName, unexpectedConsoleCallStacks) => { const newMethod = function (format, ...args) { // Ignore uncaught errors reported by jsdom From b2866b149d3645786b4a2e5c1928f75067d98df6 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Wed, 3 Apr 2024 10:44:02 -0500 Subject: [PATCH 2/7] [tests] Add assertWarnDev, assertErrorDev, assertLogDev --- .../ReactInternalTestUtils.js | 23 +- .../__tests__/ReactInternalTestUtils-test.js | 1341 +++++++++++++++++ packages/internal-test-utils/consoleMock.js | 337 ++++- 3 files changed, 1693 insertions(+), 8 deletions(-) diff --git a/packages/internal-test-utils/ReactInternalTestUtils.js b/packages/internal-test-utils/ReactInternalTestUtils.js index c6cb39fd73e08..2e4d2481519c2 100644 --- a/packages/internal-test-utils/ReactInternalTestUtils.js +++ b/packages/internal-test-utils/ReactInternalTestUtils.js @@ -10,7 +10,12 @@ 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'; import {thrownErrors, actingUpdatesScopeDepth} from './internalAct'; @@ -317,6 +322,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..613f5fee96d19 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,1338 @@ describe('ReactInternalTestUtils console mocks', () => { }); }); }); + +// Helper methods avoids invalid toWarn().toThrow() nesting +// See no-to-warn-dev-within-to-throw +const expectToWarnAndToThrow = (expectBlock, expectedErrorMessage) => { + let caughtError; + try { + expectBlock(); + } catch (error) { + caughtError = error; + } + expect(caughtError).toBeDefined(); + return stripAnsi(caughtError.message); +}; + +describe('ReactInternalTestUtils console assertions', () => { + beforeEach(() => { + jest.resetAllMocks(); + patchConsoleMethods({includeLog: true}); + }); + + afterEach(() => { + resetAllUnexpectedConsoleCalls(); + }); + + 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']); + }); + + // @gate __DEV__ + it('fails if first expected log is not included', () => { + const message = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 withoutStack passed to assertConsoleLogDev', () => { + const message = expectToWarnAndToThrow(() => { + 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." + `); + }); + + // @gate __DEV__ + it('fails if the args is greater than %s argument number', () => { + const message = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + console.log('Hi'); + assertConsoleLogDev('Hi', 'Bye'); + }); + expect(message).toMatchInlineSnapshot(` + "assertConsoleLogDev(expected) + + Expected messages should be an array of strings but was given type "string"." + `); + }); + }); + + describe('assertConsoleWarnDev', () => { + // @gate __DEV__ + it('passes if a 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']); + }); + + // @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('passes when expected withoutStack number matches the actual one', () => { + console.warn('Hello\n in div'); + console.warn('Good day'); + console.warn('Bye\n in div'); + assertConsoleWarnDev(['Hello', 'Good day', 'Bye'], {withoutStack: 1}); + }); + + // @gate __DEV__ + it('fails if first expected warning is not included', () => { + const message = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + console.warn('Hello'); + assertConsoleWarnDev(['Hello']); + }); + expect(message).toMatchInlineSnapshot(` + "assertConsoleWarnDev(expected) + + Missing component stack for: + "Hello" + + If this warning intentionally omits the component stack, add {withoutStack: true} to the assertConsoleWarnDev call." + `); + }); + + // @gate __DEV__ + it('fails if first warning does not contain a stack', () => { + const message = expectToWarnAndToThrow(() => { + 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 intentionally omits the component stack, add {withoutStack: true} to the assertConsoleWarnDev call." + `); + }); + // @gate __DEV__ + it('fails if middle warning does not contain a stack', () => { + const message = expectToWarnAndToThrow(() => { + 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 intentionally omits the component stack, add {withoutStack: true} to the assertConsoleWarnDev call." + `); + }); + + // @gate __DEV__ + it('fails if last warning does not contain a stack', () => { + const message = expectToWarnAndToThrow(() => { + 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 intentionally omits the component stack, add {withoutStack: true} to the assertConsoleWarnDev call." + `); + }); + + // @gate __DEV__ + it('fails if all warnings do not contain a stack', () => { + const message = expectToWarnAndToThrow(() => { + 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 intentionally omits the component stack, add {withoutStack: true} to the assertConsoleWarnDev call." + `); + }); + + // @gate __DEV__ + it('fails if only warning is not expected to have a stack, but does', () => { + const message = expectToWarnAndToThrow(() => { + console.warn('Hello\n in div'); + assertConsoleWarnDev(['Hello'], {withoutStack: true}); + }); + expect(message).toMatchInlineSnapshot(` + "assertConsoleWarnDev(expected) + + Unexpected component stack for: + "Hello " + + If this warning intentionally includes the component stack, remove {withoutStack: true} from the assertConsoleWarnDev() call. + If you have a mix of warnings with and without stack in one assertConsoleWarnDev() call, pass {withoutStack: N} where N is the number of warnings without stacks." + `); + }); + + // @gate __DEV__ + it('fails if warnings are not expected to have a stack, but some do', () => { + const message = expectToWarnAndToThrow(() => { + 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 intentionally includes the component stack, remove {withoutStack: true} from the assertConsoleWarnDev() call. + If you have a mix of warnings with and without stack in one assertConsoleWarnDev() call, pass {withoutStack: N} where N is the number of warnings without stacks." + `); + }); + + // @gate __DEV__ + it('fails if expected withoutStack number does not match the actual one', () => { + const message = expectToWarnAndToThrow(() => { + console.warn('Hello\n in div'); + console.warn('Good day'); + console.warn('Bye\n in div'); + assertConsoleWarnDev(['Hello', 'Good day', 'Bye'], { + withoutStack: 4, + }); + }); + expect(message).toMatchInlineSnapshot(` + "assertConsoleWarnDev(expected) + + Expected 4 warnings without a component stack but received 1: + - Expected warnings + + Received warnings + + - Hello + + Hello + Good day + - Bye + + Bye " + `); + }); + + // @gate __DEV__ + it('fails if withoutStack is invalid null value', () => { + const message = expectToWarnAndToThrow(() => { + console.warn('Hi'); + assertConsoleWarnDev(['Hi'], {withoutStack: null}); + }); + expect(message).toMatchInlineSnapshot(` + "assertConsoleWarnDev(expected) + + The second argument for assertConsoleWarnDev(), when specified, must be an object. It may have a property called "withoutStack" whose value may be a boolean or number. Instead received object." + `); + }); + + // @gate __DEV__ + it('fails if withoutStack is invalid {} value', () => { + const message = expectToWarnAndToThrow(() => { + console.warn('Hi'); + assertConsoleWarnDev(['Hi'], {withoutStack: {}}); + }); + expect(message).toMatchInlineSnapshot(` + "assertConsoleWarnDev(expected) + + The second argument for assertConsoleWarnDev(), when specified, must be an object. It may have a property called "withoutStack" whose value may be a boolean or number. Instead received object." + `); + }); + + // @gate __DEV__ + it('fails if withoutStack is invalid string value', () => { + const message = expectToWarnAndToThrow(() => { + console.warn('Hi'); + assertConsoleWarnDev(['Hi'], {withoutStack: 'haha'}); + }); + expect(message).toMatchInlineSnapshot(` + "assertConsoleWarnDev(expected) + + The second argument for assertConsoleWarnDev(), when specified, must be an object. It may have a property called "withoutStack" whose value may be a boolean or number. Instead received string." + `); + }); + + // @gate __DEV__ + it('fails if the args is greater than %s argument number', () => { + const message = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 strings are passed without an array wrapper for single log', () => { + const message = expectToWarnAndToThrow(() => { + console.warn('Hi \n in div'); + assertConsoleWarnDev('Hi', 'Bye'); + }); + expect(message).toMatchInlineSnapshot(` + "assertConsoleWarnDev(expected) + + Expected messages should be an array of strings but was given type "string"." + `); + }); + + // @gate __DEV__ + it('fails if multiple strings are passed without an array wrapper for multiple logs', () => { + const message = expectToWarnAndToThrow(() => { + 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"." + `); + }); + + // @gate __DEV__ + it('fails on more than two arguments', () => { + const message = expectToWarnAndToThrow(() => { + 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"." + `); + }); + }); + + 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']); + }); + + // @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('passes when expected withoutStack number matches the actual one', () => { + console.error('Hello\n in div'); + console.error('Good day'); + console.error('Bye\n in div'); + assertConsoleErrorDev(['Hello', 'Good day', 'Bye'], {withoutStack: 1}); + }); + + // @gate __DEV__ + it('fails if first expected error is not included', () => { + const message = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + console.error('Hello'); + assertConsoleErrorDev('Hello'); + }); + expect(message).toMatchInlineSnapshot(` + "assertConsoleErrorDev(expected) + + Expected messages should be an array of strings but was given type "string"." + `); + }); + + // @gate __DEV__ + it('fails if first error does not contain a stack', () => { + const message = expectToWarnAndToThrow(() => { + 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 intentionally omits the component stack, add {withoutStack: true} to the assertConsoleErrorDev call." + `); + }); + // @gate __DEV__ + it('fails if last error does not contain a stack', () => { + const message = expectToWarnAndToThrow(() => { + 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 intentionally omits the component stack, add {withoutStack: true} to the assertConsoleErrorDev call." + `); + }); + // @gate __DEV__ + it('fails if middle error does not contain a stack', () => { + const message = expectToWarnAndToThrow(() => { + 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 intentionally omits the component stack, add {withoutStack: true} to the assertConsoleErrorDev call." + `); + }); + // @gate __DEV__ + it('fails if all errors do not contain a stack', () => { + const message = expectToWarnAndToThrow(() => { + 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 intentionally omits the component stack, add {withoutStack: true} to the assertConsoleErrorDev call." + `); + }); + + // @gate __DEV__ + it('fails if only error is not expected to have a stack, but does', () => { + const message = expectToWarnAndToThrow(() => { + console.error('Hello\n in div'); + assertConsoleErrorDev(['Hello'], {withoutStack: true}); + }); + expect(message).toMatchInlineSnapshot(` + "assertConsoleErrorDev(expected) + + Unexpected component stack for: + "Hello " + + If this error intentionally includes the component stack, remove {withoutStack: true} from the assertConsoleErrorDev() call. + If you have a mix of errors with and without stack in one assertConsoleErrorDev() call, pass {withoutStack: N} where N is the number of errors without stacks." + `); + }); + + // @gate __DEV__ + it('fails if errors are not expected to have a stack, but some do', () => { + const message = expectToWarnAndToThrow(() => { + 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 intentionally includes the component stack, remove {withoutStack: true} from the assertConsoleErrorDev() call. + If you have a mix of errors with and without stack in one assertConsoleErrorDev() call, pass {withoutStack: N} where N is the number of errors without stacks." + `); + }); + + // @gate __DEV__ + it('fails if expected withoutStack number does not match the actual one', () => { + const message = expectToWarnAndToThrow(() => { + console.error('Hello\n in div'); + console.error('Good day'); + console.error('Bye\n in div'); + assertConsoleErrorDev(['Hello', 'Good day', 'Bye'], { + withoutStack: 4, + }); + }); + expect(message).toMatchInlineSnapshot(` + "assertConsoleErrorDev(expected) + + Expected 4 errors without a component stack but received 1: + - Expected errors + + Received errors + + - Hello + + Hello + Good day + - Bye + + Bye " + `); + }); + + // @gate __DEV__ + it('fails if multiple expected withoutStack number does not match the actual one', () => { + const message = expectToWarnAndToThrow(() => { + console.error('Hello\n in div'); + console.error('Good day'); + console.error('Good night'); + console.error('Bye\n in div'); + assertConsoleErrorDev(['Hello', 'Good day', 'Good night', 'Bye'], { + withoutStack: 4, + }); + }); + expect(message).toMatchInlineSnapshot(` + "assertConsoleErrorDev(expected) + + Expected 4 errors without a component stack but received 2: + - Expected errors + + Received errors + + - Hello + + Hello + Good day + Good night + - Bye + + Bye " + `); + }); + + // @gate __DEV__ + it('fails if withoutStack is invalid null value', () => { + const message = expectToWarnAndToThrow(() => { + console.error('Hi'); + assertConsoleErrorDev(['Hi'], {withoutStack: null}); + }); + expect(message).toMatchInlineSnapshot(` + "assertConsoleErrorDev(expected) + + The second argument for assertConsoleErrorDev(), when specified, must be an object. It may have a property called "withoutStack" whose value may be a boolean or number. Instead received object." + `); + }); + + // @gate __DEV__ + it('fails if withoutStack is invalid {} value', () => { + const message = expectToWarnAndToThrow(() => { + console.error('Hi'); + assertConsoleErrorDev(['Hi'], {withoutStack: {}}); + }); + expect(message).toMatchInlineSnapshot(` + "assertConsoleErrorDev(expected) + + The second argument for assertConsoleErrorDev(), when specified, must be an object. It may have a property called "withoutStack" whose value may be a boolean or number. Instead received object." + `); + }); + + // @gate __DEV__ + it('fails if withoutStack is invalid string value', () => { + const message = expectToWarnAndToThrow(() => { + console.error('Hi'); + assertConsoleErrorDev(['Hi'], {withoutStack: 'haha'}); + }); + expect(message).toMatchInlineSnapshot(` + "assertConsoleErrorDev(expected) + + The second argument for assertConsoleErrorDev(), when specified, must be an object. It may have a property called "withoutStack" whose value may be a boolean or number. Instead received string." + `); + }); + + // @gate __DEV__ + it('fails if the args is greater than %s argument number', () => { + const message = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + 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 = expectToWarnAndToThrow(() => { + console.error('Hi \n in div'); + assertConsoleErrorDev('Hi', 'Bye'); + }); + expect(message).toMatchInlineSnapshot(` + "assertConsoleErrorDev(expected) + + Expected messages should be an array of strings but was given type "string"." + `); + }); + + // @gate __DEV__ + it('fails if multiple strings are passed without an array wrapper for multiple logs', () => { + const message = expectToWarnAndToThrow(() => { + 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"." + `); + }); + + // @gate __DEV__ + it('fails on more than two arguments', () => { + const message = expectToWarnAndToThrow(() => { + 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"." + `); + }); + }); +}); diff --git a/packages/internal-test-utils/consoleMock.js b/packages/internal-test-utils/consoleMock.js index bfd0502ed8dfa..c90c37b2d921f 100644 --- a/packages/internal-test-utils/consoleMock.js +++ b/packages/internal-test-utils/consoleMock.js @@ -10,12 +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 || []); -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. @@ -36,6 +52,7 @@ const patchConsoleMethod = (methodName, unexpectedConsoleCallStacks) => { stack.slice(stack.indexOf('\n') + 1), util.format(format, ...args), ]); + logged.push([format, ...args]); }; console[methodName] = newMethod; @@ -89,13 +106,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); } } @@ -126,9 +151,307 @@ 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; +} + +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; + + if (consoleMethod === 'log' && withoutStack !== undefined) { + // We don't expect any console.log calls to have a stack. + throwFormattedError( + `Do not pass withoutStack to assertConsoleLogDev, console.log does not have component stacks.`, + ); + } else if ( + withoutStack !== undefined && + typeof withoutStack !== 'number' && + withoutStack !== true + ) { + throwFormattedError( + `The second argument for ${matcherName}(), when specified, must be an object. It may have a ` + + `property called "withoutStack" whose value may be a boolean or number. ` + + `Instead received ${typeof withoutStack}.`, + ); + } + + const observedLogs = clearObservedErrors(); + const unexpectedLogs = []; + const receivedLogs = []; + const logsWithoutComponentStack = []; + const logsWithComponentStack = []; + const logsMismatchingFormat = []; + const logsWithExtraComponentStack = []; + const missingExpectedLogs = Array.from(expectedMessages); + + // 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; + } + + const expectedMessage = replaceComponentStack(expectedMessages[index]); + 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, + }); + } + + // Check if log is expected, and if it has a component stack. + if ( + normalizedMessage === expectedMessage || + normalizedMessage.includes(expectedMessage) + ) { + if (isLikelyAComponentStack(normalizedMessage)) { + logsWithComponentStack.push(normalizedMessage); + } else { + logsWithoutComponentStack.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 unexpected component stacks are a failure. + if (consoleMethod !== 'log') { + if (typeof withoutStack === 'number') { + // We're expecting a particular number of warnings without stacks. + if (withoutStack !== logsWithoutComponentStack.length) { + throwFormattedError( + `Expected ${withoutStack} ${logName()}s without a component stack but received ${ + logsWithoutComponentStack.length + }:\n${printDiff()}`, + ); + } + } else if (withoutStack === true) { + // We're expecting that all warnings won't have the stack. + // If some warnings have it, it's an error. + if (logsWithComponentStack.length > 0) { + throwFormattedError( + `${logsWithComponentStack + .map( + stack => + `Unexpected component stack for:\n ${printReceived( + stack, + )}`, + ) + .join( + '\n\n', + )}\n\nIf this ${logName()} intentionally includes the component stack, remove ` + + `{withoutStack: true} from the ${matcherName}() call.\nIf you have a mix of ` + + `${logName()}s with and without stack in one ${matcherName}() call, pass ` + + `{withoutStack: N} where N is the number of ${logName()}s without stacks.`, + ); + } + } else if (withoutStack === undefined) { + // We're expecting that all warnings *do* have the stack (default). + // If some warnings don't have it, it's an error. + if (logsWithoutComponentStack.length > 0) { + throwFormattedError( + `${logsWithoutComponentStack + .map( + stack => + `Missing component stack for:\n ${printReceived(stack)}`, + ) + .join( + '\n\n', + )}\n\nIf this ${logName()} intentionally omits 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'), + ); + } + } + }; +} From 70088cb3b372a5a18ac9785ff5a90bc7a5780e5b Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Wed, 3 Apr 2024 17:50:17 -0500 Subject: [PATCH 3/7] assert console cleared before act --- .../__tests__/ReactInternalTestUtils-test.js | 181 +++++++++++++++++- packages/internal-test-utils/consoleMock.js | 47 +++++ packages/internal-test-utils/internalAct.js | 5 + 3 files changed, 230 insertions(+), 3 deletions(-) diff --git a/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js b/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js index 613f5fee96d19..bd82f393bb7d8 100644 --- a/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js +++ b/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js @@ -308,9 +308,8 @@ describe('ReactInternalTestUtils console mocks', () => { }); }); -// Helper methods avoids invalid toWarn().toThrow() nesting -// See no-to-warn-dev-within-to-throw -const expectToWarnAndToThrow = (expectBlock, expectedErrorMessage) => { +// Helper method to capture assertion failure. +const expectToWarnAndToThrow = expectBlock => { let caughtError; try { expectBlock(); @@ -321,6 +320,18 @@ const expectToWarnAndToThrow = (expectBlock, expectedErrorMessage) => { return stripAnsi(caughtError.message); }; +// Helper method to capture assertion failure with act. +const awaitExpectToWarnAndToThrow = async expectBlock => { + let caughtError; + try { + await expectBlock(); + } catch (error) { + caughtError = error; + } + expect(caughtError).toBeDefined(); + return stripAnsi(caughtError.message); +}; + describe('ReactInternalTestUtils console assertions', () => { beforeEach(() => { jest.resetAllMocks(); @@ -346,6 +357,44 @@ describe('ReactInternalTestUtils console assertions', () => { 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 awaitExpectToWarnAndToThrow(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 = expectToWarnAndToThrow(() => { @@ -648,6 +697,44 @@ describe('ReactInternalTestUtils console assertions', () => { assertConsoleWarnDev(['Hello', 'Good day', 'Bye'], {withoutStack: 1}); }); + 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 awaitExpectToWarnAndToThrow(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." + `); + }); + // @gate __DEV__ it('fails if first expected warning is not included', () => { const message = expectToWarnAndToThrow(() => { @@ -1143,6 +1230,94 @@ describe('ReactInternalTestUtils console assertions', () => { assertConsoleErrorDev(['Hello', 'Good day', 'Bye'], {withoutStack: 1}); }); + 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 awaitExpectToWarnAndToThrow(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 awaitExpectToWarnAndToThrow(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 = expectToWarnAndToThrow(() => { diff --git a/packages/internal-test-utils/consoleMock.js b/packages/internal-test-utils/consoleMock.js index c90c37b2d921f..7c4b0d99b72b0 100644 --- a/packages/internal-test-utils/consoleMock.js +++ b/packages/internal-test-utils/consoleMock.js @@ -182,6 +182,53 @@ export function clearErrors() { 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; 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( From a7750f8da24fba444d3d5fb5e33dd5e190424558 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Fri, 5 Apr 2024 11:55:26 -0500 Subject: [PATCH 4/7] Assert logs cleared when assertYieldsWereCleared --- .../ReactInternalTestUtils.js | 2 + .../__tests__/ReactInternalTestUtils-test.js | 582 +++++++++++++++++- packages/jest-react/src/JestReact.js | 2 + scripts/jest/matchers/reactTestMatchers.js | 3 +- 4 files changed, 580 insertions(+), 9 deletions(-) diff --git a/packages/internal-test-utils/ReactInternalTestUtils.js b/packages/internal-test-utils/ReactInternalTestUtils.js index 2e4d2481519c2..4d2fa37890850 100644 --- a/packages/internal-test-utils/ReactInternalTestUtils.js +++ b/packages/internal-test-utils/ReactInternalTestUtils.js @@ -17,6 +17,7 @@ import { createLogAssertion, } from './consoleMock'; export {act} from './internalAct'; +const {assertConsoleLogsCleared} = require('internal-test-utils/consoleMock'); import {thrownErrors, actingUpdatesScopeDepth} from './internalAct'; @@ -29,6 +30,7 @@ function assertYieldsWereCleared(caller) { Error.captureStackTrace(error, caller); throw error; } + assertConsoleLogsCleared(); } export async function waitForMicrotasks() { diff --git a/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js b/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js index bd82f393bb7d8..f7b232171c02a 100644 --- a/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js +++ b/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js @@ -333,15 +333,10 @@ const awaitExpectToWarnAndToThrow = async expectBlock => { }; describe('ReactInternalTestUtils console assertions', () => { - beforeEach(() => { - jest.resetAllMocks(); + beforeAll(() => { patchConsoleMethods({includeLog: true}); }); - afterEach(() => { - resetAllUnexpectedConsoleCalls(); - }); - describe('assertConsoleLogDev', () => { // @gate __DEV__ it('passes for a single log', () => { @@ -582,6 +577,8 @@ describe('ReactInternalTestUtils console assertions', () => { Do not pass withoutStack to assertConsoleLogDev, console.log does not have component stacks." `); + + assertConsoleLogDev(['Hello']); }); // @gate __DEV__ @@ -652,6 +649,7 @@ describe('ReactInternalTestUtils console assertions', () => { it('fails if first arg is not an array', () => { const message = expectToWarnAndToThrow(() => { console.log('Hi'); + console.log('Bye'); assertConsoleLogDev('Hi', 'Bye'); }); expect(message).toMatchInlineSnapshot(` @@ -659,6 +657,190 @@ describe('ReactInternalTestUtils console assertions', () => { 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 awaitExpectToWarnAndToThrow(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 awaitExpectToWarnAndToThrow(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 awaitExpectToWarnAndToThrow(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 awaitExpectToWarnAndToThrow(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 = expectToWarnAndToThrow(() => { + 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
); }); }); @@ -1046,6 +1228,8 @@ describe('ReactInternalTestUtils console assertions', () => { The second argument for assertConsoleWarnDev(), when specified, must be an object. It may have a property called "withoutStack" whose value may be a boolean or number. Instead received object." `); + + assertConsoleWarnDev(['Hi'], {withoutStack: true}); }); // @gate __DEV__ @@ -1059,6 +1243,8 @@ describe('ReactInternalTestUtils console assertions', () => { The second argument for assertConsoleWarnDev(), when specified, must be an object. It may have a property called "withoutStack" whose value may be a boolean or number. Instead received object." `); + + assertConsoleWarnDev(['Hi'], {withoutStack: true}); }); // @gate __DEV__ @@ -1072,6 +1258,8 @@ describe('ReactInternalTestUtils console assertions', () => { The second argument for assertConsoleWarnDev(), when specified, must be an object. It may have a property called "withoutStack" whose value may be a boolean or number. Instead received string." `); + + assertConsoleWarnDev(['Hi'], {withoutStack: true}); }); // @gate __DEV__ @@ -1156,6 +1344,7 @@ describe('ReactInternalTestUtils console assertions', () => { it('fails if multiple strings are passed without an array wrapper for single log', () => { const message = expectToWarnAndToThrow(() => { console.warn('Hi \n in div'); + console.warn('Bye \n in div'); assertConsoleWarnDev('Hi', 'Bye'); }); expect(message).toMatchInlineSnapshot(` @@ -1163,6 +1352,7 @@ describe('ReactInternalTestUtils console assertions', () => { Expected messages should be an array of strings but was given type "string"." `); + assertConsoleWarnDev(['Hi', 'Bye']); }); // @gate __DEV__ @@ -1177,6 +1367,7 @@ describe('ReactInternalTestUtils console assertions', () => { Expected messages should be an array of strings but was given type "string"." `); + assertConsoleWarnDev(['Hi', 'Bye']); }); // @gate __DEV__ @@ -1192,6 +1383,189 @@ describe('ReactInternalTestUtils console assertions', () => { 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 awaitExpectToWarnAndToThrow(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 awaitExpectToWarnAndToThrow(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 awaitExpectToWarnAndToThrow(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 awaitExpectToWarnAndToThrow(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 = expectToWarnAndToThrow(() => { + 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
); }); }); @@ -1462,12 +1836,15 @@ describe('ReactInternalTestUtils console assertions', () => { it('fails if only error does not contain a stack', () => { const message = expectToWarnAndToThrow(() => { console.error('Hello'); - assertConsoleErrorDev('Hello'); + assertConsoleErrorDev(['Hello']); }); expect(message).toMatchInlineSnapshot(` "assertConsoleErrorDev(expected) - Expected messages should be an array of strings but was given type "string"." + Missing component stack for: + "Hello" + + If this error intentionally omits the component stack, add {withoutStack: true} to the assertConsoleErrorDev call." `); }); @@ -1650,6 +2027,7 @@ describe('ReactInternalTestUtils console assertions', () => { The second argument for assertConsoleErrorDev(), when specified, must be an object. It may have a property called "withoutStack" whose value may be a boolean or number. Instead received object." `); + assertConsoleErrorDev(['Hi'], {withoutStack: true}); }); // @gate __DEV__ @@ -1663,6 +2041,7 @@ describe('ReactInternalTestUtils console assertions', () => { The second argument for assertConsoleErrorDev(), when specified, must be an object. It may have a property called "withoutStack" whose value may be a boolean or number. Instead received object." `); + assertConsoleErrorDev(['Hi'], {withoutStack: true}); }); // @gate __DEV__ @@ -1676,6 +2055,7 @@ describe('ReactInternalTestUtils console assertions', () => { The second argument for assertConsoleErrorDev(), when specified, must be an object. It may have a property called "withoutStack" whose value may be a boolean or number. Instead received string." `); + assertConsoleErrorDev(['Hi'], {withoutStack: true}); }); // @gate __DEV__ @@ -1778,6 +2158,7 @@ describe('ReactInternalTestUtils console assertions', () => { it('fails if multiple strings are passed without an array wrapper for single log', () => { const message = expectToWarnAndToThrow(() => { console.error('Hi \n in div'); + console.error('Bye \n in div'); assertConsoleErrorDev('Hi', 'Bye'); }); expect(message).toMatchInlineSnapshot(` @@ -1785,6 +2166,7 @@ describe('ReactInternalTestUtils console assertions', () => { Expected messages should be an array of strings but was given type "string"." `); + assertConsoleErrorDev(['Hi', 'Bye']); }); // @gate __DEV__ @@ -1799,6 +2181,7 @@ describe('ReactInternalTestUtils console assertions', () => { Expected messages should be an array of strings but was given type "string"." `); + assertConsoleErrorDev(['Hi', 'Bye']); }); // @gate __DEV__ @@ -1814,6 +2197,189 @@ describe('ReactInternalTestUtils console assertions', () => { 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 awaitExpectToWarnAndToThrow(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 awaitExpectToWarnAndToThrow(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 awaitExpectToWarnAndToThrow(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 awaitExpectToWarnAndToThrow(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 = expectToWarnAndToThrow(() => { + 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/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..91c567bdd2638 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) { @@ -35,6 +35,7 @@ function toMatchRenderedOutput(ReactNoop, expectedJSX) { if (typeof ReactNoop.getChildrenAsJSX === 'function') { const Scheduler = ReactNoop._Scheduler; assertYieldsWereCleared(Scheduler, toMatchRenderedOutput); + assertConsoleLogsCleared(); return captureAssertion(() => { expect(ReactNoop.getChildrenAsJSX()).toEqual(expectedJSX); }); From 6353a547d3bdc324df41122e0e36d73ac2564ebe Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Fri, 5 Apr 2024 12:17:41 -0500 Subject: [PATCH 5/7] Switch error after test to assetConsole methods --- packages/internal-test-utils/consoleMock.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/internal-test-utils/consoleMock.js b/packages/internal-test-utils/consoleMock.js index 7c4b0d99b72b0..e29a8d6d4ad5f 100644 --- a/packages/internal-test-utils/consoleMock.js +++ b/packages/internal-test-utils/consoleMock.js @@ -90,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( @@ -128,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; From ccb6115eac93748e694f00a11effee683127eb88 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Wed, 10 Apr 2024 17:08:18 -0400 Subject: [PATCH 6/7] Move assertConsoleLogsCleared for parity --- scripts/jest/matchers/reactTestMatchers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/jest/matchers/reactTestMatchers.js b/scripts/jest/matchers/reactTestMatchers.js index 91c567bdd2638..fbe8d00cc2301 100644 --- a/scripts/jest/matchers/reactTestMatchers.js +++ b/scripts/jest/matchers/reactTestMatchers.js @@ -29,13 +29,13 @@ function assertYieldsWereCleared(Scheduler, caller) { Error.captureStackTrace(error, caller); throw error; } + assertConsoleLogsCleared(); } function toMatchRenderedOutput(ReactNoop, expectedJSX) { if (typeof ReactNoop.getChildrenAsJSX === 'function') { const Scheduler = ReactNoop._Scheduler; assertYieldsWereCleared(Scheduler, toMatchRenderedOutput); - assertConsoleLogsCleared(); return captureAssertion(() => { expect(ReactNoop.getChildrenAsJSX()).toEqual(expectedJSX); }); From 9822c2285143022373870e45b611edd8bb6d820f Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Wed, 10 Apr 2024 17:58:28 -0400 Subject: [PATCH 7/7] Support local withoutStack options --- .../__tests__/ReactInternalTestUtils-test.js | 939 ++++++++++++------ packages/internal-test-utils/consoleMock.js | 180 ++-- 2 files changed, 752 insertions(+), 367 deletions(-) diff --git a/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js b/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js index f7b232171c02a..af2a447f49819 100644 --- a/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js +++ b/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js @@ -309,7 +309,7 @@ describe('ReactInternalTestUtils console mocks', () => { }); // Helper method to capture assertion failure. -const expectToWarnAndToThrow = expectBlock => { +const expectToThrowFailure = expectBlock => { let caughtError; try { expectBlock(); @@ -321,7 +321,7 @@ const expectToWarnAndToThrow = expectBlock => { }; // Helper method to capture assertion failure with act. -const awaitExpectToWarnAndToThrow = async expectBlock => { +const awaitExpectToThrowFailure = async expectBlock => { let caughtError; try { await expectBlock(); @@ -372,7 +372,7 @@ describe('ReactInternalTestUtils console assertions', () => { await act(() => { root.render(); }); - const message = await awaitExpectToWarnAndToThrow(async () => { + const message = await awaitExpectToThrowFailure(async () => { await act(() => { root.render(); }); @@ -392,7 +392,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if first expected log is not included', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.log('Wow'); console.log('Bye'); assertConsoleLogDev(['Hi', 'Wow', 'Bye']); @@ -413,7 +413,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if middle expected log is not included', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.log('Hi'); console.log('Bye'); assertConsoleLogDev(['Hi', 'Wow', 'Bye']); @@ -434,7 +434,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if last expected log is not included', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.log('Hi'); console.log('Wow'); assertConsoleLogDev(['Hi', 'Wow', 'Bye']); @@ -455,7 +455,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if first received log is not included', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.log('Hi'); console.log('Wow'); console.log('Bye'); @@ -477,7 +477,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if middle received log is not included', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.log('Hi'); console.log('Wow'); console.log('Bye'); @@ -499,7 +499,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if last received log is not included', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.log('Hi'); console.log('Wow'); console.log('Bye'); @@ -521,7 +521,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if both expected and received mismatch', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.log('Hi'); console.log('Wow'); console.log('Bye'); @@ -544,7 +544,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if both expected and received mismatch with multiple lines', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.log('Hi\nFoo'); console.log('Wow\nBar'); console.log('Bye\nBaz'); @@ -566,8 +566,22 @@ describe('ReactInternalTestUtils console assertions', () => { }); // @gate __DEV__ - it('fails if withoutStack passed to assertConsoleLogDev', () => { - const message = expectToWarnAndToThrow(() => { + 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}); }); @@ -583,7 +597,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if the args is greater than %s argument number', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.log('Hi %s', 'Sara', 'extra'); assertConsoleLogDev(['Hi']); }); @@ -597,7 +611,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if the args is greater than %s argument number for multiple logs', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.log('Hi %s', 'Sara', 'extra'); console.log('Bye %s', 'Sara', 'extra'); assertConsoleLogDev(['Hi', 'Bye']); @@ -615,7 +629,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if the %s argument number is greater than args', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.log('Hi %s'); assertConsoleLogDev(['Hi']); }); @@ -629,7 +643,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if the %s argument number is greater than args for multiple logs', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.log('Hi %s'); console.log('Bye %s'); assertConsoleLogDev(['Hi', 'Bye']); @@ -647,7 +661,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if first arg is not an array', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.log('Hi'); console.log('Bye'); assertConsoleLogDev('Hi', 'Bye'); @@ -680,7 +694,7 @@ describe('ReactInternalTestUtils console assertions', () => { console.log('Not asserted'); - const message = await awaitExpectToWarnAndToThrow(async () => { + const message = await awaitExpectToThrowFailure(async () => { await waitFor(['foo', 'bar']); }); expect(message).toMatchInlineSnapshot(` @@ -722,7 +736,7 @@ describe('ReactInternalTestUtils console assertions', () => { console.log('Not asserted'); - const message = await awaitExpectToWarnAndToThrow(async () => { + const message = await awaitExpectToThrowFailure(async () => { await waitForThrow('Oh no!'); }); expect(message).toMatchInlineSnapshot(` @@ -756,7 +770,7 @@ describe('ReactInternalTestUtils console assertions', () => { root.render(); console.log('Not asserted'); - const message = await awaitExpectToWarnAndToThrow(async () => { + const message = await awaitExpectToThrowFailure(async () => { await waitForPaint(['Urgent: B, Deferred: A']); }); @@ -791,7 +805,7 @@ describe('ReactInternalTestUtils console assertions', () => { console.log('Not asserted'); - const message = await awaitExpectToWarnAndToThrow(async () => { + const message = await awaitExpectToThrowFailure(async () => { await waitForAll(['foo', 'bar', 'baz']); }); expect(message).toMatchInlineSnapshot(` @@ -826,7 +840,7 @@ describe('ReactInternalTestUtils console assertions', () => { assertLog([]); await waitForAll(['foo', 'bar', 'baz']); - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { expect(root).toMatchRenderedOutput(
foobarbaz
); }); expect(message).toMatchInlineSnapshot(` @@ -846,7 +860,7 @@ describe('ReactInternalTestUtils console assertions', () => { describe('assertConsoleWarnDev', () => { // @gate __DEV__ - it('passes if a warning contains a stack', () => { + it('passes if an warning contains a stack', () => { console.warn('Hello\n in div'); assertConsoleWarnDev(['Hello']); }); @@ -859,29 +873,49 @@ describe('ReactInternalTestUtils console assertions', () => { assertConsoleWarnDev(['Hello', 'Good day', 'Bye']); }); - // @gate __DEV__ - it('passes if warnings without stack explicitly opt out', () => { - console.warn('Hello'); - assertConsoleWarnDev(['Hello'], {withoutStack: true}); + it('fails if act is called without assertConsoleWarnDev', async () => { + const Yield = ({id}) => { + console.warn(id); + return id; + }; - console.warn('Hello'); - console.warn('Good day'); - console.warn('Bye'); + function App() { + return ( +
+ + + +
+ ); + } - assertConsoleWarnDev(['Hello', 'Good day', 'Bye'], {withoutStack: true}); - }); + const root = ReactNoop.createRoot(); + await act(() => { + root.render(); + }); + const message = await awaitExpectToThrowFailure(async () => { + await act(() => { + root.render(); + }); + }); - // @gate __DEV__ - it('passes when expected withoutStack number matches the actual one', () => { - console.warn('Hello\n in div'); - console.warn('Good day'); - console.warn('Bye\n in div'); - assertConsoleWarnDev(['Hello', 'Good day', 'Bye'], {withoutStack: 1}); + 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 assertConsoleWarnDev', async () => { + 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; }; @@ -899,7 +933,7 @@ describe('ReactInternalTestUtils console assertions', () => { await act(() => { root.render(); }); - const message = await awaitExpectToWarnAndToThrow(async () => { + const message = await awaitExpectToThrowFailure(async () => { await act(() => { root.render(); }); @@ -908,18 +942,28 @@ describe('ReactInternalTestUtils console assertions', () => { 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 = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.warn('Wow \n in div'); console.warn('Bye \n in div'); assertConsoleWarnDev(['Hi', 'Wow', 'Bye']); @@ -942,7 +986,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if middle expected warning is not included', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.warn('Hi \n in div'); console.warn('Bye \n in div'); assertConsoleWarnDev(['Hi', 'Wow', 'Bye']); @@ -965,7 +1009,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if last expected warning is not included', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.warn('Hi \n in div'); console.warn('Wow \n in div'); assertConsoleWarnDev(['Hi', 'Wow', 'Bye']); @@ -988,7 +1032,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if first received warning is not included', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.warn('Hi \n in div'); console.warn('Wow \n in div'); console.warn('Bye \n in div'); @@ -1012,7 +1056,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if middle received warning is not included', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.warn('Hi \n in div'); console.warn('Wow \n in div'); console.warn('Bye \n in div'); @@ -1036,7 +1080,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if last received warning is not included', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.warn('Hi \n in div'); console.warn('Wow \n in div'); console.warn('Bye \n in div'); @@ -1060,7 +1104,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if only warning does not contain a stack', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.warn('Hello'); assertConsoleWarnDev(['Hello']); }); @@ -1070,13 +1114,14 @@ describe('ReactInternalTestUtils console assertions', () => { Missing component stack for: "Hello" - If this warning intentionally omits the component stack, add {withoutStack: true} to the assertConsoleWarnDev call." + 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 = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.warn('Hello'); console.warn('Good day\n in div'); console.warn('Bye\n in div'); @@ -1088,12 +1133,14 @@ describe('ReactInternalTestUtils console assertions', () => { Missing component stack for: "Hello" - If this warning intentionally omits the component stack, add {withoutStack: true} to the assertConsoleWarnDev call." + 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 = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.warn('Hello\n in div'); console.warn('Good day'); console.warn('Bye\n in div'); @@ -1105,13 +1152,14 @@ describe('ReactInternalTestUtils console assertions', () => { Missing component stack for: "Good day" - If this warning intentionally omits the component stack, add {withoutStack: true} to the assertConsoleWarnDev call." + 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 = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.warn('Hello\n in div'); console.warn('Good day\n in div'); console.warn('Bye'); @@ -1123,13 +1171,14 @@ describe('ReactInternalTestUtils console assertions', () => { Missing component stack for: "Bye" - If this warning intentionally omits the component stack, add {withoutStack: true} to the assertConsoleWarnDev call." + 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 = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.warn('Hello'); console.warn('Good day'); console.warn('Bye'); @@ -1147,124 +1196,278 @@ describe('ReactInternalTestUtils console assertions', () => { Missing component stack for: "Bye" - If this warning intentionally omits the component stack, add {withoutStack: true} to the assertConsoleWarnDev call." + 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 only warning is not expected to have a stack, but does', () => { - const message = expectToWarnAndToThrow(() => { - console.warn('Hello\n in div'); + describe('global withoutStack', () => { + // @gate __DEV__ + it('passes if warnings without stack explicitly opt out', () => { + console.warn('Hello'); assertConsoleWarnDev(['Hello'], {withoutStack: true}); - }); - expect(message).toMatchInlineSnapshot(` - "assertConsoleWarnDev(expected) - Unexpected component stack for: - "Hello " - - If this warning intentionally includes the component stack, remove {withoutStack: true} from the assertConsoleWarnDev() call. - If you have a mix of warnings with and without stack in one assertConsoleWarnDev() call, pass {withoutStack: N} where N is the number of warnings without stacks." - `); - }); - - // @gate __DEV__ - it('fails if warnings are not expected to have a stack, but some do', () => { - const message = expectToWarnAndToThrow(() => { - console.warn('Hello\n in div'); + console.warn('Hello'); console.warn('Good day'); - console.warn('Bye\n in div'); + console.warn('Bye'); + assertConsoleWarnDev(['Hello', 'Good day', 'Bye'], { withoutStack: true, }); }); - expect(message).toMatchInlineSnapshot(` - "assertConsoleWarnDev(expected) - Unexpected component stack for: - "Hello " + // @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) - Unexpected component stack for: - "Bye " + The second argument must be {withoutStack: true}. - If this warning intentionally includes the component stack, remove {withoutStack: true} from the assertConsoleWarnDev() call. - If you have a mix of warnings with and without stack in one assertConsoleWarnDev() call, pass {withoutStack: N} where N is the number of warnings without stacks." - `); - }); + Instead received {"withoutStack":null}." + `); + assertConsoleWarnDev(['Hi'], {withoutStack: true}); + }); - // @gate __DEV__ - it('fails if expected withoutStack number does not match the actual one', () => { - const message = expectToWarnAndToThrow(() => { + // @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', 'Bye'], { - withoutStack: 4, + 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}]." + `); }); - expect(message).toMatchInlineSnapshot(` - "assertConsoleWarnDev(expected) - Expected 4 warnings without a component stack but received 1: - - Expected warnings - + Received warnings + // @gate __DEV__ + it('fails if withoutStack is invalid {} value', () => { + const message = expectToThrowFailure(() => { + console.warn('Hi'); + assertConsoleWarnDev([['Hi', {withoutStack: {}}]]); + }); + expect(message).toMatchInlineSnapshot(` + "assertConsoleWarnDev(expected) - - Hello - + Hello - Good day - - Bye - + Bye " - `); - }); + Log entries that are arrays must be of the form [string, {withoutStack: true}] - // @gate __DEV__ - it('fails if withoutStack is invalid null value', () => { - const message = expectToWarnAndToThrow(() => { - console.warn('Hi'); - assertConsoleWarnDev(['Hi'], {withoutStack: null}); + Instead received [string, {"withoutStack":{}}]." + `); }); - expect(message).toMatchInlineSnapshot(` - "assertConsoleWarnDev(expected) - The second argument for assertConsoleWarnDev(), when specified, must be an object. It may have a property called "withoutStack" whose value may be a boolean or number. Instead received object." - `); + // @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) - assertConsoleWarnDev(['Hi'], {withoutStack: true}); - }); + Log entries that are arrays must be of the form [string, {withoutStack: true}] - // @gate __DEV__ - it('fails if withoutStack is invalid {} value', () => { - const message = expectToWarnAndToThrow(() => { - console.warn('Hi'); - assertConsoleWarnDev(['Hi'], {withoutStack: {}}); + Instead received [string, {"withoutStack":"haha"}]." + `); }); - expect(message).toMatchInlineSnapshot(` - "assertConsoleWarnDev(expected) - The second argument for assertConsoleWarnDev(), when specified, must be an object. It may have a property called "withoutStack" whose value may be a boolean or number. Instead received object." - `); + // @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) - assertConsoleWarnDev(['Hi'], {withoutStack: true}); - }); + Log entries that are arrays must be of the form [string, {withoutStack: true}] - // @gate __DEV__ - it('fails if withoutStack is invalid string value', () => { - const message = expectToWarnAndToThrow(() => { - console.warn('Hi'); - assertConsoleWarnDev(['Hi'], {withoutStack: 'haha'}); + Instead received [string, {"withoutStack":4}]." + `); }); - expect(message).toMatchInlineSnapshot(` - "assertConsoleWarnDev(expected) - The second argument for assertConsoleWarnDev(), when specified, must be an object. It may have a property called "withoutStack" whose value may be a boolean or number. Instead received string." - `); + // @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. - assertConsoleWarnDev(['Hi'], {withoutStack: true}); + 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 = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.warn('Hi %s', 'Sara', 'extra'); assertConsoleWarnDev(['Hi'], {withoutStack: true}); }); @@ -1278,7 +1481,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if the args is greater than %s argument number for multiple warnings', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.warn('Hi %s', 'Sara', 'extra'); console.warn('Bye %s', 'Sara', 'extra'); assertConsoleWarnDev(['Hi', 'Bye'], {withoutStack: true}); @@ -1296,7 +1499,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if the %s argument number is greater than args', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.warn('Hi %s'); assertConsoleWarnDev(['Hi'], {withoutStack: true}); }); @@ -1310,7 +1513,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if the %s argument number is greater than args for multiple warnings', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.warn('Hi %s'); console.warn('Bye %s'); assertConsoleWarnDev(['Hi', 'Bye'], {withoutStack: true}); @@ -1328,7 +1531,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if component stack is passed twice', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.warn('Hi %s%s', '\n in div', '\n in div'); assertConsoleWarnDev(['Hi']); }); @@ -1340,9 +1543,27 @@ describe('ReactInternalTestUtils console assertions', () => { `); }); + // @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 = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.warn('Hi \n in div'); console.warn('Bye \n in div'); assertConsoleWarnDev('Hi', 'Bye'); @@ -1357,7 +1578,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if multiple strings are passed without an array wrapper for multiple logs', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.warn('Hi \n in div'); console.warn('Bye \n in div'); assertConsoleWarnDev('Hi', 'Bye'); @@ -1372,7 +1593,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails on more than two arguments', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.warn('Hi \n in div'); console.warn('Wow \n in div'); console.warn('Bye \n in div'); @@ -1405,7 +1626,7 @@ describe('ReactInternalTestUtils console assertions', () => { console.warn('Not asserted'); - const message = await awaitExpectToWarnAndToThrow(async () => { + const message = await awaitExpectToThrowFailure(async () => { await waitFor(['foo', 'bar']); }); expect(message).toMatchInlineSnapshot(` @@ -1447,7 +1668,7 @@ describe('ReactInternalTestUtils console assertions', () => { console.warn('Not asserted'); - const message = await awaitExpectToWarnAndToThrow(async () => { + const message = await awaitExpectToThrowFailure(async () => { await waitForThrow('Oh no!'); }); expect(message).toMatchInlineSnapshot(` @@ -1481,7 +1702,7 @@ describe('ReactInternalTestUtils console assertions', () => { root.render(); console.warn('Not asserted'); - const message = await awaitExpectToWarnAndToThrow(async () => { + const message = await awaitExpectToThrowFailure(async () => { await waitForPaint(['Urgent: B, Deferred: A']); }); @@ -1516,7 +1737,7 @@ describe('ReactInternalTestUtils console assertions', () => { console.warn('Not asserted'); - const message = await awaitExpectToWarnAndToThrow(async () => { + const message = await awaitExpectToThrowFailure(async () => { await waitForAll(['foo', 'bar', 'baz']); }); expect(message).toMatchInlineSnapshot(` @@ -1551,7 +1772,7 @@ describe('ReactInternalTestUtils console assertions', () => { assertLog([]); await waitForAll(['foo', 'bar', 'baz']); - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { expect(root).toMatchRenderedOutput(
foobarbaz
); }); expect(message).toMatchInlineSnapshot(` @@ -1584,26 +1805,6 @@ describe('ReactInternalTestUtils console assertions', () => { assertConsoleErrorDev(['Hello', 'Good day', 'Bye']); }); - // @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('passes when expected withoutStack number matches the actual one', () => { - console.error('Hello\n in div'); - console.error('Good day'); - console.error('Bye\n in div'); - assertConsoleErrorDev(['Hello', 'Good day', 'Bye'], {withoutStack: 1}); - }); - it('fails if act is called without assertConsoleErrorDev', async () => { const Yield = ({id}) => { console.error(id); @@ -1624,7 +1825,7 @@ describe('ReactInternalTestUtils console assertions', () => { await act(() => { root.render(); }); - const message = await awaitExpectToWarnAndToThrow(async () => { + const message = await awaitExpectToThrowFailure(async () => { await act(() => { root.render(); }); @@ -1664,7 +1865,7 @@ describe('ReactInternalTestUtils console assertions', () => { await act(() => { root.render(); }); - const message = await awaitExpectToWarnAndToThrow(async () => { + const message = await awaitExpectToThrowFailure(async () => { await act(() => { root.render(); }); @@ -1694,7 +1895,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if first expected error is not included', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.error('Wow \n in div'); console.error('Bye \n in div'); assertConsoleErrorDev(['Hi', 'Wow', 'Bye']); @@ -1717,7 +1918,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if middle expected error is not included', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.error('Hi \n in div'); console.error('Bye \n in div'); assertConsoleErrorDev(['Hi', 'Wow', 'Bye']); @@ -1740,7 +1941,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if last expected error is not included', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.error('Hi \n in div'); console.error('Wow \n in div'); assertConsoleErrorDev(['Hi', 'Wow', 'Bye']); @@ -1763,7 +1964,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if first received error is not included', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.error('Hi \n in div'); console.error('Wow \n in div'); console.error('Bye \n in div'); @@ -1787,7 +1988,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if middle received error is not included', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.error('Hi \n in div'); console.error('Wow \n in div'); console.error('Bye \n in div'); @@ -1811,7 +2012,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if last received error is not included', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.error('Hi \n in div'); console.error('Wow \n in div'); console.error('Bye \n in div'); @@ -1834,7 +2035,7 @@ describe('ReactInternalTestUtils console assertions', () => { }); // @gate __DEV__ it('fails if only error does not contain a stack', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.error('Hello'); assertConsoleErrorDev(['Hello']); }); @@ -1844,13 +2045,14 @@ describe('ReactInternalTestUtils console assertions', () => { Missing component stack for: "Hello" - If this error intentionally omits the component stack, add {withoutStack: true} to the assertConsoleErrorDev call." + 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 = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.error('Hello\n in div'); console.error('Good day\n in div'); console.error('Bye'); @@ -1862,12 +2064,13 @@ describe('ReactInternalTestUtils console assertions', () => { Missing component stack for: "Bye" - If this error intentionally omits the component stack, add {withoutStack: true} to the assertConsoleErrorDev call." + 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 = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.error('Hello'); console.error('Good day\n in div'); console.error('Bye\n in div'); @@ -1879,12 +2082,13 @@ describe('ReactInternalTestUtils console assertions', () => { Missing component stack for: "Hello" - If this error intentionally omits the component stack, add {withoutStack: true} to the assertConsoleErrorDev call." + 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 = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.error('Hello\n in div'); console.error('Good day'); console.error('Bye\n in div'); @@ -1896,12 +2100,13 @@ describe('ReactInternalTestUtils console assertions', () => { Missing component stack for: "Good day" - If this error intentionally omits the component stack, add {withoutStack: true} to the assertConsoleErrorDev call." + 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 = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.error('Hello'); console.error('Good day'); console.error('Bye'); @@ -1919,148 +2124,278 @@ describe('ReactInternalTestUtils console assertions', () => { Missing component stack for: "Bye" - If this error intentionally omits the component stack, add {withoutStack: true} to the assertConsoleErrorDev call." + 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 only error is not expected to have a stack, but does', () => { - const message = expectToWarnAndToThrow(() => { - console.error('Hello\n in div'); + describe('global withoutStack', () => { + // @gate __DEV__ + it('passes if errors without stack explicitly opt out', () => { + console.error('Hello'); assertConsoleErrorDev(['Hello'], {withoutStack: true}); - }); - expect(message).toMatchInlineSnapshot(` - "assertConsoleErrorDev(expected) - - Unexpected component stack for: - "Hello " - - If this error intentionally includes the component stack, remove {withoutStack: true} from the assertConsoleErrorDev() call. - If you have a mix of errors with and without stack in one assertConsoleErrorDev() call, pass {withoutStack: N} where N is the number of errors without stacks." - `); - }); - // @gate __DEV__ - it('fails if errors are not expected to have a stack, but some do', () => { - const message = expectToWarnAndToThrow(() => { - console.error('Hello\n in div'); + console.error('Hello'); console.error('Good day'); - console.error('Bye\n in div'); + console.error('Bye'); + assertConsoleErrorDev(['Hello', 'Good day', 'Bye'], { withoutStack: true, }); }); - expect(message).toMatchInlineSnapshot(` - "assertConsoleErrorDev(expected) - Unexpected component stack for: - "Hello " + // @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) - Unexpected component stack for: - "Bye " + The second argument must be {withoutStack: true}. - If this error intentionally includes the component stack, remove {withoutStack: true} from the assertConsoleErrorDev() call. - If you have a mix of errors with and without stack in one assertConsoleErrorDev() call, pass {withoutStack: N} where N is the number of errors without stacks." - `); - }); + Instead received {"withoutStack":null}." + `); + assertConsoleErrorDev(['Hi'], {withoutStack: true}); + }); - // @gate __DEV__ - it('fails if expected withoutStack number does not match the actual one', () => { - const message = expectToWarnAndToThrow(() => { - console.error('Hello\n in div'); - console.error('Good day'); - console.error('Bye\n in div'); - assertConsoleErrorDev(['Hello', 'Good day', 'Bye'], { - withoutStack: 4, + // @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}); }); - expect(message).toMatchInlineSnapshot(` - "assertConsoleErrorDev(expected) - Expected 4 errors without a component stack but received 1: - - Expected errors - + Received errors + // @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) - - Hello - + Hello - Good day - - Bye - + Bye " - `); - }); + The second argument must be {withoutStack: true}. - // @gate __DEV__ - it('fails if multiple expected withoutStack number does not match the actual one', () => { - const message = expectToWarnAndToThrow(() => { + 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('Good night'); console.error('Bye\n in div'); - assertConsoleErrorDev(['Hello', 'Good day', 'Good night', 'Bye'], { - withoutStack: 4, + 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}]." + `); }); - expect(message).toMatchInlineSnapshot(` - "assertConsoleErrorDev(expected) - Expected 4 errors without a component stack but received 2: - - Expected errors - + Received errors + // @gate __DEV__ + it('fails if withoutStack is invalid {} value', () => { + const message = expectToThrowFailure(() => { + console.error('Hi'); + assertConsoleErrorDev([['Hi', {withoutStack: {}}]]); + }); + expect(message).toMatchInlineSnapshot(` + "assertConsoleErrorDev(expected) - - Hello - + Hello - Good day - Good night - - Bye - + Bye " - `); - }); + Log entries that are arrays must be of the form [string, {withoutStack: true}] - // @gate __DEV__ - it('fails if withoutStack is invalid null value', () => { - const message = expectToWarnAndToThrow(() => { - console.error('Hi'); - assertConsoleErrorDev(['Hi'], {withoutStack: null}); + Instead received [string, {"withoutStack":{}}]." + `); }); - expect(message).toMatchInlineSnapshot(` - "assertConsoleErrorDev(expected) - The second argument for assertConsoleErrorDev(), when specified, must be an object. It may have a property called "withoutStack" whose value may be a boolean or number. Instead received object." - `); - 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) - // @gate __DEV__ - it('fails if withoutStack is invalid {} value', () => { - const message = expectToWarnAndToThrow(() => { - console.error('Hi'); - assertConsoleErrorDev(['Hi'], {withoutStack: {}}); + Log entries that are arrays must be of the form [string, {withoutStack: true}] + + Instead received [string, {"withoutStack":"haha"}]." + `); }); - expect(message).toMatchInlineSnapshot(` - "assertConsoleErrorDev(expected) - The second argument for assertConsoleErrorDev(), when specified, must be an object. It may have a property called "withoutStack" whose value may be a boolean or number. Instead received object." - `); - assertConsoleErrorDev(['Hi'], {withoutStack: true}); - }); + // @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) - // @gate __DEV__ - it('fails if withoutStack is invalid string value', () => { - const message = expectToWarnAndToThrow(() => { - console.error('Hi'); - assertConsoleErrorDev(['Hi'], {withoutStack: 'haha'}); + Log entries that are arrays must be of the form [string, {withoutStack: true}] + + Instead received [string, {"withoutStack":4}]." + `); }); - expect(message).toMatchInlineSnapshot(` - "assertConsoleErrorDev(expected) - The second argument for assertConsoleErrorDev(), when specified, must be an object. It may have a property called "withoutStack" whose value may be a boolean or number. Instead received string." - `); - assertConsoleErrorDev(['Hi'], {withoutStack: true}); + // @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 = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.error('Hi %s', 'Sara', 'extra'); assertConsoleErrorDev(['Hi'], {withoutStack: true}); }); @@ -2074,7 +2409,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if the args is greater than %s argument number for multiple errors', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.error('Hi %s', 'Sara', 'extra'); console.error('Bye %s', 'Sara', 'extra'); assertConsoleErrorDev(['Hi', 'Bye'], {withoutStack: true}); @@ -2092,7 +2427,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if the %s argument number is greater than args', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.error('Hi %s'); assertConsoleErrorDev(['Hi'], {withoutStack: true}); }); @@ -2106,7 +2441,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if the %s argument number is greater than args for multiple errors', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.error('Hi %s'); console.error('Bye %s'); assertConsoleErrorDev(['Hi', 'Bye'], {withoutStack: true}); @@ -2124,7 +2459,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if component stack is passed twice', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.error('Hi %s%s', '\n in div', '\n in div'); assertConsoleErrorDev(['Hi']); }); @@ -2138,7 +2473,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if multiple logs pass component stack twice', () => { - const message = expectToWarnAndToThrow(() => { + 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']); @@ -2156,7 +2491,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if multiple strings are passed without an array wrapper for single log', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.error('Hi \n in div'); console.error('Bye \n in div'); assertConsoleErrorDev('Hi', 'Bye'); @@ -2171,7 +2506,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails if multiple strings are passed without an array wrapper for multiple logs', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.error('Hi \n in div'); console.error('Bye \n in div'); assertConsoleErrorDev('Hi', 'Bye'); @@ -2186,7 +2521,7 @@ describe('ReactInternalTestUtils console assertions', () => { // @gate __DEV__ it('fails on more than two arguments', () => { - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { console.error('Hi \n in div'); console.error('Wow \n in div'); console.error('Bye \n in div'); @@ -2219,7 +2554,7 @@ describe('ReactInternalTestUtils console assertions', () => { console.error('Not asserted'); - const message = await awaitExpectToWarnAndToThrow(async () => { + const message = await awaitExpectToThrowFailure(async () => { await waitFor(['foo', 'bar']); }); expect(message).toMatchInlineSnapshot(` @@ -2261,7 +2596,7 @@ describe('ReactInternalTestUtils console assertions', () => { console.error('Not asserted'); - const message = await awaitExpectToWarnAndToThrow(async () => { + const message = await awaitExpectToThrowFailure(async () => { await waitForThrow('Oh no!'); }); expect(message).toMatchInlineSnapshot(` @@ -2295,7 +2630,7 @@ describe('ReactInternalTestUtils console assertions', () => { root.render(); console.error('Not asserted'); - const message = await awaitExpectToWarnAndToThrow(async () => { + const message = await awaitExpectToThrowFailure(async () => { await waitForPaint(['Urgent: B, Deferred: A']); }); @@ -2330,7 +2665,7 @@ describe('ReactInternalTestUtils console assertions', () => { console.error('Not asserted'); - const message = await awaitExpectToWarnAndToThrow(async () => { + const message = await awaitExpectToThrowFailure(async () => { await waitForAll(['foo', 'bar', 'baz']); }); expect(message).toMatchInlineSnapshot(` @@ -2365,7 +2700,7 @@ describe('ReactInternalTestUtils console assertions', () => { assertLog([]); await waitForAll(['foo', 'bar', 'baz']); - const message = expectToWarnAndToThrow(() => { + const message = expectToThrowFailure(() => { expect(root).toMatchRenderedOutput(
foobarbaz
); }); expect(message).toMatchInlineSnapshot(` diff --git a/packages/internal-test-utils/consoleMock.js b/packages/internal-test-utils/consoleMock.js index e29a8d6d4ad5f..4601335f40dbe 100644 --- a/packages/internal-test-utils/consoleMock.js +++ b/packages/internal-test-utils/consoleMock.js @@ -299,31 +299,28 @@ export function createLogAssertion( const withoutStack = options.withoutStack; + // Warn about invalid global withoutStack values. if (consoleMethod === 'log' && withoutStack !== undefined) { - // We don't expect any console.log calls to have a stack. throwFormattedError( `Do not pass withoutStack to assertConsoleLogDev, console.log does not have component stacks.`, ); - } else if ( - withoutStack !== undefined && - typeof withoutStack !== 'number' && - withoutStack !== true - ) { + } else if (withoutStack !== undefined && withoutStack !== true) { + // withoutStack can only have a value true. throwFormattedError( - `The second argument for ${matcherName}(), when specified, must be an object. It may have a ` + - `property called "withoutStack" whose value may be a boolean or number. ` + - `Instead received ${typeof withoutStack}.`, + `The second argument must be {withoutStack: true}.` + + `\n\nInstead received ${JSON.stringify(options)}.`, ); } const observedLogs = clearObservedErrors(); - const unexpectedLogs = []; const receivedLogs = []; - const logsWithoutComponentStack = []; - const logsWithComponentStack = []; + const missingExpectedLogs = Array.from(expectedMessages); + + const unexpectedLogs = []; + const unexpectedMissingComponentStack = []; + const unexpectedIncludingComponentStack = []; const logsMismatchingFormat = []; const logsWithExtraComponentStack = []; - const missingExpectedLogs = Array.from(expectedMessages); // Loop over all the observed logs to determine: // - Which expected logs are missing @@ -342,7 +339,77 @@ export function createLogAssertion( return; } - const expectedMessage = replaceComponentStack(expectedMessages[index]); + 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); @@ -371,15 +438,17 @@ export function createLogAssertion( }); } - // Check if log is expected, and if it has a component stack. + // Main logic to check if log is expected, with the component stack. if ( normalizedMessage === expectedMessage || normalizedMessage.includes(expectedMessage) ) { if (isLikelyAComponentStack(normalizedMessage)) { - logsWithComponentStack.push(normalizedMessage); - } else { - logsWithoutComponentStack.push(normalizedMessage); + if (expectedWithoutStack === true) { + unexpectedIncludingComponentStack.push(normalizedMessage); + } + } else if (expectedWithoutStack !== true) { + unexpectedMissingComponentStack.push(normalizedMessage); } // Found expected log, remove it from missing. @@ -419,53 +488,34 @@ export function createLogAssertion( ); } - // Any unexpected component stacks are a failure. - if (consoleMethod !== 'log') { - if (typeof withoutStack === 'number') { - // We're expecting a particular number of warnings without stacks. - if (withoutStack !== logsWithoutComponentStack.length) { - throwFormattedError( - `Expected ${withoutStack} ${logName()}s without a component stack but received ${ - logsWithoutComponentStack.length - }:\n${printDiff()}`, - ); - } - } else if (withoutStack === true) { - // We're expecting that all warnings won't have the stack. - // If some warnings have it, it's an error. - if (logsWithComponentStack.length > 0) { - throwFormattedError( - `${logsWithComponentStack - .map( - stack => - `Unexpected component stack for:\n ${printReceived( - stack, - )}`, - ) - .join( - '\n\n', - )}\n\nIf this ${logName()} intentionally includes the component stack, remove ` + - `{withoutStack: true} from the ${matcherName}() call.\nIf you have a mix of ` + - `${logName()}s with and without stack in one ${matcherName}() call, pass ` + - `{withoutStack: N} where N is the number of ${logName()}s without stacks.`, - ); - } - } else if (withoutStack === undefined) { - // We're expecting that all warnings *do* have the stack (default). - // If some warnings don't have it, it's an error. - if (logsWithoutComponentStack.length > 0) { - throwFormattedError( - `${logsWithoutComponentStack - .map( - stack => - `Missing component stack for:\n ${printReceived(stack)}`, - ) - .join( - '\n\n', - )}\n\nIf this ${logName()} intentionally omits the component stack, add {withoutStack: true} to the ${matcherName} call.`, - ); - } - } + // 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.