@wdio/cli 7.17.0 → 7.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/build/@types/yarn-install.d.ts +16 -0
  2. package/build/@types/yarn-install.d.ts.map +1 -0
  3. package/build/@types/yarn-install.js +1 -0
  4. package/build/commands/config.d.ts +64 -0
  5. package/build/commands/config.d.ts.map +1 -0
  6. package/build/commands/config.js +236 -0
  7. package/build/commands/install.d.ts +18 -0
  8. package/build/commands/install.d.ts.map +1 -0
  9. package/build/commands/install.js +112 -0
  10. package/build/commands/repl.d.ts +36 -0
  11. package/build/commands/repl.d.ts.map +1 -0
  12. package/build/commands/repl.js +62 -0
  13. package/build/commands/run.d.ts +172 -0
  14. package/build/commands/run.d.ts.map +1 -0
  15. package/build/commands/run.js +172 -0
  16. package/build/constants.d.ts +485 -0
  17. package/build/constants.d.ts.map +1 -0
  18. package/build/constants.js +370 -0
  19. package/build/index.d.ts +5 -0
  20. package/build/index.d.ts.map +1 -0
  21. package/build/index.js +88 -0
  22. package/build/interface.d.ts +77 -0
  23. package/build/interface.d.ts.map +1 -0
  24. package/build/interface.js +248 -0
  25. package/build/launcher.d.ts +96 -0
  26. package/build/launcher.d.ts.map +1 -0
  27. package/build/launcher.js +436 -0
  28. package/build/templates/afterTest.ejs +20 -0
  29. package/build/templates/exampleFiles/cucumber/features/login.feature +12 -0
  30. package/build/templates/exampleFiles/cucumber/step_definitions/steps.js.ejs +52 -0
  31. package/build/templates/exampleFiles/jasmine/example.e2e.js.ejs +42 -0
  32. package/build/templates/exampleFiles/mocha/example.e2e.js.ejs +42 -0
  33. package/build/templates/exampleFiles/pageobjects/login.page.js.ejs +44 -0
  34. package/build/templates/exampleFiles/pageobjects/page.js.ejs +13 -0
  35. package/build/templates/exampleFiles/pageobjects/secure.page.js.ejs +19 -0
  36. package/build/templates/reporters.ejs +14 -0
  37. package/build/templates/wdio.conf.tpl.ejs +457 -0
  38. package/build/types.d.ts +110 -0
  39. package/build/types.d.ts.map +1 -0
  40. package/build/types.js +2 -0
  41. package/build/utils.d.ts +87 -0
  42. package/build/utils.d.ts.map +1 -0
  43. package/build/utils.js +360 -0
  44. package/build/watcher.d.ts +34 -0
  45. package/build/watcher.d.ts.map +1 -0
  46. package/build/watcher.js +150 -0
  47. package/package.json +7 -7
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAGhD,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAElE,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAQ/G,qBAAa,SAAU,SAAQ,kBAAkB;IACtC,MAAM,EAAE,MAAM,CAAA;gBACT,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAI9C;AAED;;GAEG;AACH,wBAAsB,cAAc,CAChC,QAAQ,EAAE,QAAQ,CAAC,eAAe,EAAE,EACpC,QAAQ,EAAE,MAAM,QAAQ,CAAC,aAAa,EACtC,GAAG,IAAI,EAAE,GAAG,EAAE,sBA2BjB;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,QAAQ,GAAG,QAAQ,EAAE,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,yBAmBhF;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACnC,cAAc,EAAE,QAAQ,GAAG,QAAQ,EAAE,EACrC,MAAM,EAAE,OAAO,CAAC,UAAU,EAC1B,YAAY,EAAE,YAAY,CAAC,kBAAkB,EAC7C,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,gBAAgB,sBAkB5B;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAE,IAAI,GAAE,YAAY,CAAC,mBAAwB,UAkBzE;AAoBD,wBAAgB,YAAY,CAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,2BASzD;AAED,wBAAgB,aAAa,CAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,sBAYxE;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,gBAAgB,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,MAAM,UAAQ,QAmC3F;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,SAAS,GAAG,gBAAgB,CAMvF;AAED,wBAAsB,uBAAuB,CAAE,OAAO,EAAE,aAAa,iBAWpE;AAED,eAAO,MAAM,sBAAsB,YAAa,MAAM,EAAE,KAAG,OAAO,GAAG,MAgBpE,CAAA;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;EAwBxD;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAE,QAAQ,EAAE,MAAM,WAExC;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAE,GAAG,EAAE,MAAM,WActC;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAE,OAAO,EAAE,aAAa,iBA6B9D;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,YAAY,CAAC,CA2BpE;AAED,wBAAgB,wBAAwB,CAAE,OAAO,EAAE,YAAY;;;;;EA+B9D;AAED,wBAAgB,eAAe,CAAE,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,EAAE,QAAQ,EAAE,MAAM,UAIhF"}
package/build/utils.js ADDED
@@ -0,0 +1,360 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getDefaultFiles = exports.getPathForFileGeneration = exports.getAnswers = exports.generateTestFiles = exports.hasPackage = exports.hasFile = exports.getCapabilities = exports.validateServiceAnswers = exports.renderConfigurationFile = exports.convertPackageHashToObject = exports.addServiceDeps = exports.replaceConfig = exports.findInConfig = exports.getRunnerName = exports.runOnCompleteHook = exports.runLauncherHook = exports.runServiceHook = exports.HookError = void 0;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const ejs_1 = __importDefault(require("ejs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const inquirer_1 = __importDefault(require("inquirer"));
11
+ const logger_1 = __importDefault(require("@wdio/logger"));
12
+ const recursive_readdir_1 = __importDefault(require("recursive-readdir"));
13
+ const webdriverio_1 = require("webdriverio");
14
+ const child_process_1 = require("child_process");
15
+ const util_1 = require("util");
16
+ const constants_1 = require("./constants");
17
+ const log = (0, logger_1.default)('@wdio/cli:utils');
18
+ const TEMPLATE_ROOT_DIR = path_1.default.join(__dirname, 'templates', 'exampleFiles');
19
+ const renderFile = (0, util_1.promisify)(ejs_1.default.renderFile);
20
+ class HookError extends webdriverio_1.SevereServiceError {
21
+ constructor(message, origin) {
22
+ super(message);
23
+ this.origin = origin;
24
+ }
25
+ }
26
+ exports.HookError = HookError;
27
+ /**
28
+ * run service launch sequences
29
+ */
30
+ async function runServiceHook(launcher, hookName, ...args) {
31
+ const start = Date.now();
32
+ return Promise.all(launcher.map(async (service) => {
33
+ try {
34
+ if (typeof service[hookName] === 'function') {
35
+ await service[hookName](...args);
36
+ }
37
+ }
38
+ catch (err) {
39
+ const message = `A service failed in the '${hookName}' hook\n${err.stack}\n\n`;
40
+ if (err instanceof webdriverio_1.SevereServiceError) {
41
+ return { status: 'rejected', reason: message, origin: hookName };
42
+ }
43
+ log.error(`${message}Continue...`);
44
+ }
45
+ })).then(results => {
46
+ if (launcher.length) {
47
+ log.debug(`Finished to run "${hookName}" hook in ${Date.now() - start}ms`);
48
+ }
49
+ const rejectedHooks = results.filter(p => p && p.status === 'rejected');
50
+ if (rejectedHooks.length) {
51
+ return Promise.reject(new HookError(`\n${rejectedHooks.map(p => p && p.reason).join()}\n\nStopping runner...`, hookName));
52
+ }
53
+ });
54
+ }
55
+ exports.runServiceHook = runServiceHook;
56
+ /**
57
+ * Run hook in service launcher
58
+ * @param {Array|Function} hook - can be array of functions or single function
59
+ * @param {Object} config
60
+ * @param {Object} capabilities
61
+ */
62
+ async function runLauncherHook(hook, ...args) {
63
+ if (typeof hook === 'function') {
64
+ hook = [hook];
65
+ }
66
+ const catchFn = (e) => {
67
+ log.error(`Error in hook: ${e.stack}`);
68
+ if (e instanceof webdriverio_1.SevereServiceError) {
69
+ throw new HookError(e.message, hook[0].name);
70
+ }
71
+ };
72
+ return Promise.all(hook.map((hook) => {
73
+ try {
74
+ return hook(...args);
75
+ }
76
+ catch (err) {
77
+ return catchFn(err);
78
+ }
79
+ })).catch(catchFn);
80
+ }
81
+ exports.runLauncherHook = runLauncherHook;
82
+ /**
83
+ * Run onCompleteHook in Launcher
84
+ * @param {Array|Function} onCompleteHook - can be array of functions or single function
85
+ * @param {*} config
86
+ * @param {*} capabilities
87
+ * @param {*} exitCode
88
+ * @param {*} results
89
+ */
90
+ async function runOnCompleteHook(onCompleteHook, config, capabilities, exitCode, results) {
91
+ if (typeof onCompleteHook === 'function') {
92
+ onCompleteHook = [onCompleteHook];
93
+ }
94
+ return Promise.all(onCompleteHook.map(async (hook) => {
95
+ try {
96
+ await hook(exitCode, config, capabilities, results);
97
+ return 0;
98
+ }
99
+ catch (err) {
100
+ log.error(`Error in onCompleteHook: ${err.stack}`);
101
+ if (err instanceof webdriverio_1.SevereServiceError) {
102
+ throw new HookError(err.message, 'onComplete');
103
+ }
104
+ return 1;
105
+ }
106
+ }));
107
+ }
108
+ exports.runOnCompleteHook = runOnCompleteHook;
109
+ /**
110
+ * get runner identification by caps
111
+ */
112
+ function getRunnerName(caps = {}) {
113
+ let runner = caps.browserName ||
114
+ caps.appPackage ||
115
+ caps.appWaitActivity ||
116
+ caps.app ||
117
+ caps.platformName ||
118
+ caps['appium:platformName'] ||
119
+ caps['appium:appPackage'] ||
120
+ caps['appium:appWaitActivity'] ||
121
+ caps['appium:app'];
122
+ // MultiRemote
123
+ if (!runner) {
124
+ runner = Object.values(caps).length === 0 || Object.values(caps).some(cap => !cap.capabilities) ? 'undefined' : 'MultiRemote';
125
+ }
126
+ return runner;
127
+ }
128
+ exports.getRunnerName = getRunnerName;
129
+ function buildNewConfigArray(str, type, change) {
130
+ var _a;
131
+ const newStr = str
132
+ .split(`${type}s: `)[1]
133
+ .replace(/'/g, '');
134
+ let newArray = ((_a = newStr.match(/(\w*)/gmi)) === null || _a === void 0 ? void 0 : _a.filter(e => !!e).concat([change])) || [];
135
+ return str
136
+ .replace('// ', '')
137
+ .replace(new RegExp(`(${type}s: )((.*\\s*)*)`), `$1[${newArray.map(e => `'${e}'`)}]`);
138
+ }
139
+ function buildNewConfigString(str, type, change) {
140
+ return str.replace(new RegExp(`(${type}: )('\\w*')`), `$1'${change}'`);
141
+ }
142
+ function findInConfig(config, type) {
143
+ let regexStr = `[\\/\\/]*[\\s]*${type}s: [\\s]*\\[([\\s]*['|"]\\w*['|"],*)*[\\s]*\\]`;
144
+ if (type === 'framework') {
145
+ regexStr = `[\\/\\/]*[\\s]*${type}: ([\\s]*['|"]\\w*['|"])`;
146
+ }
147
+ const regex = new RegExp(regexStr, 'gmi');
148
+ return config.match(regex);
149
+ }
150
+ exports.findInConfig = findInConfig;
151
+ function replaceConfig(config, type, name) {
152
+ if (type === 'framework') {
153
+ return buildNewConfigString(config, type, name);
154
+ }
155
+ const match = findInConfig(config, type);
156
+ if (!match || match.length === 0) {
157
+ return;
158
+ }
159
+ const text = match.pop() || '';
160
+ return config.replace(text, buildNewConfigArray(text, type, name));
161
+ }
162
+ exports.replaceConfig = replaceConfig;
163
+ function addServiceDeps(names, packages, update = false) {
164
+ /**
165
+ * automatically install latest Chromedriver if `wdio-chromedriver-service`
166
+ * was selected for install
167
+ */
168
+ if (names.some(({ short }) => short === 'chromedriver')) {
169
+ packages.push('chromedriver');
170
+ if (update) {
171
+ // eslint-disable-next-line no-console
172
+ console.log('\n=======', '\nPlease change path to / in your wdio.conf.js:', "\npath: '/'", '\n=======\n');
173
+ }
174
+ }
175
+ /**
176
+ * install Appium if it is not installed globally if `@wdio/appium-service`
177
+ * was selected for install
178
+ */
179
+ if (names.some(({ short }) => short === 'appium')) {
180
+ const result = (0, child_process_1.execSync)('appium --version || echo APPIUM_MISSING').toString().trim();
181
+ if (result === 'APPIUM_MISSING') {
182
+ packages.push('appium');
183
+ }
184
+ else if (update) {
185
+ // eslint-disable-next-line no-console
186
+ console.log('\n=======', '\nUsing globally installed appium', result, '\nPlease add the following to your wdio.conf.js:', "\nappium: { command: 'appium' }", '\n=======\n');
187
+ }
188
+ }
189
+ }
190
+ exports.addServiceDeps = addServiceDeps;
191
+ /**
192
+ * @todo add JSComments
193
+ */
194
+ function convertPackageHashToObject(pkg, hash = '$--$') {
195
+ const splitHash = pkg.split(hash);
196
+ return {
197
+ package: splitHash[0],
198
+ short: splitHash[1]
199
+ };
200
+ }
201
+ exports.convertPackageHashToObject = convertPackageHashToObject;
202
+ async function renderConfigurationFile(answers) {
203
+ const tplPath = path_1.default.join(__dirname, 'templates/wdio.conf.tpl.ejs');
204
+ const filename = `wdio.conf.${answers.isUsingTypeScript ? 'ts' : 'js'}`;
205
+ const renderedTpl = await renderFile(tplPath, { answers });
206
+ return fs_extra_1.default.promises.writeFile(path_1.default.join(process.cwd(), answers.isUsingTypeScript ? 'test' : '', filename), renderedTpl);
207
+ }
208
+ exports.renderConfigurationFile = renderConfigurationFile;
209
+ const validateServiceAnswers = (answers) => {
210
+ let result = true;
211
+ Object.entries(constants_1.EXCLUSIVE_SERVICES).forEach(([name, { services, message }]) => {
212
+ const exists = answers.some(answer => answer.includes(name));
213
+ const hasExclusive = services.some(service => answers.some(answer => answer.includes(service)));
214
+ if (exists && hasExclusive) {
215
+ result = `${name} cannot work together with ${services.join(', ')}\n${message}\nPlease uncheck one of them.`;
216
+ }
217
+ });
218
+ return result;
219
+ };
220
+ exports.validateServiceAnswers = validateServiceAnswers;
221
+ function getCapabilities(arg) {
222
+ const optionalCapabilites = {
223
+ platformVersion: arg.platformVersion,
224
+ udid: arg.udid,
225
+ ...(arg.deviceName && { deviceName: arg.deviceName })
226
+ };
227
+ /**
228
+ * Parsing of option property and constructing desiredCapabilities
229
+ * for Appium session. Could be application(1) or browser(2-3) session.
230
+ */
231
+ if (/.*\.(apk|app|ipa)$/.test(arg.option)) {
232
+ return {
233
+ capabilities: {
234
+ app: arg.option,
235
+ ...(arg.option.endsWith('apk') ? constants_1.ANDROID_CONFIG : constants_1.IOS_CONFIG),
236
+ ...optionalCapabilites,
237
+ }
238
+ };
239
+ }
240
+ else if (/android/.test(arg.option)) {
241
+ return { capabilities: { browserName: 'Chrome', ...constants_1.ANDROID_CONFIG, ...optionalCapabilites } };
242
+ }
243
+ else if (/ios/.test(arg.option)) {
244
+ return { capabilities: { browserName: 'Safari', ...constants_1.IOS_CONFIG, ...optionalCapabilites } };
245
+ }
246
+ return { capabilities: { browserName: arg.option } };
247
+ }
248
+ exports.getCapabilities = getCapabilities;
249
+ /**
250
+ * Check if file exists in current work directory
251
+ * @param {string} filename to check existance for
252
+ */
253
+ function hasFile(filename) {
254
+ return fs_extra_1.default.existsSync(path_1.default.join(process.cwd(), filename));
255
+ }
256
+ exports.hasFile = hasFile;
257
+ /**
258
+ * Check if package is installed
259
+ * @param {string} package to check existance for
260
+ */
261
+ function hasPackage(pkg) {
262
+ try {
263
+ /**
264
+ * this is only for testing purposes as we want to check whether
265
+ * we add `@babel/register` to the packages to install when resolving fails
266
+ */
267
+ if (process.env.JEST_WORKER_ID && process.env.WDIO_TEST_THROW_RESOLVE) {
268
+ throw new Error('resolve error');
269
+ }
270
+ require.resolve(pkg);
271
+ return true;
272
+ }
273
+ catch (err) {
274
+ return false;
275
+ }
276
+ }
277
+ exports.hasPackage = hasPackage;
278
+ /**
279
+ * generate test files based on CLI answers
280
+ */
281
+ async function generateTestFiles(answers) {
282
+ const testFiles = answers.framework === 'cucumber'
283
+ ? [path_1.default.join(TEMPLATE_ROOT_DIR, 'cucumber')]
284
+ : (answers.framework === 'mocha'
285
+ ? [path_1.default.join(TEMPLATE_ROOT_DIR, 'mocha')]
286
+ : [path_1.default.join(TEMPLATE_ROOT_DIR, 'jasmine')]);
287
+ if (answers.usePageObjects) {
288
+ testFiles.push(path_1.default.join(TEMPLATE_ROOT_DIR, 'pageobjects'));
289
+ }
290
+ const files = (await Promise.all(testFiles.map((dirPath) => (0, recursive_readdir_1.default)(dirPath, [(file, stats) => !stats.isDirectory() && !(file.endsWith('.ejs') || file.endsWith('.feature'))])))).reduce((cur, acc) => [...acc, ...(cur)], []);
291
+ for (const file of files) {
292
+ const renderedTpl = await renderFile(file, answers);
293
+ let destPath = (file.endsWith('page.js.ejs')
294
+ ? `${answers.destPageObjectRootPath}/${path_1.default.basename(file)}`
295
+ : file.includes('step_definition')
296
+ ? `${answers.stepDefinitions}`
297
+ : `${answers.destSpecRootPath}/${path_1.default.basename(file)}`).replace(/\.ejs$/, '').replace(/\.js$/, answers.isUsingTypeScript ? '.ts' : '.js');
298
+ fs_extra_1.default.ensureDirSync(path_1.default.dirname(destPath));
299
+ await fs_extra_1.default.promises.writeFile(destPath, renderedTpl);
300
+ }
301
+ }
302
+ exports.generateTestFiles = generateTestFiles;
303
+ async function getAnswers(yes) {
304
+ return yes
305
+ ? constants_1.QUESTIONNAIRE.reduce((answers, question) => Object.assign(answers, question.when && !question.when(answers)
306
+ /**
307
+ * set nothing if question doesn't apply
308
+ */
309
+ ? {}
310
+ : { [question.name]: typeof question.default !== 'undefined'
311
+ /**
312
+ * set default value if existing
313
+ */
314
+ ? typeof question.default === 'function'
315
+ ? question.default(answers)
316
+ : question.default
317
+ : question.choices && question.choices.length
318
+ /**
319
+ * pick first choice, select value if it exists
320
+ */
321
+ ? question.choices[0].value
322
+ ? question.choices[0].value
323
+ : question.choices[0]
324
+ : {}
325
+ }), {})
326
+ : await inquirer_1.default.prompt(constants_1.QUESTIONNAIRE);
327
+ }
328
+ exports.getAnswers = getAnswers;
329
+ function getPathForFileGeneration(answers) {
330
+ const destSpecRootPath = path_1.default.join(process.cwd(), path_1.default.dirname(answers.specs || '').replace(/\*\*$/, ''));
331
+ const destStepRootPath = path_1.default.join(process.cwd(), path_1.default.dirname(answers.stepDefinitions || ''));
332
+ const destPageObjectRootPath = answers.usePageObjects
333
+ ? path_1.default.join(process.cwd(), path_1.default.dirname(answers.pages || '').replace(/\*\*$/, ''))
334
+ : '';
335
+ let relativePath = (answers.generateTestFiles && answers.usePageObjects)
336
+ ? !(convertPackageHashToObject(answers.framework).short === 'cucumber')
337
+ ? path_1.default.relative(destSpecRootPath, destPageObjectRootPath)
338
+ : path_1.default.relative(destStepRootPath, destPageObjectRootPath)
339
+ : '';
340
+ /**
341
+ * On Windows, path.relative can return backslashes that could be interpreted as espace sequences in strings
342
+ */
343
+ if (process.platform === 'win32') {
344
+ relativePath = relativePath.replace(/\\/g, '/');
345
+ }
346
+ return {
347
+ destSpecRootPath: destSpecRootPath,
348
+ destStepRootPath: destStepRootPath,
349
+ destPageObjectRootPath: destPageObjectRootPath,
350
+ relativePath: relativePath
351
+ };
352
+ }
353
+ exports.getPathForFileGeneration = getPathForFileGeneration;
354
+ function getDefaultFiles(answers, filePath) {
355
+ var _a;
356
+ return ((_a = answers === null || answers === void 0 ? void 0 : answers.isUsingCompiler) === null || _a === void 0 ? void 0 : _a.toString().includes('TypeScript'))
357
+ ? `${filePath}.ts`
358
+ : `${filePath}.js`;
359
+ }
360
+ exports.getDefaultFiles = getDefaultFiles;
@@ -0,0 +1,34 @@
1
+ import type { Workers } from '@wdio/types';
2
+ import { RunCommandArguments, ValueKeyIteratee } from './types.js';
3
+ declare type Spec = string | string[];
4
+ export default class Watcher {
5
+ private _configFile;
6
+ private _args;
7
+ private _launcher;
8
+ private _specs;
9
+ constructor(_configFile: string, _args: Omit<RunCommandArguments, 'configPath'>);
10
+ watch(): Promise<void>;
11
+ /**
12
+ * return file listener callback that calls `run` method
13
+ * @param {Boolean} [passOnFile=true] if true pass on file change as parameter
14
+ * @return {Function} chokidar event callback
15
+ */
16
+ getFileListener(passOnFile?: boolean): (spec: string) => void[];
17
+ /**
18
+ * helper method to get workers from worker pool of wdio runner
19
+ * @param predicate filter by property value (see lodash.pickBy)
20
+ * @param includeBusyWorker don't filter out busy worker (default: false)
21
+ * @return Object with workers, e.g. {'0-0': { ... }}
22
+ */
23
+ getWorkers(predicate?: ValueKeyIteratee<Workers.Worker> | null | undefined, includeBusyWorker?: boolean): Workers.WorkerPool;
24
+ /**
25
+ * run workers with params
26
+ * @param params parameters to run the worker with
27
+ */
28
+ run(params?: Omit<Partial<RunCommandArguments>, 'spec'> & {
29
+ spec?: Spec;
30
+ }): void;
31
+ cleanUp(): void;
32
+ }
33
+ export {};
34
+ //# sourceMappingURL=watcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watcher.d.ts","sourceRoot":"","sources":["../src/watcher.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAgB,OAAO,EAAE,MAAM,aAAa,CAAA;AACxD,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAIlE,aAAK,IAAI,GAAG,MAAM,GAAG,MAAM,EAAE,CAAA;AAC7B,MAAM,CAAC,OAAO,OAAO,OAAO;IAKpB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,KAAK;IALjB,OAAO,CAAC,SAAS,CAAU;IAC3B,OAAO,CAAC,MAAM,CAAQ;gBAGV,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,IAAI,CAAC,mBAAmB,EAAE,YAAY,CAAC;IAYpD,KAAK;IAwCX;;;;OAIG;IACH,eAAe,CAAE,UAAU,UAAO,UAChB,MAAM;IA6BxB;;;;;OAKG;IACH,UAAU,CAAE,SAAS,CAAC,EAAE,gBAAgB,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,IAAI,GAAG,SAAS,EAAE,iBAAiB,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU;IAiB7H;;;OAGG;IACH,GAAG,CAAE,MAAM,GAAE,IAAI,CAAC,OAAO,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC,GAAG;QAAE,IAAI,CAAC,EAAE,IAAI,CAAA;KAAO;IAuC9E,OAAO;CAGV"}
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const chokidar_1 = __importDefault(require("chokidar"));
7
+ const logger_1 = __importDefault(require("@wdio/logger"));
8
+ const lodash_pickby_1 = __importDefault(require("lodash.pickby"));
9
+ const lodash_flattendeep_1 = __importDefault(require("lodash.flattendeep"));
10
+ const lodash_union_1 = __importDefault(require("lodash.union"));
11
+ const launcher_1 = __importDefault(require("./launcher"));
12
+ const log = (0, logger_1.default)('@wdio/cli:watch');
13
+ class Watcher {
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_1.default(this._configFile, this._args, true);
19
+ const specs = this._launcher.configParser.getSpecs();
20
+ const capSpecs = this._launcher.isMultiremote ? [] : (0, lodash_union_1.default)((0, lodash_flattendeep_1.default)(this._launcher.configParser.getCapabilities().map(cap => cap.specs || [])));
21
+ this._specs = [...specs, ...capSpecs];
22
+ }
23
+ async watch() {
24
+ /**
25
+ * listen on spec changes and rerun specific spec file
26
+ */
27
+ let flattenedSpecs = (0, lodash_flattendeep_1.default)(this._specs);
28
+ chokidar_1.default.watch(flattenedSpecs, { ignoreInitial: true })
29
+ .on('add', this.getFileListener())
30
+ .on('change', this.getFileListener());
31
+ /**
32
+ * listen on filesToWatch changes an rerun complete suite
33
+ */
34
+ const { filesToWatch } = this._launcher.configParser.getConfig();
35
+ if (filesToWatch.length) {
36
+ chokidar_1.default.watch(filesToWatch, { ignoreInitial: true })
37
+ .on('add', this.getFileListener(false))
38
+ .on('change', this.getFileListener(false));
39
+ }
40
+ /**
41
+ * run initial test suite
42
+ */
43
+ await this._launcher.run();
44
+ /**
45
+ * clean interface once all worker finish
46
+ */
47
+ const workers = this.getWorkers();
48
+ Object.values(workers).forEach((worker) => worker.on('exit', () => {
49
+ /**
50
+ * check if all workers have finished
51
+ */
52
+ if (Object.values(workers).find((w) => w.isBusy)) {
53
+ return;
54
+ }
55
+ this._launcher.interface.finalise();
56
+ }));
57
+ }
58
+ /**
59
+ * return file listener callback that calls `run` method
60
+ * @param {Boolean} [passOnFile=true] if true pass on file change as parameter
61
+ * @return {Function} chokidar event callback
62
+ */
63
+ getFileListener(passOnFile = true) {
64
+ return (spec) => {
65
+ const runSpecs = [];
66
+ let singleSpecFound = false;
67
+ for (let index = 0, length = this._specs.length; index < length; index += 1) {
68
+ const value = this._specs[index];
69
+ if (Array.isArray(value) && value.indexOf(spec) > -1) {
70
+ runSpecs.push(value);
71
+ }
72
+ else if (!singleSpecFound && spec === value) {
73
+ // Only need to run a singleFile once - so avoid duplicates
74
+ singleSpecFound = true;
75
+ runSpecs.push(value);
76
+ }
77
+ }
78
+ // If the runSpecs array is empty, then this must be a new file/array
79
+ // so add the spec directly to the runSpecs
80
+ if (runSpecs.length === 0) {
81
+ runSpecs.push(spec);
82
+ }
83
+ // Do not pass the `spec` command line option to `this.run()`
84
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
85
+ const { spec: _specArg, ...args } = this._args;
86
+ return runSpecs.map((spec) => {
87
+ return this.run({ ...args, ...(passOnFile ? { spec } : {}) });
88
+ });
89
+ };
90
+ }
91
+ /**
92
+ * helper method to get workers from worker pool of wdio runner
93
+ * @param predicate filter by property value (see lodash.pickBy)
94
+ * @param includeBusyWorker don't filter out busy worker (default: false)
95
+ * @return Object with workers, e.g. {'0-0': { ... }}
96
+ */
97
+ getWorkers(predicate, includeBusyWorker) {
98
+ let workers = this._launcher.runner.workerPool;
99
+ if (typeof predicate === 'function') {
100
+ workers = (0, lodash_pickby_1.default)(workers, predicate);
101
+ }
102
+ /**
103
+ * filter out busy workers, only skip if explicitly desired
104
+ */
105
+ if (!includeBusyWorker) {
106
+ workers = (0, lodash_pickby_1.default)(workers, (worker) => !worker.isBusy);
107
+ }
108
+ return workers;
109
+ }
110
+ /**
111
+ * run workers with params
112
+ * @param params parameters to run the worker with
113
+ */
114
+ run(params = {}) {
115
+ const workers = this.getWorkers((params.spec ? (worker) => {
116
+ if (Array.isArray(params.spec)) {
117
+ return params.spec === worker.specs;
118
+ }
119
+ return worker.specs.includes(params.spec);
120
+ } : undefined));
121
+ /**
122
+ * don't do anything if no worker was found
123
+ */
124
+ if (Object.keys(workers).length === 0) {
125
+ return;
126
+ }
127
+ /**
128
+ * update total worker count interface
129
+ * ToDo: this should have a cleaner solution
130
+ */
131
+ this._launcher.interface.totalWorkerCnt = Object.entries(workers).length;
132
+ /**
133
+ * clean up interface
134
+ */
135
+ this.cleanUp();
136
+ /**
137
+ * trigger new run for non busy worker
138
+ */
139
+ for (const [, worker] of Object.entries(workers)) {
140
+ const { cid, caps, specs, sessionId } = worker;
141
+ const args = Object.assign({ sessionId }, params);
142
+ worker.postMessage('run', args);
143
+ this._launcher.interface.emit('job:start', { cid, caps, specs });
144
+ }
145
+ }
146
+ cleanUp() {
147
+ this._launcher.interface.setup();
148
+ }
149
+ }
150
+ exports.default = Watcher;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wdio/cli",
3
- "version": "7.17.0",
3
+ "version": "7.18.0",
4
4
  "description": "WebdriverIO testrunner command line interface",
5
5
  "author": "Christian Bromann <christian@saucelabs.com>",
6
6
  "homepage": "https://github.com/webdriverio/webdriverio/tree/main/packages/wdio-cli",
@@ -37,10 +37,10 @@
37
37
  "@types/lodash.union": "^4.6.6",
38
38
  "@types/node": "^17.0.4",
39
39
  "@types/recursive-readdir": "^2.2.0",
40
- "@wdio/config": "7.16.16",
41
- "@wdio/logger": "7.16.0",
42
- "@wdio/types": "7.16.14",
43
- "@wdio/utils": "7.17.0",
40
+ "@wdio/config": "7.18.0",
41
+ "@wdio/logger": "7.17.3",
42
+ "@wdio/types": "7.18.0",
43
+ "@wdio/utils": "7.18.0",
44
44
  "async-exit-hook": "^2.0.1",
45
45
  "chalk": "^4.0.0",
46
46
  "chokidar": "^3.0.0",
@@ -53,7 +53,7 @@
53
53
  "lodash.union": "^4.6.0",
54
54
  "mkdirp": "^1.0.4",
55
55
  "recursive-readdir": "^2.2.2",
56
- "webdriverio": "7.17.0",
56
+ "webdriverio": "7.18.0",
57
57
  "yargs": "^17.0.0",
58
58
  "yarn-install": "^1.0.0"
59
59
  },
@@ -61,5 +61,5 @@
61
61
  "access": "public"
62
62
  },
63
63
  "types": "./build/index.d.ts",
64
- "gitHead": "e18d2cde6ff979758830bdff4c3bc82ca9818b24"
64
+ "gitHead": "44729cdd585af3ad69f2093f32377f297f5ac344"
65
65
  }