1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 */78/* eslint-disable react-internal/no-production-logging */910const chalk = require('chalk');11const util = require('util');12const shouldIgnoreConsoleError = require('./shouldIgnoreConsoleError');13const shouldIgnoreConsoleWarn = require('./shouldIgnoreConsoleWarn');14import {diff} from 'jest-diff';15import {printReceived} from 'jest-matcher-utils';1617// Annoying: need to store the log array on the global or it would18// change reference whenever you call jest.resetModules after patch.19const loggedErrors = (global.__loggedErrors = global.__loggedErrors || []);20const loggedWarns = (global.__loggedWarns = global.__loggedWarns || []);21const loggedLogs = (global.__loggedLogs = global.__loggedLogs || []);2223const patchConsoleMethod = (methodName, logged) => {24 const newMethod = function (format, ...args) {25 // Ignore uncaught errors reported by jsdom26 // and React addendums because they're too noisy.27 if (shouldIgnoreConsoleError(format, args)) {28 return;29 }3031 // Ignore certain React warnings causing test failures32 if (methodName === 'warn' && shouldIgnoreConsoleWarn(format)) {33 return;34 }3536 // Append Component Stacks. Simulates a framework or DevTools appending them.37 if (38 typeof format === 'string' &&39 (methodName === 'error' || methodName === 'warn')40 ) {41 const React = require('react');4243 // Ideally we could remove this check, but we have some tests like44 // useSyncExternalStoreShared-test that tests against React 17,45 // which doesn't have the captureOwnerStack method.46 if (React.captureOwnerStack) {47 const stack = React.captureOwnerStack();48 if (stack) {49 format += '%s';50 args.push(stack);51 }52 }53 }5455 logged.push([format, ...args]);56 };5758 console[methodName] = newMethod;5960 return newMethod;61};6263let logMethod;64export function patchConsoleMethods({includeLog} = {includeLog: false}) {65 patchConsoleMethod('error', loggedErrors);66 patchConsoleMethod('warn', loggedWarns);6768 // Only assert console.log isn't called in CI so you can debug tests in DEV.69 // The matchers will still work in DEV, so you can assert locally.70 if (includeLog) {71 logMethod = patchConsoleMethod('log', loggedLogs);72 }73}7475export function resetAllUnexpectedConsoleCalls() {76 loggedErrors.length = 0;77 loggedWarns.length = 0;78 if (logMethod) {79 loggedLogs.length = 0;80 }81}8283export function clearLogs() {84 const logs = Array.from(loggedLogs);85 loggedLogs.length = 0;86 return logs;87}8889export function clearWarnings() {90 const warnings = Array.from(loggedWarns);91 loggedWarns.length = 0;92 return warnings;93}9495export function clearErrors() {96 const errors = Array.from(loggedErrors);97 loggedErrors.length = 0;98 return errors;99}100101export function assertConsoleLogsCleared() {102 const logs = clearLogs();103 const warnings = clearWarnings();104 const errors = clearErrors();105106 if (logs.length > 0 || errors.length > 0 || warnings.length > 0) {107 let message = `${chalk.dim('asserConsoleLogsCleared')}(${chalk.red(108 'expected',109 )})\n`;110111 if (logs.length > 0) {112 message += `\nconsole.log was called without assertConsoleLogDev:\n${diff(113 '',114 logs.join('\n'),115 {116 omitAnnotationLines: true,117 },118 )}\n`;119 }120121 if (warnings.length > 0) {122 message += `\nconsole.warn was called without assertConsoleWarnDev:\n${diff(123 '',124 warnings.map(normalizeComponentStack).join('\n'),125 {126 omitAnnotationLines: true,127 },128 )}\n`;129 }130 if (errors.length > 0) {131 message += `\nconsole.error was called without assertConsoleErrorDev:\n${diff(132 '',133 errors.map(normalizeComponentStack).join('\n'),134 {135 omitAnnotationLines: true,136 },137 )}\n`;138 }139140 message += `\nYou must call one of the assertConsoleDev helpers between each act call.`;141142 const error = Error(message);143 Error.captureStackTrace(error, assertConsoleLogsCleared);144 throw error;145 }146}147148function normalizeCodeLocInfo(str) {149 if (typeof str !== 'string') {150 return str;151 }152 // This special case exists only for the special source location in153 // ReactElementValidator. That will go away if we remove source locations.154 str = str.replace(/Check your code at .+?:\d+/g, 'Check your code at **');155 // V8 format:156 // at Component (/path/filename.js:123:45)157 // React format:158 // in Component (at filename.js:123)159 return str.replace(/\n +(?:at|in) ([^(\[\n]+)[^\n]*/g, function (m, name) {160 name = name.trim();161 if (name.endsWith('.render')) {162 // Class components will have the `render` method as part of their stack trace.163 // We strip that out in our normalization to make it look more like component stacks.164 name = name.slice(0, name.length - 7);165 }166 name = name.replace(/.*\/([^\/]+):\d+:\d+/, '**/$1:**:**');167 return '\n in ' + name + ' (at **)';168 });169}170171// Expands environment placeholders like [Server] into ANSI escape sequences.172// This allows test assertions to use a cleaner syntax like "[Server] Error:"173// instead of the full escape sequence "\u001b[0m\u001b[7m Server \u001b[0mError:"174function expandEnvironmentPlaceholders(str) {175 if (typeof str !== 'string') {176 return str;177 }178 // [Environment] -> ANSI escape sequence for environment badge179 // The format is: reset + inverse + " Environment " + reset180 return str.replace(181 /^\[(\w+)] /g,182 (match, env) => '\u001b[0m\u001b[7m ' + env + ' \u001b[0m',183 );184}185186// The error stack placeholder that can be used in expected messages187const ERROR_STACK_PLACEHOLDER = '\n in <stack>';188// A marker used to protect the placeholder during normalization189const ERROR_STACK_PLACEHOLDER_MARKER = '\n in <__STACK_PLACEHOLDER__>';190191// Normalizes expected messages, handling special placeholders192function normalizeExpectedMessage(str) {193 if (typeof str !== 'string') {194 return str;195 }196 // Protect the error stack placeholder from normalization197 // (normalizeCodeLocInfo would add "(at **)" to it)198 const hasStackPlaceholder = str.includes(ERROR_STACK_PLACEHOLDER);199 let result = str;200 if (hasStackPlaceholder) {201 result = result.replace(202 ERROR_STACK_PLACEHOLDER,203 ERROR_STACK_PLACEHOLDER_MARKER,204 );205 }206 result = normalizeCodeLocInfo(result);207 result = expandEnvironmentPlaceholders(result);208 if (hasStackPlaceholder) {209 // Restore the placeholder (remove the "(at **)" that was added)210 result = result.replace(211 ERROR_STACK_PLACEHOLDER_MARKER + ' (at **)',212 ERROR_STACK_PLACEHOLDER,213 );214 }215 return result;216}217218function normalizeComponentStack(entry) {219 if (220 typeof entry[0] === 'string' &&221 entry[0].endsWith('%s') &&222 isLikelyAComponentStack(entry[entry.length - 1])223 ) {224 const clone = entry.slice(0);225 clone[clone.length - 1] = normalizeCodeLocInfo(entry[entry.length - 1]);226 return clone;227 }228 return entry;229}230231const isLikelyAComponentStack = message =>232 typeof message === 'string' &&233 (message.indexOf('<component stack>') > -1 ||234 message.includes('\n in ') ||235 message.includes('\n at '));236237// Error stack traces start with "*Error:" and contain "at" frames with file paths238// Component stacks contain "in ComponentName" patterns239// This helps validate that \n in <stack> is used correctly240const isLikelyAnErrorStackTrace = message =>241 typeof message === 'string' &&242 message.includes('Error:') &&243 // Has "at" frames typical of error stacks (with file:line:col)244 /\n\s+at .+\(.*:\d+:\d+\)/.test(message);245246export function createLogAssertion(247 consoleMethod,248 matcherName,249 clearObservedErrors,250) {251 function logName() {252 switch (consoleMethod) {253 case 'log':254 return 'log';255 case 'error':256 return 'error';257 case 'warn':258 return 'warning';259 }260 }261262 return function assertConsoleLog(expectedMessages, options = {}) {263 if (__DEV__) {264 // eslint-disable-next-line no-inner-declarations265 function throwFormattedError(message) {266 const error = new Error(267 `${chalk.dim(matcherName)}(${chalk.red(268 'expected',269 )})\n\n${message.trim()}`,270 );271 Error.captureStackTrace(error, assertConsoleLog);272 throw error;273 }274275 // Warn about incorrect usage first arg.276 if (!Array.isArray(expectedMessages)) {277 throwFormattedError(278 `Expected messages should be an array of strings ` +279 `but was given type "${typeof expectedMessages}".`,280 );281 }282283 // Warn about incorrect usage second arg.284 if (options != null) {285 if (typeof options !== 'object' || Array.isArray(options)) {286 throwFormattedError(287 `The second argument should be an object. ` +288 'Did you forget to wrap the messages into an array?',289 );290 }291 }292293 const observedLogs = clearObservedErrors();294 const receivedLogs = [];295 const missingExpectedLogs = Array.from(expectedMessages);296297 const unexpectedLogs = [];298 const unexpectedMissingErrorStack = [];299 const unexpectedIncludingErrorStack = [];300 const logsMismatchingFormat = [];301 const logsWithExtraComponentStack = [];302 const stackTracePlaceholderMisuses = [];303304 // Loop over all the observed logs to determine:305 // - Which expected logs are missing306 // - Which received logs are unexpected307 // - Which logs have a component stack308 // - Which logs have the wrong format309 // - Which logs have extra stacks310 for (let index = 0; index < observedLogs.length; index++) {311 const log = observedLogs[index];312 const [format, ...args] = log;313 const message = util.format(format, ...args);314315 // Ignore uncaught errors reported by jsdom316 // and React addendums because they're too noisy.317 if (shouldIgnoreConsoleError(format, args)) {318 return;319 }320321 let expectedMessage;322 const expectedMessageOrArray = expectedMessages[index];323 if (typeof expectedMessageOrArray === 'string') {324 expectedMessage = normalizeExpectedMessage(expectedMessageOrArray);325 } else if (expectedMessageOrArray != null) {326 throwFormattedError(327 `The expected message for ${matcherName}() must be a string. ` +328 `Instead received ${JSON.stringify(expectedMessageOrArray)}.`,329 );330 }331332 const normalizedMessage = normalizeCodeLocInfo(message);333 receivedLogs.push(normalizedMessage);334335 // Check the number of %s interpolations.336 // We'll fail the test if they mismatch.337 let argIndex = 0;338 // console.* could have been called with a non-string e.g. `console.error(new Error())`339 // eslint-disable-next-line react-internal/safe-string-coercion340 String(format).replace(/%s|%c|%o/g, () => argIndex++);341 if (argIndex !== args.length) {342 if (format.includes('%c%s')) {343 // We intentionally use mismatching formatting when printing badging because we don't know344 // the best default to use for different types because the default varies by platform.345 } else {346 logsMismatchingFormat.push({347 format,348 args,349 expectedArgCount: argIndex,350 });351 }352 }353354 // Check for extra component stacks355 if (356 args.length >= 2 &&357 isLikelyAComponentStack(args[args.length - 1]) &&358 isLikelyAComponentStack(args[args.length - 2])359 ) {360 logsWithExtraComponentStack.push({361 format,362 });363 }364365 // Main logic to check if log is expected, with the component stack.366 // Check for exact match OR if the message matches with a component stack appended367 let matchesExpectedMessage = false;368 let expectsErrorStack = false;369 const hasErrorStack = isLikelyAnErrorStackTrace(message);370371 if (typeof expectedMessage === 'string') {372 if (normalizedMessage === expectedMessage) {373 matchesExpectedMessage = true;374 } else if (expectedMessage.includes('\n in <stack>')) {375 expectsErrorStack = true;376 // \n in <stack> is ONLY for JavaScript Error stack traces (e.g., "Error: message\n at fn (file.js:1:2)")377 // NOT for React component stacks (e.g., "\n in ComponentName (at **)").378 // Validate that the actual message looks like an error stack trace.379 if (!hasErrorStack) {380 // The actual message doesn't look like an error stack trace.381 // This is likely a misuse - someone used \n in <stack> for a component stack.382 stackTracePlaceholderMisuses.push({383 expected: expectedMessage,384 received: normalizedMessage,385 });386 }387388 const expectedMessageWithoutStack = expectedMessage.replace(389 '\n in <stack>',390 '',391 );392 if (normalizedMessage.startsWith(expectedMessageWithoutStack)) {393 // Remove the stack trace394 const remainder = normalizedMessage.slice(395 expectedMessageWithoutStack.length,396 );397398 // After normalization, both error stacks and component stacks look like399 // component stacks (at frames are converted to "in ... (at **)" format).400 // So we check isLikelyAComponentStack for matching purposes.401 if (isLikelyAComponentStack(remainder)) {402 const messageWithoutStack = normalizedMessage.replace(403 remainder,404 '',405 );406 if (messageWithoutStack === expectedMessageWithoutStack) {407 matchesExpectedMessage = true;408 }409 } else if (remainder === '') {410 // \n in <stack> was expected but there's no stack at all411 matchesExpectedMessage = true;412 }413 } else if (normalizedMessage === expectedMessageWithoutStack) {414 // \n in <stack> was expected but actual has no stack at all (exact match without stack)415 matchesExpectedMessage = true;416 }417 } else if (418 hasErrorStack &&419 !expectedMessage.includes('\n in <stack>') &&420 normalizedMessage.startsWith(expectedMessage)421 ) {422 matchesExpectedMessage = true;423 }424 }425426 if (matchesExpectedMessage) {427 // Check for unexpected/missing error stacks428 if (hasErrorStack && !expectsErrorStack) {429 // Error stack is present but \n in <stack> was not in the expected message430 unexpectedIncludingErrorStack.push(normalizedMessage);431 } else if (432 expectsErrorStack &&433 !hasErrorStack &&434 !isLikelyAComponentStack(normalizedMessage)435 ) {436 // \n in <stack> was expected but the actual message doesn't have any stack at all437 // (if it has a component stack, stackTracePlaceholderMisuses already handles it)438 unexpectedMissingErrorStack.push(normalizedMessage);439 }440441 // Found expected log, remove it from missing.442 missingExpectedLogs.splice(0, 1);443 } else {444 unexpectedLogs.push(normalizedMessage);445 }446 }447448 // Helper for pretty printing diffs consistently.449 // We inline multi-line logs for better diff printing.450 // eslint-disable-next-line no-inner-declarations451 function printDiff() {452 return `${diff(453 expectedMessages454 .map(message => message.replace('\n', ' '))455 .join('\n'),456 receivedLogs.map(message => message.replace('\n', ' ')).join('\n'),457 {458 aAnnotation: `Expected ${logName()}s`,459 bAnnotation: `Received ${logName()}s`,460 },461 )}`;462 }463464 // Wrong %s formatting is a failure.465 // This is a common mistake when creating new warnings.466 if (logsMismatchingFormat.length > 0) {467 throwFormattedError(468 logsMismatchingFormat469 .map(470 item =>471 `Received ${item.args.length} arguments for a message with ${472 item.expectedArgCount473 } placeholders:\n ${printReceived(item.format)}`,474 )475 .join('\n\n'),476 );477 }478479 // Any unexpected warnings should be treated as a failure.480 if (unexpectedLogs.length > 0) {481 throwFormattedError(482 `Unexpected ${logName()}(s) recorded.\n\n${printDiff()}`,483 );484 }485486 // Any remaining messages indicate a failed expectations.487 if (missingExpectedLogs.length > 0) {488 throwFormattedError(489 `Expected ${logName()} was not recorded.\n\n${printDiff()}`,490 );491 }492493 // Any logs that include an error stack trace but \n in <stack> wasn't expected.494 if (unexpectedIncludingErrorStack.length > 0) {495 throwFormattedError(496 `${unexpectedIncludingErrorStack497 .map(498 stack =>499 `Unexpected error stack trace for:\n ${printReceived(stack)}`,500 )501 .join(502 '\n\n',503 )}\n\nIf this ${logName()} should include an error stack trace, add \\n in <stack> to your expected message ` +504 `(e.g., "Error: message\\n in <stack>").`,505 );506 }507508 // Any logs that are missing an error stack trace when \n in <stack> was expected.509 if (unexpectedMissingErrorStack.length > 0) {510 throwFormattedError(511 `${unexpectedMissingErrorStack512 .map(513 stack =>514 `Missing error stack trace for:\n ${printReceived(stack)}`,515 )516 .join(517 '\n\n',518 )}\n\nThe expected message uses \\n in <stack> but the actual ${logName()} doesn't include an error stack trace.` +519 `\nIf this ${logName()} should not have an error stack trace, remove \\n in <stack> from your expected message.`,520 );521 }522523 // Duplicate component stacks is a failure.524 // This used to be a common mistake when creating new warnings,525 // but might not be an issue anymore.526 if (logsWithExtraComponentStack.length > 0) {527 throwFormattedError(528 logsWithExtraComponentStack529 .map(530 item =>531 `Received more than one component stack for a warning:\n ${printReceived(532 item.format,533 )}`,534 )535 .join('\n\n'),536 );537 }538539 // Using \n in <stack> for component stacks is a misuse.540 // \n in <stack> should only be used for JavaScript Error stack traces,541 // not for React component stacks.542 if (stackTracePlaceholderMisuses.length > 0) {543 throwFormattedError(544 `${stackTracePlaceholderMisuses545 .map(546 item =>547 `Incorrect use of \\n in <stack> placeholder. The placeholder is for JavaScript Error ` +548 `stack traces (messages starting with "Error:"), not for React component stacks.\n\n` +549 `Expected: ${printReceived(item.expected)}\n` +550 `Received: ${printReceived(item.received)}\n\n` +551 `If this ${logName()} has a component stack, include the full component stack in your expected message ` +552 `(e.g., "Warning message\\n in ComponentName (at **)").`,553 )554 .join('\n\n')}`,555 );556 }557 }558 };559}
Findings
✓ No findings reported for this file.