'use strict';

Object.defineProperty(exports, '__esModule', {
  value: true
});
exports.default = watch;

function path() {
  const data = _interopRequireWildcard(require('path'));

  path = function () {
    return data;
  };

  return data;
}

function _ansiEscapes() {
  const data = _interopRequireDefault(require('ansi-escapes'));

  _ansiEscapes = function () {
    return data;
  };

  return data;
}

function _chalk() {
  const data = _interopRequireDefault(require('chalk'));

  _chalk = function () {
    return data;
  };

  return data;
}

function _exit() {
  const data = _interopRequireDefault(require('exit'));

  _exit = function () {
    return data;
  };

  return data;
}

function _slash() {
  const data = _interopRequireDefault(require('slash'));

  _slash = function () {
    return data;
  };

  return data;
}

function _jestMessageUtil() {
  const data = require('jest-message-util');

  _jestMessageUtil = function () {
    return data;
  };

  return data;
}

function _jestUtil() {
  const data = require('jest-util');

  _jestUtil = function () {
    return data;
  };

  return data;
}

function _jestValidate() {
  const data = require('jest-validate');

  _jestValidate = function () {
    return data;
  };

  return data;
}

function _jestWatcher() {
  const data = require('jest-watcher');

  _jestWatcher = function () {
    return data;
  };

  return data;
}

var _FailedTestsCache = _interopRequireDefault(require('./FailedTestsCache'));

var _SearchSource = _interopRequireDefault(require('./SearchSource'));

var _getChangedFilesPromise = _interopRequireDefault(
  require('./getChangedFilesPromise')
);

var _activeFiltersMessage = _interopRequireDefault(
  require('./lib/activeFiltersMessage')
);

var _createContext = _interopRequireDefault(require('./lib/createContext'));

var _isValidPath = _interopRequireDefault(require('./lib/isValidPath'));

var _updateGlobalConfig = _interopRequireDefault(
  require('./lib/updateGlobalConfig')
);

var _watchPluginsHelpers = require('./lib/watchPluginsHelpers');

var _FailedTestsInteractive = _interopRequireDefault(
  require('./plugins/FailedTestsInteractive')
);

var _Quit = _interopRequireDefault(require('./plugins/Quit'));

var _TestNamePattern = _interopRequireDefault(
  require('./plugins/TestNamePattern')
);

var _TestPathPattern = _interopRequireDefault(
  require('./plugins/TestPathPattern')
);

var _UpdateSnapshots = _interopRequireDefault(
  require('./plugins/UpdateSnapshots')
);

var _UpdateSnapshotsInteractive = _interopRequireDefault(
  require('./plugins/UpdateSnapshotsInteractive')
);

var _runJest = _interopRequireDefault(require('./runJest'));

function _interopRequireDefault(obj) {
  return obj && obj.__esModule ? obj : {default: obj};
}

function _getRequireWildcardCache(nodeInterop) {
  if (typeof WeakMap !== 'function') return null;
  var cacheBabelInterop = new WeakMap();
  var cacheNodeInterop = new WeakMap();
  return (_getRequireWildcardCache = function (nodeInterop) {
    return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
  })(nodeInterop);
}

function _interopRequireWildcard(obj, nodeInterop) {
  if (!nodeInterop && obj && obj.__esModule) {
    return obj;
  }
  if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) {
    return {default: obj};
  }
  var cache = _getRequireWildcardCache(nodeInterop);
  if (cache && cache.has(obj)) {
    return cache.get(obj);
  }
  var newObj = {};
  var hasPropertyDescriptor =
    Object.defineProperty && Object.getOwnPropertyDescriptor;
  for (var key in obj) {
    if (key !== 'default' && Object.prototype.hasOwnProperty.call(obj, key)) {
      var desc = hasPropertyDescriptor
        ? Object.getOwnPropertyDescriptor(obj, key)
        : null;
      if (desc && (desc.get || desc.set)) {
        Object.defineProperty(newObj, key, desc);
      } else {
        newObj[key] = obj[key];
      }
    }
  }
  newObj.default = obj;
  if (cache) {
    cache.set(obj, newObj);
  }
  return newObj;
}

