codeceptjs 3.5.9-beta.1 → 3.5.9-beta.2
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/docs/build/Appium.js +6 -6
- package/docs/build/Playwright.js +63 -1
- package/docs/helpers/Appium.md +1 -1
- package/docs/helpers/Playwright.md +341 -303
- package/docs/internal-api.md +111 -0
- package/docs/plugins.md +4 -2
- package/lib/command/run-multiple.js +1 -1
- package/lib/command/run-workers.js +30 -4
- package/lib/command/workers/runTests.js +23 -0
- package/lib/event.js +2 -0
- package/lib/helper/Appium.js +10 -10
- package/lib/helper/Playwright.js +76 -5
- package/lib/helper/Puppeteer.js +7 -3
- package/lib/helper/WebDriver.js +6 -2
- package/lib/interfaces/gherkin.js +8 -1
- package/lib/interfaces/scenarioConfig.js +1 -0
- package/lib/locator.js +2 -2
- package/lib/plugin/autoLogin.js +4 -2
- package/lib/plugin/retryFailedStep.js +5 -0
- package/lib/plugin/stepByStepReport.js +2 -2
- package/lib/ui.js +1 -0
- package/lib/workers.js +2 -0
- package/package.json +4 -4
- package/typings/promiseBasedTypes.d.ts +31 -1
- package/typings/types.d.ts +34 -1
package/docs/internal-api.md
CHANGED
|
@@ -96,7 +96,117 @@ module.exports = function() {
|
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
98
|
```
|
|
99
|
+
You could get test stats when running with workers
|
|
99
100
|
|
|
101
|
+
```js
|
|
102
|
+
const { event } = require('codeceptjs');
|
|
103
|
+
|
|
104
|
+
module.exports = function() {
|
|
105
|
+
|
|
106
|
+
event.dispatcher.on(event.workers.result, function (result) {
|
|
107
|
+
|
|
108
|
+
console.log(result);
|
|
109
|
+
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// in console log
|
|
114
|
+
FAIL | 7 passed, 1 failed, 1 skipped // 2s
|
|
115
|
+
{
|
|
116
|
+
"tests": {
|
|
117
|
+
"passed": [
|
|
118
|
+
{
|
|
119
|
+
"type": "test",
|
|
120
|
+
"title": "Assert @C3",
|
|
121
|
+
"body": "() => { }",
|
|
122
|
+
"async": 0,
|
|
123
|
+
"sync": true,
|
|
124
|
+
"_timeout": 2000,
|
|
125
|
+
"_slow": 75,
|
|
126
|
+
"_retries": -1,
|
|
127
|
+
"timedOut": false,
|
|
128
|
+
"_currentRetry": 0,
|
|
129
|
+
"pending": false,
|
|
130
|
+
"opts": {},
|
|
131
|
+
"tags": [
|
|
132
|
+
"@C3"
|
|
133
|
+
],
|
|
134
|
+
"uid": "xe4q1HdqpRrZG5dPe0JG+A",
|
|
135
|
+
"workerIndex": 3,
|
|
136
|
+
"retries": -1,
|
|
137
|
+
"duration": 493,
|
|
138
|
+
"err": null,
|
|
139
|
+
"parent": {
|
|
140
|
+
"title": "My",
|
|
141
|
+
"ctx": {},
|
|
142
|
+
"suites": [],
|
|
143
|
+
"tests": [],
|
|
144
|
+
"root": false,
|
|
145
|
+
"pending": false,
|
|
146
|
+
"_retries": -1,
|
|
147
|
+
"_beforeEach": [],
|
|
148
|
+
"_beforeAll": [],
|
|
149
|
+
"_afterEach": [],
|
|
150
|
+
"_afterAll": [],
|
|
151
|
+
"_timeout": 2000,
|
|
152
|
+
"_slow": 75,
|
|
153
|
+
"_bail": false,
|
|
154
|
+
"_onlyTests": [],
|
|
155
|
+
"_onlySuites": [],
|
|
156
|
+
"delayed": false
|
|
157
|
+
},
|
|
158
|
+
"steps": [
|
|
159
|
+
{
|
|
160
|
+
"actor": "I",
|
|
161
|
+
"name": "amOnPage",
|
|
162
|
+
"status": "success",
|
|
163
|
+
"agrs": [
|
|
164
|
+
"https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST"
|
|
165
|
+
],
|
|
166
|
+
"startedAt": 1698760652610,
|
|
167
|
+
"startTime": 1698760652611,
|
|
168
|
+
"endTime": 1698760653098,
|
|
169
|
+
"finishedAt": 1698760653098,
|
|
170
|
+
"duration": 488
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"actor": "I",
|
|
174
|
+
"name": "grabCurrentUrl",
|
|
175
|
+
"status": "success",
|
|
176
|
+
"agrs": [],
|
|
177
|
+
"startedAt": 1698760653098,
|
|
178
|
+
"startTime": 1698760653098,
|
|
179
|
+
"endTime": 1698760653099,
|
|
180
|
+
"finishedAt": 1698760653099,
|
|
181
|
+
"duration": 1
|
|
182
|
+
}
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
],
|
|
186
|
+
"failed": [],
|
|
187
|
+
"skipped": []
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
CodeceptJS also exposes the env var `process.env.RUNS_WITH_WORKERS` when running tests with `run-workers` command so that you could handle the events better in your plugins/helpers
|
|
193
|
+
|
|
194
|
+
```js
|
|
195
|
+
const { event } = require('codeceptjs');
|
|
196
|
+
|
|
197
|
+
module.exports = function() {
|
|
198
|
+
// this event would trigger the `_publishResultsToTestrail` when running `run-workers` command
|
|
199
|
+
event.dispatcher.on(event.workers.result, async () => {
|
|
200
|
+
await _publishResultsToTestrail();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// this event would not trigger the `_publishResultsToTestrail` multiple times when running `run-workers` command
|
|
204
|
+
event.dispatcher.on(event.all.result, async () => {
|
|
205
|
+
// when running `run` command, this env var is undefined
|
|
206
|
+
if (!process.env.RUNS_WITH_WORKERS) await _publishResultsToTestrail();
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
```
|
|
100
210
|
Available events:
|
|
101
211
|
|
|
102
212
|
* `event.test.before(test)` - *async* when `Before` hooks from helpers and from test is executed
|
|
@@ -119,6 +229,7 @@ Available events:
|
|
|
119
229
|
* `event.all.result` - when results are printed
|
|
120
230
|
* `event.workers.before` - before spawning workers in parallel run
|
|
121
231
|
* `event.workers.after` - after workers finished in parallel run
|
|
232
|
+
* `event.workers.result` - test results after workers finished in parallel run
|
|
122
233
|
|
|
123
234
|
|
|
124
235
|
> *sync* - means that event is fired in the moment of the action happening.
|
package/docs/plugins.md
CHANGED
|
@@ -65,12 +65,14 @@ If a session expires automatically logs in again.
|
|
|
65
65
|
```js
|
|
66
66
|
// inside a test file
|
|
67
67
|
// use login to inject auto-login function
|
|
68
|
+
Feature('Login');
|
|
69
|
+
|
|
68
70
|
Before(({ login }) => {
|
|
69
71
|
login('user'); // login using user session
|
|
70
72
|
});
|
|
71
73
|
|
|
72
|
-
// Alternatively log in for one scenario
|
|
73
|
-
Scenario('log me in', ( {I, login} ) => {
|
|
74
|
+
// Alternatively log in for one scenario.
|
|
75
|
+
Scenario('log me in', ( { I, login } ) => {
|
|
74
76
|
login('admin');
|
|
75
77
|
I.see('I am logged in');
|
|
76
78
|
});
|
|
@@ -124,7 +124,7 @@ function executeRun(runName, runConfig) {
|
|
|
124
124
|
if (browserConfig.outputName) {
|
|
125
125
|
outputDir += typeof browserConfig.outputName === 'function' ? browserConfig.outputName() : browserConfig.outputName;
|
|
126
126
|
} else {
|
|
127
|
-
const hash = crypto.createHash('
|
|
127
|
+
const hash = crypto.createHash('sha256');
|
|
128
128
|
hash.update(JSON.stringify(browserConfig));
|
|
129
129
|
outputDir += hash.digest('hex');
|
|
130
130
|
}
|
|
@@ -7,6 +7,11 @@ const Workers = require('../workers');
|
|
|
7
7
|
module.exports = async function (workerCount, selectedRuns, options) {
|
|
8
8
|
process.env.profile = options.profile;
|
|
9
9
|
|
|
10
|
+
const suiteArr = [];
|
|
11
|
+
const passedTestArr = [];
|
|
12
|
+
const failedTestArr = [];
|
|
13
|
+
const skippedTestArr = [];
|
|
14
|
+
|
|
10
15
|
const { config: testConfig, override = '' } = options;
|
|
11
16
|
const overrideConfigs = tryOrDefault(() => JSON.parse(override), {});
|
|
12
17
|
const by = options.suites ? 'suite' : 'test';
|
|
@@ -26,15 +31,36 @@ module.exports = async function (workerCount, selectedRuns, options) {
|
|
|
26
31
|
|
|
27
32
|
const workers = new Workers(numberOfWorkers, config);
|
|
28
33
|
workers.overrideConfig(overrideConfigs);
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
|
|
35
|
+
workers.on(event.suite.before, (suite) => {
|
|
36
|
+
suiteArr.push(suite);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
workers.on(event.test.failed, (test) => {
|
|
40
|
+
failedTestArr.push(test);
|
|
41
|
+
output.test.failed(test);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
workers.on(event.test.passed, (test) => {
|
|
45
|
+
passedTestArr.push(test);
|
|
46
|
+
output.test.passed(test);
|
|
31
47
|
});
|
|
32
48
|
|
|
33
|
-
workers.on(event.test.
|
|
34
|
-
|
|
49
|
+
workers.on(event.test.skipped, (test) => {
|
|
50
|
+
skippedTestArr.push(test);
|
|
51
|
+
output.test.passed(test);
|
|
35
52
|
});
|
|
36
53
|
|
|
37
54
|
workers.on(event.all.result, () => {
|
|
55
|
+
// expose test stats after all workers finished their execution
|
|
56
|
+
event.dispatcher.emit(event.workers.result, {
|
|
57
|
+
suites: suiteArr,
|
|
58
|
+
tests: {
|
|
59
|
+
passed: passedTestArr,
|
|
60
|
+
failed: failedTestArr,
|
|
61
|
+
skipped: skippedTestArr,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
38
64
|
workers.printResults();
|
|
39
65
|
});
|
|
40
66
|
|
|
@@ -132,9 +132,32 @@ function initializeListeners() {
|
|
|
132
132
|
duration: test.duration || 0,
|
|
133
133
|
err,
|
|
134
134
|
parent,
|
|
135
|
+
steps: test.steps ? simplifyStepsInTestObject(test.steps, err) : [],
|
|
135
136
|
};
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
function simplifyStepsInTestObject(steps, err) {
|
|
140
|
+
steps = [...steps];
|
|
141
|
+
const _steps = [];
|
|
142
|
+
|
|
143
|
+
for (step of steps) {
|
|
144
|
+
_steps.push({
|
|
145
|
+
actor: step.actor,
|
|
146
|
+
name: step.name,
|
|
147
|
+
status: step.status,
|
|
148
|
+
agrs: step.args,
|
|
149
|
+
startedAt: step.startedAt,
|
|
150
|
+
startTime: step.startTime,
|
|
151
|
+
endTime: step.endTime,
|
|
152
|
+
finishedAt: step.finishedAt,
|
|
153
|
+
duration: step.duration,
|
|
154
|
+
err,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return _steps;
|
|
159
|
+
}
|
|
160
|
+
|
|
138
161
|
function simplifyStep(step, err = null) {
|
|
139
162
|
step = { ...step };
|
|
140
163
|
|
package/lib/event.js
CHANGED
|
@@ -127,10 +127,12 @@ module.exports = {
|
|
|
127
127
|
* @inner
|
|
128
128
|
* @property {'workers.before'} before
|
|
129
129
|
* @property {'workers.after'} after
|
|
130
|
+
* @property {'workers.result'} result
|
|
130
131
|
*/
|
|
131
132
|
workers: {
|
|
132
133
|
before: 'workers.before',
|
|
133
134
|
after: 'workers.after',
|
|
135
|
+
result: 'workers.result',
|
|
134
136
|
},
|
|
135
137
|
|
|
136
138
|
/**
|
package/lib/helper/Appium.js
CHANGED
|
@@ -368,7 +368,7 @@ class Appium extends Webdriver {
|
|
|
368
368
|
if (context.web) return this.switchToWeb(context.web);
|
|
369
369
|
if (context.webview) return this.switchToWeb(context.webview);
|
|
370
370
|
}
|
|
371
|
-
return this.
|
|
371
|
+
return this.switchToContext(context);
|
|
372
372
|
}
|
|
373
373
|
|
|
374
374
|
_withinEnd() {
|
|
@@ -423,8 +423,8 @@ class Appium extends Webdriver {
|
|
|
423
423
|
async runOnIOS(caps, fn) {
|
|
424
424
|
if (this.platform !== 'ios') return;
|
|
425
425
|
recorder.session.start('iOS-only actions');
|
|
426
|
-
this._runWithCaps(caps, fn);
|
|
427
|
-
recorder.add('restore from iOS session', () => recorder.session.restore());
|
|
426
|
+
await this._runWithCaps(caps, fn);
|
|
427
|
+
await recorder.add('restore from iOS session', () => recorder.session.restore());
|
|
428
428
|
return recorder.promise();
|
|
429
429
|
}
|
|
430
430
|
|
|
@@ -465,8 +465,8 @@ class Appium extends Webdriver {
|
|
|
465
465
|
async runOnAndroid(caps, fn) {
|
|
466
466
|
if (this.platform !== 'android') return;
|
|
467
467
|
recorder.session.start('Android-only actions');
|
|
468
|
-
this._runWithCaps(caps, fn);
|
|
469
|
-
recorder.add('restore from Android session', () => recorder.session.restore());
|
|
468
|
+
await this._runWithCaps(caps, fn);
|
|
469
|
+
await recorder.add('restore from Android session', () => recorder.session.restore());
|
|
470
470
|
return recorder.promise();
|
|
471
471
|
}
|
|
472
472
|
|
|
@@ -834,7 +834,7 @@ class Appium extends Webdriver {
|
|
|
834
834
|
*
|
|
835
835
|
* @param {*} context the context to switch to
|
|
836
836
|
*/
|
|
837
|
-
async
|
|
837
|
+
async switchToContext(context) {
|
|
838
838
|
return this.browser.switchContext(context);
|
|
839
839
|
}
|
|
840
840
|
|
|
@@ -858,11 +858,11 @@ class Appium extends Webdriver {
|
|
|
858
858
|
this.isWeb = true;
|
|
859
859
|
this.defaultContext = 'body';
|
|
860
860
|
|
|
861
|
-
if (context) return this.
|
|
861
|
+
if (context) return this.switchToContext(context);
|
|
862
862
|
const contexts = await this.grabAllContexts();
|
|
863
863
|
this.debugSection('Contexts', contexts.toString());
|
|
864
864
|
for (const idx in contexts) {
|
|
865
|
-
if (contexts[idx].match(/^WEBVIEW/)) return this.
|
|
865
|
+
if (contexts[idx].match(/^WEBVIEW/)) return this.switchToContext(contexts[idx]);
|
|
866
866
|
}
|
|
867
867
|
|
|
868
868
|
throw new Error('No WEBVIEW could be guessed, please specify one in params');
|
|
@@ -885,8 +885,8 @@ class Appium extends Webdriver {
|
|
|
885
885
|
this.isWeb = false;
|
|
886
886
|
this.defaultContext = '//*';
|
|
887
887
|
|
|
888
|
-
if (context) return this.
|
|
889
|
-
return this.
|
|
888
|
+
if (context) return this.switchToContext(context);
|
|
889
|
+
return this.switchToContext('NATIVE_APP');
|
|
890
890
|
}
|
|
891
891
|
|
|
892
892
|
/**
|
package/lib/helper/Playwright.js
CHANGED
|
@@ -94,6 +94,7 @@ const pathSeparator = path.sep;
|
|
|
94
94
|
* @prop {boolean} [ignoreHTTPSErrors] - Allows access to untrustworthy pages, e.g. to a page with an expired certificate. Default value is `false`
|
|
95
95
|
* @prop {boolean} [bypassCSP] - bypass Content Security Policy or CSP
|
|
96
96
|
* @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
|
|
97
|
+
* @prop {object} [recordHar] - record HAR and will be saved to `output/har`. See more of [HAR options](https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har).
|
|
97
98
|
*/
|
|
98
99
|
const config = {};
|
|
99
100
|
|
|
@@ -141,6 +142,21 @@ const config = {};
|
|
|
141
142
|
* * `trace`: enables trace recording for failed tests; trace are saved into `output/trace` folder
|
|
142
143
|
* * `keepTraceForPassedTests`: - save trace for passed tests
|
|
143
144
|
*
|
|
145
|
+
* #### HAR Recording Customization
|
|
146
|
+
*
|
|
147
|
+
* A HAR file is an HTTP Archive file that contains a record of all the network requests that are made when a page is loaded.
|
|
148
|
+
* It contains information about the request and response headers, cookies, content, timings, and more. You can use HAR files to mock network requests in your tests.
|
|
149
|
+
* HAR will be saved to `output/har`. More info could be found here https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har.
|
|
150
|
+
*
|
|
151
|
+
* ```
|
|
152
|
+
* ...
|
|
153
|
+
* recordHar: {
|
|
154
|
+
* mode: 'minimal', // possible values: 'minimal'|'full'.
|
|
155
|
+
* content: 'embed' // possible values: "omit"|"embed"|"attach".
|
|
156
|
+
* }
|
|
157
|
+
* ...
|
|
158
|
+
*```
|
|
159
|
+
*
|
|
144
160
|
* #### Example #1: Wait for 0 network connections.
|
|
145
161
|
*
|
|
146
162
|
* ```js
|
|
@@ -346,7 +362,7 @@ class Playwright extends Helper {
|
|
|
346
362
|
ignoreLog: ['warning', 'log'],
|
|
347
363
|
uniqueScreenshotNames: false,
|
|
348
364
|
manualStart: false,
|
|
349
|
-
getPageTimeout:
|
|
365
|
+
getPageTimeout: 30000,
|
|
350
366
|
waitForNavigation: 'load',
|
|
351
367
|
restart: false,
|
|
352
368
|
keepCookies: false,
|
|
@@ -455,9 +471,10 @@ class Playwright extends Helper {
|
|
|
455
471
|
}
|
|
456
472
|
}
|
|
457
473
|
|
|
458
|
-
async _before() {
|
|
474
|
+
async _before(test) {
|
|
475
|
+
this.currentRunningTest = test;
|
|
459
476
|
recorder.retry({
|
|
460
|
-
retries:
|
|
477
|
+
retries: process.env.FAILED_STEP_RETRIES || 3,
|
|
461
478
|
when: err => {
|
|
462
479
|
if (!err || typeof (err.message) !== 'string') {
|
|
463
480
|
return false;
|
|
@@ -487,6 +504,15 @@ class Playwright extends Helper {
|
|
|
487
504
|
}
|
|
488
505
|
if (this.options.bypassCSP) contextOptions.bypassCSP = this.options.bypassCSP;
|
|
489
506
|
if (this.options.recordVideo) contextOptions.recordVideo = this.options.recordVideo;
|
|
507
|
+
if (this.options.recordHar) {
|
|
508
|
+
const harExt = this.options.recordHar.content && this.options.recordHar.content === 'attach' ? 'zip' : 'har';
|
|
509
|
+
const fileName = `${`${global.output_dir}${path.sep}har${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.${harExt}`;
|
|
510
|
+
const dir = path.dirname(fileName);
|
|
511
|
+
if (!fileExists(dir)) fs.mkdirSync(dir);
|
|
512
|
+
this.options.recordHar.path = fileName;
|
|
513
|
+
this.currentRunningTest.artifacts.har = fileName;
|
|
514
|
+
contextOptions.recordHar = this.options.recordHar;
|
|
515
|
+
}
|
|
490
516
|
if (this.storageState) contextOptions.storageState = this.storageState;
|
|
491
517
|
if (this.options.userAgent) contextOptions.userAgent = this.options.userAgent;
|
|
492
518
|
if (this.options.locale) contextOptions.locale = this.options.locale;
|
|
@@ -834,6 +860,7 @@ class Playwright extends Helper {
|
|
|
834
860
|
this.context = null;
|
|
835
861
|
this.frame = null;
|
|
836
862
|
popupStore.clear();
|
|
863
|
+
if (this.options.recordHar) await this.browserContext.close();
|
|
837
864
|
await this.browser.close();
|
|
838
865
|
}
|
|
839
866
|
|
|
@@ -1089,6 +1116,33 @@ class Playwright extends Helper {
|
|
|
1089
1116
|
return this.page.reload({ timeout: this.options.getPageTimeout, waitUntil: this.options.waitForNavigation });
|
|
1090
1117
|
}
|
|
1091
1118
|
|
|
1119
|
+
/**
|
|
1120
|
+
* Replaying from HAR
|
|
1121
|
+
*
|
|
1122
|
+
* ```js
|
|
1123
|
+
* // Replay API requests from HAR.
|
|
1124
|
+
* // Either use a matching response from the HAR,
|
|
1125
|
+
* // or abort the request if nothing matches.
|
|
1126
|
+
* I.replayFromHar('./output/har/something.har', { url: "*\/**\/api/v1/fruits" });
|
|
1127
|
+
* I.amOnPage('https://demo.playwright.dev/api-mocking');
|
|
1128
|
+
* I.see('CodeceptJS');
|
|
1129
|
+
* ```
|
|
1130
|
+
*
|
|
1131
|
+
* @param {string} harFilePath Path to recorded HAR file
|
|
1132
|
+
* @param {object} [opts] [Options for replaying from HAR](https://playwright.dev/docs/api/class-page#page-route-from-har)
|
|
1133
|
+
*
|
|
1134
|
+
* @returns Promise<void>
|
|
1135
|
+
*/
|
|
1136
|
+
async replayFromHar(harFilePath, opts) {
|
|
1137
|
+
const file = path.join(global.codecept_dir, harFilePath);
|
|
1138
|
+
|
|
1139
|
+
if (!fileExists(file)) {
|
|
1140
|
+
throw new Error(`File at ${file} cannot be found on local system`);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
await this.page.routeFromHAR(harFilePath, opts);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1092
1146
|
/**
|
|
1093
1147
|
* {{> scrollPageToTop }}
|
|
1094
1148
|
*/
|
|
@@ -1711,8 +1765,15 @@ class Playwright extends Helper {
|
|
|
1711
1765
|
const el = els[0];
|
|
1712
1766
|
|
|
1713
1767
|
await highlightActiveElement.call(this, el);
|
|
1768
|
+
let optionToSelect = '';
|
|
1769
|
+
|
|
1770
|
+
try {
|
|
1771
|
+
optionToSelect = await el.locator('option', { hasText: option }).textContent();
|
|
1772
|
+
} catch (e) {
|
|
1773
|
+
optionToSelect = option;
|
|
1774
|
+
}
|
|
1714
1775
|
|
|
1715
|
-
if (!Array.isArray(option)) option = [
|
|
1776
|
+
if (!Array.isArray(option)) option = [optionToSelect];
|
|
1716
1777
|
|
|
1717
1778
|
await el.selectOption(option);
|
|
1718
1779
|
return this._waitForAction();
|
|
@@ -2062,7 +2123,9 @@ class Playwright extends Helper {
|
|
|
2062
2123
|
let chunked = chunkArray(props, values.length);
|
|
2063
2124
|
chunked = chunked.filter((val) => {
|
|
2064
2125
|
for (let i = 0; i < val.length; ++i) {
|
|
2065
|
-
|
|
2126
|
+
const _acutal = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(val[i], 10);
|
|
2127
|
+
const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
|
|
2128
|
+
if (_acutal !== _expected) return false;
|
|
2066
2129
|
}
|
|
2067
2130
|
return true;
|
|
2068
2131
|
});
|
|
@@ -2262,6 +2325,10 @@ class Playwright extends Helper {
|
|
|
2262
2325
|
test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.failed`);
|
|
2263
2326
|
}
|
|
2264
2327
|
}
|
|
2328
|
+
|
|
2329
|
+
if (this.options.recordHar) {
|
|
2330
|
+
test.artifacts.har = this.currentRunningTest.artifacts.har;
|
|
2331
|
+
}
|
|
2265
2332
|
}
|
|
2266
2333
|
|
|
2267
2334
|
async _passed(test) {
|
|
@@ -2289,6 +2356,10 @@ class Playwright extends Helper {
|
|
|
2289
2356
|
await this.browserContext.tracing.stop();
|
|
2290
2357
|
}
|
|
2291
2358
|
}
|
|
2359
|
+
|
|
2360
|
+
if (this.options.recordHar) {
|
|
2361
|
+
test.artifacts.har = this.currentRunningTest.artifacts.har;
|
|
2362
|
+
}
|
|
2292
2363
|
}
|
|
2293
2364
|
|
|
2294
2365
|
/**
|
package/lib/helper/Puppeteer.js
CHANGED
|
@@ -297,7 +297,7 @@ class Puppeteer extends Helper {
|
|
|
297
297
|
this.sessionPages = {};
|
|
298
298
|
this.currentRunningTest = test;
|
|
299
299
|
recorder.retry({
|
|
300
|
-
retries: 3,
|
|
300
|
+
retries: process.env.FAILED_STEP_RETRIES || 3,
|
|
301
301
|
when: err => {
|
|
302
302
|
if (!err || typeof (err.message) !== 'string') {
|
|
303
303
|
return false;
|
|
@@ -1784,7 +1784,9 @@ class Puppeteer extends Helper {
|
|
|
1784
1784
|
let chunked = chunkArray(props, values.length);
|
|
1785
1785
|
chunked = chunked.filter((val) => {
|
|
1786
1786
|
for (let i = 0; i < val.length; ++i) {
|
|
1787
|
-
|
|
1787
|
+
const _acutal = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(val[i], 10);
|
|
1788
|
+
const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
|
|
1789
|
+
if (_acutal !== _expected) return false;
|
|
1788
1790
|
}
|
|
1789
1791
|
return true;
|
|
1790
1792
|
});
|
|
@@ -1815,7 +1817,9 @@ class Puppeteer extends Helper {
|
|
|
1815
1817
|
let chunked = chunkArray(attrs, values.length);
|
|
1816
1818
|
chunked = chunked.filter((val) => {
|
|
1817
1819
|
for (let i = 0; i < val.length; ++i) {
|
|
1818
|
-
|
|
1820
|
+
const _acutal = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(val[i], 10);
|
|
1821
|
+
const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
|
|
1822
|
+
if (_acutal !== _expected) return false;
|
|
1819
1823
|
}
|
|
1820
1824
|
return true;
|
|
1821
1825
|
});
|
package/lib/helper/WebDriver.js
CHANGED
|
@@ -1508,7 +1508,9 @@ class WebDriver extends Helper {
|
|
|
1508
1508
|
let chunked = chunkArray(props, values.length);
|
|
1509
1509
|
chunked = chunked.filter((val) => {
|
|
1510
1510
|
for (let i = 0; i < val.length; ++i) {
|
|
1511
|
-
|
|
1511
|
+
const _acutal = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(val[i], 10);
|
|
1512
|
+
const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
|
|
1513
|
+
if (_acutal !== _expected) return false;
|
|
1512
1514
|
}
|
|
1513
1515
|
return true;
|
|
1514
1516
|
});
|
|
@@ -1535,7 +1537,9 @@ class WebDriver extends Helper {
|
|
|
1535
1537
|
let chunked = chunkArray(attrs, values.length);
|
|
1536
1538
|
chunked = chunked.filter((val) => {
|
|
1537
1539
|
for (let i = 0; i < val.length; ++i) {
|
|
1538
|
-
|
|
1540
|
+
const _acutal = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(val[i], 10);
|
|
1541
|
+
const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
|
|
1542
|
+
if (_acutal !== _expected) return false;
|
|
1539
1543
|
}
|
|
1540
1544
|
return true;
|
|
1541
1545
|
});
|
|
@@ -119,7 +119,14 @@ module.exports = (text, file) => {
|
|
|
119
119
|
});
|
|
120
120
|
}
|
|
121
121
|
const tags = child.scenario.tags.map(t => t.name).concat(examples.tags.map(t => t.name));
|
|
122
|
-
|
|
122
|
+
let title = `${child.scenario.name} ${JSON.stringify(current)} ${tags.join(' ')}`.trim();
|
|
123
|
+
|
|
124
|
+
for (const [key, value] of Object.entries(current)) {
|
|
125
|
+
if (title.includes(`<${key}>`)) {
|
|
126
|
+
title = title.replace(JSON.stringify(current), '').replace(`<${key}>`, value);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
123
130
|
const test = new Test(title, async () => runSteps(addExampleInTable(exampleSteps, current)));
|
|
124
131
|
test.tags = suite.tags.concat(tags);
|
|
125
132
|
test.file = file;
|
package/lib/locator.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const cssToXPath = require('
|
|
1
|
+
const cssToXPath = require('convert-cssxpath');
|
|
2
2
|
const { sprintf } = require('sprintf-js');
|
|
3
3
|
|
|
4
4
|
const { xpathLocator } = require('./utils');
|
|
@@ -162,7 +162,7 @@ class Locator {
|
|
|
162
162
|
*/
|
|
163
163
|
toXPath() {
|
|
164
164
|
if (this.isXPath()) return this.value;
|
|
165
|
-
if (this.isCSS()) return cssToXPath(this.value);
|
|
165
|
+
if (this.isCSS()) return cssToXPath.convert(this.value);
|
|
166
166
|
|
|
167
167
|
throw new Error('Can\'t be converted to XPath');
|
|
168
168
|
}
|
package/lib/plugin/autoLogin.js
CHANGED
|
@@ -38,12 +38,14 @@ const defaultConfig = {
|
|
|
38
38
|
* ```js
|
|
39
39
|
* // inside a test file
|
|
40
40
|
* // use login to inject auto-login function
|
|
41
|
+
* Feature('Login');
|
|
42
|
+
*
|
|
41
43
|
* Before(({ login }) => {
|
|
42
44
|
* login('user'); // login using user session
|
|
43
45
|
* });
|
|
44
46
|
*
|
|
45
|
-
* // Alternatively log in for one scenario
|
|
46
|
-
* Scenario('log me in', ( {I, login} ) => {
|
|
47
|
+
* // Alternatively log in for one scenario.
|
|
48
|
+
* Scenario('log me in', ( { I, login } ) => {
|
|
47
49
|
* login('admin');
|
|
48
50
|
* I.see('I am logged in');
|
|
49
51
|
* });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const event = require('../event');
|
|
2
2
|
const recorder = require('../recorder');
|
|
3
|
+
const container = require('../container');
|
|
3
4
|
|
|
4
5
|
const defaultConfig = {
|
|
5
6
|
retries: 3,
|
|
@@ -98,6 +99,8 @@ module.exports = (config) => {
|
|
|
98
99
|
config.when = when;
|
|
99
100
|
|
|
100
101
|
event.dispatcher.on(event.step.started, (step) => {
|
|
102
|
+
if (container.plugins('tryTo')) return;
|
|
103
|
+
|
|
101
104
|
// if a step is ignored - return
|
|
102
105
|
for (const ignored of config.ignoredSteps) {
|
|
103
106
|
if (step.name === ignored) return;
|
|
@@ -114,6 +117,8 @@ module.exports = (config) => {
|
|
|
114
117
|
|
|
115
118
|
event.dispatcher.on(event.test.before, (test) => {
|
|
116
119
|
if (test && test.disableRetryFailedStep) return; // disable retry when a test is not active
|
|
120
|
+
// this env var is used to set the retries inside _before() block of helpers
|
|
121
|
+
process.env.FAILED_STEP_RETRIES = config.retries;
|
|
117
122
|
recorder.retry(config);
|
|
118
123
|
});
|
|
119
124
|
};
|
|
@@ -89,8 +89,8 @@ module.exports = function (config) {
|
|
|
89
89
|
const reportDir = config.output ? path.resolve(global.codecept_dir, config.output) : defaultConfig.output;
|
|
90
90
|
|
|
91
91
|
event.dispatcher.on(event.test.before, (test) => {
|
|
92
|
-
const
|
|
93
|
-
dir = path.join(reportDir, `record_${
|
|
92
|
+
const sha256hash = crypto.createHash('sha256').update(test.file + test.title).digest('hex');
|
|
93
|
+
dir = path.join(reportDir, `record_${sha256hash}`);
|
|
94
94
|
mkdirp.sync(dir);
|
|
95
95
|
stepNum = 0;
|
|
96
96
|
error = null;
|
package/lib/ui.js
CHANGED
|
@@ -168,6 +168,7 @@ module.exports = function (suite) {
|
|
|
168
168
|
context.Scenario.only = function (title, opts, fn) {
|
|
169
169
|
const reString = `^${escapeRe(`${suites[0].title}: ${title}`.replace(/( \| {.+})?$/g, ''))}`;
|
|
170
170
|
mocha.grep(new RegExp(reString));
|
|
171
|
+
process.env.SCENARIO_ONLY = true;
|
|
171
172
|
return addScenario(title, opts, fn);
|
|
172
173
|
};
|
|
173
174
|
|
package/lib/workers.js
CHANGED
|
@@ -359,6 +359,7 @@ class Workers extends EventEmitter {
|
|
|
359
359
|
this.stats.start = new Date();
|
|
360
360
|
recorder.startUnlessRunning();
|
|
361
361
|
event.dispatcher.emit(event.workers.before);
|
|
362
|
+
process.env.RUNS_WITH_WORKERS = 'true';
|
|
362
363
|
recorder.add('starting workers', () => {
|
|
363
364
|
for (const worker of this.workers) {
|
|
364
365
|
const workerThread = createWorker(worker);
|
|
@@ -492,6 +493,7 @@ class Workers extends EventEmitter {
|
|
|
492
493
|
}
|
|
493
494
|
|
|
494
495
|
output.result(this.stats.passes, this.stats.failures, this.stats.pending, ms(this.stats.duration));
|
|
496
|
+
process.env.RUNS_WITH_WORKERS = 'false';
|
|
495
497
|
}
|
|
496
498
|
}
|
|
497
499
|
|