"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.TestRun = void 0;
exports.createApplyRebaselinesTask = createApplyRebaselinesTask;
exports.createClearCacheTask = createClearCacheTask;
exports.createGlobalSetupTasks = createGlobalSetupTasks;
exports.createListFilesTask = createListFilesTask;
exports.createLoadTask = createLoadTask;
exports.createPluginSetupTasks = createPluginSetupTasks;
exports.createReportBeginTask = createReportBeginTask;
exports.createRunTestsTasks = createRunTestsTasks;
exports.createStartDevServerTask = createStartDevServerTask;
exports.runTasks = runTasks;
exports.runTasksDeferCleanup = runTasksDeferCleanup;
var _fs = _interopRequireDefault(require("fs"));
var _path = _interopRequireDefault(require("path"));
var _util = require("util");
var _utilsBundle = require("playwright-core/lib/utilsBundle");
var _utils = require("playwright-core/lib/utils");
var _dispatcher = require("./dispatcher");
var _testGroups = require("../runner/testGroups");
var _taskRunner = require("./taskRunner");
var _loadUtils = require("./loadUtils");
var _util2 = require("../util");
var _test = require("../common/test");
var _projectUtils = require("./projectUtils");
var _failureTracker = require("./failureTracker");
var _vcs = require("./vcs");
var _compilationCache = require("../transform/compilationCache");
var _rebase = require("./rebase");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
 * Copyright Microsoft Corporation. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

