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/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
|
*/
|
|
@@ -1250,6 +1304,22 @@ class Playwright extends Helper {
|
|
|
1250
1304
|
return findFields.call(this, locator);
|
|
1251
1305
|
}
|
|
1252
1306
|
|
|
1307
|
+
/**
|
|
1308
|
+
* {{> grabWebElements }}
|
|
1309
|
+
*
|
|
1310
|
+
*/
|
|
1311
|
+
async grabWebElements(locator) {
|
|
1312
|
+
return this._locate(locator);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* {{> grabWebElement }}
|
|
1317
|
+
*
|
|
1318
|
+
*/
|
|
1319
|
+
async grabWebElement(locator) {
|
|
1320
|
+
return this._locateElement(locator);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1253
1323
|
/**
|
|
1254
1324
|
* Switch focus to a particular tab by its number. It waits tabs loading and then switch tab
|
|
1255
1325
|
*
|
|
@@ -1711,8 +1781,15 @@ class Playwright extends Helper {
|
|
|
1711
1781
|
const el = els[0];
|
|
1712
1782
|
|
|
1713
1783
|
await highlightActiveElement.call(this, el);
|
|
1784
|
+
let optionToSelect = '';
|
|
1785
|
+
|
|
1786
|
+
try {
|
|
1787
|
+
optionToSelect = await el.locator('option', { hasText: option }).textContent();
|
|
1788
|
+
} catch (e) {
|
|
1789
|
+
optionToSelect = option;
|
|
1790
|
+
}
|
|
1714
1791
|
|
|
1715
|
-
if (!Array.isArray(option)) option = [
|
|
1792
|
+
if (!Array.isArray(option)) option = [optionToSelect];
|
|
1716
1793
|
|
|
1717
1794
|
await el.selectOption(option);
|
|
1718
1795
|
return this._waitForAction();
|
|
@@ -2042,19 +2119,17 @@ class Playwright extends Helper {
|
|
|
2042
2119
|
|
|
2043
2120
|
const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties);
|
|
2044
2121
|
const elemAmount = res.length;
|
|
2045
|
-
const commands = [];
|
|
2046
2122
|
let props = [];
|
|
2047
2123
|
|
|
2048
2124
|
for (const element of res) {
|
|
2049
|
-
const
|
|
2050
|
-
|
|
2051
|
-
Object.keys(cssPropertiesCamelCase).forEach(prop => {
|
|
2125
|
+
for (const prop of Object.keys(cssProperties)) {
|
|
2126
|
+
const cssProp = await this.grabCssPropertyFrom(locator, prop);
|
|
2052
2127
|
if (isColorProperty(prop)) {
|
|
2053
|
-
props.push(convertColorToRGBA(
|
|
2128
|
+
props.push(convertColorToRGBA(cssProp));
|
|
2054
2129
|
} else {
|
|
2055
|
-
props.push(
|
|
2130
|
+
props.push(cssProp);
|
|
2056
2131
|
}
|
|
2057
|
-
}
|
|
2132
|
+
}
|
|
2058
2133
|
}
|
|
2059
2134
|
|
|
2060
2135
|
const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key]);
|
|
@@ -2062,7 +2137,8 @@ class Playwright extends Helper {
|
|
|
2062
2137
|
let chunked = chunkArray(props, values.length);
|
|
2063
2138
|
chunked = chunked.filter((val) => {
|
|
2064
2139
|
for (let i = 0; i < val.length; ++i) {
|
|
2065
|
-
|
|
2140
|
+
// eslint-disable-next-line eqeqeq
|
|
2141
|
+
if (val[i] != values[i]) return false;
|
|
2066
2142
|
}
|
|
2067
2143
|
return true;
|
|
2068
2144
|
});
|
|
@@ -2091,7 +2167,7 @@ class Playwright extends Helper {
|
|
|
2091
2167
|
let chunked = chunkArray(attrs, values.length);
|
|
2092
2168
|
chunked = chunked.filter((val) => {
|
|
2093
2169
|
for (let i = 0; i < val.length; ++i) {
|
|
2094
|
-
if (val[i]
|
|
2170
|
+
if (!val[i].includes(values[i])) return false;
|
|
2095
2171
|
}
|
|
2096
2172
|
return true;
|
|
2097
2173
|
});
|
|
@@ -2262,6 +2338,10 @@ class Playwright extends Helper {
|
|
|
2262
2338
|
test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.failed`);
|
|
2263
2339
|
}
|
|
2264
2340
|
}
|
|
2341
|
+
|
|
2342
|
+
if (this.options.recordHar) {
|
|
2343
|
+
test.artifacts.har = this.currentRunningTest.artifacts.har;
|
|
2344
|
+
}
|
|
2265
2345
|
}
|
|
2266
2346
|
|
|
2267
2347
|
async _passed(test) {
|
|
@@ -2289,6 +2369,10 @@ class Playwright extends Helper {
|
|
|
2289
2369
|
await this.browserContext.tracing.stop();
|
|
2290
2370
|
}
|
|
2291
2371
|
}
|
|
2372
|
+
|
|
2373
|
+
if (this.options.recordHar) {
|
|
2374
|
+
test.artifacts.har = this.currentRunningTest.artifacts.har;
|
|
2375
|
+
}
|
|
2292
2376
|
}
|
|
2293
2377
|
|
|
2294
2378
|
/**
|
|
@@ -2581,7 +2665,7 @@ class Playwright extends Helper {
|
|
|
2581
2665
|
const _contextObject = this.frame ? this.frame : contextObject;
|
|
2582
2666
|
let count = 0;
|
|
2583
2667
|
do {
|
|
2584
|
-
waiter = await _contextObject.locator(`:has-text(
|
|
2668
|
+
waiter = await _contextObject.locator(`:has-text("${text}")`).first().isVisible();
|
|
2585
2669
|
if (waiter) break;
|
|
2586
2670
|
await this.wait(1);
|
|
2587
2671
|
count += 1000;
|
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;
|
|
@@ -917,6 +917,14 @@ class Puppeteer extends Helper {
|
|
|
917
917
|
return findFields.call(this, locator);
|
|
918
918
|
}
|
|
919
919
|
|
|
920
|
+
/**
|
|
921
|
+
* {{> grabWebElements }}
|
|
922
|
+
*
|
|
923
|
+
*/
|
|
924
|
+
async grabWebElements(locator) {
|
|
925
|
+
return this._locate(locator);
|
|
926
|
+
}
|
|
927
|
+
|
|
920
928
|
/**
|
|
921
929
|
* Switch focus to a particular tab by its number. It waits tabs loading and then switch tab
|
|
922
930
|
*
|
|
@@ -1762,29 +1770,26 @@ class Puppeteer extends Helper {
|
|
|
1762
1770
|
|
|
1763
1771
|
const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties);
|
|
1764
1772
|
const elemAmount = res.length;
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
.
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
}));
|
|
1779
|
-
});
|
|
1780
|
-
});
|
|
1781
|
-
let props = await Promise.all(commands);
|
|
1773
|
+
let props = [];
|
|
1774
|
+
|
|
1775
|
+
for (const element of res) {
|
|
1776
|
+
for (const prop of Object.keys(cssProperties)) {
|
|
1777
|
+
const cssProp = await this.grabCssPropertyFrom(locator, prop);
|
|
1778
|
+
if (isColorProperty(prop)) {
|
|
1779
|
+
props.push(convertColorToRGBA(cssProp));
|
|
1780
|
+
} else {
|
|
1781
|
+
props.push(cssProp);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1782
1786
|
const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key]);
|
|
1783
1787
|
if (!Array.isArray(props)) props = [props];
|
|
1784
1788
|
let chunked = chunkArray(props, values.length);
|
|
1785
1789
|
chunked = chunked.filter((val) => {
|
|
1786
1790
|
for (let i = 0; i < val.length; ++i) {
|
|
1787
|
-
|
|
1791
|
+
// eslint-disable-next-line eqeqeq
|
|
1792
|
+
if (val[i] != values[i]) return false;
|
|
1788
1793
|
}
|
|
1789
1794
|
return true;
|
|
1790
1795
|
});
|
|
@@ -1815,7 +1820,9 @@ class Puppeteer extends Helper {
|
|
|
1815
1820
|
let chunked = chunkArray(attrs, values.length);
|
|
1816
1821
|
chunked = chunked.filter((val) => {
|
|
1817
1822
|
for (let i = 0; i < val.length; ++i) {
|
|
1818
|
-
|
|
1823
|
+
const _actual = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(values[i], 10);
|
|
1824
|
+
const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
|
|
1825
|
+
if (!_actual.includes(_expected)) return false;
|
|
1819
1826
|
}
|
|
1820
1827
|
return true;
|
|
1821
1828
|
});
|
package/lib/helper/WebDriver.js
CHANGED
|
@@ -869,6 +869,14 @@ class WebDriver extends Helper {
|
|
|
869
869
|
return findFields.call(this, locator).then(res => res);
|
|
870
870
|
}
|
|
871
871
|
|
|
872
|
+
/**
|
|
873
|
+
* {{> grabWebElements }}
|
|
874
|
+
*
|
|
875
|
+
*/
|
|
876
|
+
async grabWebElements(locator) {
|
|
877
|
+
return this._locate(locator);
|
|
878
|
+
}
|
|
879
|
+
|
|
872
880
|
/**
|
|
873
881
|
* Set [WebDriver timeouts](https://webdriver.io/docs/timeouts.html) in realtime.
|
|
874
882
|
*
|
|
@@ -1508,7 +1516,9 @@ class WebDriver extends Helper {
|
|
|
1508
1516
|
let chunked = chunkArray(props, values.length);
|
|
1509
1517
|
chunked = chunked.filter((val) => {
|
|
1510
1518
|
for (let i = 0; i < val.length; ++i) {
|
|
1511
|
-
|
|
1519
|
+
const _acutal = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(val[i], 10);
|
|
1520
|
+
const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
|
|
1521
|
+
if (_acutal !== _expected) return false;
|
|
1512
1522
|
}
|
|
1513
1523
|
return true;
|
|
1514
1524
|
});
|
|
@@ -1535,7 +1545,9 @@ class WebDriver extends Helper {
|
|
|
1535
1545
|
let chunked = chunkArray(attrs, values.length);
|
|
1536
1546
|
chunked = chunked.filter((val) => {
|
|
1537
1547
|
for (let i = 0; i < val.length; ++i) {
|
|
1538
|
-
|
|
1548
|
+
const _acutal = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(val[i], 10);
|
|
1549
|
+
const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
|
|
1550
|
+
if (_acutal !== _expected) return false;
|
|
1539
1551
|
}
|
|
1540
1552
|
return true;
|
|
1541
1553
|
});
|
package/lib/html.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const { parse, serialize } = require('parse5');
|
|
2
|
-
const { minify } = require('html-minifier');
|
|
2
|
+
const { minify } = require('html-minifier-terser');
|
|
3
3
|
|
|
4
|
-
function minifyHtml(html) {
|
|
4
|
+
async function minifyHtml(html) {
|
|
5
5
|
return minify(html, {
|
|
6
6
|
collapseWhitespace: true,
|
|
7
7
|
removeComments: true,
|
|
@@ -11,7 +11,7 @@ function minifyHtml(html) {
|
|
|
11
11
|
removeStyleLinkTypeAttributes: true,
|
|
12
12
|
collapseBooleanAttributes: true,
|
|
13
13
|
useShortDoctype: true,
|
|
14
|
-
})
|
|
14
|
+
});
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const defaultHtmlOpts = {
|
|
@@ -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/pause.js
CHANGED
|
@@ -18,8 +18,7 @@ let nextStep;
|
|
|
18
18
|
let finish;
|
|
19
19
|
let next;
|
|
20
20
|
let registeredVariables = {};
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
let aiAssistant;
|
|
23
22
|
/**
|
|
24
23
|
* Pauses test execution and starts interactive shell
|
|
25
24
|
* @param {Object<string, *>} [passedObject]
|
|
@@ -45,6 +44,8 @@ function pauseSession(passedObject = {}) {
|
|
|
45
44
|
let vars = Object.keys(registeredVariables).join(', ');
|
|
46
45
|
if (vars) vars = `(vars: ${vars})`;
|
|
47
46
|
|
|
47
|
+
aiAssistant = AiAssistant.getInstance();
|
|
48
|
+
|
|
48
49
|
output.print(colors.yellow(' Interactive shell started'));
|
|
49
50
|
output.print(colors.yellow(' Use JavaScript syntax to try steps in action'));
|
|
50
51
|
output.print(colors.yellow(` - Press ${colors.bold('ENTER')} to run the next step`));
|
|
@@ -102,7 +103,9 @@ async function parseInput(cmd) {
|
|
|
102
103
|
let isAiCommand = false;
|
|
103
104
|
let $res;
|
|
104
105
|
try {
|
|
106
|
+
// eslint-disable-next-line
|
|
105
107
|
const locate = global.locate; // enable locate in this context
|
|
108
|
+
// eslint-disable-next-line
|
|
106
109
|
const I = container.support('I');
|
|
107
110
|
if (cmd.trim().startsWith('=>')) {
|
|
108
111
|
isCustomCommand = true;
|
|
@@ -115,7 +118,7 @@ async function parseInput(cmd) {
|
|
|
115
118
|
executeCommand = executeCommand.then(async () => {
|
|
116
119
|
try {
|
|
117
120
|
const html = await res;
|
|
118
|
-
aiAssistant.setHtmlContext(html);
|
|
121
|
+
await aiAssistant.setHtmlContext(html);
|
|
119
122
|
} catch (err) {
|
|
120
123
|
output.print(output.styles.error(' ERROR '), 'Can\'t get HTML context', err.stack);
|
|
121
124
|
return;
|
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
|
* });
|
package/lib/plugin/heal.js
CHANGED
|
@@ -8,6 +8,7 @@ const output = require('../output');
|
|
|
8
8
|
const supportedHelpers = require('./standardActingHelpers');
|
|
9
9
|
|
|
10
10
|
const defaultConfig = {
|
|
11
|
+
healTries: 1,
|
|
11
12
|
healLimit: 2,
|
|
12
13
|
healSteps: [
|
|
13
14
|
'click',
|
|
@@ -54,11 +55,14 @@ const defaultConfig = {
|
|
|
54
55
|
*
|
|
55
56
|
*/
|
|
56
57
|
module.exports = function (config = {}) {
|
|
57
|
-
const aiAssistant =
|
|
58
|
+
const aiAssistant = AiAssistant.getInstance();
|
|
58
59
|
|
|
59
60
|
let currentTest = null;
|
|
60
61
|
let currentStep = null;
|
|
61
62
|
let healedSteps = 0;
|
|
63
|
+
let caughtError;
|
|
64
|
+
let healTries = 0;
|
|
65
|
+
let isHealing = false;
|
|
62
66
|
|
|
63
67
|
const healSuggestions = [];
|
|
64
68
|
|
|
@@ -67,20 +71,35 @@ module.exports = function (config = {}) {
|
|
|
67
71
|
event.dispatcher.on(event.test.before, (test) => {
|
|
68
72
|
currentTest = test;
|
|
69
73
|
healedSteps = 0;
|
|
74
|
+
caughtError = null;
|
|
70
75
|
});
|
|
71
76
|
|
|
72
77
|
event.dispatcher.on(event.step.started, step => currentStep = step);
|
|
73
78
|
|
|
74
|
-
event.dispatcher.on(event.step.
|
|
79
|
+
event.dispatcher.on(event.step.after, (step) => {
|
|
80
|
+
if (isHealing) return;
|
|
75
81
|
const store = require('../store');
|
|
76
82
|
if (store.debugMode) return;
|
|
77
|
-
|
|
78
83
|
recorder.catchWithoutStop(async (err) => {
|
|
79
|
-
|
|
84
|
+
isHealing = true;
|
|
85
|
+
if (caughtError === err) throw err; // avoid double handling
|
|
86
|
+
caughtError = err;
|
|
87
|
+
if (!aiAssistant.isEnabled) {
|
|
88
|
+
output.print(colors.yellow('Heal plugin can\'t operate, AI assistant is disabled. Please set OPENAI_API_KEY env variable to enable it.'));
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
80
91
|
if (!currentStep) throw err;
|
|
81
92
|
if (!config.healSteps.includes(currentStep.name)) throw err;
|
|
82
93
|
const test = currentTest;
|
|
83
94
|
|
|
95
|
+
if (healTries >= config.healTries) {
|
|
96
|
+
output.print(colors.bold.red(`Healing failed for ${config.healTries} time(s)`));
|
|
97
|
+
output.print('AI couldn\'t identify the correct solution');
|
|
98
|
+
output.print('Probably the entire flow has changed and the test should be updated');
|
|
99
|
+
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
|
|
84
103
|
if (healedSteps >= config.healLimit) {
|
|
85
104
|
output.print(colors.bold.red(`Can't heal more than ${config.healLimit} step(s) in a test`));
|
|
86
105
|
output.print('Entire flow can be broken, please check it manually');
|
|
@@ -111,9 +130,17 @@ module.exports = function (config = {}) {
|
|
|
111
130
|
|
|
112
131
|
if (!html) throw err;
|
|
113
132
|
|
|
114
|
-
|
|
133
|
+
healTries++;
|
|
134
|
+
await aiAssistant.setHtmlContext(html);
|
|
115
135
|
await tryToHeal(step, err);
|
|
116
|
-
|
|
136
|
+
|
|
137
|
+
recorder.add('close healing session', () => {
|
|
138
|
+
recorder.session.restore('heal');
|
|
139
|
+
recorder.ignoreErr(err);
|
|
140
|
+
});
|
|
141
|
+
await recorder.promise();
|
|
142
|
+
|
|
143
|
+
isHealing = false;
|
|
117
144
|
});
|
|
118
145
|
});
|
|
119
146
|
|
|
@@ -155,6 +182,9 @@ module.exports = function (config = {}) {
|
|
|
155
182
|
for (const codeSnippet of codeSnippets) {
|
|
156
183
|
try {
|
|
157
184
|
debug('Executing', codeSnippet);
|
|
185
|
+
recorder.catch((e) => {
|
|
186
|
+
console.log(e);
|
|
187
|
+
});
|
|
158
188
|
await eval(codeSnippet); // eslint-disable-line
|
|
159
189
|
|
|
160
190
|
healSuggestions.push({
|
|
@@ -163,14 +193,17 @@ module.exports = function (config = {}) {
|
|
|
163
193
|
snippet: codeSnippet,
|
|
164
194
|
});
|
|
165
195
|
|
|
166
|
-
output.print(colors.bold.green(' Code healed successfully'));
|
|
196
|
+
recorder.add('healed', () => output.print(colors.bold.green(' Code healed successfully')));
|
|
167
197
|
healedSteps++;
|
|
168
198
|
return;
|
|
169
199
|
} catch (err) {
|
|
170
200
|
debug('Failed to execute code', err);
|
|
201
|
+
recorder.ignoreErr(err); // healing ded not help
|
|
202
|
+
// recorder.catch(() => output.print(colors.bold.red(' Failed healing code')));
|
|
171
203
|
}
|
|
172
204
|
}
|
|
173
205
|
|
|
174
206
|
output.debug(`Couldn't heal the code for ${failedStep.toCode()}`);
|
|
175
207
|
}
|
|
208
|
+
return recorder.promise();
|
|
176
209
|
};
|
|
@@ -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/recorder.js
CHANGED
|
@@ -11,6 +11,7 @@ let errFn;
|
|
|
11
11
|
let queueId = 0;
|
|
12
12
|
let sessionId = null;
|
|
13
13
|
let asyncErr = null;
|
|
14
|
+
let ignoredErrs = [];
|
|
14
15
|
|
|
15
16
|
let tasks = [];
|
|
16
17
|
let oldPromises = [];
|
|
@@ -93,6 +94,7 @@ module.exports = {
|
|
|
93
94
|
promise = Promise.resolve();
|
|
94
95
|
oldPromises = [];
|
|
95
96
|
tasks = [];
|
|
97
|
+
ignoredErrs = [];
|
|
96
98
|
this.session.running = false;
|
|
97
99
|
// reset this retries makes the retryFailedStep plugin won't work if there is Before/BeforeSuit block due to retries is undefined on Scenario
|
|
98
100
|
// this.retries = [];
|
|
@@ -226,9 +228,10 @@ module.exports = {
|
|
|
226
228
|
* @inner
|
|
227
229
|
*/
|
|
228
230
|
catch(customErrFn) {
|
|
229
|
-
|
|
231
|
+
const fnDescription = customErrFn?.toString()?.replace(/\s{2,}/g, ' ').replace(/\n/g, ' ')?.slice(0, 50);
|
|
232
|
+
debug(`${currentQueue()}Queued | catch with error handler ${fnDescription || ''}`);
|
|
230
233
|
return promise = promise.catch((err) => {
|
|
231
|
-
log(`${currentQueue()}Error | ${err}
|
|
234
|
+
log(`${currentQueue()}Error | ${err} ${fnDescription}...`);
|
|
232
235
|
if (!(err instanceof Error)) { // strange things may happen
|
|
233
236
|
err = new Error(`[Wrapped Error] ${printObjectProperties(err)}`); // we should be prepared for them
|
|
234
237
|
}
|
|
@@ -247,15 +250,15 @@ module.exports = {
|
|
|
247
250
|
* @inner
|
|
248
251
|
*/
|
|
249
252
|
catchWithoutStop(customErrFn) {
|
|
253
|
+
const fnDescription = customErrFn?.toString()?.replace(/\s{2,}/g, ' ').replace(/\n/g, ' ')?.slice(0, 50);
|
|
250
254
|
return promise = promise.catch((err) => {
|
|
251
|
-
|
|
255
|
+
if (ignoredErrs.includes(err)) return; // already caught
|
|
256
|
+
log(`${currentQueue()}Error (Non-Terminated) | ${err} | ${fnDescription || ''}...`);
|
|
252
257
|
if (!(err instanceof Error)) { // strange things may happen
|
|
253
258
|
err = new Error(`[Wrapped Error] ${JSON.stringify(err)}`); // we should be prepared for them
|
|
254
259
|
}
|
|
255
260
|
if (customErrFn) {
|
|
256
261
|
return customErrFn(err);
|
|
257
|
-
} if (errFn) {
|
|
258
|
-
return errFn(err);
|
|
259
262
|
}
|
|
260
263
|
});
|
|
261
264
|
},
|
|
@@ -274,6 +277,10 @@ module.exports = {
|
|
|
274
277
|
});
|
|
275
278
|
},
|
|
276
279
|
|
|
280
|
+
ignoreErr(err) {
|
|
281
|
+
ignoredErrs.push(err);
|
|
282
|
+
},
|
|
283
|
+
|
|
277
284
|
/**
|
|
278
285
|
* @param {*} err
|
|
279
286
|
* @inner
|
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
|
|