UNPKG

@wdio/cli

Version:
157 lines (156 loc) 6.16 kB
import url from 'node:url'; import chokidar from 'chokidar'; import logger from '@wdio/logger'; import pickBy from 'lodash.pickby'; import flattenDeep from 'lodash.flattendeep'; import union from 'lodash.union'; import Launcher from './launcher.js'; const log = logger('@wdio/cli:watch'); export default class Watcher { _configFile; _args; _launcher; _specs = []; constructor(_configFile, _args) { this._configFile = _configFile; this._args = _args; log.info('Starting launcher in watch mode'); this._launcher = new Launcher(this._configFile, this._args, true); } async watch() { await this._launcher.configParser.initialize(); const specs = this._launcher.configParser.getSpecs(); const capSpecs = this._launcher.isMultiremote ? [] : union(flattenDeep(this._launcher.configParser.getCapabilities().map(cap => cap.specs || []))); this._specs = [...specs, ...capSpecs]; /** * listen on spec changes and rerun specific spec file */ const flattenedSpecs = flattenDeep(this._specs).map((fileUrl) => url.fileURLToPath(fileUrl)); chokidar.watch(flattenedSpecs, { ignoreInitial: true }) .on('add', this.getFileListener()) .on('change', this.getFileListener()); /** * listen on filesToWatch changes an rerun complete suite */ const { filesToWatch } = this._launcher.configParser.getConfig(); if (filesToWatch.length) { chokidar.watch(filesToWatch, { ignoreInitial: true }) .on('add', this.getFileListener(false)) .on('change', this.getFileListener(false)); } /** * run initial test suite */ await this._launcher.run(); /** * clean interface once all worker finish */ const workers = this.getWorkers(); Object.values(workers).forEach((worker) => worker.on('exit', () => { /** * check if all workers have finished */ if (Object.values(workers).find((w) => w.isBusy)) { return; } this._launcher.interface?.finalise(); })); } /** * return file listener callback that calls `run` method * @param {Boolean} [passOnFile=true] if true pass on file change as parameter * @return {Function} chokidar event callback */ getFileListener(passOnFile = true) { return (spec) => { const runSpecs = []; let singleSpecFound = false; for (let index = 0, length = this._specs.length; index < length; index += 1) { const value = this._specs[index]; if (Array.isArray(value) && value.indexOf(spec) > -1) { runSpecs.push(value); } else if (!singleSpecFound && spec === value) { // Only need to run a singleFile once - so avoid duplicates singleSpecFound = true; runSpecs.push(value); } } // If the runSpecs array is empty, then this must be a new file/array // so add the spec directly to the runSpecs if (runSpecs.length === 0) { runSpecs.push(url.pathToFileURL(spec).href); } // Do not pass the `spec` command line option to `this.run()` // eslint-disable-next-line @typescript-eslint/no-unused-vars const { spec: _, ...args } = this._args; return runSpecs.map((spec) => { return this.run({ ...args, ...(passOnFile ? { spec: [spec] } : {}) }); }); }; } /** * helper method to get workers from worker pool of wdio runner * @param predicate filter by property value (see lodash.pickBy) * @param includeBusyWorker don't filter out busy worker (default: false) * @return Object with workers, e.g. {'0-0': { ... }} */ getWorkers(predicate, includeBusyWorker) { if (!this._launcher.runner) { throw new Error('Internal Error: no runner initialized, call run() first'); } let workers = this._launcher.runner.workerPool; if (typeof predicate === 'function') { workers = pickBy(workers, predicate); } /** * filter out busy workers, only skip if explicitly desired */ if (!includeBusyWorker) { workers = pickBy(workers, (worker) => !worker.isBusy); } return workers; } /** * run workers with params * @param params parameters to run the worker with */ run(params = {}) { const workers = this.getWorkers((params.spec ? (worker) => Boolean(worker.specs.find((s) => params.spec?.includes(s))) : undefined)); /** * don't do anything if no worker was found */ if (Object.keys(workers).length === 0 || !this._launcher.interface) { return; } /** * update total worker count interface * ToDo: this should have a cleaner solution */ this._launcher.interface.totalWorkerCnt = Object.entries(workers).length; /** * clean up interface */ this.cleanUp(); /** * trigger new run for non busy worker */ for (const [, worker] of Object.entries(workers)) { const { cid, capabilities, specs, sessionId } = worker; const { hostname, path, port, protocol, automationProtocol } = worker.config; const args = Object.assign({ sessionId, baseUrl: worker.config.baseUrl, hostname, path, port, protocol, automationProtocol }, params); worker.postMessage('run', args); this._launcher.interface.emit('job:start', { cid, caps: capabilities, specs }); } } cleanUp() { this._launcher.interface?.setup(); } }