codeceptjs 3.5.9 → 3.5.10
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/README.md +14 -16
- package/docs/build/Appium.js +13 -13
- package/docs/build/Playwright.js +116 -14
- package/docs/build/Puppeteer.js +36 -20
- package/docs/build/WebDriver.js +23 -2
- package/docs/helpers/Appium.md +2 -2
- package/docs/helpers/Playwright.md +371 -303
- package/docs/helpers/Puppeteer.md +15 -0
- package/docs/helpers/WebDriver.md +15 -0
- package/docs/internal-api.md +1 -0
- package/docs/parallel.md +114 -2
- package/docs/plugins.md +4 -2
- package/docs/webapi/grabWebElement.mustache +9 -0
- package/docs/webapi/grabWebElements.mustache +9 -0
- package/lib/ai.js +12 -3
- package/lib/colorUtils.js +10 -0
- 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 +13 -13
- package/lib/helper/Playwright.js +98 -14
- package/lib/helper/Puppeteer.js +27 -20
- package/lib/helper/WebDriver.js +14 -2
- package/lib/html.js +3 -3
- package/lib/interfaces/gherkin.js +8 -1
- package/lib/interfaces/scenarioConfig.js +1 -0
- package/lib/locator.js +2 -2
- package/lib/pause.js +6 -3
- package/lib/plugin/autoLogin.js +4 -2
- package/lib/plugin/heal.js +40 -7
- package/lib/plugin/retryFailedStep.js +5 -0
- package/lib/plugin/stepByStepReport.js +2 -2
- package/lib/recorder.js +12 -5
- package/lib/ui.js +1 -0
- package/lib/workers.js +2 -0
- package/package.json +17 -14
- package/typings/promiseBasedTypes.d.ts +76 -2
- package/typings/types.d.ts +80 -2
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Reference: [Helpers API](https://github.com/codeceptjs/CodeceptJS/tree/master/do
|
|
|
24
24
|
## Supercharged E2E Testing
|
|
25
25
|
|
|
26
26
|
CodeceptJS is a new testing framework for end-to-end testing with WebDriver (or others).
|
|
27
|
-
It abstracts browser interaction to simple steps that are written from a user perspective.
|
|
27
|
+
It abstracts browser interaction to simple steps that are written from a user's perspective.
|
|
28
28
|
A simple test that verifies the "Welcome" text is present on a main page of a site will look like:
|
|
29
29
|
|
|
30
30
|
```js
|
|
@@ -57,27 +57,27 @@ And more to come...
|
|
|
57
57
|
|
|
58
58
|
CodeceptJS is a successor of [Codeception](http://codeception.com), a popular full-stack testing framework for PHP.
|
|
59
59
|
With CodeceptJS your scenario-driven functional and acceptance tests will be as simple and clean as they can be.
|
|
60
|
-
You don't need to worry about asynchronous nature of NodeJS or about various APIs of Selenium, Puppeteer, TestCafe, etc. as CodeceptJS unifies them and makes them work as they are synchronous.
|
|
60
|
+
You don't need to worry about asynchronous nature of NodeJS or about various APIs of Playwright, Selenium, Puppeteer, TestCafe, etc. as CodeceptJS unifies them and makes them work as they are synchronous.
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
## Features
|
|
64
64
|
|
|
65
|
-
* 🪄 **AI-powered** with GPT features to assist and heal failing tests
|
|
66
|
-
* Based on [Mocha](https://mochajs.org/) testing framework.
|
|
67
|
-
* Designed for scenario driven acceptance testing in BDD-style
|
|
68
|
-
* Uses ES6 natively without transpiler.
|
|
65
|
+
* 🪄 **AI-powered** with GPT features to assist and heal failing tests.
|
|
66
|
+
* ☕ Based on [Mocha](https://mochajs.org/) testing framework.
|
|
67
|
+
* 💼 Designed for scenario driven acceptance testing in BDD-style.
|
|
68
|
+
* 💻 Uses ES6 natively without transpiler.
|
|
69
69
|
* Also plays nice with TypeScript.
|
|
70
|
-
* Smart locators: use names, labels, matching text, CSS or XPath to locate elements.
|
|
71
|
-
* Interactive debugging shell: pause test at any point and try different commands in a browser.
|
|
70
|
+
* </> Smart locators: use names, labels, matching text, CSS or XPath to locate elements.
|
|
71
|
+
* 🌐 Interactive debugging shell: pause test at any point and try different commands in a browser.
|
|
72
72
|
* Easily create tests, pageobjects, stepobjects with CLI generators.
|
|
73
73
|
|
|
74
|
-
##
|
|
74
|
+
## Installation
|
|
75
75
|
|
|
76
76
|
```sh
|
|
77
77
|
npm i codeceptjs --save
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
Move to directory where you'd like to have your tests (and
|
|
80
|
+
Move to directory where you'd like to have your tests (and CodeceptJS config) stored, and execute:
|
|
81
81
|
|
|
82
82
|
```sh
|
|
83
83
|
npx codeceptjs init
|
|
@@ -131,8 +131,8 @@ Scenario('test some forms', ({ I }) => {
|
|
|
131
131
|
});
|
|
132
132
|
```
|
|
133
133
|
|
|
134
|
-
All actions are performed by I object; assertions functions start with `see` function.
|
|
135
|
-
In
|
|
134
|
+
All actions are performed by `I` object; assertions functions start with `see` function.
|
|
135
|
+
In these examples all methods of `I` are taken from WebDriver helper, see [reference](https://github.com/codeceptjs/CodeceptJS/blob/master/docs/helpers/WebDriver.md) to learn how to use them.
|
|
136
136
|
|
|
137
137
|
Let's execute this test with `run` command. Additional option `--steps` will show us the running process. We recommend use `--steps` or `--debug` during development.
|
|
138
138
|
|
|
@@ -198,17 +198,15 @@ In this case 'User is valid' string will be searched only inside elements locate
|
|
|
198
198
|
### Grabbers
|
|
199
199
|
|
|
200
200
|
In case you need to return a value from a webpage and use it directly in test, you should use methods with `grab` prefix.
|
|
201
|
-
They are expected to be used inside async/await functions, and their results will be available in test:
|
|
201
|
+
They are expected to be used inside `async/await` functions, and their results will be available in test:
|
|
202
202
|
|
|
203
203
|
```js
|
|
204
|
-
const assert = require('assert');
|
|
205
|
-
|
|
206
204
|
Feature('CodeceptJS Demonstration');
|
|
207
205
|
|
|
208
206
|
Scenario('test page title', async ({ I }) => {
|
|
209
207
|
I.amOnPage('http://simple-form-bootstrap.plataformatec.com.br/documentation');
|
|
210
208
|
const title = await I.grabTitle();
|
|
211
|
-
|
|
209
|
+
I.expectEqual(title, 'Example application with SimpleForm and Twitter Bootstrap'); // Avaiable with Expect helper. -> https://codecept.io/helpers/Expect/
|
|
212
210
|
});
|
|
213
211
|
```
|
|
214
212
|
|
package/docs/build/Appium.js
CHANGED
|
@@ -276,7 +276,7 @@ class Appium extends Webdriver {
|
|
|
276
276
|
const _convertedCaps = {};
|
|
277
277
|
for (const [key, value] of Object.entries(capabilities)) {
|
|
278
278
|
if (!key.startsWith(vendorPrefix.appium)) {
|
|
279
|
-
if (key !== 'platformName') {
|
|
279
|
+
if (key !== 'platformName' && key !== 'bstack:options') {
|
|
280
280
|
_convertedCaps[`${vendorPrefix.appium}:${key}`] = value;
|
|
281
281
|
} else {
|
|
282
282
|
_convertedCaps[`${key}`] = value;
|
|
@@ -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
|
/**
|
|
@@ -1424,10 +1424,10 @@ class Appium extends Webdriver {
|
|
|
1424
1424
|
*
|
|
1425
1425
|
* @return {Promise<void>}
|
|
1426
1426
|
*
|
|
1427
|
-
* Appium: support
|
|
1427
|
+
* Appium: support both Android and iOS
|
|
1428
1428
|
*/
|
|
1429
1429
|
async closeApp() {
|
|
1430
|
-
onlyForApps.call(this
|
|
1430
|
+
onlyForApps.call(this);
|
|
1431
1431
|
return this.browser.closeApp();
|
|
1432
1432
|
}
|
|
1433
1433
|
|
package/docs/build/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;
|
|
@@ -842,6 +868,7 @@ class Playwright extends Helper {
|
|
|
842
868
|
this.context = null;
|
|
843
869
|
this.frame = null;
|
|
844
870
|
popupStore.clear();
|
|
871
|
+
if (this.options.recordHar) await this.browserContext.close();
|
|
845
872
|
await this.browser.close();
|
|
846
873
|
}
|
|
847
874
|
|
|
@@ -1172,6 +1199,33 @@ class Playwright extends Helper {
|
|
|
1172
1199
|
return this.page.reload({ timeout: this.options.getPageTimeout, waitUntil: this.options.waitForNavigation });
|
|
1173
1200
|
}
|
|
1174
1201
|
|
|
1202
|
+
/**
|
|
1203
|
+
* Replaying from HAR
|
|
1204
|
+
*
|
|
1205
|
+
* ```js
|
|
1206
|
+
* // Replay API requests from HAR.
|
|
1207
|
+
* // Either use a matching response from the HAR,
|
|
1208
|
+
* // or abort the request if nothing matches.
|
|
1209
|
+
* I.replayFromHar('./output/har/something.har', { url: "*\/**\/api/v1/fruits" });
|
|
1210
|
+
* I.amOnPage('https://demo.playwright.dev/api-mocking');
|
|
1211
|
+
* I.see('CodeceptJS');
|
|
1212
|
+
* ```
|
|
1213
|
+
*
|
|
1214
|
+
* @param {string} harFilePath Path to recorded HAR file
|
|
1215
|
+
* @param {object} [opts] [Options for replaying from HAR](https://playwright.dev/docs/api/class-page#page-route-from-har)
|
|
1216
|
+
*
|
|
1217
|
+
* @returns Promise<void>
|
|
1218
|
+
*/
|
|
1219
|
+
async replayFromHar(harFilePath, opts) {
|
|
1220
|
+
const file = path.join(global.codecept_dir, harFilePath);
|
|
1221
|
+
|
|
1222
|
+
if (!fileExists(file)) {
|
|
1223
|
+
throw new Error(`File at ${file} cannot be found on local system`);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
await this.page.routeFromHAR(harFilePath, opts);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1175
1229
|
/**
|
|
1176
1230
|
* Scroll page to the top.
|
|
1177
1231
|
*
|
|
@@ -1396,6 +1450,40 @@ class Playwright extends Helper {
|
|
|
1396
1450
|
return findFields.call(this, locator);
|
|
1397
1451
|
}
|
|
1398
1452
|
|
|
1453
|
+
/**
|
|
1454
|
+
* Grab WebElements for given locator
|
|
1455
|
+
* Resumes test execution, so **should be used inside an async function with `await`** operator.
|
|
1456
|
+
*
|
|
1457
|
+
* ```js
|
|
1458
|
+
* const webElements = await I.grabWebElements('#button');
|
|
1459
|
+
* ```
|
|
1460
|
+
*
|
|
1461
|
+
* @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
|
|
1462
|
+
* @returns {Promise<*>} WebElement of being used Web helper
|
|
1463
|
+
*
|
|
1464
|
+
*
|
|
1465
|
+
*/
|
|
1466
|
+
async grabWebElements(locator) {
|
|
1467
|
+
return this._locate(locator);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* Grab WebElement for given locator
|
|
1472
|
+
* Resumes test execution, so **should be used inside an async function with `await`** operator.
|
|
1473
|
+
*
|
|
1474
|
+
* ```js
|
|
1475
|
+
* const webElement = await I.grabWebElement('#button');
|
|
1476
|
+
* ```
|
|
1477
|
+
*
|
|
1478
|
+
* @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
|
|
1479
|
+
* @returns {Promise<*>} WebElement of being used Web helper
|
|
1480
|
+
*
|
|
1481
|
+
*
|
|
1482
|
+
*/
|
|
1483
|
+
async grabWebElement(locator) {
|
|
1484
|
+
return this._locateElement(locator);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1399
1487
|
/**
|
|
1400
1488
|
* Switch focus to a particular tab by its number. It waits tabs loading and then switch tab
|
|
1401
1489
|
*
|
|
@@ -2211,8 +2299,15 @@ class Playwright extends Helper {
|
|
|
2211
2299
|
const el = els[0];
|
|
2212
2300
|
|
|
2213
2301
|
await highlightActiveElement.call(this, el);
|
|
2302
|
+
let optionToSelect = '';
|
|
2214
2303
|
|
|
2215
|
-
|
|
2304
|
+
try {
|
|
2305
|
+
optionToSelect = await el.locator('option', { hasText: option }).textContent();
|
|
2306
|
+
} catch (e) {
|
|
2307
|
+
optionToSelect = option;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
if (!Array.isArray(option)) option = [optionToSelect];
|
|
2216
2311
|
|
|
2217
2312
|
await el.selectOption(option);
|
|
2218
2313
|
return this._waitForAction();
|
|
@@ -2802,19 +2897,17 @@ class Playwright extends Helper {
|
|
|
2802
2897
|
|
|
2803
2898
|
const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties);
|
|
2804
2899
|
const elemAmount = res.length;
|
|
2805
|
-
const commands = [];
|
|
2806
2900
|
let props = [];
|
|
2807
2901
|
|
|
2808
2902
|
for (const element of res) {
|
|
2809
|
-
const
|
|
2810
|
-
|
|
2811
|
-
Object.keys(cssPropertiesCamelCase).forEach(prop => {
|
|
2903
|
+
for (const prop of Object.keys(cssProperties)) {
|
|
2904
|
+
const cssProp = await this.grabCssPropertyFrom(locator, prop);
|
|
2812
2905
|
if (isColorProperty(prop)) {
|
|
2813
|
-
props.push(convertColorToRGBA(
|
|
2906
|
+
props.push(convertColorToRGBA(cssProp));
|
|
2814
2907
|
} else {
|
|
2815
|
-
props.push(
|
|
2908
|
+
props.push(cssProp);
|
|
2816
2909
|
}
|
|
2817
|
-
}
|
|
2910
|
+
}
|
|
2818
2911
|
}
|
|
2819
2912
|
|
|
2820
2913
|
const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key]);
|
|
@@ -2822,7 +2915,8 @@ class Playwright extends Helper {
|
|
|
2822
2915
|
let chunked = chunkArray(props, values.length);
|
|
2823
2916
|
chunked = chunked.filter((val) => {
|
|
2824
2917
|
for (let i = 0; i < val.length; ++i) {
|
|
2825
|
-
|
|
2918
|
+
// eslint-disable-next-line eqeqeq
|
|
2919
|
+
if (val[i] != values[i]) return false;
|
|
2826
2920
|
}
|
|
2827
2921
|
return true;
|
|
2828
2922
|
});
|
|
@@ -2860,7 +2954,7 @@ class Playwright extends Helper {
|
|
|
2860
2954
|
let chunked = chunkArray(attrs, values.length);
|
|
2861
2955
|
chunked = chunked.filter((val) => {
|
|
2862
2956
|
for (let i = 0; i < val.length; ++i) {
|
|
2863
|
-
if (val[i]
|
|
2957
|
+
if (!val[i].includes(values[i])) return false;
|
|
2864
2958
|
}
|
|
2865
2959
|
return true;
|
|
2866
2960
|
});
|
|
@@ -3083,6 +3177,10 @@ class Playwright extends Helper {
|
|
|
3083
3177
|
test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.failed`);
|
|
3084
3178
|
}
|
|
3085
3179
|
}
|
|
3180
|
+
|
|
3181
|
+
if (this.options.recordHar) {
|
|
3182
|
+
test.artifacts.har = this.currentRunningTest.artifacts.har;
|
|
3183
|
+
}
|
|
3086
3184
|
}
|
|
3087
3185
|
|
|
3088
3186
|
async _passed(test) {
|
|
@@ -3110,6 +3208,10 @@ class Playwright extends Helper {
|
|
|
3110
3208
|
await this.browserContext.tracing.stop();
|
|
3111
3209
|
}
|
|
3112
3210
|
}
|
|
3211
|
+
|
|
3212
|
+
if (this.options.recordHar) {
|
|
3213
|
+
test.artifacts.har = this.currentRunningTest.artifacts.har;
|
|
3214
|
+
}
|
|
3113
3215
|
}
|
|
3114
3216
|
|
|
3115
3217
|
/**
|
|
@@ -3520,7 +3622,7 @@ class Playwright extends Helper {
|
|
|
3520
3622
|
const _contextObject = this.frame ? this.frame : contextObject;
|
|
3521
3623
|
let count = 0;
|
|
3522
3624
|
do {
|
|
3523
|
-
waiter = await _contextObject.locator(`:has-text(
|
|
3625
|
+
waiter = await _contextObject.locator(`:has-text("${text}")`).first().isVisible();
|
|
3524
3626
|
if (waiter) break;
|
|
3525
3627
|
await this.wait(1);
|
|
3526
3628
|
count += 1000;
|
package/docs/build/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;
|
|
@@ -1063,6 +1063,23 @@ class Puppeteer extends Helper {
|
|
|
1063
1063
|
return findFields.call(this, locator);
|
|
1064
1064
|
}
|
|
1065
1065
|
|
|
1066
|
+
/**
|
|
1067
|
+
* Grab WebElements for given locator
|
|
1068
|
+
* Resumes test execution, so **should be used inside an async function with `await`** operator.
|
|
1069
|
+
*
|
|
1070
|
+
* ```js
|
|
1071
|
+
* const webElements = await I.grabWebElements('#button');
|
|
1072
|
+
* ```
|
|
1073
|
+
*
|
|
1074
|
+
* @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
|
|
1075
|
+
* @returns {Promise<*>} WebElement of being used Web helper
|
|
1076
|
+
*
|
|
1077
|
+
*
|
|
1078
|
+
*/
|
|
1079
|
+
async grabWebElements(locator) {
|
|
1080
|
+
return this._locate(locator);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1066
1083
|
/**
|
|
1067
1084
|
* Switch focus to a particular tab by its number. It waits tabs loading and then switch tab
|
|
1068
1085
|
*
|
|
@@ -2589,29 +2606,26 @@ class Puppeteer extends Helper {
|
|
|
2589
2606
|
|
|
2590
2607
|
const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties);
|
|
2591
2608
|
const elemAmount = res.length;
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
.
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
}));
|
|
2606
|
-
});
|
|
2607
|
-
});
|
|
2608
|
-
let props = await Promise.all(commands);
|
|
2609
|
+
let props = [];
|
|
2610
|
+
|
|
2611
|
+
for (const element of res) {
|
|
2612
|
+
for (const prop of Object.keys(cssProperties)) {
|
|
2613
|
+
const cssProp = await this.grabCssPropertyFrom(locator, prop);
|
|
2614
|
+
if (isColorProperty(prop)) {
|
|
2615
|
+
props.push(convertColorToRGBA(cssProp));
|
|
2616
|
+
} else {
|
|
2617
|
+
props.push(cssProp);
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2609
2622
|
const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key]);
|
|
2610
2623
|
if (!Array.isArray(props)) props = [props];
|
|
2611
2624
|
let chunked = chunkArray(props, values.length);
|
|
2612
2625
|
chunked = chunked.filter((val) => {
|
|
2613
2626
|
for (let i = 0; i < val.length; ++i) {
|
|
2614
|
-
|
|
2627
|
+
// eslint-disable-next-line eqeqeq
|
|
2628
|
+
if (val[i] != values[i]) return false;
|
|
2615
2629
|
}
|
|
2616
2630
|
return true;
|
|
2617
2631
|
});
|
|
@@ -2651,7 +2665,9 @@ class Puppeteer extends Helper {
|
|
|
2651
2665
|
let chunked = chunkArray(attrs, values.length);
|
|
2652
2666
|
chunked = chunked.filter((val) => {
|
|
2653
2667
|
for (let i = 0; i < val.length; ++i) {
|
|
2654
|
-
|
|
2668
|
+
const _actual = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(values[i], 10);
|
|
2669
|
+
const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
|
|
2670
|
+
if (!_actual.includes(_expected)) return false;
|
|
2655
2671
|
}
|
|
2656
2672
|
return true;
|
|
2657
2673
|
});
|
package/docs/build/WebDriver.js
CHANGED
|
@@ -869,6 +869,23 @@ class WebDriver extends Helper {
|
|
|
869
869
|
return findFields.call(this, locator).then(res => res);
|
|
870
870
|
}
|
|
871
871
|
|
|
872
|
+
/**
|
|
873
|
+
* Grab WebElements for given locator
|
|
874
|
+
* Resumes test execution, so **should be used inside an async function with `await`** operator.
|
|
875
|
+
*
|
|
876
|
+
* ```js
|
|
877
|
+
* const webElements = await I.grabWebElements('#button');
|
|
878
|
+
* ```
|
|
879
|
+
*
|
|
880
|
+
* @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
|
|
881
|
+
* @returns {Promise<*>} WebElement of being used Web helper
|
|
882
|
+
*
|
|
883
|
+
*
|
|
884
|
+
*/
|
|
885
|
+
async grabWebElements(locator) {
|
|
886
|
+
return this._locate(locator);
|
|
887
|
+
}
|
|
888
|
+
|
|
872
889
|
/**
|
|
873
890
|
* Set [WebDriver timeouts](https://webdriver.io/docs/timeouts.html) in realtime.
|
|
874
891
|
*
|
|
@@ -2013,7 +2030,9 @@ class WebDriver extends Helper {
|
|
|
2013
2030
|
let chunked = chunkArray(props, values.length);
|
|
2014
2031
|
chunked = chunked.filter((val) => {
|
|
2015
2032
|
for (let i = 0; i < val.length; ++i) {
|
|
2016
|
-
|
|
2033
|
+
const _acutal = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(val[i], 10);
|
|
2034
|
+
const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
|
|
2035
|
+
if (_acutal !== _expected) return false;
|
|
2017
2036
|
}
|
|
2018
2037
|
return true;
|
|
2019
2038
|
});
|
|
@@ -2049,7 +2068,9 @@ class WebDriver extends Helper {
|
|
|
2049
2068
|
let chunked = chunkArray(attrs, values.length);
|
|
2050
2069
|
chunked = chunked.filter((val) => {
|
|
2051
2070
|
for (let i = 0; i < val.length; ++i) {
|
|
2052
|
-
|
|
2071
|
+
const _acutal = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(val[i], 10);
|
|
2072
|
+
const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
|
|
2073
|
+
if (_acutal !== _expected) return false;
|
|
2053
2074
|
}
|
|
2054
2075
|
return true;
|
|
2055
2076
|
});
|
package/docs/helpers/Appium.md
CHANGED
|
@@ -440,7 +440,7 @@ let settings = await I.grabSettings();
|
|
|
440
440
|
|
|
441
441
|
Returns **[Promise][6]<[string][5]>** Appium: support Android and iOS
|
|
442
442
|
|
|
443
|
-
###
|
|
443
|
+
### switchToContext
|
|
444
444
|
|
|
445
445
|
Switch to the specified context.
|
|
446
446
|
|
|
@@ -871,7 +871,7 @@ Close the given application.
|
|
|
871
871
|
I.closeApp();
|
|
872
872
|
```
|
|
873
873
|
|
|
874
|
-
Returns **[Promise][6]<void>** Appium: support
|
|
874
|
+
Returns **[Promise][6]<void>** Appium: support both Android and iOS
|
|
875
875
|
|
|
876
876
|
### appendField
|
|
877
877
|
|