codeceptjs 3.5.15 → 3.6.0-beta.1.ai-healers
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 +2 -2
- package/bin/codecept.js +66 -30
- package/docs/advanced.md +351 -0
- package/docs/ai.md +365 -0
- package/docs/api.md +323 -0
- package/docs/basics.md +979 -0
- package/docs/bdd.md +539 -0
- package/docs/best.md +237 -0
- package/docs/books.md +37 -0
- package/docs/bootstrap.md +135 -0
- package/docs/build/AI.js +124 -0
- package/docs/build/ApiDataFactory.js +410 -0
- package/docs/build/Appium.js +2027 -0
- package/docs/build/Expect.js +422 -0
- package/docs/build/FileSystem.js +228 -0
- package/docs/build/GraphQL.js +229 -0
- package/docs/build/GraphQLDataFactory.js +309 -0
- package/docs/build/JSONResponse.js +338 -0
- package/docs/build/Mochawesome.js +71 -0
- package/docs/build/Nightmare.js +2152 -0
- package/docs/build/Playwright.js +5110 -0
- package/docs/build/Protractor.js +2706 -0
- package/docs/build/Puppeteer.js +3905 -0
- package/docs/build/REST.js +344 -0
- package/docs/build/TestCafe.js +2125 -0
- package/docs/build/WebDriver.js +4240 -0
- package/docs/changelog.md +2572 -0
- package/docs/commands.md +266 -0
- package/docs/community-helpers.md +58 -0
- package/docs/configuration.md +157 -0
- package/docs/continuous-integration.md +22 -0
- package/docs/custom-helpers.md +306 -0
- package/docs/data.md +379 -0
- package/docs/detox.md +235 -0
- package/docs/docker.md +136 -0
- package/docs/email.md +183 -0
- package/docs/examples.md +149 -0
- package/docs/heal.md +186 -0
- package/docs/helpers/ApiDataFactory.md +266 -0
- package/docs/helpers/Appium.md +1374 -0
- package/docs/helpers/Detox.md +586 -0
- package/docs/helpers/Expect.md +275 -0
- package/docs/helpers/FileSystem.md +152 -0
- package/docs/helpers/GraphQL.md +151 -0
- package/docs/helpers/GraphQLDataFactory.md +226 -0
- package/docs/helpers/JSONResponse.md +254 -0
- package/docs/helpers/Mochawesome.md +8 -0
- package/docs/helpers/MockRequest.md +377 -0
- package/docs/helpers/Nightmare.md +1305 -0
- package/docs/helpers/OpenAI.md +70 -0
- package/docs/helpers/Playwright.md +2759 -0
- package/docs/helpers/Polly.md +44 -0
- package/docs/helpers/Protractor.md +1769 -0
- package/docs/helpers/Puppeteer-firefox.md +86 -0
- package/docs/helpers/Puppeteer.md +2317 -0
- package/docs/helpers/REST.md +218 -0
- package/docs/helpers/TestCafe.md +1321 -0
- package/docs/helpers/WebDriver.md +2547 -0
- package/docs/hooks.md +340 -0
- package/docs/index.md +111 -0
- package/docs/installation.md +75 -0
- package/docs/internal-api.md +266 -0
- package/docs/locators.md +339 -0
- package/docs/mobile-react-native-locators.md +67 -0
- package/docs/mobile.md +338 -0
- package/docs/pageobjects.md +291 -0
- package/docs/parallel.md +400 -0
- package/docs/playwright.md +632 -0
- package/docs/plugins.md +1247 -0
- package/docs/puppeteer.md +316 -0
- package/docs/quickstart.md +162 -0
- package/docs/react.md +70 -0
- package/docs/reports.md +392 -0
- package/docs/secrets.md +36 -0
- package/docs/shadow.md +68 -0
- package/docs/shared/keys.mustache +31 -0
- package/docs/shared/react.mustache +1 -0
- package/docs/testcafe.md +174 -0
- package/docs/translation.md +247 -0
- package/docs/tutorial.md +271 -0
- package/docs/typescript.md +180 -0
- package/docs/ui.md +59 -0
- package/docs/videos.md +28 -0
- package/docs/visual.md +202 -0
- package/docs/vue.md +143 -0
- package/docs/webdriver.md +701 -0
- package/docs/wiki/Books-&-Posts.md +27 -0
- package/docs/wiki/Community-Helpers-&-Plugins.md +53 -0
- package/docs/wiki/Converting-Playwright-to-Istanbul-Coverage.md +61 -0
- package/docs/wiki/Examples.md +145 -0
- package/docs/wiki/Google-Summer-of-Code-(GSoC)-2020.md +68 -0
- package/docs/wiki/Home.md +16 -0
- package/docs/wiki/Migration-to-Appium-v2---CodeceptJS.md +83 -0
- package/docs/wiki/Release-Process.md +24 -0
- package/docs/wiki/Roadmap.md +23 -0
- package/docs/wiki/Tests.md +1393 -0
- package/docs/wiki/Upgrading-to-CodeceptJS-3.md +153 -0
- package/docs/wiki/Videos.md +19 -0
- package/lib/actor.js +3 -6
- package/lib/ai.js +152 -80
- package/lib/cli.js +1 -0
- package/lib/command/dryRun.js +13 -44
- package/lib/command/generate.js +34 -0
- package/lib/command/run-workers.js +3 -0
- package/lib/command/run.js +3 -0
- package/lib/container.js +2 -0
- package/lib/heal.js +172 -0
- package/lib/helper/AI.js +124 -0
- package/lib/helper/Appium.js +12 -36
- package/lib/helper/Expect.js +8 -11
- package/lib/helper/JSONResponse.js +8 -8
- package/lib/helper/Playwright.js +240 -100
- package/lib/helper/Puppeteer.js +68 -182
- package/lib/helper/REST.js +1 -4
- package/lib/helper/WebDriver.js +10 -324
- package/lib/index.js +3 -0
- package/lib/listener/steps.js +0 -2
- package/lib/locator.js +4 -13
- package/lib/plugin/coverage.js +99 -112
- package/lib/plugin/heal.js +26 -117
- package/lib/recorder.js +11 -5
- package/lib/step.js +1 -3
- package/lib/store.js +2 -0
- package/lib/template/heal.js +39 -0
- package/package.json +35 -47
- package/typings/index.d.ts +0 -17
- package/typings/promiseBasedTypes.d.ts +57 -340
- package/typings/types.d.ts +73 -433
- package/docs/webapi/dontSeeTraffic.mustache +0 -13
- package/docs/webapi/flushNetworkTraffics.mustache +0 -5
- package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
- package/docs/webapi/seeTraffic.mustache +0 -36
- package/docs/webapi/startRecordingTraffic.mustache +0 -8
- package/docs/webapi/stopRecordingTraffic.mustache +0 -5
- package/docs/webapi/waitForCookie.mustache +0 -9
- package/lib/helper/MockServer.js +0 -221
- package/lib/helper/errors/ElementAssertion.js +0 -38
- package/lib/helper/networkTraffics/utils.js +0 -137
- /package/{lib/helper → docs/build}/OpenAI.js +0 -0
package/lib/plugin/coverage.js
CHANGED
|
@@ -1,67 +1,44 @@
|
|
|
1
1
|
const debugModule = require('debug');
|
|
2
|
-
const
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
3
5
|
const Container = require('../container');
|
|
4
6
|
const recorder = require('../recorder');
|
|
5
7
|
const event = require('../event');
|
|
6
8
|
const output = require('../output');
|
|
7
|
-
const {
|
|
9
|
+
const { clearString } = require('../utils');
|
|
8
10
|
|
|
9
11
|
const defaultConfig = {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
coverageDir: 'output/coverage',
|
|
13
|
+
uniqueFileName: true,
|
|
12
14
|
};
|
|
13
15
|
|
|
14
16
|
const supportedHelpers = ['Puppeteer', 'Playwright'];
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
page.coverage.startJSCoverage({
|
|
41
|
-
resetOnNavigation: false,
|
|
42
|
-
includeRawScriptCoverage: true,
|
|
43
|
-
}),
|
|
44
|
-
page.coverage.startCSSCoverage({
|
|
45
|
-
resetOnNavigation: false,
|
|
46
|
-
}),
|
|
47
|
-
]);
|
|
48
|
-
},
|
|
49
|
-
takeCoverage: async (page, coverageReport) => {
|
|
50
|
-
const [jsCoverage, cssCoverage] = await Promise.all([
|
|
51
|
-
page.coverage.stopJSCoverage(),
|
|
52
|
-
page.coverage.stopCSSCoverage(),
|
|
53
|
-
]);
|
|
54
|
-
// to raw V8 script coverage
|
|
55
|
-
const coverageList = [...jsCoverage.map((it) => {
|
|
56
|
-
return {
|
|
57
|
-
source: it.text,
|
|
58
|
-
...it.rawScriptCoverage,
|
|
59
|
-
};
|
|
60
|
-
}), ...cssCoverage];
|
|
61
|
-
await coverageReport.add(coverageList);
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
};
|
|
18
|
+
function buildFileName(test, uniqueFileName) {
|
|
19
|
+
let fileName = clearString(test.title);
|
|
20
|
+
|
|
21
|
+
// This prevent data driven to be included in the failed screenshot file name
|
|
22
|
+
if (fileName.indexOf('{') !== -1) {
|
|
23
|
+
fileName = fileName.substr(0, fileName.indexOf('{') - 3).trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (test.ctx && test.ctx.test && test.ctx.test.type === 'hook') {
|
|
27
|
+
fileName = clearString(`${test.title}_${test.ctx.test.title}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (uniqueFileName) {
|
|
31
|
+
const uuid = test.uuid
|
|
32
|
+
|| test.ctx.test.uuid
|
|
33
|
+
|| Math.floor(new Date().getTime() / 1000);
|
|
34
|
+
|
|
35
|
+
fileName = `${fileName.substring(0, 10)}_${uuid}.coverage.json`;
|
|
36
|
+
} else {
|
|
37
|
+
fileName = `${fileName}.coverage.json`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return fileName;
|
|
41
|
+
}
|
|
65
42
|
|
|
66
43
|
/**
|
|
67
44
|
* Dumps code coverage from Playwright/Puppeteer after every test.
|
|
@@ -72,84 +49,94 @@ const v8CoverageHelpers = {
|
|
|
72
49
|
* ```js
|
|
73
50
|
* plugins: {
|
|
74
51
|
* coverage: {
|
|
75
|
-
* enabled: true
|
|
76
|
-
* debug: true,
|
|
77
|
-
* name: 'CodeceptJS Coverage Report',
|
|
78
|
-
* outputDir: 'output/coverage'
|
|
52
|
+
* enabled: true
|
|
79
53
|
* }
|
|
80
54
|
* }
|
|
81
55
|
* ```
|
|
82
56
|
*
|
|
83
|
-
* Possible config options
|
|
84
|
-
*
|
|
85
|
-
* * `debug`: debug info. By default, false.
|
|
86
|
-
* * `name`: coverage report name.
|
|
87
|
-
* * `outputDir`: path to coverage report.
|
|
88
|
-
* * `sourceFilter`: filter the source files.
|
|
89
|
-
* * `sourcePath`: option to resolve a custom path.
|
|
57
|
+
* Possible config options:
|
|
90
58
|
*
|
|
59
|
+
* * `coverageDir`: directory to dump coverage files
|
|
60
|
+
* * `uniqueFileName`: generate a unique filename by adding uuid
|
|
91
61
|
*/
|
|
92
62
|
module.exports = function (config) {
|
|
93
|
-
config = deepMerge(defaultConfig, config);
|
|
94
|
-
|
|
95
|
-
if (config.debug) config.logging = 'debug';
|
|
96
|
-
|
|
97
63
|
const helpers = Container.helpers();
|
|
98
64
|
let coverageRunning = false;
|
|
65
|
+
let helper;
|
|
99
66
|
|
|
100
|
-
|
|
101
|
-
const helperName
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
67
|
+
let debug;
|
|
68
|
+
for (const helperName of supportedHelpers) {
|
|
69
|
+
if (Object.keys(helpers).indexOf(helperName) > -1) {
|
|
70
|
+
helper = helpers[helperName];
|
|
71
|
+
debug = debugModule(`codeceptjs:plugin:${helperName.toLowerCase()}Coverage`);
|
|
72
|
+
}
|
|
106
73
|
}
|
|
107
74
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const v8Helper = v8CoverageHelpers[helperName];
|
|
75
|
+
if (!helper) {
|
|
76
|
+
console.error('Coverage is only supported in Puppeteer, Playwright');
|
|
77
|
+
return; // no helpers for screenshot
|
|
78
|
+
}
|
|
113
79
|
|
|
114
|
-
const
|
|
115
|
-
...config,
|
|
116
|
-
};
|
|
117
|
-
const coverageReport = new CoverageReport(coverageOptions);
|
|
118
|
-
coverageReport.cleanCache();
|
|
80
|
+
const options = Object.assign(defaultConfig, helper.options, config);
|
|
119
81
|
|
|
120
|
-
event.dispatcher.on(event.all.
|
|
121
|
-
output.
|
|
122
|
-
await coverageReport.generate();
|
|
82
|
+
event.dispatcher.on(event.all.before, async () => {
|
|
83
|
+
output.debug('*** Collecting coverage for tests ****');
|
|
123
84
|
});
|
|
124
85
|
|
|
125
|
-
// we're going to try to "start" coverage before each step because this is
|
|
86
|
+
// Hack! we're going to try to "start" coverage before each step because this is
|
|
126
87
|
// when the browser is already up and is ready to start coverage.
|
|
127
|
-
event.dispatcher.on(event.step.before, () => {
|
|
128
|
-
recorder.add(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
88
|
+
event.dispatcher.on(event.step.before, async () => {
|
|
89
|
+
recorder.add(
|
|
90
|
+
'starting coverage',
|
|
91
|
+
async () => {
|
|
92
|
+
try {
|
|
93
|
+
if (!coverageRunning && helper.page && helper.page.coverage) {
|
|
94
|
+
debug('--> starting coverage <--');
|
|
95
|
+
coverageRunning = true;
|
|
96
|
+
await helper.page.coverage.startJSCoverage();
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error(err);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
true,
|
|
103
|
+
);
|
|
139
104
|
});
|
|
140
105
|
|
|
141
106
|
// Save coverage data after every test run
|
|
142
|
-
event.dispatcher.on(event.test.after, (test) => {
|
|
143
|
-
recorder.add(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
107
|
+
event.dispatcher.on(event.test.after, async (test) => {
|
|
108
|
+
recorder.add(
|
|
109
|
+
'saving coverage',
|
|
110
|
+
async () => {
|
|
111
|
+
try {
|
|
112
|
+
if (coverageRunning && helper.page && helper.page.coverage) {
|
|
113
|
+
debug('--> stopping coverage <--');
|
|
114
|
+
coverageRunning = false;
|
|
115
|
+
const coverage = await helper.page.coverage.stopJSCoverage();
|
|
116
|
+
|
|
117
|
+
const coverageDir = path.resolve(
|
|
118
|
+
process.cwd(),
|
|
119
|
+
options.coverageDir,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Checking if coverageDir already exists, if not, create new one
|
|
123
|
+
|
|
124
|
+
if (!fs.existsSync(coverageDir)) {
|
|
125
|
+
fs.mkdirSync(coverageDir, { recursive: true });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const coveragePath = path.resolve(
|
|
129
|
+
coverageDir,
|
|
130
|
+
buildFileName(test, options.uniqueFileName),
|
|
131
|
+
);
|
|
132
|
+
output.print(`writing ${coveragePath}`);
|
|
133
|
+
fs.writeFileSync(coveragePath, JSON.stringify(coverage));
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.error(err);
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
true,
|
|
140
|
+
);
|
|
154
141
|
});
|
|
155
142
|
};
|
package/lib/plugin/heal.js
CHANGED
|
@@ -1,33 +1,19 @@
|
|
|
1
1
|
const debug = require('debug')('codeceptjs:heal');
|
|
2
2
|
const colors = require('chalk');
|
|
3
|
-
const Container = require('../container');
|
|
4
|
-
const AiAssistant = require('../ai');
|
|
5
3
|
const recorder = require('../recorder');
|
|
6
4
|
const event = require('../event');
|
|
7
5
|
const output = require('../output');
|
|
8
|
-
const
|
|
6
|
+
const heal = require('../heal');
|
|
7
|
+
const store = require('../store');
|
|
9
8
|
|
|
10
9
|
const defaultConfig = {
|
|
11
|
-
healTries: 1,
|
|
12
10
|
healLimit: 2,
|
|
13
|
-
healSteps: [
|
|
14
|
-
'click',
|
|
15
|
-
'fillField',
|
|
16
|
-
'appendField',
|
|
17
|
-
'selectOption',
|
|
18
|
-
'attachFile',
|
|
19
|
-
'checkOption',
|
|
20
|
-
'uncheckOption',
|
|
21
|
-
'doubleClick',
|
|
22
|
-
],
|
|
23
11
|
};
|
|
24
12
|
|
|
25
13
|
/**
|
|
26
|
-
* Self-healing tests with
|
|
14
|
+
* Self-healing tests with AI.
|
|
27
15
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* To use it you need to set OPENAI_API_KEY env variable and enable plugin inside the config.
|
|
16
|
+
* Read more about heaking in [Self-Healing Tests](https://codecept.io/heal/)
|
|
31
17
|
*
|
|
32
18
|
* ```js
|
|
33
19
|
* plugins: {
|
|
@@ -40,22 +26,15 @@ const defaultConfig = {
|
|
|
40
26
|
* More config options are available:
|
|
41
27
|
*
|
|
42
28
|
* * `healLimit` - how many steps can be healed in a single test (default: 2)
|
|
43
|
-
* * `healSteps` - which steps can be healed (default: all steps that interact with UI, see list below)
|
|
44
|
-
*
|
|
45
|
-
* Steps to heal:
|
|
46
|
-
*
|
|
47
|
-
* * `click`
|
|
48
|
-
* * `fillField`
|
|
49
|
-
* * `appendField`
|
|
50
|
-
* * `selectOption`
|
|
51
|
-
* * `attachFile`
|
|
52
|
-
* * `checkOption`
|
|
53
|
-
* * `uncheckOption`
|
|
54
|
-
* * `doubleClick`
|
|
55
29
|
*
|
|
56
30
|
*/
|
|
57
31
|
module.exports = function (config = {}) {
|
|
58
|
-
|
|
32
|
+
if (store.debugMode && !process.env.DEBUG) {
|
|
33
|
+
event.dispatcher.on(event.test.failed, () => {
|
|
34
|
+
output.plugin('heal', 'Healing is disabled in --debug mode, use DEBUG="codeceptjs:heal" to enable it in debug mode');
|
|
35
|
+
});
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
59
38
|
|
|
60
39
|
let currentTest = null;
|
|
61
40
|
let currentStep = null;
|
|
@@ -64,8 +43,6 @@ module.exports = function (config = {}) {
|
|
|
64
43
|
let healTries = 0;
|
|
65
44
|
let isHealing = false;
|
|
66
45
|
|
|
67
|
-
const healSuggestions = [];
|
|
68
|
-
|
|
69
46
|
config = Object.assign(defaultConfig, config);
|
|
70
47
|
|
|
71
48
|
event.dispatcher.on(event.test.before, (test) => {
|
|
@@ -78,63 +55,27 @@ module.exports = function (config = {}) {
|
|
|
78
55
|
|
|
79
56
|
event.dispatcher.on(event.step.after, (step) => {
|
|
80
57
|
if (isHealing) return;
|
|
81
|
-
|
|
82
|
-
|
|
58
|
+
if (healTries >= config.healLimit) return; // out of limit
|
|
59
|
+
|
|
60
|
+
if (!heal.hasCorrespondingRecipes(step)) return;
|
|
61
|
+
|
|
83
62
|
recorder.catchWithoutStop(async (err) => {
|
|
84
63
|
isHealing = true;
|
|
85
64
|
if (caughtError === err) throw err; // avoid double handling
|
|
86
65
|
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
|
-
}
|
|
91
|
-
if (!currentStep) throw err;
|
|
92
|
-
if (!config.healSteps.includes(currentStep.name)) throw err;
|
|
93
|
-
const test = currentTest;
|
|
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
66
|
|
|
103
|
-
|
|
104
|
-
output.print(colors.bold.red(`Can't heal more than ${config.healLimit} step(s) in a test`));
|
|
105
|
-
output.print('Entire flow can be broken, please check it manually');
|
|
106
|
-
output.print('or increase healing limit in heal plugin config');
|
|
107
|
-
|
|
108
|
-
throw err;
|
|
109
|
-
}
|
|
67
|
+
const test = currentTest;
|
|
110
68
|
|
|
111
69
|
recorder.session.start('heal');
|
|
112
|
-
const helpers = Container.helpers();
|
|
113
|
-
let helper;
|
|
114
|
-
|
|
115
|
-
for (const helperName of supportedHelpers) {
|
|
116
|
-
if (Object.keys(helpers).indexOf(helperName) > -1) {
|
|
117
|
-
helper = helpers[helperName];
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
70
|
|
|
121
|
-
if (!helper) throw err; // no helpers for html
|
|
122
|
-
|
|
123
|
-
const step = test.steps[test.steps.length - 1];
|
|
124
71
|
debug('Self-healing started', step.toCode());
|
|
125
72
|
|
|
126
|
-
|
|
127
|
-
output.level(0);
|
|
128
|
-
const html = await helper.grabHTMLFrom('body');
|
|
129
|
-
output.level(currentOutputLevel);
|
|
130
|
-
|
|
131
|
-
if (!html) throw err;
|
|
73
|
+
await heal.healStep(step, err, { test });
|
|
132
74
|
|
|
133
75
|
healTries++;
|
|
134
|
-
await aiAssistant.setHtmlContext(html);
|
|
135
|
-
await tryToHeal(step, err);
|
|
136
76
|
|
|
137
77
|
recorder.add('close healing session', () => {
|
|
78
|
+
recorder.reset();
|
|
138
79
|
recorder.session.restore('heal');
|
|
139
80
|
recorder.ignoreErr(err);
|
|
140
81
|
});
|
|
@@ -145,7 +86,7 @@ module.exports = function (config = {}) {
|
|
|
145
86
|
});
|
|
146
87
|
|
|
147
88
|
event.dispatcher.on(event.all.result, () => {
|
|
148
|
-
if (!
|
|
89
|
+
if (!heal.fixes?.length) return;
|
|
149
90
|
|
|
150
91
|
const { print } = output;
|
|
151
92
|
|
|
@@ -153,16 +94,20 @@ module.exports = function (config = {}) {
|
|
|
153
94
|
print('===================');
|
|
154
95
|
print(colors.bold.green('Self-Healing Report:'));
|
|
155
96
|
|
|
156
|
-
print(`${colors.bold(
|
|
97
|
+
print(`${colors.bold(heal.fixes.length)} ${heal.fixes.length === 1 ? 'step was' : 'steps were'} healed`);
|
|
98
|
+
|
|
99
|
+
const suggestions = heal.fixes.filter(fix => fix.recipe && heal.recipes[fix.recipe].suggest);
|
|
100
|
+
|
|
101
|
+
if (!suggestions.length) return;
|
|
157
102
|
|
|
158
103
|
let i = 1;
|
|
159
104
|
print('');
|
|
160
105
|
print('Suggested changes:');
|
|
161
106
|
print('');
|
|
162
107
|
|
|
163
|
-
for (const suggestion of
|
|
164
|
-
print(`${i}. To fix ${colors.bold.
|
|
165
|
-
print('Replace the failed code
|
|
108
|
+
for (const suggestion of suggestions) {
|
|
109
|
+
print(`${i}. To fix ${colors.bold.magenta(suggestion.test?.title)}`);
|
|
110
|
+
print(' Replace the failed code:', colors.gray(`(suggested by ${colors.bold(suggestion.recipe)})`));
|
|
166
111
|
print(colors.red(`- ${suggestion.step.toCode()}`));
|
|
167
112
|
print(colors.green(`+ ${suggestion.snippet}`));
|
|
168
113
|
print(suggestion.step.line());
|
|
@@ -170,40 +115,4 @@ module.exports = function (config = {}) {
|
|
|
170
115
|
i++;
|
|
171
116
|
}
|
|
172
117
|
});
|
|
173
|
-
|
|
174
|
-
async function tryToHeal(failedStep, err) {
|
|
175
|
-
output.debug(`Running OpenAI to heal ${failedStep.toCode()} step`);
|
|
176
|
-
|
|
177
|
-
const codeSnippets = await aiAssistant.healFailedStep(failedStep, err, currentTest);
|
|
178
|
-
|
|
179
|
-
output.debug(`Received ${codeSnippets.length} suggestions from OpenAI`);
|
|
180
|
-
const I = Container.support('I'); // eslint-disable-line
|
|
181
|
-
|
|
182
|
-
for (const codeSnippet of codeSnippets) {
|
|
183
|
-
try {
|
|
184
|
-
debug('Executing', codeSnippet);
|
|
185
|
-
recorder.catch((e) => {
|
|
186
|
-
console.log(e);
|
|
187
|
-
});
|
|
188
|
-
await eval(codeSnippet); // eslint-disable-line
|
|
189
|
-
|
|
190
|
-
healSuggestions.push({
|
|
191
|
-
test: currentTest,
|
|
192
|
-
step: failedStep,
|
|
193
|
-
snippet: codeSnippet,
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
recorder.add('healed', () => output.print(colors.bold.green(' Code healed successfully')));
|
|
197
|
-
healedSteps++;
|
|
198
|
-
return;
|
|
199
|
-
} catch (err) {
|
|
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')));
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
output.debug(`Couldn't heal the code for ${failedStep.toCode()}`);
|
|
207
|
-
}
|
|
208
|
-
return recorder.promise();
|
|
209
118
|
};
|
package/lib/recorder.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const debug = require('debug')('codeceptjs:recorder');
|
|
2
2
|
const promiseRetry = require('promise-retry');
|
|
3
|
+
const chalk = require('chalk');
|
|
3
4
|
const { printObjectProperties } = require('./utils');
|
|
4
5
|
const { log } = require('./output');
|
|
5
6
|
|
|
@@ -179,7 +180,7 @@ module.exports = {
|
|
|
179
180
|
return;
|
|
180
181
|
}
|
|
181
182
|
tasks.push(taskName);
|
|
182
|
-
debug(`${currentQueue()}Queued | ${taskName}`);
|
|
183
|
+
debug(chalk.gray(`${currentQueue()} Queued | ${taskName}`));
|
|
183
184
|
|
|
184
185
|
return promise = Promise.resolve(promise).then((res) => {
|
|
185
186
|
// prefer options for non-conditional retries
|
|
@@ -190,11 +191,14 @@ module.exports = {
|
|
|
190
191
|
return Promise.race([promise, Promise.resolve(res).then(fn)]).finally(() => clearTimeout(timer));
|
|
191
192
|
}
|
|
192
193
|
|
|
194
|
+
debug(`${currentQueue()} Running | ${taskName}`);
|
|
195
|
+
|
|
193
196
|
const retryRules = this.retries.slice().reverse();
|
|
194
197
|
return promiseRetry(Object.assign(defaultRetryOptions, retryOpts), (retry, number) => {
|
|
195
198
|
if (number > 1) log(`${currentQueue()}Retrying... Attempt #${number}`);
|
|
196
199
|
const [promise, timer] = getTimeoutPromise(timeout, taskName);
|
|
197
200
|
return Promise.race([promise, Promise.resolve(res).then(fn)]).finally(() => clearTimeout(timer)).catch((err) => {
|
|
201
|
+
if (ignoredErrs.includes(err)) return;
|
|
198
202
|
for (const retryObj of retryRules) {
|
|
199
203
|
if (!retryObj.when) return retry(err);
|
|
200
204
|
if (retryObj.when && retryObj.when(err)) return retry(err);
|
|
@@ -229,7 +233,7 @@ module.exports = {
|
|
|
229
233
|
*/
|
|
230
234
|
catch(customErrFn) {
|
|
231
235
|
const fnDescription = customErrFn?.toString()?.replace(/\s{2,}/g, ' ').replace(/\n/g, ' ')?.slice(0, 50);
|
|
232
|
-
debug(`${currentQueue()}Queued | catch with error handler ${fnDescription || ''}`);
|
|
236
|
+
debug(chalk.gray(`${currentQueue()} Queued | catch with error handler ${fnDescription || ''}`));
|
|
233
237
|
return promise = promise.catch((err) => {
|
|
234
238
|
log(`${currentQueue()}Error | ${err} ${fnDescription}...`);
|
|
235
239
|
if (!(err instanceof Error)) { // strange things may happen
|
|
@@ -253,7 +257,7 @@ module.exports = {
|
|
|
253
257
|
const fnDescription = customErrFn?.toString()?.replace(/\s{2,}/g, ' ').replace(/\n/g, ' ')?.slice(0, 50);
|
|
254
258
|
return promise = promise.catch((err) => {
|
|
255
259
|
if (ignoredErrs.includes(err)) return; // already caught
|
|
256
|
-
log(`${currentQueue()}Error (Non-Terminated) | ${err} | ${fnDescription || ''}...`);
|
|
260
|
+
log(`${currentQueue()} Error (Non-Terminated) | ${err} | ${fnDescription || ''}...`);
|
|
257
261
|
if (!(err instanceof Error)) { // strange things may happen
|
|
258
262
|
err = new Error(`[Wrapped Error] ${JSON.stringify(err)}`); // we should be prepared for them
|
|
259
263
|
}
|
|
@@ -272,7 +276,9 @@ module.exports = {
|
|
|
272
276
|
*/
|
|
273
277
|
|
|
274
278
|
throw(err) {
|
|
279
|
+
if (ignoredErrs.includes(err)) return promise; // already caught
|
|
275
280
|
return this.add(`throw error: ${err.message}`, () => {
|
|
281
|
+
if (ignoredErrs.includes(err)) return; // already caught
|
|
276
282
|
throw err;
|
|
277
283
|
});
|
|
278
284
|
},
|
|
@@ -313,8 +319,8 @@ module.exports = {
|
|
|
313
319
|
* @inner
|
|
314
320
|
*/
|
|
315
321
|
stop() {
|
|
316
|
-
|
|
317
|
-
log(`${currentQueue()}Stopping recording promises`);
|
|
322
|
+
debug(this.toString());
|
|
323
|
+
log(`${currentQueue()} Stopping recording promises`);
|
|
318
324
|
running = false;
|
|
319
325
|
},
|
|
320
326
|
|
package/lib/step.js
CHANGED
|
@@ -119,9 +119,7 @@ class Step {
|
|
|
119
119
|
}
|
|
120
120
|
let result;
|
|
121
121
|
try {
|
|
122
|
-
|
|
123
|
-
result = this.helper[this.helperMethod].apply(this.helper, this.args);
|
|
124
|
-
}
|
|
122
|
+
result = this.helper[this.helperMethod].apply(this.helper, this.args);
|
|
125
123
|
this.setStatus('success');
|
|
126
124
|
} catch (err) {
|
|
127
125
|
this.setStatus('failed');
|
package/lib/store.js
CHANGED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const { heal, ai } = require('codeceptjs');
|
|
2
|
+
|
|
3
|
+
heal.addRecipe('ai', {
|
|
4
|
+
priority: 10,
|
|
5
|
+
prepare: {
|
|
6
|
+
html: ({ I }) => I.grabHTMLFrom('body'),
|
|
7
|
+
},
|
|
8
|
+
steps: [
|
|
9
|
+
'click',
|
|
10
|
+
'fillField',
|
|
11
|
+
'appendField',
|
|
12
|
+
'selectOption',
|
|
13
|
+
'attachFile',
|
|
14
|
+
'checkOption',
|
|
15
|
+
'uncheckOption',
|
|
16
|
+
'doubleClick',
|
|
17
|
+
],
|
|
18
|
+
fn: async (args) => {
|
|
19
|
+
return ai.healFailedStep(args);
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
heal.addRecipe('clickAndType', {
|
|
24
|
+
priority: 1,
|
|
25
|
+
steps: [
|
|
26
|
+
'fillField',
|
|
27
|
+
'appendField',
|
|
28
|
+
],
|
|
29
|
+
fn: async ({ step }) => {
|
|
30
|
+
const locator = step.args[0];
|
|
31
|
+
const text = step.args[1];
|
|
32
|
+
|
|
33
|
+
return ({ I }) => {
|
|
34
|
+
I.click(locator);
|
|
35
|
+
I.wait(1); // to open modal or something
|
|
36
|
+
I.type(text);
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
});
|