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