@wdio/cli 7.17.0 → 7.17.3
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.
- package/build/@types/yarn-install.d.ts +16 -0
- package/build/@types/yarn-install.d.ts.map +1 -0
- package/build/@types/yarn-install.js +1 -0
- package/build/commands/config.d.ts +64 -0
- package/build/commands/config.d.ts.map +1 -0
- package/build/commands/config.js +236 -0
- package/build/commands/install.d.ts +18 -0
- package/build/commands/install.d.ts.map +1 -0
- package/build/commands/install.js +112 -0
- package/build/commands/repl.d.ts +36 -0
- package/build/commands/repl.d.ts.map +1 -0
- package/build/commands/repl.js +62 -0
- package/build/commands/run.d.ts +172 -0
- package/build/commands/run.d.ts.map +1 -0
- package/build/commands/run.js +172 -0
- package/build/constants.d.ts +485 -0
- package/build/constants.d.ts.map +1 -0
- package/build/constants.js +370 -0
- package/build/index.d.ts +5 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +88 -0
- package/build/interface.d.ts +75 -0
- package/build/interface.d.ts.map +1 -0
- package/build/interface.js +245 -0
- package/build/launcher.d.ts +95 -0
- package/build/launcher.d.ts.map +1 -0
- package/build/launcher.js +418 -0
- package/build/templates/afterTest.ejs +20 -0
- package/build/templates/exampleFiles/cucumber/features/login.feature +12 -0
- package/build/templates/exampleFiles/cucumber/step_definitions/steps.js.ejs +52 -0
- package/build/templates/exampleFiles/jasmine/example.e2e.js.ejs +42 -0
- package/build/templates/exampleFiles/mocha/example.e2e.js.ejs +42 -0
- package/build/templates/exampleFiles/pageobjects/login.page.js.ejs +44 -0
- package/build/templates/exampleFiles/pageobjects/page.js.ejs +13 -0
- package/build/templates/exampleFiles/pageobjects/secure.page.js.ejs +19 -0
- package/build/templates/reporters.ejs +14 -0
- package/build/templates/wdio.conf.tpl.ejs +448 -0
- package/build/types.d.ts +110 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/utils.d.ts +82 -0
- package/build/utils.d.ts.map +1 -0
- package/build/utils.js +345 -0
- package/build/watcher.d.ts +34 -0
- package/build/watcher.d.ts.map +1 -0
- package/build/watcher.js +150 -0
- package/package.json +7 -7
|
@@ -0,0 +1,418 @@
|
|
|
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 path_1 = __importDefault(require("path"));
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const async_exit_hook_1 = __importDefault(require("async-exit-hook"));
|
|
9
|
+
const logger_1 = __importDefault(require("@wdio/logger"));
|
|
10
|
+
const config_1 = require("@wdio/config");
|
|
11
|
+
const utils_1 = require("@wdio/utils");
|
|
12
|
+
const interface_1 = __importDefault(require("./interface"));
|
|
13
|
+
const utils_2 = require("./utils");
|
|
14
|
+
const log = (0, logger_1.default)('@wdio/cli:launcher');
|
|
15
|
+
class Launcher {
|
|
16
|
+
constructor(_configFilePath, _args = {}, _isWatchMode = false) {
|
|
17
|
+
this._configFilePath = _configFilePath;
|
|
18
|
+
this._args = _args;
|
|
19
|
+
this._isWatchMode = _isWatchMode;
|
|
20
|
+
this._exitCode = 0;
|
|
21
|
+
this._hasTriggeredExitRoutine = false;
|
|
22
|
+
this._schedule = [];
|
|
23
|
+
this._rid = [];
|
|
24
|
+
this._runnerStarted = 0;
|
|
25
|
+
this._runnerFailed = 0;
|
|
26
|
+
this.configParser = new config_1.ConfigParser();
|
|
27
|
+
/**
|
|
28
|
+
* merge auto compile opts to understand how to parse the config
|
|
29
|
+
*/
|
|
30
|
+
if (_args.autoCompileOpts) {
|
|
31
|
+
this.configParser.merge({ autoCompileOpts: _args.autoCompileOpts });
|
|
32
|
+
}
|
|
33
|
+
this.configParser.autoCompile();
|
|
34
|
+
this.configParser.addConfigFile(_configFilePath);
|
|
35
|
+
this.configParser.merge(_args);
|
|
36
|
+
const config = this.configParser.getConfig();
|
|
37
|
+
/**
|
|
38
|
+
* assign parsed autocompile options into args so it can be used within the worker
|
|
39
|
+
* without having to read the config again
|
|
40
|
+
*/
|
|
41
|
+
this._args.autoCompileOpts = config.autoCompileOpts;
|
|
42
|
+
const capabilities = this.configParser.getCapabilities();
|
|
43
|
+
this.isMultiremote = !Array.isArray(capabilities);
|
|
44
|
+
if (config.outputDir) {
|
|
45
|
+
fs_extra_1.default.ensureDirSync(path_1.default.join(config.outputDir));
|
|
46
|
+
process.env.WDIO_LOG_PATH = path_1.default.join(config.outputDir, 'wdio.log');
|
|
47
|
+
}
|
|
48
|
+
logger_1.default.setLogLevelsConfig(config.logLevels, config.logLevel);
|
|
49
|
+
const totalWorkerCnt = Array.isArray(capabilities)
|
|
50
|
+
? capabilities
|
|
51
|
+
.map((c) => this.configParser.getSpecs(c.specs, c.exclude).length)
|
|
52
|
+
.reduce((a, b) => a + b, 0)
|
|
53
|
+
: 1;
|
|
54
|
+
const Runner = (0, utils_1.initialisePlugin)(config.runner, 'runner').default;
|
|
55
|
+
this.runner = new Runner(_configFilePath, config);
|
|
56
|
+
this.interface = new interface_1.default(config, totalWorkerCnt, this._isWatchMode);
|
|
57
|
+
config.runnerEnv.FORCE_COLOR = Number(this.interface.hasAnsiSupport);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* run sequence
|
|
61
|
+
* @return {Promise} that only gets resolves with either an exitCode or an error
|
|
62
|
+
*/
|
|
63
|
+
async run() {
|
|
64
|
+
/**
|
|
65
|
+
* catches ctrl+c event
|
|
66
|
+
*/
|
|
67
|
+
(0, async_exit_hook_1.default)(this.exitHandler.bind(this));
|
|
68
|
+
let exitCode = 0;
|
|
69
|
+
let error = undefined;
|
|
70
|
+
try {
|
|
71
|
+
const config = this.configParser.getConfig();
|
|
72
|
+
const caps = this.configParser.getCapabilities();
|
|
73
|
+
const { ignoredWorkerServices, launcherServices } = (0, utils_1.initialiseLauncherService)(config, caps);
|
|
74
|
+
this._launcher = launcherServices;
|
|
75
|
+
this._args.ignoredWorkerServices = ignoredWorkerServices;
|
|
76
|
+
/**
|
|
77
|
+
* run pre test tasks for runner plugins
|
|
78
|
+
* (e.g. deploy Lambda function to AWS)
|
|
79
|
+
*/
|
|
80
|
+
await this.runner.initialise();
|
|
81
|
+
/**
|
|
82
|
+
* run onPrepare hook
|
|
83
|
+
*/
|
|
84
|
+
log.info('Run onPrepare hook');
|
|
85
|
+
await (0, utils_2.runLauncherHook)(config.onPrepare, config, caps);
|
|
86
|
+
await (0, utils_2.runServiceHook)(this._launcher, 'onPrepare', config, caps);
|
|
87
|
+
exitCode = await this.runMode(config, caps);
|
|
88
|
+
/**
|
|
89
|
+
* run onComplete hook
|
|
90
|
+
* even if it fails we still want to see result and end logger stream
|
|
91
|
+
*/
|
|
92
|
+
log.info('Run onComplete hook');
|
|
93
|
+
await (0, utils_2.runServiceHook)(this._launcher, 'onComplete', exitCode, config, caps);
|
|
94
|
+
const onCompleteResults = await (0, utils_2.runOnCompleteHook)(config.onComplete, config, caps, exitCode, this.interface.result);
|
|
95
|
+
// if any of the onComplete hooks failed, update the exit code
|
|
96
|
+
exitCode = onCompleteResults.includes(1) ? 1 : exitCode;
|
|
97
|
+
await logger_1.default.waitForBuffer();
|
|
98
|
+
this.interface.finalise();
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
error = err;
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
if (!this._hasTriggeredExitRoutine) {
|
|
105
|
+
this._hasTriggeredExitRoutine = true;
|
|
106
|
+
await this.runner.shutdown();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (error) {
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
return exitCode;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* run without triggering onPrepare/onComplete hooks
|
|
116
|
+
*/
|
|
117
|
+
runMode(config, caps) {
|
|
118
|
+
/**
|
|
119
|
+
* fail if no caps were found
|
|
120
|
+
*/
|
|
121
|
+
if (!caps) {
|
|
122
|
+
return new Promise((resolve) => {
|
|
123
|
+
log.error('Missing capabilities, exiting with failure');
|
|
124
|
+
return resolve(1);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* avoid retries in watch mode
|
|
129
|
+
*/
|
|
130
|
+
const specFileRetries = this._isWatchMode ? 0 : config.specFileRetries;
|
|
131
|
+
/**
|
|
132
|
+
* schedule test runs
|
|
133
|
+
*/
|
|
134
|
+
let cid = 0;
|
|
135
|
+
if (this.isMultiremote) {
|
|
136
|
+
/**
|
|
137
|
+
* Multiremote mode
|
|
138
|
+
*/
|
|
139
|
+
this._schedule.push({
|
|
140
|
+
cid: cid++,
|
|
141
|
+
caps: caps,
|
|
142
|
+
specs: this.formatSpecs(caps, specFileRetries),
|
|
143
|
+
availableInstances: config.maxInstances || 1,
|
|
144
|
+
runningInstances: 0
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
/**
|
|
149
|
+
* Regular mode
|
|
150
|
+
*/
|
|
151
|
+
for (let capabilities of caps) {
|
|
152
|
+
this._schedule.push({
|
|
153
|
+
cid: cid++,
|
|
154
|
+
caps: capabilities,
|
|
155
|
+
specs: this.formatSpecs(capabilities, specFileRetries),
|
|
156
|
+
availableInstances: capabilities.maxInstances || config.maxInstancesPerCapability,
|
|
157
|
+
runningInstances: 0
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return new Promise((resolve) => {
|
|
162
|
+
this._resolve = resolve;
|
|
163
|
+
/**
|
|
164
|
+
* fail if no specs were found or specified
|
|
165
|
+
*/
|
|
166
|
+
if (Object.values(this._schedule).reduce((specCnt, schedule) => specCnt + schedule.specs.length, 0) === 0) {
|
|
167
|
+
log.error('No specs found to run, exiting with failure');
|
|
168
|
+
return resolve(1);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* return immediately if no spec was run
|
|
172
|
+
*/
|
|
173
|
+
if (this.runSpecs()) {
|
|
174
|
+
resolve(0);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Format the specs into an array of objects with files and retries
|
|
180
|
+
*/
|
|
181
|
+
formatSpecs(capabilities, specFileRetries) {
|
|
182
|
+
let files = [];
|
|
183
|
+
files = this.configParser.getSpecs(capabilities.specs, capabilities.exclude);
|
|
184
|
+
return files.map(file => {
|
|
185
|
+
if (typeof file === 'string') {
|
|
186
|
+
return { files: [file], retries: specFileRetries };
|
|
187
|
+
}
|
|
188
|
+
else if (Array.isArray(file)) {
|
|
189
|
+
return { files: file, retries: specFileRetries };
|
|
190
|
+
}
|
|
191
|
+
log.warn('Unexpected entry in specs that is neither string nor array: ', file);
|
|
192
|
+
// Returning an empty structure to avoid undefined
|
|
193
|
+
return { files: [], retries: specFileRetries };
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* run multiple single remote tests
|
|
198
|
+
* @return {Boolean} true if all specs have been run and all instances have finished
|
|
199
|
+
*/
|
|
200
|
+
runSpecs() {
|
|
201
|
+
let config = this.configParser.getConfig();
|
|
202
|
+
/**
|
|
203
|
+
* stop spawning new processes when CTRL+C was triggered
|
|
204
|
+
*/
|
|
205
|
+
if (this._hasTriggeredExitRoutine) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
while (this.getNumberOfRunningInstances() < config.maxInstances) {
|
|
209
|
+
let schedulableCaps = this._schedule
|
|
210
|
+
/**
|
|
211
|
+
* bail if number of errors exceeds allowed
|
|
212
|
+
*/
|
|
213
|
+
.filter(() => {
|
|
214
|
+
const filter = typeof config.bail !== 'number' || config.bail < 1 ||
|
|
215
|
+
config.bail > this._runnerFailed;
|
|
216
|
+
/**
|
|
217
|
+
* clear number of specs when filter is false
|
|
218
|
+
*/
|
|
219
|
+
if (!filter) {
|
|
220
|
+
this._schedule.forEach((t) => { t.specs = []; });
|
|
221
|
+
}
|
|
222
|
+
return filter;
|
|
223
|
+
})
|
|
224
|
+
/**
|
|
225
|
+
* make sure complete number of running instances is not higher than general maxInstances number
|
|
226
|
+
*/
|
|
227
|
+
.filter(() => this.getNumberOfRunningInstances() < config.maxInstances)
|
|
228
|
+
/**
|
|
229
|
+
* make sure the capability has available capacities
|
|
230
|
+
*/
|
|
231
|
+
.filter((a) => a.availableInstances > 0)
|
|
232
|
+
/**
|
|
233
|
+
* make sure capability has still caps to run
|
|
234
|
+
*/
|
|
235
|
+
.filter((a) => a.specs.length > 0)
|
|
236
|
+
/**
|
|
237
|
+
* make sure we are running caps with less running instances first
|
|
238
|
+
*/
|
|
239
|
+
.sort((a, b) => a.runningInstances - b.runningInstances);
|
|
240
|
+
/**
|
|
241
|
+
* continue if no capability were schedulable
|
|
242
|
+
*/
|
|
243
|
+
if (schedulableCaps.length === 0) {
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
let specs = schedulableCaps[0].specs.shift();
|
|
247
|
+
this.startInstance(specs.files, schedulableCaps[0].caps, schedulableCaps[0].cid, specs.rid, specs.retries);
|
|
248
|
+
schedulableCaps[0].availableInstances--;
|
|
249
|
+
schedulableCaps[0].runningInstances++;
|
|
250
|
+
}
|
|
251
|
+
return this.getNumberOfRunningInstances() === 0 && this.getNumberOfSpecsLeft() === 0;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* gets number of all running instances
|
|
255
|
+
* @return {number} number of running instances
|
|
256
|
+
*/
|
|
257
|
+
getNumberOfRunningInstances() {
|
|
258
|
+
return this._schedule.map((a) => a.runningInstances).reduce((a, b) => a + b);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* get number of total specs left to complete whole suites
|
|
262
|
+
* @return {number} specs left to complete suite
|
|
263
|
+
*/
|
|
264
|
+
getNumberOfSpecsLeft() {
|
|
265
|
+
return this._schedule.map((a) => a.specs.length).reduce((a, b) => a + b);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Start instance in a child process.
|
|
269
|
+
* @param {Array} specs Specs to run
|
|
270
|
+
* @param {Number} cid Capabilities ID
|
|
271
|
+
* @param {String} rid Runner ID override
|
|
272
|
+
* @param {Number} retries Number of retries remaining
|
|
273
|
+
*/
|
|
274
|
+
async startInstance(specs, caps, cid, rid, retries) {
|
|
275
|
+
let config = this.configParser.getConfig();
|
|
276
|
+
// wait before retrying the spec file
|
|
277
|
+
if (typeof config.specFileRetriesDelay === 'number' && config.specFileRetries > 0 && config.specFileRetries !== retries) {
|
|
278
|
+
await (0, utils_1.sleep)(config.specFileRetriesDelay * 1000);
|
|
279
|
+
}
|
|
280
|
+
// Retried tests receive the cid of the failing test as rid
|
|
281
|
+
// so they can run with the same cid of the failing test.
|
|
282
|
+
const runnerId = rid || this.getRunnerId(cid);
|
|
283
|
+
let processNumber = this._runnerStarted + 1;
|
|
284
|
+
// process.debugPort defaults to 5858 and is set even when process
|
|
285
|
+
// is not being debugged.
|
|
286
|
+
let debugArgs = [];
|
|
287
|
+
let debugType;
|
|
288
|
+
let debugHost = '';
|
|
289
|
+
let debugPort = process.debugPort;
|
|
290
|
+
for (let i in process.execArgv) {
|
|
291
|
+
const debugArgs = process.execArgv[i].match('--(debug|inspect)(?:-brk)?(?:=(.*):)?');
|
|
292
|
+
if (debugArgs) {
|
|
293
|
+
let [, type, host] = debugArgs;
|
|
294
|
+
if (type) {
|
|
295
|
+
debugType = type;
|
|
296
|
+
}
|
|
297
|
+
if (host) {
|
|
298
|
+
debugHost = `${host}:`;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (debugType) {
|
|
303
|
+
debugArgs.push(`--${debugType}=${debugHost}${(debugPort + processNumber)}`);
|
|
304
|
+
}
|
|
305
|
+
// if you would like to add --debug-brk, use a different port, etc...
|
|
306
|
+
let capExecArgs = [...(config.execArgv || [])];
|
|
307
|
+
// The default value for child.fork execArgs is process.execArgs,
|
|
308
|
+
// so continue to use this unless another value is specified in config.
|
|
309
|
+
let defaultArgs = (capExecArgs.length) ? process.execArgv : [];
|
|
310
|
+
// If an arg appears multiple times the last occurrence is used
|
|
311
|
+
let execArgv = [...defaultArgs, ...debugArgs, ...capExecArgs];
|
|
312
|
+
// set '--no-wasm-code-gc' deliberatively as it causes problems with
|
|
313
|
+
// @wdio/sync and recent TypeScript compiles
|
|
314
|
+
if (!execArgv.includes('--no-wasm-code-gc')) {
|
|
315
|
+
execArgv.push('--no-wasm-code-gc');
|
|
316
|
+
}
|
|
317
|
+
// bump up worker count
|
|
318
|
+
this._runnerStarted++;
|
|
319
|
+
// run worker hook to allow modify runtime and capabilities of a specific worker
|
|
320
|
+
log.info('Run onWorkerStart hook');
|
|
321
|
+
await (0, utils_2.runLauncherHook)(config.onWorkerStart, runnerId, caps, specs, this._args, execArgv);
|
|
322
|
+
await (0, utils_2.runServiceHook)(this._launcher, 'onWorkerStart', runnerId, caps, specs, this._args, execArgv);
|
|
323
|
+
// prefer launcher settings in capabilities over general launcher
|
|
324
|
+
const worker = this.runner.run({
|
|
325
|
+
cid: runnerId,
|
|
326
|
+
command: 'run',
|
|
327
|
+
configFile: this._configFilePath,
|
|
328
|
+
args: { ...this._args, ...((config === null || config === void 0 ? void 0 : config.autoCompileOpts) ? { autoCompileOpts: config.autoCompileOpts } : {}) },
|
|
329
|
+
caps,
|
|
330
|
+
specs,
|
|
331
|
+
execArgv,
|
|
332
|
+
retries
|
|
333
|
+
});
|
|
334
|
+
worker.on('message', this.interface.onMessage.bind(this.interface));
|
|
335
|
+
worker.on('error', this.interface.onMessage.bind(this.interface));
|
|
336
|
+
worker.on('exit', this.endHandler.bind(this));
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* generates a runner id
|
|
340
|
+
* @param {Number} cid capability id (unique identifier for a capability)
|
|
341
|
+
* @return {String} runner id (combination of cid and test id e.g. 0a, 0b, 1a, 1b ...)
|
|
342
|
+
*/
|
|
343
|
+
getRunnerId(cid) {
|
|
344
|
+
if (!this._rid[cid]) {
|
|
345
|
+
this._rid[cid] = 0;
|
|
346
|
+
}
|
|
347
|
+
return `${cid}-${this._rid[cid]++}`;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Close test runner process once all child processes have exited
|
|
351
|
+
* @param {Number} cid Capabilities ID
|
|
352
|
+
* @param {Number} exitCode exit code of child process
|
|
353
|
+
* @param {Array} specs Specs that were run
|
|
354
|
+
* @param {Number} retries Number or retries remaining
|
|
355
|
+
*/
|
|
356
|
+
endHandler({ cid: rid, exitCode, specs, retries }) {
|
|
357
|
+
const passed = this._isWatchModeHalted() || exitCode === 0;
|
|
358
|
+
if (!passed && retries > 0) {
|
|
359
|
+
// Default is true, so test for false explicitly
|
|
360
|
+
const requeue = this.configParser.getConfig().specFileRetriesDeferred !== false ? 'push' : 'unshift';
|
|
361
|
+
this._schedule[parseInt(rid, 10)].specs[requeue]({ files: specs, retries: retries - 1, rid });
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
this._exitCode = this._isWatchModeHalted() ? 0 : this._exitCode || exitCode;
|
|
365
|
+
this._runnerFailed += !passed ? 1 : 0;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* avoid emitting job:end if watch mode has been stopped by user
|
|
369
|
+
*/
|
|
370
|
+
if (!this._isWatchModeHalted()) {
|
|
371
|
+
this.interface.emit('job:end', { cid: rid, passed, retries });
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Update schedule now this process has ended
|
|
375
|
+
*/
|
|
376
|
+
// get cid (capability id) from rid (runner id)
|
|
377
|
+
const cid = parseInt(rid, 10);
|
|
378
|
+
this._schedule[cid].availableInstances++;
|
|
379
|
+
this._schedule[cid].runningInstances--;
|
|
380
|
+
/**
|
|
381
|
+
* do nothing if
|
|
382
|
+
* - there are specs to be executed
|
|
383
|
+
* - we are running watch mode
|
|
384
|
+
*/
|
|
385
|
+
const shouldRunSpecs = this.runSpecs();
|
|
386
|
+
if (!shouldRunSpecs || (this._isWatchMode && !this._hasTriggeredExitRoutine)) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (this._resolve) {
|
|
390
|
+
this._resolve(passed ? this._exitCode : 1);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* We need exitHandler to catch SIGINT / SIGTERM events.
|
|
395
|
+
* Make sure all started selenium sessions get closed properly and prevent
|
|
396
|
+
* having dead driver processes. To do so let the runner end its Selenium
|
|
397
|
+
* session first before killing
|
|
398
|
+
*/
|
|
399
|
+
exitHandler(callback) {
|
|
400
|
+
if (!callback) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (this._hasTriggeredExitRoutine) {
|
|
404
|
+
return callback();
|
|
405
|
+
}
|
|
406
|
+
this._hasTriggeredExitRoutine = true;
|
|
407
|
+
this.interface.sigintTrigger();
|
|
408
|
+
return this.runner.shutdown().then(callback);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* returns true if user stopped watch mode, ex with ctrl+c
|
|
412
|
+
* @returns {boolean}
|
|
413
|
+
*/
|
|
414
|
+
_isWatchModeHalted() {
|
|
415
|
+
return this._isWatchMode && this._hasTriggeredExitRoutine;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
exports.default = Launcher;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Function to be executed after a test (in Mocha/Jasmine only)
|
|
3
|
+
* @param {Object} test test object
|
|
4
|
+
* @param {Object} context scope object the test was executed with
|
|
5
|
+
* @param {Error} result.error error object in case the test fails, otherwise `undefined`
|
|
6
|
+
* @param {Any} result.result return object of test function
|
|
7
|
+
* @param {Number} result.duration duration of test
|
|
8
|
+
* @param {Boolean} result.passed true if test has passed, otherwise false
|
|
9
|
+
* @param {Object} result.retries informations to spec related retries, e.g. `{ attempts: 0, limit: 0 }`
|
|
10
|
+
*/<%
|
|
11
|
+
if (reporters.length && reporters.includes('allure')) {%>
|
|
12
|
+
afterTest: async function(test, context, { error, result, duration, passed, retries }) {
|
|
13
|
+
if (!passed) {
|
|
14
|
+
await browser.takeScreenshot();
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
<% } else { %>
|
|
18
|
+
// afterTest: function(test, context, { error, result, duration, passed, retries }) {
|
|
19
|
+
// },
|
|
20
|
+
<% } %>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Feature: The Internet Guinea Pig Website
|
|
2
|
+
|
|
3
|
+
Scenario Outline: As a user, I can log into the secure area
|
|
4
|
+
|
|
5
|
+
Given I am on the login page
|
|
6
|
+
When I login with <username> and <password>
|
|
7
|
+
Then I should see a flash message saying <message>
|
|
8
|
+
|
|
9
|
+
Examples:
|
|
10
|
+
| username | password | message |
|
|
11
|
+
| tomsmith | SuperSecretPassword! | You logged into a secure area! |
|
|
12
|
+
| foobar | barfoo | Your username is invalid! |
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<%- isUsingTypeScript || isUsingBabel
|
|
2
|
+
? "import { Given, When, Then } from '@wdio/cucumber-framework';"
|
|
3
|
+
: "const { Given, When, Then } = require('@wdio/cucumber-framework');" %>
|
|
4
|
+
<%
|
|
5
|
+
/**
|
|
6
|
+
* step definition without page objects
|
|
7
|
+
*/
|
|
8
|
+
if (usePageObjects) { %>
|
|
9
|
+
<%- isUsingTypeScript || isUsingBabel
|
|
10
|
+
? `import LoginPage from '${relativePath}/login.page';`
|
|
11
|
+
: `const LoginPage = require('${relativePath}/login.page');` %>
|
|
12
|
+
<%- isUsingTypeScript || isUsingBabel
|
|
13
|
+
? `import SecurePage from '${relativePath}/secure.page';`
|
|
14
|
+
: `const SecurePage = require('${relativePath}/secure.page');` %>
|
|
15
|
+
|
|
16
|
+
const pages = {
|
|
17
|
+
login: LoginPage
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
Given(/^I am on the (\w+) page$/, async (page) => {
|
|
21
|
+
await pages[page].open()
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
When(/^I login with (\w+) and (.+)$/, async (username, password) => {
|
|
25
|
+
await LoginPage.login(username, password)
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
Then(/^I should see a flash message saying (.*)$/, async (message) => {
|
|
29
|
+
await expect(SecurePage.flashAlert).toBeExisting();
|
|
30
|
+
await expect(SecurePage.flashAlert).toHaveTextContaining(message);
|
|
31
|
+
});
|
|
32
|
+
<% } else {
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* step definition with page objects
|
|
36
|
+
*/
|
|
37
|
+
%>
|
|
38
|
+
Given(/^I am on the (\w+) page$/, async (page) => {
|
|
39
|
+
await browser.url(`https://the-internet.herokuapp.com/${page}`);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
When(/^I login with (\w+) and (.+)$/, async (username, password) => {
|
|
43
|
+
await $('#username').setValue(username);
|
|
44
|
+
await $('#password').setValue(password);
|
|
45
|
+
await $('button[type="submit"]').click();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
Then(/^I should see a flash message saying (.*)$/, async (message) => {
|
|
49
|
+
await expect($('#flash')).toBeExisting();
|
|
50
|
+
await expect($('#flash')).toHaveTextContaining(message);
|
|
51
|
+
});
|
|
52
|
+
<% } %>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<%
|
|
2
|
+
/**
|
|
3
|
+
* step definition without page objects
|
|
4
|
+
*/
|
|
5
|
+
if (usePageObjects) {
|
|
6
|
+
%><%- isUsingTypeScript || isUsingBabel
|
|
7
|
+
? `import LoginPage from '${relativePath}/login.page';`
|
|
8
|
+
: `const LoginPage = require('${relativePath}/login.page');` %>
|
|
9
|
+
<%- isUsingTypeScript || isUsingBabel
|
|
10
|
+
? `import SecurePage from '${relativePath}/secure.page';`
|
|
11
|
+
: `const SecurePage = require('${relativePath}/secure.page');` %>
|
|
12
|
+
|
|
13
|
+
describe('My Login application', () => {
|
|
14
|
+
it('should login with valid credentials', async () => {
|
|
15
|
+
await LoginPage.open();
|
|
16
|
+
|
|
17
|
+
await LoginPage.login('tomsmith', 'SuperSecretPassword!');
|
|
18
|
+
await expect(SecurePage.flashAlert).toBeExisting();
|
|
19
|
+
await expect(SecurePage.flashAlert).toHaveTextContaining(
|
|
20
|
+
'You logged into a secure area!');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
<% } else {
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* step definition with page objects
|
|
28
|
+
*/
|
|
29
|
+
%>describe('My Login application', () => {
|
|
30
|
+
it('should login with valid credentials', async () => {
|
|
31
|
+
await browser.url(`https://the-internet.herokuapp.com/login`);
|
|
32
|
+
|
|
33
|
+
await $('#username').setValue('tomsmith');
|
|
34
|
+
await $('#password').setValue('SuperSecretPassword!');
|
|
35
|
+
await $('button[type="submit"]').click();
|
|
36
|
+
|
|
37
|
+
await expect($('#flash')).toBeExisting();
|
|
38
|
+
await expect($('#flash')).toHaveTextContaining(
|
|
39
|
+
'You logged into a secure area!');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
<% } %>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<%
|
|
2
|
+
/**
|
|
3
|
+
* test with page objects
|
|
4
|
+
*/
|
|
5
|
+
if (usePageObjects) {
|
|
6
|
+
%><%- isUsingTypeScript || isUsingBabel
|
|
7
|
+
? `import LoginPage from '${relativePath}/login.page';`
|
|
8
|
+
: `const LoginPage = require('${relativePath}/login.page');` %>
|
|
9
|
+
<%- isUsingTypeScript || isUsingBabel
|
|
10
|
+
? `import SecurePage from '${relativePath}/secure.page';`
|
|
11
|
+
: `const SecurePage = require('${relativePath}/secure.page');` %>
|
|
12
|
+
|
|
13
|
+
describe('My Login application', () => {
|
|
14
|
+
it('should login with valid credentials', async () => {
|
|
15
|
+
await LoginPage.open();
|
|
16
|
+
|
|
17
|
+
await LoginPage.login('tomsmith', 'SuperSecretPassword!');
|
|
18
|
+
await expect(SecurePage.flashAlert).toBeExisting();
|
|
19
|
+
await expect(SecurePage.flashAlert).toHaveTextContaining(
|
|
20
|
+
'You logged into a secure area!');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
<% } else {
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* test without page objects
|
|
28
|
+
*/
|
|
29
|
+
%>describe('My Login application', () => {
|
|
30
|
+
it('should login with valid credentials', async () => {
|
|
31
|
+
await browser.url(`https://the-internet.herokuapp.com/login`);
|
|
32
|
+
|
|
33
|
+
await $('#username').setValue('tomsmith');
|
|
34
|
+
await $('#password').setValue('SuperSecretPassword!');
|
|
35
|
+
await $('button[type="submit"]').click();
|
|
36
|
+
|
|
37
|
+
await expect($('#flash')).toBeExisting();
|
|
38
|
+
await expect($('#flash')).toHaveTextContaining(
|
|
39
|
+
'You logged into a secure area!');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
<% } %>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<%- isUsingTypeScript ? "import { ChainablePromiseElement } from 'webdriverio';" : "" %>
|
|
2
|
+
|
|
3
|
+
<%- isUsingTypeScript || isUsingBabel
|
|
4
|
+
? "import Page from './page';"
|
|
5
|
+
: "const Page = require('./page');" %>
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* sub page containing specific selectors and methods for a specific page
|
|
9
|
+
*/
|
|
10
|
+
class LoginPage extends Page {
|
|
11
|
+
/**
|
|
12
|
+
* define selectors using getter methods
|
|
13
|
+
*/
|
|
14
|
+
<%- isUsingTypeScript ? "public " : "" %>get inputUsername () {
|
|
15
|
+
return $('#username');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
<%- isUsingTypeScript ? "public " : "" %>get inputPassword () {
|
|
19
|
+
return $('#password');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
<%- isUsingTypeScript ? "public " : "" %>get btnSubmit () {
|
|
23
|
+
return $('button[type="submit"]');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* a method to encapsule automation code to interact with the page
|
|
28
|
+
* e.g. to login using username and password
|
|
29
|
+
*/
|
|
30
|
+
<%- isUsingTypeScript ? "public " : "" %>async login (username<%- isUsingTypeScript ? ": string": "" %>, password<%- isUsingTypeScript ? ": string": "" %>) {
|
|
31
|
+
await this.inputUsername.setValue(username);
|
|
32
|
+
await this.inputPassword.setValue(password);
|
|
33
|
+
await this.btnSubmit.click();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* overwrite specific options to adapt it to page object
|
|
38
|
+
*/
|
|
39
|
+
<%- isUsingTypeScript ? "public " : "" %>open () {
|
|
40
|
+
return super.open('login');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
<%- isUsingTypeScript || isUsingBabel ? "export default": "module.exports =" %> new LoginPage();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* main page object containing all methods, selectors and functionality
|
|
3
|
+
* that is shared across all page objects
|
|
4
|
+
*/
|
|
5
|
+
<%- isUsingTypeScript || isUsingBabel ? "export default" : "module.exports =" %> class Page {
|
|
6
|
+
/**
|
|
7
|
+
* Opens a sub page of the page
|
|
8
|
+
* @param path path of the sub page (e.g. /path/to/page.html)
|
|
9
|
+
*/
|
|
10
|
+
<%- isUsingTypeScript ? "public " : "" %>open (path<%- isUsingTypeScript ? ": string" : "" %>) {
|
|
11
|
+
return browser.url(`https://the-internet.herokuapp.com/${path}`)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<%- isUsingTypeScript ? "import { ChainablePromiseElement } from 'webdriverio';" : "" %>
|
|
2
|
+
|
|
3
|
+
<%- isUsingTypeScript || isUsingBabel
|
|
4
|
+
? "import Page from './page';"
|
|
5
|
+
: "const Page = require('./page');" %>
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* sub page containing specific selectors and methods for a specific page
|
|
9
|
+
*/
|
|
10
|
+
class SecurePage extends Page {
|
|
11
|
+
/**
|
|
12
|
+
* define selectors using getter methods
|
|
13
|
+
*/
|
|
14
|
+
<%- isUsingTypeScript ? "public " : "" %>get flashAlert () {
|
|
15
|
+
return $('#flash');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
<%- isUsingTypeScript || isUsingBabel ? "export default": "module.exports =" %> new SecurePage();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Test reporter for stdout.
|
|
2
|
+
// The only one supported by default is 'dot'
|
|
3
|
+
// see also: https://webdriver.io/docs/dot-reporter<%
|
|
4
|
+
if (reporters.length) { %>
|
|
5
|
+
reporters: [<%- reporters.map(reporter => {
|
|
6
|
+
if (reporter === 'allure') {
|
|
7
|
+
return `['${reporter}', {outputDir: 'allure-results'}]`
|
|
8
|
+
} else {
|
|
9
|
+
return `'${reporter}'`
|
|
10
|
+
}
|
|
11
|
+
})%>],
|
|
12
|
+
<%} else { %>
|
|
13
|
+
// reporters: ['dot'],
|
|
14
|
+
<%}%>
|