From ac17a2ae72b579f4b9f1a139e9f0cc47fa009bd9 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Wed, 3 Apr 2024 10:44:02 -0500 Subject: [PATCH] [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..32dd4f495b28e 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 assertLogDev = createLogAssertion( + 'log', + 'assertLogDev', + clearLogs, +); +export const assertWarnDev = createLogAssertion( + 'warn', + 'assertWarnDev', + clearWarnings, +); +export const assertErrorDev = createLogAssertion( + 'error', + 'assertErrorDev', + 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..6eb93c10bcc33 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 { + assertLogDev, + assertWarnDev, + assertErrorDev, +} = 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('assertLogDev', () => { + // @gate __DEV__ + it('passes for a single log', () => { + console.log('Hello'); + assertLogDev(['Hello']); + }); + + // @gate __DEV__ + it('passes for multiple logs', () => { + console.log('Hello'); + console.log('Good day'); + console.log('Bye'); + assertLogDev(['Hello', 'Good day', 'Bye']); + }); + + // @gate __DEV__ + it('fails if first expected log is not included', () => { + const message = expectToWarnAndToThrow(() => { + console.log('Wow'); + console.log('Bye'); + assertLogDev(['Hi', 'Wow', 'Bye']); + }); + expect(message).toMatchInlineSnapshot(` + "assertLogDev(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'); + assertLogDev(['Hi', 'Wow', 'Bye']); + }); + expect(message).toMatchInlineSnapshot(` + "assertLogDev(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'); + assertLogDev(['Hi', 'Wow', 'Bye']); + }); + expect(message).toMatchInlineSnapshot(` + "assertLogDev(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'); + assertLogDev(['Wow', 'Bye']); + }); + expect(message).toMatchInlineSnapshot(` + "assertLogDev(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'); + assertLogDev(['Hi', 'Bye']); + }); + expect(message).toMatchInlineSnapshot(` + "assertLogDev(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'); + assertLogDev(['Hi', 'Wow']); + }); + expect(message).toMatchInlineSnapshot(` + "assertLogDev(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'); + assertLogDev(['Hi', 'Wow', 'Yikes']); + }); + expect(message).toMatchInlineSnapshot(` + "assertLogDev(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'); + assertLogDev(['Hi\nFoo', 'Wow\nBar', 'Yikes\nFaz']); + }); + expect(message).toMatchInlineSnapshot(` + "assertLogDev(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 assertLogDev', () => { + const message = expectToWarnAndToThrow(() => { + console.log('Hello'); + assertLogDev(['Hello'], {withoutStack: true}); + }); + + expect(message).toMatchInlineSnapshot(` + "assertLogDev(expected) + + Do not pass withoutStack to assertLogDev, 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'); + assertLogDev(['Hi']); + }); + expect(message).toMatchInlineSnapshot(` + "assertLogDev(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'); + assertLogDev(['Hi', 'Bye']); + }); + expect(message).toMatchInlineSnapshot(` + "assertLogDev(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'); + assertLogDev(['Hi']); + }); + expect(message).toMatchInlineSnapshot(` + "assertLogDev(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'); + assertLogDev(['Hi', 'Bye']); + }); + expect(message).toMatchInlineSnapshot(` + "assertLogDev(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'); + assertLogDev('Hi', 'Bye'); + }); + expect(message).toMatchInlineSnapshot(` + "assertLogDev(expected) + + Expected messages should be an array of strings but was given type "string"." + `); + }); + }); + + describe('assertWarnDev', () => { + // @gate __DEV__ + it('passes if a warning contains a stack', () => { + console.warn('Hello\n in div'); + assertWarnDev(['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'); + assertWarnDev(['Hello', 'Good day', 'Bye']); + }); + + // @gate __DEV__ + it('passes if warnings without stack explicitly opt out', () => { + console.warn('Hello'); + assertWarnDev(['Hello'], {withoutStack: true}); + + console.warn('Hello'); + console.warn('Good day'); + console.warn('Bye'); + + assertWarnDev(['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'); + assertWarnDev(['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'); + assertWarnDev(['Hi', 'Wow', 'Bye']); + }); + expect(message).toMatchInlineSnapshot(` + "assertWarnDev(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'); + assertWarnDev(['Hi', 'Wow', 'Bye']); + }); + expect(message).toMatchInlineSnapshot(` + "assertWarnDev(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'); + assertWarnDev(['Hi', 'Wow', 'Bye']); + }); + expect(message).toMatchInlineSnapshot(` + "assertWarnDev(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'); + assertWarnDev(['Wow', 'Bye']); + }); + expect(message).toMatchInlineSnapshot(` + "assertWarnDev(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'); + assertWarnDev(['Hi', 'Bye']); + }); + expect(message).toMatchInlineSnapshot(` + "assertWarnDev(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'); + assertWarnDev(['Hi', 'Wow']); + }); + expect(message).toMatchInlineSnapshot(` + "assertWarnDev(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'); + assertWarnDev(['Hello']); + }); + expect(message).toMatchInlineSnapshot(` + "assertWarnDev(expected) + + Missing component stack for: + "Hello" + + If this warning intentionally omits the component stack, add {withoutStack: true} to the assertWarnDev 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'); + assertWarnDev(['Hello', 'Good day', 'Bye']); + }); + expect(message).toMatchInlineSnapshot(` + "assertWarnDev(expected) + + Missing component stack for: + "Hello" + + If this warning intentionally omits the component stack, add {withoutStack: true} to the assertWarnDev 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'); + assertWarnDev(['Hello', 'Good day', 'Bye']); + }); + expect(message).toMatchInlineSnapshot(` + "assertWarnDev(expected) + + Missing component stack for: + "Good day" + + If this warning intentionally omits the component stack, add {withoutStack: true} to the assertWarnDev 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'); + assertWarnDev(['Hello', 'Good day', 'Bye']); + }); + expect(message).toMatchInlineSnapshot(` + "assertWarnDev(expected) + + Missing component stack for: + "Bye" + + If this warning intentionally omits the component stack, add {withoutStack: true} to the assertWarnDev 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'); + assertWarnDev(['Hello', 'Good day', 'Bye']); + }); + expect(message).toMatchInlineSnapshot(` + "assertWarnDev(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 assertWarnDev 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'); + assertWarnDev(['Hello'], {withoutStack: true}); + }); + expect(message).toMatchInlineSnapshot(` + "assertWarnDev(expected) + + Unexpected component stack for: + "Hello " + + If this warning intentionally includes the component stack, remove {withoutStack: true} from the assertWarnDev() call. + If you have a mix of warnings with and without stack in one assertWarnDev() 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'); + assertWarnDev(['Hello', 'Good day', 'Bye'], { + withoutStack: true, + }); + }); + expect(message).toMatchInlineSnapshot(` + "assertWarnDev(expected) + + Unexpected component stack for: + "Hello " + + Unexpected component stack for: + "Bye " + + If this warning intentionally includes the component stack, remove {withoutStack: true} from the assertWarnDev() call. + If you have a mix of warnings with and without stack in one assertWarnDev() 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'); + assertWarnDev(['Hello', 'Good day', 'Bye'], { + withoutStack: 4, + }); + }); + expect(message).toMatchInlineSnapshot(` + "assertWarnDev(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'); + assertWarnDev(['Hi'], {withoutStack: null}); + }); + expect(message).toMatchInlineSnapshot(` + "assertWarnDev(expected) + + The second argument for assertWarnDev(), 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'); + assertWarnDev(['Hi'], {withoutStack: {}}); + }); + expect(message).toMatchInlineSnapshot(` + "assertWarnDev(expected) + + The second argument for assertWarnDev(), 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'); + assertWarnDev(['Hi'], {withoutStack: 'haha'}); + }); + expect(message).toMatchInlineSnapshot(` + "assertWarnDev(expected) + + The second argument for assertWarnDev(), 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'); + assertWarnDev(['Hi'], {withoutStack: true}); + }); + expect(message).toMatchInlineSnapshot(` + "assertWarnDev(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'); + assertWarnDev(['Hi', 'Bye'], {withoutStack: true}); + }); + expect(message).toMatchInlineSnapshot(` + "assertWarnDev(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'); + assertWarnDev(['Hi'], {withoutStack: true}); + }); + expect(message).toMatchInlineSnapshot(` + "assertWarnDev(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'); + assertWarnDev(['Hi', 'Bye'], {withoutStack: true}); + }); + expect(message).toMatchInlineSnapshot(` + "assertWarnDev(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'); + assertWarnDev(['Hi']); + }); + expect(message).toMatchInlineSnapshot(` + "assertWarnDev(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'); + assertWarnDev('Hi', 'Bye'); + }); + expect(message).toMatchInlineSnapshot(` + "assertWarnDev(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'); + assertWarnDev('Hi', 'Bye'); + }); + expect(message).toMatchInlineSnapshot(` + "assertWarnDev(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'); + assertWarnDev('Hi', undefined, 'Bye'); + }); + expect(message).toMatchInlineSnapshot(` + "assertWarnDev(expected) + + Expected messages should be an array of strings but was given type "string"." + `); + }); + }); + + describe('assertErrorDev', () => { + // @gate __DEV__ + it('passes if an error contains a stack', () => { + console.error('Hello\n in div'); + assertErrorDev(['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'); + assertErrorDev(['Hello', 'Good day', 'Bye']); + }); + + // @gate __DEV__ + it('passes if errors without stack explicitly opt out', () => { + console.error('Hello'); + assertErrorDev(['Hello'], {withoutStack: true}); + + console.error('Hello'); + console.error('Good day'); + console.error('Bye'); + + assertErrorDev(['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'); + assertErrorDev(['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'); + assertErrorDev(['Hi', 'Wow', 'Bye']); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(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'); + assertErrorDev(['Hi', 'Wow', 'Bye']); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(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'); + assertErrorDev(['Hi', 'Wow', 'Bye']); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(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'); + assertErrorDev(['Wow', 'Bye']); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(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'); + assertErrorDev(['Hi', 'Bye']); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(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'); + assertErrorDev(['Hi', 'Wow']); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(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'); + assertErrorDev('Hello'); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(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'); + assertErrorDev(['Hello', 'Good day', 'Bye']); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(expected) + + Missing component stack for: + "Bye" + + If this error intentionally omits the component stack, add {withoutStack: true} to the assertErrorDev 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'); + assertErrorDev(['Hello', 'Good day', 'Bye']); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(expected) + + Missing component stack for: + "Hello" + + If this error intentionally omits the component stack, add {withoutStack: true} to the assertErrorDev 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'); + assertErrorDev(['Hello', 'Good day', 'Bye']); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(expected) + + Missing component stack for: + "Good day" + + If this error intentionally omits the component stack, add {withoutStack: true} to the assertErrorDev 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'); + assertErrorDev(['Hello', 'Good day', 'Bye']); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(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 assertErrorDev 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'); + assertErrorDev(['Hello'], {withoutStack: true}); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(expected) + + Unexpected component stack for: + "Hello " + + If this error intentionally includes the component stack, remove {withoutStack: true} from the assertErrorDev() call. + If you have a mix of errors with and without stack in one assertErrorDev() 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'); + assertErrorDev(['Hello', 'Good day', 'Bye'], { + withoutStack: true, + }); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(expected) + + Unexpected component stack for: + "Hello " + + Unexpected component stack for: + "Bye " + + If this error intentionally includes the component stack, remove {withoutStack: true} from the assertErrorDev() call. + If you have a mix of errors with and without stack in one assertErrorDev() 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'); + assertErrorDev(['Hello', 'Good day', 'Bye'], { + withoutStack: 4, + }); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(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'); + assertErrorDev(['Hello', 'Good day', 'Good night', 'Bye'], { + withoutStack: 4, + }); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(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'); + assertErrorDev(['Hi'], {withoutStack: null}); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(expected) + + The second argument for assertErrorDev(), 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'); + assertErrorDev(['Hi'], {withoutStack: {}}); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(expected) + + The second argument for assertErrorDev(), 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'); + assertErrorDev(['Hi'], {withoutStack: 'haha'}); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(expected) + + The second argument for assertErrorDev(), 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'); + assertErrorDev(['Hi'], {withoutStack: true}); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(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'); + assertErrorDev(['Hi', 'Bye'], {withoutStack: true}); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(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'); + assertErrorDev(['Hi'], {withoutStack: true}); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(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'); + assertErrorDev(['Hi', 'Bye'], {withoutStack: true}); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(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'); + assertErrorDev(['Hi']); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(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'); + assertErrorDev(['Hi', 'Bye']); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(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'); + assertErrorDev('Hi', 'Bye'); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(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'); + assertErrorDev('Hi', 'Bye'); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(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'); + assertErrorDev('Hi', undefined, 'Bye'); + }); + expect(message).toMatchInlineSnapshot(` + "assertErrorDev(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..28424ed03075d 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 assertLogDev, 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'), + ); + } + } + }; +}