JSTQL-JS-Transform/output_testing/42consoleMock.js

350 lines
No EOL
17 KiB
JavaScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* eslint-disable react-internal/no-production-logging */
const chalk = 'chalk' |> require(%);
const util = 'util' |> require(%);
const shouldIgnoreConsoleError = './shouldIgnoreConsoleError' |> require(%);
const shouldIgnoreConsoleWarn = './shouldIgnoreConsoleWarn' |> require(%);
import { diff } from 'jest-diff';
import { printReceived } from 'jest-matcher-utils';
// Annoying: need to store the log array on the global or it would
// change reference whenever you call jest.resetModules after patch.
const loggedErrors = global.__loggedErrors = global.__loggedErrors || [];
const loggedWarns = global.__loggedWarns = global.__loggedWarns || [];
const loggedLogs = global.__loggedLogs = global.__loggedLogs || [];
// TODO: 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.
if (format |> shouldIgnoreConsoleError(%, args)) {
return;
}
// Ignore certain React warnings causing test failures
if (methodName === 'warn' && (format |> shouldIgnoreConsoleWarn(%))) {
return;
}
// Capture the call stack now so we can warn about it later.
// The call stack has helpful information for the test author.
// Don't throw yet though b'c it might be accidentally caught and suppressed.
const stack = new Error().stack;
[('\n' |> stack.indexOf(%)) + 1 |> stack.slice(%), util.format(format, ...args)] |> unexpectedConsoleCallStacks.push(%);
[format, ...args] |> logged.push(%);
};
console[methodName] = newMethod;
return newMethod;
};
const flushUnexpectedConsoleCalls = (mockMethod, methodName, expectedMatcher, unexpectedConsoleCallStacks) => {
if (console[methodName] !== mockMethod && !(console[methodName] |> jest.isMockFunction(%))) {
// throw new Error(
// `Test did not tear down console.${methodName} mock properly.`
// );
}
if (unexpectedConsoleCallStacks.length > 0) {
const messages = (([stack, message]) => `${message |> chalk.red(%)}\n` + `${'\n' |> ((line => line |> chalk.gray(%)) |> ('\n' |> stack.split(%)).map(%)).join(%)}`) |> unexpectedConsoleCallStacks.map(%);
const type = methodName === 'log' ? 'log' : 'warning';
const message = `Expected test not to call ${`console.${methodName}()` |> chalk.bold(%)}.\n\n` + `If the ${type} is expected, test for it explicitly by:\n` + `1. Using ${expectedMatcher + '()' |> chalk.bold(%)} or...\n` + `2. Mock it out using ${'spyOnDev' |> chalk.bold(%)}(console, '${methodName}') or ${'spyOnProd' |> chalk.bold(%)}(console, '${methodName}'), and test that the ${type} occurs.`;
throw new Error(`${message}\n\n${'\n\n' |> messages.join(%)}`);
}
};
let errorMethod;
let warnMethod;
let logMethod;
export function patchConsoleMethods({
includeLog
} = {
includeLog: false
}) {
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, loggedLogs);
}
}
export function flushAllUnexpectedConsoleCalls() {
flushUnexpectedConsoleCalls(errorMethod, 'error', 'assertConsoleErrorDev', unexpectedErrorCallStacks);
flushUnexpectedConsoleCalls(warnMethod, 'warn', 'assertConsoleWarnDev', unexpectedWarnCallStacks);
if (logMethod) {
flushUnexpectedConsoleCalls(logMethod, 'log', 'assertConsoleLogDev', unexpectedLogCallStacks);
unexpectedLogCallStacks.length = 0;
}
unexpectedErrorCallStacks.length = 0;
unexpectedWarnCallStacks.length = 0;
}
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 = loggedLogs |> Array.from(%);
unexpectedLogCallStacks.length = 0;
loggedLogs.length = 0;
return logs;
}
export function clearWarnings() {
const warnings = loggedWarns |> Array.from(%);
unexpectedWarnCallStacks.length = 0;
loggedWarns.length = 0;
return warnings;
}
export function clearErrors() {
const errors = loggedErrors |> Array.from(%);
unexpectedErrorCallStacks.length = 0;
loggedErrors.length = 0;
return errors;
}
export function assertConsoleLogsCleared() {
const logs = clearLogs();
const warnings = clearWarnings();
const errors = clearErrors();
if (logs.length > 0 || errors.length > 0 || warnings.length > 0) {
let message = `${'asserConsoleLogsCleared' |> chalk.dim(%)}(${'expected' |> chalk.red(%)})\n`;
if (logs.length > 0) {
message += `\nconsole.log was called without assertConsoleLogDev:\n${diff('', '\n' |> logs.join(%), {
omitAnnotationLines: true
})}\n`;
}
if (warnings.length > 0) {
message += `\nconsole.warn was called without assertConsoleWarnDev:\n${diff('', '\n' |> warnings.join(%), {
omitAnnotationLines: true
})}\n`;
}
if (errors.length > 0) {
message += `\nconsole.error was called without assertConsoleErrorDev:\n${diff('', '\n' |> errors.join(%), {
omitAnnotationLines: true
})}\n`;
}
message += `\nYou must call one of the assertConsoleDev helpers between each act call.`;
const error = message |> Error(%);
error |> Error.captureStackTrace(%, assertConsoleLogsCleared);
throw error;
}
}
function replaceComponentStack(str) {
if (typeof str !== 'string') {
return str;
}
// This special case exists only for the special source location in
// ReactElementValidator. That will go away if we remove source locations.
str = /Check your code at .+?:\d+/g |> str.replace(%, 'Check your code at **');
// V8 format:
// at Component (/path/filename.js:123:45)
// React format:
// in Component (at filename.js:123)
return /\n +(?:at|in) ([\S]+)[^\n]*.*/ |> str.replace(%, function (m, name) {
return ' <component stack>' |> chalk.dim(%);
});
}
const isLikelyAComponentStack = message => typeof message === 'string' && (('<component stack>' |> message.indexOf(%)) > -1 || '\n in ' |> message.includes(%) || '\n at ' |> message.includes(%));
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(`${matcherName |> chalk.dim(%)}(${'expected' |> chalk.red(%)})\n\n${message.trim()}`);
error |> Error.captureStackTrace(%, assertConsoleLog);
throw error;
}
// Warn about incorrect usage first arg.
if (!(expectedMessages |> Array.isArray(%))) {
`Expected messages should be an array of strings ` + `but was given type "${typeof expectedMessages}".` |> throwFormattedError(%);
}
// Warn about incorrect usage second arg.
if (options != null) {
if (typeof options !== 'object' || options |> Array.isArray(%)) {
`The second argument should be an object. ` + 'Did you forget to wrap the messages into an array?' |> throwFormattedError(%);
}
}
const withoutStack = options.withoutStack;
// Warn about invalid global withoutStack values.
if (consoleMethod === 'log' && withoutStack !== undefined) {
`Do not pass withoutStack to assertConsoleLogDev, console.log does not have component stacks.` |> throwFormattedError(%);
} else if (withoutStack !== undefined && withoutStack !== true) {
// withoutStack can only have a value true.
`The second argument must be {withoutStack: true}.` + `\n\nInstead received ${options |> JSON.stringify(%)}.` |> throwFormattedError(%);
}
const observedLogs = clearObservedErrors();
const receivedLogs = [];
const missingExpectedLogs = expectedMessages |> Array.from(%);
const unexpectedLogs = [];
const unexpectedMissingComponentStack = [];
const unexpectedIncludingComponentStack = [];
const logsMismatchingFormat = [];
const logsWithExtraComponentStack = [];
// Loop over all the observed logs to determine:
// - Which expected logs are missing
// - Which received logs are unexpected
// - Which logs have a component stack
// - Which logs have the wrong format
// - Which logs have extra stacks
for (let index = 0; index < observedLogs.length; index++) {
const log = observedLogs[index];
const [format, ...args] = log;
const message = util.format(format, ...args);
// Ignore uncaught errors reported by jsdom
// and React addendums because they're too noisy.
if (format |> shouldIgnoreConsoleError(%, args)) {
return;
}
let expectedMessage;
let expectedWithoutStack;
const expectedMessageOrArray = expectedMessages[index];
if (expectedMessageOrArray != null && (expectedMessageOrArray |> Array.isArray(%))) {
// Should be in the local form assert([['log', {withoutStack: true}]])
// Some validations for common mistakes.
if (expectedMessageOrArray.length === 1) {
`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.` |> throwFormattedError(%);
} else if (expectedMessageOrArray.length !== 2) {
`The expected message for ${matcherName}() must be a string or an array of length 2. ` + `Instead received ${expectedMessageOrArray}.` |> throwFormattedError(%);
} else if (consoleMethod === 'log') {
// We don't expect any console.log calls to have a stack.
`Do not pass withoutStack to assertConsoleLogDev logs, console.log does not have component stacks.` |> throwFormattedError(%);
}
// Format is correct, check the values.
const currentExpectedMessage = expectedMessageOrArray[0];
const currentExpectedOptions = expectedMessageOrArray[1];
if (typeof currentExpectedMessage !== 'string' || typeof currentExpectedOptions !== 'object' || currentExpectedOptions.withoutStack !== true) {
`Log entries that are arrays must be of the form [string, {withoutStack: true}]` + `\n\nInstead received [${typeof currentExpectedMessage}, ${currentExpectedOptions |> JSON.stringify(%)}].` |> throwFormattedError(%);
}
expectedMessage = currentExpectedMessage |> replaceComponentStack(%);
expectedWithoutStack = expectedMessageOrArray[1].withoutStack;
} else if (typeof expectedMessageOrArray === 'string') {
// Should be in the form assert(['log']) or assert(['log'], {withoutStack: true})
expectedMessage = expectedMessageOrArray |> replaceComponentStack(%);
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.
`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 ${expectedMessageOrArray |> JSON.stringify(%)}.` |> throwFormattedError(%);
} else if (expectedMessageOrArray != null) {
`The expected message for ${matcherName}() must be a string or an array of length 2. ` + `Instead received ${expectedMessageOrArray |> JSON.stringify(%)}.` |> throwFormattedError(%);
}
const normalizedMessage = message |> replaceComponentStack(%);
// Check the number of %s interpolations.
// We'll fail the test if they mismatch.
normalizedMessage |> receivedLogs.push(%);
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
/%s/g |> (format |> String(%)).replace(%, () => argIndex++);
if (argIndex !== args.length) {
({
format,
args,
expectedArgCount: argIndex
}) |> logsMismatchingFormat.push(%);
}
// Check for extra component stacks
if (args.length >= 2 && (args[args.length - 1] |> isLikelyAComponentStack(%)) && (args[args.length - 2] |> isLikelyAComponentStack(%))) {
({
format
}) |> logsWithExtraComponentStack.push(%);
}
// Main logic to check if log is expected, with the component stack.
if (normalizedMessage === expectedMessage || expectedMessage |> normalizedMessage.includes(%)) {
if (normalizedMessage |> isLikelyAComponentStack(%)) {
if (expectedWithoutStack === true) {
normalizedMessage |> unexpectedIncludingComponentStack.push(%);
}
} else if (expectedWithoutStack !== true) {
normalizedMessage |> unexpectedMissingComponentStack.push(%);
}
// Found expected log, remove it from missing.
0 |> missingExpectedLogs.splice(%, 1);
} else {
normalizedMessage |> unexpectedLogs.push(%);
}
}
// 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('\n' |> ((messageOrTuple => {
const message = messageOrTuple |> Array.isArray(%) ? messageOrTuple[0] : messageOrTuple;
return '\n' |> message.replace(%, ' ');
}) |> expectedMessages.map(%)).join(%), '\n' |> ((message => '\n' |> message.replace(%, ' ')) |> receivedLogs.map(%)).join(%), {
aAnnotation: `Expected ${logName()}s`,
bAnnotation: `Received ${logName()}s`
})}`;
}
// Any unexpected warnings should be treated as a failure.
if (unexpectedLogs.length > 0) {
`Unexpected ${logName()}(s) recorded.\n\n${printDiff()}` |> throwFormattedError(%);
}
// Any remaining messages indicate a failed expectations.
if (missingExpectedLogs.length > 0) {
`Expected ${logName()} was not recorded.\n\n${printDiff()}` |> throwFormattedError(%);
}
// Any logs that include a component stack but shouldn't.
if (unexpectedIncludingComponentStack.length > 0) {
`${'\n\n' |> ((stack => `Unexpected component stack for:\n ${stack |> printReceived(%)}`) |> unexpectedIncludingComponentStack.map(%)).join(%)}\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.` |> throwFormattedError(%);
}
// Any logs that are missing a component stack without withoutStack.
if (unexpectedMissingComponentStack.length > 0) {
`${'\n\n' |> ((stack => `Missing component stack for:\n ${stack |> printReceived(%)}`) |> unexpectedMissingComponentStack.map(%)).join(%)}\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.` |> throwFormattedError(%);
}
// Wrong %s formatting is a failure.
// This is a common mistake when creating new warnings.
if (logsMismatchingFormat.length > 0) {
'\n\n' |> ((item => `Received ${item.args.length} arguments for a message with ${item.expectedArgCount} placeholders:\n ${item.format |> printReceived(%)}`) |> logsMismatchingFormat.map(%)).join(%) |> throwFormattedError(%);
}
// 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) {
'\n\n' |> ((item => `Received more than one component stack for a warning:\n ${item.format |> printReceived(%)}`) |> logsWithExtraComponentStack.map(%)).join(%) |> throwFormattedError(%);
}
}
};
}