@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,436 @@
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
+ * Also ensure that user hooks are run before service hooks so that e.g.
92
+ * a user can use plugin service, e.g. shared store service is still
93
+ * available running hooks in this order
94
+ */
95
+ log.info('Run onComplete hook');
96
+ const onCompleteResults = await (0, utils_2.runOnCompleteHook)(config.onComplete, config, caps, exitCode, this.interface.result);
97
+ await (0, utils_2.runServiceHook)(this._launcher, 'onComplete', exitCode, config, caps);
98
+ // if any of the onComplete hooks failed, update the exit code
99
+ exitCode = onCompleteResults.includes(1) ? 1 : exitCode;
100
+ await logger_1.default.waitForBuffer();
101
+ this.interface.finalise();
102
+ }
103
+ catch (err) {
104
+ error = err;
105
+ }
106
+ finally {
107
+ if (!this._hasTriggeredExitRoutine) {
108
+ this._hasTriggeredExitRoutine = true;
109
+ await this.runner.shutdown();
110
+ }
111
+ }
112
+ if (error) {
113
+ this.interface.logHookError(error);
114
+ throw error;
115
+ }
116
+ return exitCode;
117
+ }
118
+ /**
119
+ * run without triggering onPrepare/onComplete hooks
120
+ */
121
+ runMode(config, caps) {
122
+ /**
123
+ * fail if no caps were found
124
+ */
125
+ if (!caps) {
126
+ return new Promise((resolve) => {
127
+ log.error('Missing capabilities, exiting with failure');
128
+ return resolve(1);
129
+ });
130
+ }
131
+ /**
132
+ * avoid retries in watch mode
133
+ */
134
+ const specFileRetries = this._isWatchMode ? 0 : config.specFileRetries;
135
+ /**
136
+ * schedule test runs
137
+ */
138
+ let cid = 0;
139
+ if (this.isMultiremote) {
140
+ /**
141
+ * Multiremote mode
142
+ */
143
+ this._schedule.push({
144
+ cid: cid++,
145
+ caps: caps,
146
+ specs: this.formatSpecs(caps, specFileRetries),
147
+ availableInstances: config.maxInstances || 1,
148
+ runningInstances: 0
149
+ });
150
+ }
151
+ else {
152
+ /**
153
+ * Regular mode
154
+ */
155
+ for (let capabilities of caps) {
156
+ this._schedule.push({
157
+ cid: cid++,
158
+ caps: capabilities,
159
+ specs: this.formatSpecs(capabilities, specFileRetries),
160
+ availableInstances: capabilities.maxInstances || config.maxInstancesPerCapability,
161
+ runningInstances: 0
162
+ });
163
+ }
164
+ }
165
+ return new Promise((resolve) => {
166
+ this._resolve = resolve;
167
+ /**
168
+ * fail if no specs were found or specified
169
+ */
170
+ if (Object.values(this._schedule).reduce((specCnt, schedule) => specCnt + schedule.specs.length, 0) === 0) {
171
+ log.error('No specs found to run, exiting with failure');
172
+ return resolve(1);
173
+ }
174
+ /**
175
+ * return immediately if no spec was run
176
+ */
177
+ if (this.runSpecs()) {
178
+ resolve(0);
179
+ }
180
+ });
181
+ }
182
+ /**
183
+ * Format the specs into an array of objects with files and retries
184
+ */
185
+ formatSpecs(capabilities, specFileRetries) {
186
+ let files = [];
187
+ files = this.configParser.getSpecs(capabilities.specs, capabilities.exclude);
188
+ return files.map(file => {
189
+ if (typeof file === 'string') {
190
+ return { files: [file], retries: specFileRetries };
191
+ }
192
+ else if (Array.isArray(file)) {
193
+ return { files: file, retries: specFileRetries };
194
+ }
195
+ log.warn('Unexpected entry in specs that is neither string nor array: ', file);
196
+ // Returning an empty structure to avoid undefined
197
+ return { files: [], retries: specFileRetries };
198
+ });
199
+ }
200
+ /**
201
+ * run multiple single remote tests
202
+ * @return {Boolean} true if all specs have been run and all instances have finished
203
+ */
204
+ runSpecs() {
205
+ let config = this.configParser.getConfig();
206
+ /**
207
+ * stop spawning new processes when CTRL+C was triggered
208
+ */
209
+ if (this._hasTriggeredExitRoutine) {
210
+ return true;
211
+ }
212
+ while (this.getNumberOfRunningInstances() < config.maxInstances) {
213
+ let schedulableCaps = this._schedule
214
+ /**
215
+ * bail if number of errors exceeds allowed
216
+ */
217
+ .filter(() => {
218
+ const filter = typeof config.bail !== 'number' || config.bail < 1 ||
219
+ config.bail > this._runnerFailed;
220
+ /**
221
+ * clear number of specs when filter is false
222
+ */
223
+ if (!filter) {
224
+ this._schedule.forEach((t) => { t.specs = []; });
225
+ }
226
+ return filter;
227
+ })
228
+ /**
229
+ * make sure complete number of running instances is not higher than general maxInstances number
230
+ */
231
+ .filter(() => this.getNumberOfRunningInstances() < config.maxInstances)
232
+ /**
233
+ * make sure the capability has available capacities
234
+ */
235
+ .filter((a) => a.availableInstances > 0)
236
+ /**
237
+ * make sure capability has still caps to run
238
+ */
239
+ .filter((a) => a.specs.length > 0)
240
+ /**
241
+ * make sure we are running caps with less running instances first
242
+ */
243
+ .sort((a, b) => a.runningInstances - b.runningInstances);
244
+ /**
245
+ * continue if no capability were schedulable
246
+ */
247
+ if (schedulableCaps.length === 0) {
248
+ break;
249
+ }
250
+ let specs = schedulableCaps[0].specs.shift();
251
+ this.startInstance(specs.files, schedulableCaps[0].caps, schedulableCaps[0].cid, specs.rid, specs.retries);
252
+ schedulableCaps[0].availableInstances--;
253
+ schedulableCaps[0].runningInstances++;
254
+ }
255
+ return this.getNumberOfRunningInstances() === 0 && this.getNumberOfSpecsLeft() === 0;
256
+ }
257
+ /**
258
+ * gets number of all running instances
259
+ * @return {number} number of running instances
260
+ */
261
+ getNumberOfRunningInstances() {
262
+ return this._schedule.map((a) => a.runningInstances).reduce((a, b) => a + b);
263
+ }
264
+ /**
265
+ * get number of total specs left to complete whole suites
266
+ * @return {number} specs left to complete suite
267
+ */
268
+ getNumberOfSpecsLeft() {
269
+ return this._schedule.map((a) => a.specs.length).reduce((a, b) => a + b);
270
+ }
271
+ /**
272
+ * Start instance in a child process.
273
+ * @param {Array} specs Specs to run
274
+ * @param {Number} cid Capabilities ID
275
+ * @param {String} rid Runner ID override
276
+ * @param {Number} retries Number of retries remaining
277
+ */
278
+ async startInstance(specs, caps, cid, rid, retries) {
279
+ let config = this.configParser.getConfig();
280
+ // wait before retrying the spec file
281
+ if (typeof config.specFileRetriesDelay === 'number' && config.specFileRetries > 0 && config.specFileRetries !== retries) {
282
+ await (0, utils_1.sleep)(config.specFileRetriesDelay * 1000);
283
+ }
284
+ // Retried tests receive the cid of the failing test as rid
285
+ // so they can run with the same cid of the failing test.
286
+ const runnerId = rid || this.getRunnerId(cid);
287
+ let processNumber = this._runnerStarted + 1;
288
+ // process.debugPort defaults to 5858 and is set even when process
289
+ // is not being debugged.
290
+ let debugArgs = [];
291
+ let debugType;
292
+ let debugHost = '';
293
+ let debugPort = process.debugPort;
294
+ for (let i in process.execArgv) {
295
+ const debugArgs = process.execArgv[i].match('--(debug|inspect)(?:-brk)?(?:=(.*):)?');
296
+ if (debugArgs) {
297
+ let [, type, host] = debugArgs;
298
+ if (type) {
299
+ debugType = type;
300
+ }
301
+ if (host) {
302
+ debugHost = `${host}:`;
303
+ }
304
+ }
305
+ }
306
+ if (debugType) {
307
+ debugArgs.push(`--${debugType}=${debugHost}${(debugPort + processNumber)}`);
308
+ }
309
+ // if you would like to add --debug-brk, use a different port, etc...
310
+ let capExecArgs = [...(config.execArgv || [])];
311
+ // The default value for child.fork execArgs is process.execArgs,
312
+ // so continue to use this unless another value is specified in config.
313
+ let defaultArgs = (capExecArgs.length) ? process.execArgv : [];
314
+ // If an arg appears multiple times the last occurrence is used
315
+ let execArgv = [...defaultArgs, ...debugArgs, ...capExecArgs];
316
+ // set '--no-wasm-code-gc' deliberatively as it causes problems with
317
+ // @wdio/sync and recent TypeScript compiles
318
+ if (!execArgv.includes('--no-wasm-code-gc')) {
319
+ execArgv.push('--no-wasm-code-gc');
320
+ }
321
+ // bump up worker count
322
+ this._runnerStarted++;
323
+ // run worker hook to allow modify runtime and capabilities of a specific worker
324
+ log.info('Run onWorkerStart hook');
325
+ await (0, utils_2.runLauncherHook)(config.onWorkerStart, runnerId, caps, specs, this._args, execArgv)
326
+ .catch((error) => this._workerHookError(error));
327
+ await (0, utils_2.runServiceHook)(this._launcher, 'onWorkerStart', runnerId, caps, specs, this._args, execArgv)
328
+ .catch((error) => this._workerHookError(error));
329
+ // prefer launcher settings in capabilities over general launcher
330
+ const worker = this.runner.run({
331
+ cid: runnerId,
332
+ command: 'run',
333
+ configFile: this._configFilePath,
334
+ args: { ...this._args, ...((config === null || config === void 0 ? void 0 : config.autoCompileOpts) ? { autoCompileOpts: config.autoCompileOpts } : {}) },
335
+ caps,
336
+ specs,
337
+ execArgv,
338
+ retries
339
+ });
340
+ worker.on('message', this.interface.onMessage.bind(this.interface));
341
+ worker.on('error', this.interface.onMessage.bind(this.interface));
342
+ worker.on('exit', this.endHandler.bind(this));
343
+ }
344
+ _workerHookError(error) {
345
+ this.interface.logHookError(error);
346
+ if (this._resolve) {
347
+ this._resolve(1);
348
+ }
349
+ }
350
+ /**
351
+ * generates a runner id
352
+ * @param {Number} cid capability id (unique identifier for a capability)
353
+ * @return {String} runner id (combination of cid and test id e.g. 0a, 0b, 1a, 1b ...)
354
+ */
355
+ getRunnerId(cid) {
356
+ if (!this._rid[cid]) {
357
+ this._rid[cid] = 0;
358
+ }
359
+ return `${cid}-${this._rid[cid]++}`;
360
+ }
361
+ /**
362
+ * Close test runner process once all child processes have exited
363
+ * @param {Number} cid Capabilities ID
364
+ * @param {Number} exitCode exit code of child process
365
+ * @param {Array} specs Specs that were run
366
+ * @param {Number} retries Number or retries remaining
367
+ */
368
+ async endHandler({ cid: rid, exitCode, specs, retries }) {
369
+ const passed = this._isWatchModeHalted() || exitCode === 0;
370
+ if (!passed && retries > 0) {
371
+ // Default is true, so test for false explicitly
372
+ const requeue = this.configParser.getConfig().specFileRetriesDeferred !== false ? 'push' : 'unshift';
373
+ this._schedule[parseInt(rid, 10)].specs[requeue]({ files: specs, retries: retries - 1, rid });
374
+ }
375
+ else {
376
+ this._exitCode = this._isWatchModeHalted() ? 0 : this._exitCode || exitCode;
377
+ this._runnerFailed += !passed ? 1 : 0;
378
+ }
379
+ /**
380
+ * avoid emitting job:end if watch mode has been stopped by user
381
+ */
382
+ if (!this._isWatchModeHalted()) {
383
+ this.interface.emit('job:end', { cid: rid, passed, retries });
384
+ }
385
+ /**
386
+ * Update schedule now this process has ended
387
+ * get cid (capability id) from rid (runner id)
388
+ */
389
+ const cid = parseInt(rid, 10);
390
+ this._schedule[cid].availableInstances++;
391
+ this._schedule[cid].runningInstances--;
392
+ log.info('Run onWorkerEnd hook');
393
+ const config = this.configParser.getConfig();
394
+ await (0, utils_2.runLauncherHook)(config.onWorkerEnd, rid, exitCode, specs, retries)
395
+ .catch((error) => this._workerHookError(error));
396
+ await (0, utils_2.runServiceHook)(this._launcher, 'onWorkerEnd', rid, exitCode, specs, retries)
397
+ .catch((error) => this._workerHookError(error));
398
+ /**
399
+ * do nothing if
400
+ * - there are specs to be executed
401
+ * - we are running watch mode
402
+ */
403
+ const shouldRunSpecs = this.runSpecs();
404
+ if (!shouldRunSpecs || (this._isWatchMode && !this._hasTriggeredExitRoutine)) {
405
+ return;
406
+ }
407
+ if (this._resolve) {
408
+ this._resolve(passed ? this._exitCode : 1);
409
+ }
410
+ }
411
+ /**
412
+ * We need exitHandler to catch SIGINT / SIGTERM events.
413
+ * Make sure all started selenium sessions get closed properly and prevent
414
+ * having dead driver processes. To do so let the runner end its Selenium
415
+ * session first before killing
416
+ */
417
+ exitHandler(callback) {
418
+ if (!callback) {
419
+ return;
420
+ }
421
+ if (this._hasTriggeredExitRoutine) {
422
+ return callback();
423
+ }
424
+ this._hasTriggeredExitRoutine = true;
425
+ this.interface.sigintTrigger();
426
+ return this.runner.shutdown().then(callback);
427
+ }
428
+ /**
429
+ * returns true if user stopped watch mode, ex with ctrl+c
430
+ * @returns {boolean}
431
+ */
432
+ _isWatchModeHalted() {
433
+ return this._isWatchMode && this._hasTriggeredExitRoutine;
434
+ }
435
+ }
436
+ 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();