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