const readDirAsync = (0, _util.promisify)(_fs.default.readdir);
class TestRun {
  constructor(config, reporter) {
    this.config = void 0;
    this.reporter = void 0;
    this.failureTracker = void 0;
    this.rootSuite = undefined;
    this.phases = [];
    this.projectFiles = new Map();
    this.projectSuites = new Map();
    this.config = config;
    this.reporter = reporter;
    this.failureTracker = new _failureTracker.FailureTracker(config);
  }
}
exports.TestRun = TestRun;
async function runTasks(testRun, tasks, globalTimeout, cancelPromise) {
  const deadline = globalTimeout ? (0, _utils.monotonicTime)() + globalTimeout : 0;
  const taskRunner = new _taskRunner.TaskRunner(testRun.reporter, globalTimeout || 0);
  for (const task of tasks) taskRunner.addTask(task);
  testRun.reporter.onConfigure(testRun.config.config);
  const status = await taskRunner.run(testRun, deadline, cancelPromise);
  return await finishTaskRun(testRun, status);
}
async function runTasksDeferCleanup(testRun, tasks) {
  const taskRunner = new _taskRunner.TaskRunner(testRun.reporter, 0);
  for (const task of tasks) taskRunner.addTask(task);
  testRun.reporter.onConfigure(testRun.config.config);
  const {
    status,
    cleanup
  } = await taskRunner.runDeferCleanup(testRun, 0);
  return {
    status: await finishTaskRun(testRun, status),
    cleanup
  };
}
async function finishTaskRun(testRun, status) {
  if (status === 'passed') status = testRun.failureTracker.result();
  const modifiedResult = await testRun.reporter.onEnd({
    status
  });
  if (modifiedResult && modifiedResult.status) status = modifiedResult.status;
  await testRun.reporter.onExit();
  return status;
}
function createGlobalSetupTasks(config) {
  const tasks = [];
  if (!config.configCLIOverrides.preserveOutputDir && !process.env.PW_TEST_NO_REMOVE_OUTPUT_DIRS) tasks.push(createRemoveOutputDirsTask());
  tasks.push(...createPluginSetupTasks(config), ...config.globalTeardowns.map(file => createGlobalTeardownTask(file, config)).reverse(), ...config.globalSetups.map(file => createGlobalSetupTask(file, config)));
  return tasks;
}
function createRunTestsTasks(config) {
  return [createPhasesTask(), createReportBeginTask(), ...config.plugins.map(plugin => createPluginBeginTask(plugin)), createRunTestsTask()];
}
function createClearCacheTask(config) {
  return {
    title: 'clear cache',
    setup: async () => {
      await (0, _util2.removeDirAndLogToConsole)(_compilationCache.cacheDir);
      for (const plugin of config.plugins) {
        var _plugin$instance, _plugin$instance$clea;
        await ((_plugin$instance = plugin.instance) === null || _plugin$instance === void 0 || (_plugin$instance$clea = _plugin$instance.clearCache) === null || _plugin$instance$clea === void 0 ? void 0 : _plugin$instance$clea.call(_plugin$instance));
      }
    }
  };
}
function createReportBeginTask() {
  return {
    title: 'report begin',
    setup: async testRun => {
      var _testRun$reporter$onB, _testRun$reporter;
      (_testRun$reporter$onB = (_testRun$reporter = testRun.reporter).onBegin) === null || _testRun$reporter$onB === void 0 || _testRun$reporter$onB.call(_testRun$reporter, testRun.rootSuite);
    },
    teardown: async ({}) => {}
  };
}
function createPluginSetupTasks(config) {
  return config.plugins.map(plugin => ({
    title: 'plugin setup',
    setup: async ({
      reporter
    }) => {
      var _plugin$instance2, _plugin$instance2$set;
      if (typeof plugin.factory === 'function') plugin.instance = await plugin.factory();else plugin.instance = plugin.factory;
      await ((_plugin$instance2 = plugin.instance) === null || _plugin$instance2 === void 0 || (_plugin$instance2$set = _plugin$instance2.setup) === null || _plugin$instance2$set === void 0 ? void 0 : _plugin$instance2$set.call(_plugin$instance2, config.config, config.configDir, reporter));
    },
    teardown: async () => {
      var _plugin$instance3, _plugin$instance3$tea;
      await ((_plugin$instance3 = plugin.instance) === null || _plugin$instance3 === void 0 || (_plugin$instance3$tea = _plugin$instance3.teardown) === null || _plugin$instance3$tea === void 0 ? void 0 : _plugin$instance3$tea.call(_plugin$instance3));
    }
  }));
}
function createPluginBeginTask(plugin) {
  return {
    title: 'plugin begin',
    setup: async testRun => {
      var _plugin$instance4, _plugin$instance4$beg;
      await ((_plugin$instance4 = plugin.instance) === null || _plugin$instance4 === void 0 || (_plugin$instance4$beg = _plugin$instance4.begin) === null || _plugin$instance4$beg === void 0 ? void 0 : _plugin$instance4$beg.call(_plugin$instance4, testRun.rootSuite));
    },
    teardown: async () => {
      var _plugin$instance5, _plugin$instance5$end;
      await ((_plugin$instance5 = plugin.instance) === null || _plugin$instance5 === void 0 || (_plugin$instance5$end = _plugin$instance5.end) === null || _plugin$instance5$end === void 0 ? void 0 : _plugin$instance5$end.call(_plugin$instance5));
    }
  };
}
function createGlobalSetupTask(file, config) {
  let title = 'global setup';
  if (config.globalSetups.length > 1) title += ` (${file})`;
  let globalSetupResult;
  return {
    title,
    setup: async ({
      config
    }) => {
      const setupHook = await (0, _loadUtils.loadGlobalHook)(config, file);
      globalSetupResult = await setupHook(config.config);
    },
    teardown: async () => {
      if (typeof globalSetupResult === 'function') await globalSetupResult();
    }
  };
}
function createGlobalTeardownTask(file, config) {
  let title = 'global teardown';
  if (config.globalTeardowns.length > 1) title += ` (${file})`;
  return {
    title,
    teardown: async ({
      config
    }) => {
      const teardownHook = await (0, _loadUtils.loadGlobalHook)(config, file);
      await teardownHook(config.config);
    }
  };
}
function createRemoveOutputDirsTask() {
  return {
    title: 'clear output',
    setup: async ({
      config
    }) => {
      const outputDirs = new Set();
      const projects = (0, _projectUtils.filterProjects)(config.projects, config.cliProjectFilter);
      projects.forEach(p => outputDirs.add(p.project.outputDir));
      await Promise.all(Array.from(outputDirs).map(outputDir => (0, _utils.removeFolders)([outputDir]).then(async ([error]) => {
        if (!error) return;
        if (error.code === 'EBUSY') {
          // We failed to remove folder, might be due to the whole folder being mounted inside a container:
          //   https://github.com/microsoft/playwright/issues/12106
          // Do a best-effort to remove all files inside of it instead.
          const entries = await readDirAsync(outputDir).catch(e => []);
          await Promise.all(entries.map(entry => (0, _utils.removeFolders)([_path.default.join(outputDir, entry)])));
        } else {
          throw error;
        }
      })));
    }
  };
}
function createListFilesTask() {
  return {
    title: 'load tests',
    setup: async (testRun, errors) => {
      testRun.rootSuite = await (0, _loadUtils.createRootSuite)(testRun, errors, false);
      testRun.failureTracker.onRootSuite(testRun.rootSuite);
      await (0, _loadUtils.collectProjectsAndTestFiles)(testRun, false);
      for (const [project, files] of testRun.projectFiles) {
        const projectSuite = new _test.Suite(project.project.name, 'project');
        projectSuite._fullProject = project;
        testRun.rootSuite._addSuite(projectSuite);
        const suites = files.map(file => {
          const title = _path.default.relative(testRun.config.config.rootDir, file);
          const suite = new _test.Suite(title, 'file');
          suite.location = {
            file,
            line: 0,
            column: 0
          };
          projectSuite._addSuite(suite);
          return suite;
        });
        testRun.projectSuites.set(project, suites);
      }
    }
  };
}
function createLoadTask(mode, options) {
  return {
    title: 'load tests',
    setup: async (testRun, errors, softErrors) => {
      await (0, _loadUtils.collectProjectsAndTestFiles)(testRun, !!options.doNotRunDepsOutsideProjectFilter);
      await (0, _loadUtils.loadFileSuites)(testRun, mode, options.failOnLoadErrors ? errors : softErrors);
      if (testRun.config.cliOnlyChanged || options.populateDependencies) {
        for (const plugin of testRun.config.plugins) {
          var _plugin$instance6, _plugin$instance6$pop;
          await ((_plugin$instance6 = plugin.instance) === null || _plugin$instance6 === void 0 || (_plugin$instance6$pop = _plugin$instance6.populateDependencies) === null || _plugin$instance6$pop === void 0 ? void 0 : _plugin$instance6$pop.call(_plugin$instance6));
        }
      }
      let cliOnlyChangedMatcher = undefined;
      if (testRun.config.cliOnlyChanged) {
        const changedFiles = await (0, _vcs.detectChangedTestFiles)(testRun.config.cliOnlyChanged, testRun.config.configDir);
        cliOnlyChangedMatcher = file => changedFiles.has(file);
      }
      testRun.rootSuite = await (0, _loadUtils.createRootSuite)(testRun, options.failOnLoadErrors ? errors : softErrors, !!options.filterOnly, cliOnlyChangedMatcher);
      testRun.failureTracker.onRootSuite(testRun.rootSuite);
      // Fail when no tests.
      if (options.failOnLoadErrors && !testRun.rootSuite.allTests().length && !testRun.config.cliPassWithNoTests && !testRun.config.config.shard && !testRun.config.cliOnlyChanged) {
        if (testRun.config.cliArgs.length) {
          throw new Error([`No tests found.`, `Make sure that arguments are regular expressions matching test files.`, `You may need to escape symbols like "$" or "*" and quote the arguments.`].join('\n'));
        }
        throw new Error(`No tests found`);
      }
    }
  };
}
function createApplyRebaselinesTask() {
  return {
    title: 'apply rebaselines',
    teardown: async ({
      config,
      reporter
    }) => {
      await (0, _rebase.applySuggestedRebaselines)(config, reporter);
    }
  };
}
function createPhasesTask() {
  return {
    title: 'create phases',
    setup: async testRun => {
      let maxConcurrentTestGroups = 0;
      const processed = new Set();
      const projectToSuite = new Map(testRun.rootSuite.suites.map(suite => [suite._fullProject, suite]));
      const allProjects = [...projectToSuite.keys()];
      const teardownToSetups = (0, _projectUtils.buildTeardownToSetupsMap)(allProjects);
      const teardownToSetupsDependents = new Map();
      for (const [teardown, setups] of teardownToSetups) {
        const closure = (0, _projectUtils.buildDependentProjects)(setups, allProjects);
        closure.delete(teardown);
        teardownToSetupsDependents.set(teardown, [...closure]);
      }
      for (let i = 0; i < projectToSuite.size; i++) {
        // Find all projects that have all their dependencies processed by previous phases.
        const phaseProjects = [];
        for (const project of projectToSuite.keys()) {
          if (processed.has(project)) continue;
          const projectsThatShouldFinishFirst = [...project.deps, ...(teardownToSetupsDependents.get(project) || [])];
          if (projectsThatShouldFinishFirst.find(p => !processed.has(p))) continue;
          phaseProjects.push(project);
        }

        // Create a new phase.
        for (const project of phaseProjects) processed.add(project);
        if (phaseProjects.length) {
          let testGroupsInPhase = 0;
          const phase = {
            dispatcher: new _dispatcher.Dispatcher(testRun.config, testRun.reporter, testRun.failureTracker),
            projects: []
          };
          testRun.phases.push(phase);
          for (const project of phaseProjects) {
            const projectSuite = projectToSuite.get(project);
            const testGroups = (0, _testGroups.createTestGroups)(projectSuite, testRun.config.config.workers);
            phase.projects.push({
              project,
              projectSuite,
              testGroups
            });
            testGroupsInPhase += testGroups.length;
          }
          (0, _utilsBundle.debug)('pw:test:task')(`created phase #${testRun.phases.length} with ${phase.projects.map(p => p.project.project.name).sort()} projects, ${testGroupsInPhase} testGroups`);
          maxConcurrentTestGroups = Math.max(maxConcurrentTestGroups, testGroupsInPhase);
        }
      }
      testRun.config.config.metadata.actualWorkers = Math.min(testRun.config.config.workers, maxConcurrentTestGroups);
    }
  };
}
function createRunTestsTask() {
  return {
    title: 'test suite',
    setup: async ({
      phases,
      failureTracker
    }) => {
      const successfulProjects = new Set();
      const extraEnvByProjectId = new Map();
      const teardownToSetups = (0, _projectUtils.buildTeardownToSetupsMap)(phases.map(phase => phase.projects.map(p => p.project)).flat());
      for (const {
        dispatcher,
        projects
      } of phases) {
        // Each phase contains dispatcher and a set of test groups.
        // We don't want to run the test groups belonging to the projects
        // that depend on the projects that failed previously.
        const phaseTestGroups = [];
        for (const {
          project,
          testGroups
        } of projects) {
          // Inherit extra environment variables from dependencies.
          let extraEnv = {};
          for (const dep of project.deps) extraEnv = {
            ...extraEnv,
            ...extraEnvByProjectId.get(dep.id)
          };
          for (const setup of teardownToSetups.get(project) || []) extraEnv = {
            ...extraEnv,
            ...extraEnvByProjectId.get(setup.id)
          };
          extraEnvByProjectId.set(project.id, extraEnv);
          const hasFailedDeps = project.deps.some(p => !successfulProjects.has(p));
          if (!hasFailedDeps) phaseTestGroups.push(...testGroups);
        }
        if (phaseTestGroups.length) {
          await dispatcher.run(phaseTestGroups, extraEnvByProjectId);
          await dispatcher.stop();
          for (const [projectId, envProduced] of dispatcher.producedEnvByProjectId()) {
            const extraEnv = extraEnvByProjectId.get(projectId) || {};
            extraEnvByProjectId.set(projectId, {
              ...extraEnv,
              ...envProduced
            });
          }
        }

        // If the worker broke, fail everything, we have no way of knowing which
        // projects failed.
        if (!failureTracker.hasWorkerErrors()) {
          for (const {
            project,
            projectSuite
          } of projects) {
            const hasFailedDeps = project.deps.some(p => !successfulProjects.has(p));
            if (!hasFailedDeps && !projectSuite.allTests().some(test => !test.ok())) successfulProjects.add(project);
          }
        }
      }
    },
    teardown: async ({
      phases
    }) => {
      for (const {
        dispatcher
      } of phases.reverse()) await dispatcher.stop();
    }
  };
}
function createStartDevServerTask() {
  return {
    title: 'start dev server',
    setup: async ({
      config
    }, errors, softErrors) => {
      if (config.plugins.some(plugin => !!plugin.devServerCleanup)) {
        errors.push({
          message: `DevServer is already running`
        });
        return;
      }
      for (const plugin of config.plugins) {
        var _plugin$instance7, _plugin$instance7$sta;
        plugin.devServerCleanup = await ((_plugin$instance7 = plugin.instance) === null || _plugin$instance7 === void 0 || (_plugin$instance7$sta = _plugin$instance7.startDevServer) === null || _plugin$instance7$sta === void 0 ? void 0 : _plugin$instance7$sta.call(_plugin$instance7));
      }
      if (!config.plugins.some(plugin => !!plugin.devServerCleanup)) errors.push({
        message: `DevServer is not available in the package you are using. Did you mean to use component testing?`
      });
    },
    teardown: async ({
      config
    }) => {
      for (const plugin of config.plugins) {
        var _plugin$devServerClea;
        await ((_plugin$devServerClea = plugin.devServerCleanup) === null || _plugin$devServerClea === void 0 ? void 0 : _plugin$devServerClea.call(plugin));
        plugin.devServerCleanup = undefined;
      }
    }
  };
}