1. // copied from scripts/jest/matchers/toWarnDev.js
    
  2. 'use strict';
    
  3. 
    
  4. const {diff: jestDiff} = require('jest-diff');
    
  5. const util = require('util');
    
  6. 
    
  7. function shouldIgnoreConsoleError(format, args) {
    
  8.   if (__DEV__) {
    
  9.     if (typeof format === 'string') {
    
  10.       if (format.indexOf('Error: Uncaught [') === 0) {
    
  11.         // This looks like an uncaught error from invokeGuardedCallback() wrapper
    
  12.         // in development that is reported by jsdom. Ignore because it's noisy.
    
  13.         return true;
    
  14.       }
    
  15.       if (format.indexOf('The above error occurred') === 0) {
    
  16.         // This looks like an error addendum from ReactFiberErrorLogger.
    
  17.         // Ignore it too.
    
  18.         return true;
    
  19.       }
    
  20.     }
    
  21.   } else {
    
  22.     if (
    
  23.       format != null &&
    
  24.       typeof format.message === 'string' &&
    
  25.       typeof format.stack === 'string' &&
    
  26.       args.length === 0
    
  27.     ) {
    
  28.       // In production, ReactFiberErrorLogger logs error objects directly.
    
  29.       // They are noisy too so we'll try to ignore them.
    
  30.       return true;
    
  31.     }
    
  32.   }
    
  33.   // Looks legit
    
  34.   return false;
    
  35. }
    
  36. 
    
  37. function normalizeCodeLocInfo(str) {
    
  38.   return str && str.replace(/at .+?:\d+/g, 'at **');
    
  39. }
    
  40. 
    
  41. const createMatcherFor = consoleMethod =>
    
  42.   function matcher(callback, expectedMessages, options = {}) {
    
  43.     if (__DEV__) {
    
  44.       // Warn about incorrect usage of matcher.
    
  45.       if (typeof expectedMessages === 'string') {
    
  46.         expectedMessages = [expectedMessages];
    
  47.       } else if (!Array.isArray(expectedMessages)) {
    
  48.         throw Error(
    
  49.           `toWarnDev() requires a parameter of type string or an array of strings ` +
    
  50.             `but was given ${typeof expectedMessages}.`
    
  51.         );
    
  52.       }
    
  53.       if (
    
  54.         options != null &&
    
  55.         (typeof options !== 'object' || Array.isArray(options))
    
  56.       ) {
    
  57.         throw new Error(
    
  58.           'toWarnDev() second argument, when present, should be an object. ' +
    
  59.             'Did you forget to wrap the messages into an array?'
    
  60.         );
    
  61.       }
    
  62.       if (arguments.length > 3) {
    
  63.         // `matcher` comes from Jest, so it's more than 2 in practice
    
  64.         throw new Error(
    
  65.           'toWarnDev() received more than two arguments. ' +
    
  66.             'Did you forget to wrap the messages into an array?'
    
  67.         );
    
  68.       }
    
  69. 
    
  70.       const withoutStack = options.withoutStack;
    
  71.       const warningsWithoutComponentStack = [];
    
  72.       const warningsWithComponentStack = [];
    
  73.       const unexpectedWarnings = [];
    
  74. 
    
  75.       let lastWarningWithMismatchingFormat = null;
    
  76.       let lastWarningWithExtraComponentStack = null;
    
  77. 
    
  78.       // Catch errors thrown by the callback,
    
  79.       // But only rethrow them if all test expectations have been satisfied.
    
  80.       // Otherwise an Error in the callback can mask a failed expectation,
    
  81.       // and result in a test that passes when it shouldn't.
    
  82.       let caughtError;
    
  83. 
    
  84.       const isLikelyAComponentStack = message =>
    
  85.         typeof message === 'string' && message.includes('\n    in ');
    
  86. 
    
  87.       const consoleSpy = (format, ...args) => {
    
  88.         // Ignore uncaught errors reported by jsdom
    
  89.         // and React addendums because they're too noisy.
    
  90.         if (
    
  91.           consoleMethod === 'error' &&
    
  92.           shouldIgnoreConsoleError(format, args)
    
  93.         ) {
    
  94.           return;
    
  95.         }
    
  96. 
    
  97.         const message = util.format(format, ...args);
    
  98.         const normalizedMessage = normalizeCodeLocInfo(message);
    
  99. 
    
  100.         // Remember if the number of %s interpolations
    
  101.         // doesn't match the number of arguments.
    
  102.         // We'll fail the test if it happens.
    
  103.         let argIndex = 0;
    
  104.         format.replace(/%s/g, () => argIndex++);
    
  105.         if (argIndex !== args.length) {
    
  106.           lastWarningWithMismatchingFormat = {
    
  107.             format,
    
  108.             args,
    
  109.             expectedArgCount: argIndex,
    
  110.           };
    
  111.         }
    
  112. 
    
  113.         // Protect against accidentally passing a component stack
    
  114.         // to warning() which already injects the component stack.
    
  115.         if (
    
  116.           args.length >= 2 &&
    
  117.           isLikelyAComponentStack(args[args.length - 1]) &&
    
  118.           isLikelyAComponentStack(args[args.length - 2])
    
  119.         ) {
    
  120.           lastWarningWithExtraComponentStack = {
    
  121.             format,
    
  122.           };
    
  123.         }
    
  124. 
    
  125.         for (let index = 0; index < expectedMessages.length; index++) {
    
  126.           const expectedMessage = expectedMessages[index];
    
  127.           if (
    
  128.             normalizedMessage === expectedMessage ||
    
  129.             normalizedMessage.includes(expectedMessage)
    
  130.           ) {
    
  131.             if (isLikelyAComponentStack(normalizedMessage)) {
    
  132.               warningsWithComponentStack.push(normalizedMessage);
    
  133.             } else {
    
  134.               warningsWithoutComponentStack.push(normalizedMessage);
    
  135.             }
    
  136.             expectedMessages.splice(index, 1);
    
  137.             return;
    
  138.           }
    
  139.         }
    
  140. 
    
  141.         let errorMessage;
    
  142.         if (expectedMessages.length === 0) {
    
  143.           errorMessage =
    
  144.             'Unexpected warning recorded: ' +
    
  145.             this.utils.printReceived(normalizedMessage);
    
  146.         } else if (expectedMessages.length === 1) {
    
  147.           errorMessage =
    
  148.             'Unexpected warning recorded: ' +
    
  149.             jestDiff(expectedMessages[0], normalizedMessage);
    
  150.         } else {
    
  151.           errorMessage =
    
  152.             'Unexpected warning recorded: ' +
    
  153.             jestDiff(expectedMessages, [normalizedMessage]);
    
  154.         }
    
  155. 
    
  156.         // Record the call stack for unexpected warnings.
    
  157.         // We don't throw an Error here though,
    
  158.         // Because it might be suppressed by ReactFiberScheduler.
    
  159.         unexpectedWarnings.push(new Error(errorMessage));
    
  160.       };
    
  161. 
    
  162.       // TODO Decide whether we need to support nested toWarn* expectations.
    
  163.       // If we don't need it, add a check here to see if this is already our spy,
    
  164.       // And throw an error.
    
  165.       const originalMethod = console[consoleMethod];
    
  166. 
    
  167.       // Avoid using Jest's built-in spy since it can't be removed.
    
  168.       console[consoleMethod] = consoleSpy;
    
  169. 
    
  170.       try {
    
  171.         callback();
    
  172.       } catch (error) {
    
  173.         caughtError = error;
    
  174.       } finally {
    
  175.         // Restore the unspied method so that unexpected errors fail tests.
    
  176.         console[consoleMethod] = originalMethod;
    
  177. 
    
  178.         // Any unexpected Errors thrown by the callback should fail the test.
    
  179.         // This should take precedence since unexpected errors could block warnings.
    
  180.         if (caughtError) {
    
  181.           throw caughtError;
    
  182.         }
    
  183. 
    
  184.         // Any unexpected warnings should be treated as a failure.
    
  185.         if (unexpectedWarnings.length > 0) {
    
  186.           return {
    
  187.             message: () => unexpectedWarnings[0].stack,
    
  188.             pass: false,
    
  189.           };
    
  190.         }
    
  191. 
    
  192.         // Any remaining messages indicate a failed expectations.
    
  193.         if (expectedMessages.length > 0) {
    
  194.           return {
    
  195.             message: () =>
    
  196.               `Expected warning was not recorded:\n  ${this.utils.printReceived(
    
  197.                 expectedMessages[0]
    
  198.               )}`,
    
  199.             pass: false,
    
  200.           };
    
  201.         }
    
  202. 
    
  203.         if (typeof withoutStack === 'number') {
    
  204.           // We're expecting a particular number of warnings without stacks.
    
  205.           if (withoutStack !== warningsWithoutComponentStack.length) {
    
  206.             return {
    
  207.               message: () =>
    
  208.                 `Expected ${withoutStack} warnings without a component stack but received ${warningsWithoutComponentStack.length}:\n` +
    
  209.                 warningsWithoutComponentStack.map(warning =>
    
  210.                   this.utils.printReceived(warning)
    
  211.                 ),
    
  212.               pass: false,
    
  213.             };
    
  214.           }
    
  215.         } else if (withoutStack === true) {
    
  216.           // We're expecting that all warnings won't have the stack.
    
  217.           // If some warnings have it, it's an error.
    
  218.           if (warningsWithComponentStack.length > 0) {
    
  219.             return {
    
  220.               message: () =>
    
  221.                 `Received warning unexpectedly includes a component stack:\n  ${this.utils.printReceived(
    
  222.                   warningsWithComponentStack[0]
    
  223.                 )}\nIf this warning intentionally includes the component stack, remove ` +
    
  224.                 `{withoutStack: true} from the toWarnDev() call. If you have a mix of ` +
    
  225.                 `warnings with and without stack in one toWarnDev() call, pass ` +
    
  226.                 `{withoutStack: N} where N is the number of warnings without stacks.`,
    
  227.               pass: false,
    
  228.             };
    
  229.           }
    
  230.         } else if (withoutStack === false || withoutStack === undefined) {
    
  231.           // We're expecting that all warnings *do* have the stack (default).
    
  232.           // If some warnings don't have it, it's an error.
    
  233.           if (warningsWithoutComponentStack.length > 0) {
    
  234.             return {
    
  235.               message: () =>
    
  236.                 `Received warning unexpectedly does not include a component stack:\n  ${this.utils.printReceived(
    
  237.                   warningsWithoutComponentStack[0]
    
  238.                 )}\nIf this warning intentionally omits the component stack, add ` +
    
  239.                 `{withoutStack: true} to the toWarnDev() call.`,
    
  240.               pass: false,
    
  241.             };
    
  242.           }
    
  243.         } else {
    
  244.           throw Error(
    
  245.             `The second argument for toWarnDev(), when specified, must be an object. It may have a ` +
    
  246.               `property called "withoutStack" whose value may be undefined, boolean, or a number. ` +
    
  247.               `Instead received ${typeof withoutStack}.`
    
  248.           );
    
  249.         }
    
  250. 
    
  251.         if (lastWarningWithMismatchingFormat !== null) {
    
  252.           return {
    
  253.             message: () =>
    
  254.               `Received ${
    
  255.                 lastWarningWithMismatchingFormat.args.length
    
  256.               } arguments for a message with ${
    
  257.                 lastWarningWithMismatchingFormat.expectedArgCount
    
  258.               } placeholders:\n  ${this.utils.printReceived(
    
  259.                 lastWarningWithMismatchingFormat.format
    
  260.               )}`,
    
  261.             pass: false,
    
  262.           };
    
  263.         }
    
  264. 
    
  265.         if (lastWarningWithExtraComponentStack !== null) {
    
  266.           return {
    
  267.             message: () =>
    
  268.               `Received more than one component stack for a warning:\n  ${this.utils.printReceived(
    
  269.                 lastWarningWithExtraComponentStack.format
    
  270.               )}\nDid you accidentally pass a stack to warning() as the last argument? ` +
    
  271.               `Don't forget warning() already injects the component stack automatically.`,
    
  272.             pass: false,
    
  273.           };
    
  274.         }
    
  275. 
    
  276.         return {pass: true};
    
  277.       }
    
  278.     } else {
    
  279.       // Any uncaught errors or warnings should fail tests in production mode.
    
  280.       callback();
    
  281. 
    
  282.       return {pass: true};
    
  283.     }
    
  284.   };
    
  285. 
    
  286. module.exports = {
    
  287.   toLowPriorityWarnDev: createMatcherFor('warn'),
    
  288.   toWarnDev: createMatcherFor('error'),
    
  289. };