UNPKG

6.16 kBJavaScriptView Raw
1import url from 'node:url';
2import chokidar from 'chokidar';
3import logger from '@wdio/logger';
4import pickBy from 'lodash.pickby';
5import flattenDeep from 'lodash.flattendeep';
6import union from 'lodash.union';
7import Launcher from './launcher.js';
8const log = logger('@wdio/cli:watch');
9export default class Watcher {
10 _configFile;
11 _args;
12 _launcher;
13 _specs = [];
14 constructor(_configFile, _args) {
15 this._configFile = _configFile;
16 this._args = _args;
17 log.info('Starting launcher in watch mode');
18 this._launcher = new Launcher(this._configFile, this._args, true);
19 }
20 async watch() {
21 await this._launcher.configParser.initialize();
22 const specs = this._launcher.configParser.getSpecs();
23 const capSpecs = this._launcher.isMultiremote
24 ? []
25 : union(flattenDeep(this._launcher.configParser.getCapabilities().map(cap => cap.specs || [])));
26 this._specs = [...specs, ...capSpecs];
27 /**
28 * listen on spec changes and rerun specific spec file
29 */
30 const flattenedSpecs = flattenDeep(this._specs).map((fileUrl) => url.fileURLToPath(fileUrl));
31 chokidar.watch(flattenedSpecs, { ignoreInitial: true })
32 .on('add', this.getFileListener())
33 .on('change', this.getFileListener());
34 /**
35 * listen on filesToWatch changes an rerun complete suite
36 */
37 const { filesToWatch } = this._launcher.configParser.getConfig();
38 if (filesToWatch.length) {
39 chokidar.watch(filesToWatch, { ignoreInitial: true })
40 .on('add', this.getFileListener(false))
41 .on('change', this.getFileListener(false));
42 }
43 /**
44 * run initial test suite
45 */
46 await this._launcher.run();
47 /**
48 * clean interface once all worker finish
49 */
50 const workers = this.getWorkers();
51 Object.values(workers).forEach((worker) => worker.on('exit', () => {
52 /**
53 * check if all workers have finished
54 */
55 if (Object.values(workers).find((w) => w.isBusy)) {
56 return;
57 }
58 this._launcher.interface?.finalise();
59 }));
60 }
61 /**
62 * return file listener callback that calls `run` method
63 * @param {Boolean} [passOnFile=true] if true pass on file change as parameter
64 * @return {Function} chokidar event callback
65 */
66 getFileListener(passOnFile = true) {
67 return (spec) => {
68 const runSpecs = [];
69 let singleSpecFound = false;
70 for (let index = 0, length = this._specs.length; index < length; index += 1) {
71 const value = this._specs[index];
72 if (Array.isArray(value) && value.indexOf(spec) > -1) {
73 runSpecs.push(value);
74 }
75 else if (!singleSpecFound && spec === value) {
76 // Only need to run a singleFile once - so avoid duplicates
77 singleSpecFound = true;
78 runSpecs.push(value);
79 }
80 }
81 // If the runSpecs array is empty, then this must be a new file/array
82 // so add the spec directly to the runSpecs
83 if (runSpecs.length === 0) {
84 runSpecs.push(url.pathToFileURL(spec).href);
85 }
86 // Do not pass the `spec` command line option to `this.run()`
87 // eslint-disable-next-line @typescript-eslint/no-unused-vars
88 const { spec: _, ...args } = this._args;
89 return runSpecs.map((spec) => {
90 return this.run({
91 ...args,
92 ...(passOnFile ? { spec: [spec] } : {})
93 });
94 });
95 };
96 }
97 /**
98 * helper method to get workers from worker pool of wdio runner
99 * @param predicate filter by property value (see lodash.pickBy)
100 * @param includeBusyWorker don't filter out busy worker (default: false)
101 * @return Object with workers, e.g. {'0-0': { ... }}
102 */
103 getWorkers(predicate, includeBusyWorker) {
104 if (!this._launcher.runner) {
105 throw new Error('Internal Error: no runner initialized, call run() first');
106 }
107 let workers = this._launcher.runner.workerPool;
108 if (typeof predicate === 'function') {
109 workers = pickBy(workers, predicate);
110 }
111 /**
112 * filter out busy workers, only skip if explicitly desired
113 */
114 if (!includeBusyWorker) {
115 workers = pickBy(workers, (worker) => !worker.isBusy);
116 }
117 return workers;
118 }
119 /**
120 * run workers with params
121 * @param params parameters to run the worker with
122 */
123 run(params = {}) {
124 const workers = this.getWorkers((params.spec
125 ? (worker) => Boolean(worker.specs.find((s) => params.spec?.includes(s)))
126 : undefined));
127 /**
128 * don't do anything if no worker was found
129 */
130 if (Object.keys(workers).length === 0 || !this._launcher.interface) {
131 return;
132 }
133 /**
134 * update total worker count interface
135 * ToDo: this should have a cleaner solution
136 */
137 this._launcher.interface.totalWorkerCnt = Object.entries(workers).length;
138 /**
139 * clean up interface
140 */
141 this.cleanUp();
142 /**
143 * trigger new run for non busy worker
144 */
145 for (const [, worker] of Object.entries(workers)) {
146 const { cid, capabilities, specs, sessionId } = worker;
147 const { hostname, path, port, protocol, automationProtocol } = worker.config;
148 const args = Object.assign({ sessionId, baseUrl: worker.config.baseUrl, hostname, path, port, protocol, automationProtocol }, params);
149 worker.postMessage('run', args);
150 this._launcher.interface.emit('job:start', { cid, caps: capabilities, specs });
151 }
152 }
153 cleanUp() {
154 this._launcher.interface?.setup();
155 }
156}