codeceptjs 3.5.9-beta.2 → 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 +7 -7
- package/docs/build/Playwright.js +53 -13
- package/docs/build/Puppeteer.js +36 -20
- package/docs/build/WebDriver.js +23 -2
- package/docs/helpers/Appium.md +1 -1
- package/docs/helpers/Playwright.md +30 -0
- package/docs/helpers/Puppeteer.md +15 -0
- package/docs/helpers/WebDriver.md +15 -0
- package/docs/internal-api.md +0 -110
- package/docs/parallel.md +114 -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/helper/Appium.js +3 -3
- package/lib/helper/Playwright.js +25 -12
- package/lib/helper/Puppeteer.js +25 -22
- package/lib/helper/WebDriver.js +8 -0
- package/lib/html.js +3 -3
- package/lib/pause.js +6 -3
- package/lib/plugin/heal.js +40 -7
- package/lib/recorder.js +12 -5
- package/package.json +14 -11
- package/typings/promiseBasedTypes.d.ts +45 -1
- package/typings/types.d.ts +46 -1
package/docs/parallel.md
CHANGED
|
@@ -26,12 +26,124 @@ This command is similar to `run`, however, steps output can't be shown in worker
|
|
|
26
26
|
|
|
27
27
|
Each worker spins an instance of CodeceptJS, executes a group of tests, and sends back report to the main process.
|
|
28
28
|
|
|
29
|
-
By default the tests are assigned one by one to the available workers this may lead to multiple execution of `BeforeSuite()`. Use the option `--suites` to
|
|
29
|
+
By default, the tests are assigned one by one to the available workers this may lead to multiple execution of `BeforeSuite()`. Use the option `--suites` to assign the suites one by one to the workers.
|
|
30
30
|
|
|
31
31
|
```sh
|
|
32
32
|
npx codeceptjs run-workers --suites 2
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
## Test stats with Parallel Execution by Workers
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
const { event } = require('codeceptjs');
|
|
39
|
+
|
|
40
|
+
module.exports = function() {
|
|
41
|
+
|
|
42
|
+
event.dispatcher.on(event.workers.result, function (result) {
|
|
43
|
+
|
|
44
|
+
console.log(result);
|
|
45
|
+
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// in console log
|
|
50
|
+
FAIL | 7 passed, 1 failed, 1 skipped // 2s
|
|
51
|
+
{
|
|
52
|
+
"tests": {
|
|
53
|
+
"passed": [
|
|
54
|
+
{
|
|
55
|
+
"type": "test",
|
|
56
|
+
"title": "Assert @C3",
|
|
57
|
+
"body": "() => { }",
|
|
58
|
+
"async": 0,
|
|
59
|
+
"sync": true,
|
|
60
|
+
"_timeout": 2000,
|
|
61
|
+
"_slow": 75,
|
|
62
|
+
"_retries": -1,
|
|
63
|
+
"timedOut": false,
|
|
64
|
+
"_currentRetry": 0,
|
|
65
|
+
"pending": false,
|
|
66
|
+
"opts": {},
|
|
67
|
+
"tags": [
|
|
68
|
+
"@C3"
|
|
69
|
+
],
|
|
70
|
+
"uid": "xe4q1HdqpRrZG5dPe0JG+A",
|
|
71
|
+
"workerIndex": 3,
|
|
72
|
+
"retries": -1,
|
|
73
|
+
"duration": 493,
|
|
74
|
+
"err": null,
|
|
75
|
+
"parent": {
|
|
76
|
+
"title": "My",
|
|
77
|
+
"ctx": {},
|
|
78
|
+
"suites": [],
|
|
79
|
+
"tests": [],
|
|
80
|
+
"root": false,
|
|
81
|
+
"pending": false,
|
|
82
|
+
"_retries": -1,
|
|
83
|
+
"_beforeEach": [],
|
|
84
|
+
"_beforeAll": [],
|
|
85
|
+
"_afterEach": [],
|
|
86
|
+
"_afterAll": [],
|
|
87
|
+
"_timeout": 2000,
|
|
88
|
+
"_slow": 75,
|
|
89
|
+
"_bail": false,
|
|
90
|
+
"_onlyTests": [],
|
|
91
|
+
"_onlySuites": [],
|
|
92
|
+
"delayed": false
|
|
93
|
+
},
|
|
94
|
+
"steps": [
|
|
95
|
+
{
|
|
96
|
+
"actor": "I",
|
|
97
|
+
"name": "amOnPage",
|
|
98
|
+
"status": "success",
|
|
99
|
+
"agrs": [
|
|
100
|
+
"https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST"
|
|
101
|
+
],
|
|
102
|
+
"startedAt": 1698760652610,
|
|
103
|
+
"startTime": 1698760652611,
|
|
104
|
+
"endTime": 1698760653098,
|
|
105
|
+
"finishedAt": 1698760653098,
|
|
106
|
+
"duration": 488
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"actor": "I",
|
|
110
|
+
"name": "grabCurrentUrl",
|
|
111
|
+
"status": "success",
|
|
112
|
+
"agrs": [],
|
|
113
|
+
"startedAt": 1698760653098,
|
|
114
|
+
"startTime": 1698760653098,
|
|
115
|
+
"endTime": 1698760653099,
|
|
116
|
+
"finishedAt": 1698760653099,
|
|
117
|
+
"duration": 1
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
],
|
|
122
|
+
"failed": [],
|
|
123
|
+
"skipped": []
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
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
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
const { event } = require('codeceptjs');
|
|
132
|
+
|
|
133
|
+
module.exports = function() {
|
|
134
|
+
// this event would trigger the `_publishResultsToTestrail` when running `run-workers` command
|
|
135
|
+
event.dispatcher.on(event.workers.result, async () => {
|
|
136
|
+
await _publishResultsToTestrail();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// this event would not trigger the `_publishResultsToTestrail` multiple times when running `run-workers` command
|
|
140
|
+
event.dispatcher.on(event.all.result, async () => {
|
|
141
|
+
// when running `run` command, this env var is undefined
|
|
142
|
+
if (!process.env.RUNS_WITH_WORKERS) await _publishResultsToTestrail();
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
35
147
|
## Parallel Execution by Workers on Multiple Browsers
|
|
36
148
|
|
|
37
149
|
To run tests in parallel across multiple browsers, modify your `codecept.conf.js` file to configure multiple browsers on which you want to run your tests and your tests will run across multiple browsers.
|
|
@@ -236,7 +348,7 @@ customWorkers.on(event.all.result, () => {
|
|
|
236
348
|
|
|
237
349
|
### Emitting messages to the parent worker
|
|
238
350
|
|
|
239
|
-
Child workers can send non
|
|
351
|
+
Child workers can send non-test events to the main process. This is useful if you want to pass along information not related to the tests event cycles itself such as `event.test.success`.
|
|
240
352
|
|
|
241
353
|
```js
|
|
242
354
|
// inside main process
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Grab WebElement for given locator
|
|
2
|
+
Resumes test execution, so **should be used inside an async function with `await`** operator.
|
|
3
|
+
|
|
4
|
+
```js
|
|
5
|
+
const webElement = await I.grabWebElement('#button');
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
@param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
|
|
9
|
+
@returns {Promise<*>} WebElement of being used Web helper
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Grab WebElements for given locator
|
|
2
|
+
Resumes test execution, so **should be used inside an async function with `await`** operator.
|
|
3
|
+
|
|
4
|
+
```js
|
|
5
|
+
const webElements = await I.grabWebElements('#button');
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
@param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
|
|
9
|
+
@returns {Promise<*>} WebElement of being used Web helper
|
package/lib/ai.js
CHANGED
|
@@ -16,6 +16,8 @@ const htmlConfig = {
|
|
|
16
16
|
html: {},
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
const aiInstance = null;
|
|
20
|
+
|
|
19
21
|
class AiAssistant {
|
|
20
22
|
constructor() {
|
|
21
23
|
this.config = config.get('ai', defaultConfig);
|
|
@@ -26,7 +28,10 @@ class AiAssistant {
|
|
|
26
28
|
|
|
27
29
|
this.isEnabled = !!process.env.OPENAI_API_KEY;
|
|
28
30
|
|
|
29
|
-
if (!this.isEnabled)
|
|
31
|
+
if (!this.isEnabled) {
|
|
32
|
+
debug('No OpenAI API key provided. AI assistant is disabled.');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
30
35
|
|
|
31
36
|
const configuration = new Configuration({
|
|
32
37
|
apiKey: process.env.OPENAI_API_KEY,
|
|
@@ -35,13 +40,17 @@ class AiAssistant {
|
|
|
35
40
|
this.openai = new OpenAIApi(configuration);
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
|
|
43
|
+
static getInstance() {
|
|
44
|
+
return aiInstance || new AiAssistant();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async setHtmlContext(html) {
|
|
39
48
|
let processedHTML = html;
|
|
40
49
|
|
|
41
50
|
if (this.htmlConfig.simplify) {
|
|
42
51
|
processedHTML = removeNonInteractiveElements(processedHTML, this.htmlConfig);
|
|
43
52
|
}
|
|
44
|
-
if (this.htmlConfig.minify) processedHTML = minifyHtml(processedHTML);
|
|
53
|
+
if (this.htmlConfig.minify) processedHTML = await minifyHtml(processedHTML);
|
|
45
54
|
if (this.htmlConfig.maxLength) processedHTML = splitByChunks(processedHTML, this.htmlConfig.maxLength)[0];
|
|
46
55
|
|
|
47
56
|
debug(processedHTML);
|
package/lib/colorUtils.js
CHANGED
|
@@ -226,15 +226,25 @@ function isColorProperty(prop) {
|
|
|
226
226
|
'color',
|
|
227
227
|
'background',
|
|
228
228
|
'backgroundColor',
|
|
229
|
+
'background-color',
|
|
229
230
|
'borderColor',
|
|
231
|
+
'border-color',
|
|
230
232
|
'borderBottomColor',
|
|
233
|
+
'border-bottom-color',
|
|
231
234
|
'borderLeftColor',
|
|
235
|
+
'border-left-color',
|
|
232
236
|
'borderRightColor',
|
|
233
237
|
'borderTopColor',
|
|
234
238
|
'caretColor',
|
|
235
239
|
'columnRuleColor',
|
|
236
240
|
'outlineColor',
|
|
237
241
|
'textDecorationColor',
|
|
242
|
+
'border-right-color',
|
|
243
|
+
'border-top-color',
|
|
244
|
+
'caret-color',
|
|
245
|
+
'column-rule-color',
|
|
246
|
+
'outline-color',
|
|
247
|
+
'text-decoration-color',
|
|
238
248
|
].indexOf(prop) > -1;
|
|
239
249
|
}
|
|
240
250
|
|
package/lib/helper/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;
|
|
@@ -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/lib/helper/Playwright.js
CHANGED
|
@@ -1304,6 +1304,22 @@ class Playwright extends Helper {
|
|
|
1304
1304
|
return findFields.call(this, locator);
|
|
1305
1305
|
}
|
|
1306
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
|
+
|
|
1307
1323
|
/**
|
|
1308
1324
|
* Switch focus to a particular tab by its number. It waits tabs loading and then switch tab
|
|
1309
1325
|
*
|
|
@@ -2103,19 +2119,17 @@ class Playwright extends Helper {
|
|
|
2103
2119
|
|
|
2104
2120
|
const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties);
|
|
2105
2121
|
const elemAmount = res.length;
|
|
2106
|
-
const commands = [];
|
|
2107
2122
|
let props = [];
|
|
2108
2123
|
|
|
2109
2124
|
for (const element of res) {
|
|
2110
|
-
const
|
|
2111
|
-
|
|
2112
|
-
Object.keys(cssPropertiesCamelCase).forEach(prop => {
|
|
2125
|
+
for (const prop of Object.keys(cssProperties)) {
|
|
2126
|
+
const cssProp = await this.grabCssPropertyFrom(locator, prop);
|
|
2113
2127
|
if (isColorProperty(prop)) {
|
|
2114
|
-
props.push(convertColorToRGBA(
|
|
2128
|
+
props.push(convertColorToRGBA(cssProp));
|
|
2115
2129
|
} else {
|
|
2116
|
-
props.push(
|
|
2130
|
+
props.push(cssProp);
|
|
2117
2131
|
}
|
|
2118
|
-
}
|
|
2132
|
+
}
|
|
2119
2133
|
}
|
|
2120
2134
|
|
|
2121
2135
|
const values = Object.keys(cssPropertiesCamelCase).map(key => cssPropertiesCamelCase[key]);
|
|
@@ -2123,9 +2137,8 @@ class Playwright extends Helper {
|
|
|
2123
2137
|
let chunked = chunkArray(props, values.length);
|
|
2124
2138
|
chunked = chunked.filter((val) => {
|
|
2125
2139
|
for (let i = 0; i < val.length; ++i) {
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
if (_acutal !== _expected) return false;
|
|
2140
|
+
// eslint-disable-next-line eqeqeq
|
|
2141
|
+
if (val[i] != values[i]) return false;
|
|
2129
2142
|
}
|
|
2130
2143
|
return true;
|
|
2131
2144
|
});
|
|
@@ -2154,7 +2167,7 @@ class Playwright extends Helper {
|
|
|
2154
2167
|
let chunked = chunkArray(attrs, values.length);
|
|
2155
2168
|
chunked = chunked.filter((val) => {
|
|
2156
2169
|
for (let i = 0; i < val.length; ++i) {
|
|
2157
|
-
if (val[i]
|
|
2170
|
+
if (!val[i].includes(values[i])) return false;
|
|
2158
2171
|
}
|
|
2159
2172
|
return true;
|
|
2160
2173
|
});
|
|
@@ -2652,7 +2665,7 @@ class Playwright extends Helper {
|
|
|
2652
2665
|
const _contextObject = this.frame ? this.frame : contextObject;
|
|
2653
2666
|
let count = 0;
|
|
2654
2667
|
do {
|
|
2655
|
-
waiter = await _contextObject.locator(`:has-text(
|
|
2668
|
+
waiter = await _contextObject.locator(`:has-text("${text}")`).first().isVisible();
|
|
2656
2669
|
if (waiter) break;
|
|
2657
2670
|
await this.wait(1);
|
|
2658
2671
|
count += 1000;
|
package/lib/helper/Puppeteer.js
CHANGED
|
@@ -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,31 +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
|
-
|
|
1788
|
-
|
|
1789
|
-
if (_acutal !== _expected) return false;
|
|
1791
|
+
// eslint-disable-next-line eqeqeq
|
|
1792
|
+
if (val[i] != values[i]) return false;
|
|
1790
1793
|
}
|
|
1791
1794
|
return true;
|
|
1792
1795
|
});
|
|
@@ -1817,9 +1820,9 @@ class Puppeteer extends Helper {
|
|
|
1817
1820
|
let chunked = chunkArray(attrs, values.length);
|
|
1818
1821
|
chunked = chunked.filter((val) => {
|
|
1819
1822
|
for (let i = 0; i < val.length; ++i) {
|
|
1820
|
-
const
|
|
1823
|
+
const _actual = Number.isNaN(val[i]) || (typeof values[i]) === 'string' ? val[i] : Number.parseInt(values[i], 10);
|
|
1821
1824
|
const _expected = Number.isNaN(values[i]) || (typeof values[i]) === 'string' ? values[i] : Number.parseInt(values[i], 10);
|
|
1822
|
-
if (
|
|
1825
|
+
if (!_actual.includes(_expected)) return false;
|
|
1823
1826
|
}
|
|
1824
1827
|
return true;
|
|
1825
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
|
*
|
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 = {
|
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/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
|
};
|
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
|