@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.
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 +75 -0
  23. package/build/interface.d.ts.map +1 -0
  24. package/build/interface.js +245 -0
  25. package/build/launcher.d.ts +95 -0
  26. package/build/launcher.d.ts.map +1 -0
  27. package/build/launcher.js +418 -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 +448 -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 +82 -0
  42. package/build/utils.d.ts.map +1 -0
  43. package/build/utils.js +345 -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,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
+ <%}%>