/**
 * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
const {print: preRunMessagePrint} = _jestUtil().preRunMessage;

let hasExitListener = false;
const INTERNAL_PLUGINS = [
  _FailedTestsInteractive.default,
  _TestPathPattern.default,
  _TestNamePattern.default,
  _UpdateSnapshots.default,
  _UpdateSnapshotsInteractive.default,
  _Quit.default
];
const RESERVED_KEY_PLUGINS = new Map([
  [
    _UpdateSnapshots.default,
    {
      forbiddenOverwriteMessage: 'updating snapshots',
      key: 'u'
    }
  ],
  [
    _UpdateSnapshotsInteractive.default,
    {
      forbiddenOverwriteMessage: 'updating snapshots interactively',
      key: 'i'
    }
  ],
  [
    _Quit.default,
    {
      forbiddenOverwriteMessage: 'quitting watch mode'
    }
  ]
]);

async function watch(
  initialGlobalConfig,
  contexts,
  outputStream,
  hasteMapInstances,
  stdin = process.stdin,
  hooks = new (_jestWatcher().JestHook)(),
  filter
) {
  // `globalConfig` will be constantly updated and reassigned as a result of
  // watch mode interactions.
  let globalConfig = initialGlobalConfig;
  let activePlugin;
  globalConfig = (0, _updateGlobalConfig.default)(globalConfig, {
    mode: globalConfig.watch ? 'watch' : 'watchAll',
    passWithNoTests: true
  });

  const updateConfigAndRun = ({
    bail,
    changedSince,
    collectCoverage,
    collectCoverageFrom,
    collectCoverageOnlyFrom,
    coverageDirectory,
    coverageReporters,
    findRelatedTests,
    mode,
    nonFlagArgs,
    notify,
    notifyMode,
    onlyFailures,
    reporters,
    testNamePattern,
    testPathPattern,
    updateSnapshot,
    verbose
  } = {}) => {
    const previousUpdateSnapshot = globalConfig.updateSnapshot;
    globalConfig = (0, _updateGlobalConfig.default)(globalConfig, {
      bail,
      changedSince,
      collectCoverage,
      collectCoverageFrom,
      collectCoverageOnlyFrom,
      coverageDirectory,
      coverageReporters,
      findRelatedTests,
      mode,
      nonFlagArgs,
      notify,
      notifyMode,
      onlyFailures,
      reporters,
      testNamePattern,
      testPathPattern,
      updateSnapshot,
      verbose
    });
    startRun(globalConfig);
    globalConfig = (0, _updateGlobalConfig.default)(globalConfig, {
      // updateSnapshot is not sticky after a run.
      updateSnapshot:
        previousUpdateSnapshot === 'all' ? 'none' : previousUpdateSnapshot
    });
  };

  const watchPlugins = INTERNAL_PLUGINS.map(
    InternalPlugin =>
      new InternalPlugin({
        stdin,
        stdout: outputStream
      })
  );
  watchPlugins.forEach(plugin => {
    const hookSubscriber = hooks.getSubscriber();

    if (plugin.apply) {
      plugin.apply(hookSubscriber);
    }
  });

  if (globalConfig.watchPlugins != null) {
    const watchPluginKeys = new Map();

    for (const plugin of watchPlugins) {
      const reservedInfo = RESERVED_KEY_PLUGINS.get(plugin.constructor) || {};
      const key = reservedInfo.key || getPluginKey(plugin, globalConfig);

      if (!key) {
        continue;
      }

      const {forbiddenOverwriteMessage} = reservedInfo;
      watchPluginKeys.set(key, {
        forbiddenOverwriteMessage,
        overwritable: forbiddenOverwriteMessage == null,
        plugin
      });
    }

    for (const pluginWithConfig of globalConfig.watchPlugins) {
      let plugin;

      try {
        const ThirdPartyPlugin = await (0, _jestUtil().requireOrImportModule)(
          pluginWithConfig.path
        );
        plugin = new ThirdPartyPlugin({
          config: pluginWithConfig.config,
          stdin,
          stdout: outputStream
        });
      } catch (error) {
        const errorWithContext = new Error(
          `Failed to initialize watch plugin "${_chalk().default.bold(
            (0, _slash().default)(
              path().relative(process.cwd(), pluginWithConfig.path)
            )
          )}":\n\n${(0, _jestMessageUtil().formatExecError)(
            error,
            contexts[0].config,
            {
              noStackTrace: false
            }
          )}`
        );
        delete errorWithContext.stack;
        return Promise.reject(errorWithContext);
      }

      checkForConflicts(watchPluginKeys, plugin, globalConfig);
      const hookSubscriber = hooks.getSubscriber();

      if (plugin.apply) {
        plugin.apply(hookSubscriber);
      }

      watchPlugins.push(plugin);
    }
  }

  const failedTestsCache = new _FailedTestsCache.default();
  let searchSources = contexts.map(context => ({
    context,
    searchSource: new _SearchSource.default(context)
  }));
  let isRunning = false;
  let testWatcher;
  let shouldDisplayWatchUsage = true;
  let isWatchUsageDisplayed = false;

  const emitFileChange = () => {
    if (hooks.isUsed('onFileChange')) {
      const projects = searchSources.map(({context, searchSource}) => ({
        config: context.config,
        testPaths: searchSource.findMatchingTests('').tests.map(t => t.path)
      }));
      hooks.getEmitter().onFileChange({
        projects
      });
    }
  };

  emitFileChange();
  hasteMapInstances.forEach((hasteMapInstance, index) => {
    hasteMapInstance.on('change', ({eventsQueue, hasteFS, moduleMap}) => {
      const validPaths = eventsQueue.filter(({filePath}) =>
        (0, _isValidPath.default)(globalConfig, filePath)
      );

      if (validPaths.length) {
        const context = (contexts[index] = (0, _createContext.default)(
          contexts[index].config,
          {
            hasteFS,
            moduleMap
          }
        ));
        activePlugin = null;
        searchSources = searchSources.slice();
        searchSources[index] = {
          context,
          searchSource: new _SearchSource.default(context)
        };
        emitFileChange();
        startRun(globalConfig);
      }
    });
  });

  if (!hasExitListener) {
    hasExitListener = true;
    process.on('exit', () => {
      if (activePlugin) {
        outputStream.write(_ansiEscapes().default.cursorDown());
        outputStream.write(_ansiEscapes().default.eraseDown);
      }
    });
  }

  const startRun = globalConfig => {
    if (isRunning) {
      return Promise.resolve(null);
    }

    testWatcher = new (_jestWatcher().TestWatcher)({
      isWatchMode: true
    });
    _jestUtil().isInteractive &&
      outputStream.write(_jestUtil().specialChars.CLEAR);
    preRunMessagePrint(outputStream);
    isRunning = true;
    const configs = contexts.map(context => context.config);
    const changedFilesPromise = (0, _getChangedFilesPromise.default)(
      globalConfig,
      configs
    );
    return (0, _runJest.default)({
      changedFilesPromise,
      contexts,
      failedTestsCache,
      filter,
      globalConfig,
      jestHooks: hooks.getEmitter(),
      onComplete: results => {
        isRunning = false;
        hooks.getEmitter().onTestRunComplete(results); // Create a new testWatcher instance so that re-runs won't be blocked.
        // The old instance that was passed to Jest will still be interrupted
        // and prevent test runs from the previous run.

        testWatcher = new (_jestWatcher().TestWatcher)({
          isWatchMode: true
        }); // Do not show any Watch Usage related stuff when running in a
        // non-interactive environment

        if (_jestUtil().isInteractive) {
          if (shouldDisplayWatchUsage) {
            outputStream.write(usage(globalConfig, watchPlugins));
            shouldDisplayWatchUsage = false; // hide Watch Usage after first run

            isWatchUsageDisplayed = true;
          } else {
            outputStream.write(showToggleUsagePrompt());
            shouldDisplayWatchUsage = false;
            isWatchUsageDisplayed = false;
          }
        } else {
          outputStream.write('\n');
        }

        failedTestsCache.setTestResults(results.testResults);
      },
      outputStream,
      startRun,
      testWatcher
    }).catch(
      (
        error // Errors thrown inside `runJest`, e.g. by resolvers, are caught here for
      ) =>
        // continuous watch mode execution. We need to reprint them to the
        // terminal and give just a little bit of extra space so they fit below
        // `preRunMessagePrint` message nicely.
        console.error(
          `\n\n${(0, _jestMessageUtil().formatExecError)(
            error,
            contexts[0].config,
            {
              noStackTrace: false
            }
          )}`
        )
    );
  };

  const onKeypress = key => {
    if (
      key === _jestWatcher().KEYS.CONTROL_C ||
      key === _jestWatcher().KEYS.CONTROL_D
    ) {
      if (typeof stdin.setRawMode === 'function') {
        stdin.setRawMode(false);
      }

      outputStream.write('\n');
      (0, _exit().default)(0);
      return;
    }

    if (activePlugin != null && activePlugin.onKey) {
      // if a plugin is activate, Jest should let it handle keystrokes, so ignore
      // them here
      activePlugin.onKey(key);
      return;
    } // Abort test run

    const pluginKeys = (0, _watchPluginsHelpers.getSortedUsageRows)(
      watchPlugins,
      globalConfig
    ).map(usage => Number(usage.key).toString(16));

    if (
      isRunning &&
      testWatcher &&
      ['q', _jestWatcher().KEYS.ENTER, 'a', 'o', 'f']
        .concat(pluginKeys)
        .includes(key)
    ) {
      testWatcher.setState({
        interrupted: true
      });
      return;
    }

    const matchingWatchPlugin = (0,
    _watchPluginsHelpers.filterInteractivePlugins)(
      watchPlugins,
      globalConfig
    ).find(plugin => getPluginKey(plugin, globalConfig) === key);

    if (matchingWatchPlugin != null) {
      if (isRunning) {
        testWatcher.setState({
          interrupted: true
        });
        return;
      } // "activate" the plugin, which has jest ignore keystrokes so the plugin
      // can handle them

      activePlugin = matchingWatchPlugin;

      if (activePlugin.run) {
        activePlugin.run(globalConfig, updateConfigAndRun).then(
          shouldRerun => {
            activePlugin = null;

            if (shouldRerun) {
              updateConfigAndRun();
            }
          },
          () => {
            activePlugin = null;
            onCancelPatternPrompt();
          }
        );
      } else {
        activePlugin = null;
      }
    }

    switch (key) {
      case _jestWatcher().KEYS.ENTER:
        startRun(globalConfig);
        break;

      case 'a':
        globalConfig = (0, _updateGlobalConfig.default)(globalConfig, {
          mode: 'watchAll',
          testNamePattern: '',
          testPathPattern: ''
        });
        startRun(globalConfig);
        break;

      case 'c':
        updateConfigAndRun({
          mode: 'watch',
          testNamePattern: '',
          testPathPattern: ''
        });
        break;

      case 'f':
        globalConfig = (0, _updateGlobalConfig.default)(globalConfig, {
          onlyFailures: !globalConfig.onlyFailures
        });
        startRun(globalConfig);
        break;

      case 'o':
        globalConfig = (0, _updateGlobalConfig.default)(globalConfig, {
          mode: 'watch',
          testNamePattern: '',
          testPathPattern: ''
        });
        startRun(globalConfig);
        break;

      case '?':
        break;

      case 'w':
        if (!shouldDisplayWatchUsage && !isWatchUsageDisplayed) {
          outputStream.write(_ansiEscapes().default.cursorUp());
          outputStream.write(_ansiEscapes().default.eraseDown);
          outputStream.write(usage(globalConfig, watchPlugins));
          isWatchUsageDisplayed = true;
          shouldDisplayWatchUsage = false;
        }

        break;
    }
  };

  const onCancelPatternPrompt = () => {
    outputStream.write(_ansiEscapes().default.cursorHide);
    outputStream.write(_jestUtil().specialChars.CLEAR);
    outputStream.write(usage(globalConfig, watchPlugins));
    outputStream.write(_ansiEscapes().default.cursorShow);
  };

  if (typeof stdin.setRawMode === 'function') {
    stdin.setRawMode(true);
    stdin.resume();
    stdin.setEncoding('utf8');
    stdin.on('data', onKeypress);
  }

  startRun(globalConfig);
  return Promise.resolve();
}

const checkForConflicts = (watchPluginKeys, plugin, globalConfig) => {
  const key = getPluginKey(plugin, globalConfig);

  if (!key) {
    return;
  }

  const conflictor = watchPluginKeys.get(key);

  if (!conflictor || conflictor.overwritable) {
    watchPluginKeys.set(key, {
      overwritable: false,
      plugin
    });
    return;
  }

  let error;

  if (conflictor.forbiddenOverwriteMessage) {
    error = `
  Watch plugin ${_chalk().default.bold.red(
    getPluginIdentifier(plugin)
  )} attempted to register key ${_chalk().default.bold.red(`<${key}>`)},
  that is reserved internally for ${_chalk().default.bold.red(
    conflictor.forbiddenOverwriteMessage
  )}.
  Please change the configuration key for this plugin.`.trim();
  } else {
    const plugins = [conflictor.plugin, plugin]
      .map(p => _chalk().default.bold.red(getPluginIdentifier(p)))
      .join(' and ');
    error = `
  Watch plugins ${plugins} both attempted to register key ${_chalk().default.bold.red(
      `<${key}>`
    )}.
  Please change the key configuration for one of the conflicting plugins to avoid overlap.`.trim();
  }

  throw new (_jestValidate().ValidationError)(
    'Watch plugin configuration error',
    error
  );
};

const getPluginIdentifier = (
  plugin // This breaks as `displayName` is not defined as a static, but since
) =>
  // WatchPlugin is an interface, and it is my understanding interface
  // static fields are not definable anymore, no idea how to circumvent
  // this :-(
  // @ts-expect-error: leave `displayName` be.
  plugin.constructor.displayName || plugin.constructor.name;

const getPluginKey = (plugin, globalConfig) => {
  if (typeof plugin.getUsageInfo === 'function') {
    return (
      plugin.getUsageInfo(globalConfig) || {
        key: null
      }
    ).key;
  }

  return null;
};

const usage = (globalConfig, watchPlugins, delimiter = '\n') => {
  const messages = [
    (0, _activeFiltersMessage.default)(globalConfig),
    globalConfig.testPathPattern || globalConfig.testNamePattern
      ? `${_chalk().default.dim(' \u203A Press ')}c${_chalk().default.dim(
          ' to clear filters.'
        )}`
      : null,
    `\n${_chalk().default.bold('Watch Usage')}`,
    globalConfig.watch
      ? `${_chalk().default.dim(' \u203A Press ')}a${_chalk().default.dim(
          ' to run all tests.'
        )}`
      : null,
    globalConfig.onlyFailures
      ? `${_chalk().default.dim(' \u203A Press ')}f${_chalk().default.dim(
          ' to quit "only failed tests" mode.'
        )}`
      : `${_chalk().default.dim(' \u203A Press ')}f${_chalk().default.dim(
          ' to run only failed tests.'
        )}`,
    (globalConfig.watchAll ||
      globalConfig.testPathPattern ||
      globalConfig.testNamePattern) &&
    !globalConfig.noSCM
      ? `${_chalk().default.dim(' \u203A Press ')}o${_chalk().default.dim(
          ' to only run tests related to changed files.'
        )}`
      : null,
    ...(0, _watchPluginsHelpers.getSortedUsageRows)(
      watchPlugins,
      globalConfig
    ).map(
      plugin =>
        `${_chalk().default.dim(' \u203A Press')} ${
          plugin.key
        } ${_chalk().default.dim(`to ${plugin.prompt}.`)}`
    ),
    `${_chalk().default.dim(' \u203A Press ')}Enter${_chalk().default.dim(
      ' to trigger a test run.'
    )}`
  ];
  return `${messages.filter(message => !!message).join(delimiter)}\n`;
};

const showToggleUsagePrompt = () =>
  '\n' +
  `${_chalk().default.bold('Watch Usage: ')}${_chalk().default.dim(
    'Press '
  )}w${_chalk().default.dim(' to show more.')}`;
