codeceptjs 3.5.15 → 3.6.0
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/bin/codecept.js +68 -31
- package/docs/webapi/startRecordingWebSocketMessages.mustache +8 -0
- package/docs/webapi/stopRecordingWebSocketMessages.mustache +7 -0
- package/lib/ai.js +152 -80
- package/lib/cli.js +1 -0
- 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/{OpenAI.js → AI.js} +10 -12
- package/lib/helper/Playwright.js +32 -156
- package/lib/helper/Puppeteer.js +222 -3
- package/lib/helper/WebDriver.js +6 -144
- package/lib/helper/extras/PlaywrightReactVueLocator.js +6 -1
- package/lib/helper/network/actions.js +123 -0
- package/lib/helper/{networkTraffics → network}/utils.js +50 -0
- package/lib/index.js +3 -0
- package/lib/listener/steps.js +0 -2
- package/lib/locator.js +23 -1
- package/lib/plugin/heal.js +26 -117
- package/lib/recorder.js +11 -5
- package/lib/store.js +2 -0
- package/lib/template/heal.js +39 -0
- package/package.json +14 -15
- package/typings/index.d.ts +2 -2
- package/typings/promiseBasedTypes.d.ts +206 -25
- package/typings/types.d.ts +219 -26
package/bin/codecept.js
CHANGED
|
@@ -4,6 +4,33 @@ const Codecept = require('../lib/codecept');
|
|
|
4
4
|
const { print, error } = require('../lib/output');
|
|
5
5
|
const { printError } = require('../lib/command/utils');
|
|
6
6
|
|
|
7
|
+
const commandFlags = {
|
|
8
|
+
ai: {
|
|
9
|
+
flag: '--ai',
|
|
10
|
+
description: 'enable AI assistant',
|
|
11
|
+
},
|
|
12
|
+
verbose: {
|
|
13
|
+
flag: '--verbose',
|
|
14
|
+
description: 'output internal logging information',
|
|
15
|
+
},
|
|
16
|
+
debug: {
|
|
17
|
+
flag: '--debug',
|
|
18
|
+
description: 'output additional information',
|
|
19
|
+
},
|
|
20
|
+
config: {
|
|
21
|
+
flag: '-c, --config [file]',
|
|
22
|
+
description: 'configuration file to be used',
|
|
23
|
+
},
|
|
24
|
+
profile: {
|
|
25
|
+
flag: '--profile [value]',
|
|
26
|
+
description: 'configuration profile to be used',
|
|
27
|
+
},
|
|
28
|
+
steps: {
|
|
29
|
+
flag: '--steps',
|
|
30
|
+
description: 'show step-by-step execution',
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
7
34
|
const errorHandler = (fn) => async (...args) => {
|
|
8
35
|
try {
|
|
9
36
|
await fn(...args);
|
|
@@ -35,9 +62,10 @@ program.command('migrate [path]')
|
|
|
35
62
|
program.command('shell [path]')
|
|
36
63
|
.alias('sh')
|
|
37
64
|
.description('Interactive shell')
|
|
38
|
-
.option(
|
|
39
|
-
.option(
|
|
40
|
-
.option(
|
|
65
|
+
.option(commandFlags.verbose.flag, commandFlags.verbose.description)
|
|
66
|
+
.option(commandFlags.profile.flag, commandFlags.profile.description)
|
|
67
|
+
.option(commandFlags.ai.flag, commandFlags.ai.description)
|
|
68
|
+
.option(commandFlags.config.flag, commandFlags.config.description)
|
|
41
69
|
.action(errorHandler(require('../lib/command/interactive')));
|
|
42
70
|
|
|
43
71
|
program.command('list [path]')
|
|
@@ -47,27 +75,27 @@ program.command('list [path]')
|
|
|
47
75
|
|
|
48
76
|
program.command('def [path]')
|
|
49
77
|
.description('Generates TypeScript definitions for all I actions.')
|
|
50
|
-
.option(
|
|
78
|
+
.option(commandFlags.config.flag, commandFlags.config.description)
|
|
51
79
|
.option('-o, --output [folder]', 'target folder to paste definitions')
|
|
52
80
|
.action(errorHandler(require('../lib/command/definitions')));
|
|
53
81
|
|
|
54
82
|
program.command('gherkin:init [path]')
|
|
55
83
|
.alias('bdd:init')
|
|
56
84
|
.description('Prepare CodeceptJS to run feature files.')
|
|
57
|
-
.option(
|
|
85
|
+
.option(commandFlags.config.flag, commandFlags.config.description)
|
|
58
86
|
.action(errorHandler(require('../lib/command/gherkin/init')));
|
|
59
87
|
|
|
60
88
|
program.command('gherkin:steps [path]')
|
|
61
89
|
.alias('bdd:steps')
|
|
62
90
|
.description('Prints all defined gherkin steps.')
|
|
63
|
-
.option(
|
|
91
|
+
.option(commandFlags.config.flag, commandFlags.config.description)
|
|
64
92
|
.action(errorHandler(require('../lib/command/gherkin/steps')));
|
|
65
93
|
|
|
66
94
|
program.command('gherkin:snippets [path]')
|
|
67
95
|
.alias('bdd:snippets')
|
|
68
96
|
.description('Generate step definitions from steps.')
|
|
69
97
|
.option('--dry-run', "don't save snippets to file")
|
|
70
|
-
.option(
|
|
98
|
+
.option(commandFlags.config.flag, commandFlags.config.description)
|
|
71
99
|
.option('--feature [file]', 'feature files(s) to scan')
|
|
72
100
|
.option('--path [file]', 'file in which to place the new snippets')
|
|
73
101
|
.action(errorHandler(require('../lib/command/gherkin/snippets')));
|
|
@@ -93,16 +121,22 @@ program.command('generate:helper [path]')
|
|
|
93
121
|
.description('Generates a new helper')
|
|
94
122
|
.action(errorHandler(require('../lib/command/generate').helper));
|
|
95
123
|
|
|
124
|
+
program.command('generate:heal [path]')
|
|
125
|
+
.alias('gr')
|
|
126
|
+
.description('Generates basic heal recipes')
|
|
127
|
+
.action(errorHandler(require('../lib/command/generate').heal));
|
|
128
|
+
|
|
96
129
|
program.command('run [test]')
|
|
97
130
|
.description('Executes tests')
|
|
98
131
|
|
|
99
132
|
// codecept-only options
|
|
100
|
-
.option(
|
|
101
|
-
.option(
|
|
102
|
-
.option(
|
|
133
|
+
.option(commandFlags.ai.flag, commandFlags.ai.description)
|
|
134
|
+
.option(commandFlags.steps.flag, commandFlags.steps.description)
|
|
135
|
+
.option(commandFlags.debug.flag, commandFlags.debug.description)
|
|
136
|
+
.option(commandFlags.verbose.flag, commandFlags.verbose.description)
|
|
103
137
|
.option('-o, --override [value]', 'override current config options')
|
|
104
|
-
.option(
|
|
105
|
-
.option(
|
|
138
|
+
.option(commandFlags.profile.flag, commandFlags.profile.description)
|
|
139
|
+
.option(commandFlags.config.flag, commandFlags.config.description)
|
|
106
140
|
.option('--features', 'run only *.feature files and skip tests')
|
|
107
141
|
.option('--tests', 'run only JS test files and skip features')
|
|
108
142
|
.option('--no-timeouts', 'disable all timeouts')
|
|
@@ -132,16 +166,17 @@ program.command('run [test]')
|
|
|
132
166
|
|
|
133
167
|
program.command('run-workers <workers> [selectedRuns...]')
|
|
134
168
|
.description('Executes tests in workers')
|
|
135
|
-
.option(
|
|
169
|
+
.option(commandFlags.config.flag, commandFlags.config.description)
|
|
136
170
|
.option('-g, --grep <pattern>', 'only run tests matching <pattern>')
|
|
137
171
|
.option('-i, --invert', 'inverts --grep matches')
|
|
138
172
|
.option('-o, --override [value]', 'override current config options')
|
|
139
173
|
.option('--suites', 'parallel execution of suites not single tests')
|
|
140
|
-
.option(
|
|
141
|
-
.option(
|
|
174
|
+
.option(commandFlags.debug.flag, commandFlags.debug.description)
|
|
175
|
+
.option(commandFlags.verbose.flag, commandFlags.verbose.description)
|
|
142
176
|
.option('--features', 'run only *.feature files and skip tests')
|
|
143
177
|
.option('--tests', 'run only JS test files and skip features')
|
|
144
|
-
.option(
|
|
178
|
+
.option(commandFlags.profile.flag, commandFlags.profile.description)
|
|
179
|
+
.option(commandFlags.ai.flag, commandFlags.ai.description)
|
|
145
180
|
.option('-p, --plugins <k=v,k2=v2,...>', 'enable plugins, comma-separated')
|
|
146
181
|
.option('-O, --reporter-options <k=v,k2=v2,...>', 'reporter-specific options')
|
|
147
182
|
.option('-R, --reporter <name>', 'specify the reporter to use')
|
|
@@ -149,17 +184,18 @@ program.command('run-workers <workers> [selectedRuns...]')
|
|
|
149
184
|
|
|
150
185
|
program.command('run-multiple [suites...]')
|
|
151
186
|
.description('Executes tests multiple')
|
|
152
|
-
.option(
|
|
153
|
-
.option(
|
|
187
|
+
.option(commandFlags.config.flag, commandFlags.config.description)
|
|
188
|
+
.option(commandFlags.profile.flag, commandFlags.profile.description)
|
|
154
189
|
.option('--all', 'run all suites')
|
|
155
190
|
.option('--features', 'run only *.feature files and skip tests')
|
|
156
191
|
.option('--tests', 'run only JS test files and skip features')
|
|
192
|
+
.option(commandFlags.ai.flag, commandFlags.ai.description)
|
|
157
193
|
.option('-g, --grep <pattern>', 'only run tests matching <pattern>')
|
|
158
194
|
.option('-f, --fgrep <string>', 'only run tests containing <string>')
|
|
159
195
|
.option('-i, --invert', 'inverts --grep and --fgrep matches')
|
|
160
|
-
.option(
|
|
161
|
-
.option(
|
|
162
|
-
.option(
|
|
196
|
+
.option(commandFlags.steps.flag, commandFlags.steps.description)
|
|
197
|
+
.option(commandFlags.verbose.flag, commandFlags.verbose.description)
|
|
198
|
+
.option(commandFlags.debug.flag, commandFlags.debug.description)
|
|
163
199
|
.option('-p, --plugins <k=v,k2=v2,...>', 'enable plugins, comma-separated')
|
|
164
200
|
.option('-o, --override [value]', 'override current config options')
|
|
165
201
|
.option('-O, --reporter-options <k=v,k2=v2,...>', 'reporter-specific options')
|
|
@@ -180,28 +216,28 @@ program.command('dry-run [test]')
|
|
|
180
216
|
.description('Prints step-by-step scenario for a test without actually running it')
|
|
181
217
|
.option('-p, --plugins <k=v,k2=v2,...>', 'enable plugins, comma-separated')
|
|
182
218
|
.option('--bootstrap', 'enable bootstrap & teardown scripts for dry-run')
|
|
183
|
-
.option(
|
|
219
|
+
.option(commandFlags.config.flag, commandFlags.config.description)
|
|
184
220
|
.option('--all', 'run all suites')
|
|
185
221
|
.option('--features', 'run only *.feature files and skip tests')
|
|
186
222
|
.option('--tests', 'run only JS test files and skip features')
|
|
187
223
|
.option('-g, --grep <pattern>', 'only run tests matching <pattern>')
|
|
188
224
|
.option('-f, --fgrep <string>', 'only run tests containing <string>')
|
|
189
225
|
.option('-i, --invert', 'inverts --grep and --fgrep matches')
|
|
190
|
-
.option(
|
|
191
|
-
.option(
|
|
192
|
-
.option(
|
|
226
|
+
.option(commandFlags.steps.flag, commandFlags.steps.description)
|
|
227
|
+
.option(commandFlags.verbose.flag, commandFlags.verbose.description)
|
|
228
|
+
.option(commandFlags.debug.flag, commandFlags.debug.description)
|
|
193
229
|
.action(errorHandler(require('../lib/command/dryRun')));
|
|
194
230
|
|
|
195
231
|
program.command('run-rerun [test]')
|
|
196
232
|
.description('Executes tests in more than one test suite run')
|
|
197
233
|
|
|
198
234
|
// codecept-only options
|
|
199
|
-
.option(
|
|
200
|
-
.option(
|
|
201
|
-
.option(
|
|
235
|
+
.option(commandFlags.steps.flag, commandFlags.steps.description)
|
|
236
|
+
.option(commandFlags.debug.flag, commandFlags.debug.description)
|
|
237
|
+
.option(commandFlags.verbose.flag, commandFlags.verbose.description)
|
|
202
238
|
.option('-o, --override [value]', 'override current config options')
|
|
203
|
-
.option(
|
|
204
|
-
.option(
|
|
239
|
+
.option(commandFlags.profile.flag, commandFlags.profile.description)
|
|
240
|
+
.option(commandFlags.config.flag, commandFlags.config.description)
|
|
205
241
|
.option('--features', 'run only *.feature files and skip tests')
|
|
206
242
|
.option('--tests', 'run only JS test files and skip features')
|
|
207
243
|
.option('-p, --plugins <k=v,k2=v2,...>', 'enable plugins, comma-separated')
|
|
@@ -236,5 +272,6 @@ program.on('command:*', (cmd) => {
|
|
|
236
272
|
|
|
237
273
|
if (process.argv.length <= 2) {
|
|
238
274
|
program.outputHelp();
|
|
275
|
+
} else {
|
|
276
|
+
program.parse(process.argv);
|
|
239
277
|
}
|
|
240
|
-
program.parse(process.argv);
|
package/lib/ai.js
CHANGED
|
@@ -1,47 +1,118 @@
|
|
|
1
|
-
const { Configuration, OpenAIApi } = require('openai');
|
|
2
1
|
const debug = require('debug')('codeceptjs:ai');
|
|
3
|
-
const config = require('./config');
|
|
4
2
|
const output = require('./output');
|
|
3
|
+
const event = require('./event');
|
|
5
4
|
const { removeNonInteractiveElements, minifyHtml, splitByChunks } = require('./html');
|
|
6
5
|
|
|
7
|
-
const
|
|
8
|
-
model: 'gpt-3.5-turbo-16k',
|
|
9
|
-
temperature: 0.1,
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
const htmlConfig = {
|
|
6
|
+
const defaultHtmlConfig = {
|
|
13
7
|
maxLength: 50000,
|
|
14
8
|
simplify: true,
|
|
15
9
|
minify: true,
|
|
16
10
|
html: {},
|
|
17
11
|
};
|
|
18
12
|
|
|
19
|
-
const
|
|
13
|
+
const defaultPrompts = {
|
|
14
|
+
writeStep: (html, input) => [{
|
|
15
|
+
role: 'user',
|
|
16
|
+
content: `I am test engineer writing test in CodeceptJS
|
|
17
|
+
I have opened web page and I want to use CodeceptJS to ${input} on this page
|
|
18
|
+
Provide me valid CodeceptJS code to accomplish it
|
|
19
|
+
Use only locators from this HTML: \n\n${html}`,
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
|
|
23
|
+
healStep: (html, { step, error, prevSteps }) => {
|
|
24
|
+
return [{
|
|
25
|
+
role: 'user',
|
|
26
|
+
content: `As a test automation engineer I am testing web application using CodeceptJS.
|
|
27
|
+
I want to heal a test that fails. Here is the list of executed steps: ${prevSteps.map(s => s.toString()).join(', ')}
|
|
28
|
+
Propose how to adjust ${step.toCode()} step to fix the test.
|
|
29
|
+
Use locators in order of preference: semantic locator by text, CSS, XPath. Use codeblocks marked with \`\`\`
|
|
30
|
+
Here is the error message: ${error.message}
|
|
31
|
+
Here is HTML code of a page where the failure has happened: \n\n${html}`,
|
|
32
|
+
}];
|
|
33
|
+
},
|
|
34
|
+
};
|
|
20
35
|
|
|
21
36
|
class AiAssistant {
|
|
22
37
|
constructor() {
|
|
23
|
-
this.
|
|
24
|
-
this.
|
|
25
|
-
|
|
26
|
-
this.
|
|
38
|
+
this.totalTime = 0;
|
|
39
|
+
this.numTokens = 0;
|
|
40
|
+
|
|
41
|
+
this.reset();
|
|
42
|
+
this.connectToEvents();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
enable(config = {}) {
|
|
46
|
+
debug('Enabling AI assistant');
|
|
47
|
+
this.isEnabled = true;
|
|
48
|
+
|
|
49
|
+
const { html, prompts, ...aiConfig } = config;
|
|
50
|
+
|
|
51
|
+
this.config = Object.assign(this.config, aiConfig);
|
|
52
|
+
this.htmlConfig = Object.assign(defaultHtmlConfig, html);
|
|
53
|
+
this.prompts = Object.assign(defaultPrompts, prompts);
|
|
54
|
+
|
|
55
|
+
debug('Config', this.config);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
reset() {
|
|
59
|
+
this.numTokens = 0;
|
|
60
|
+
this.isEnabled = false;
|
|
61
|
+
this.config = {
|
|
62
|
+
maxTokens: 1000000,
|
|
63
|
+
request: null,
|
|
64
|
+
response: parseCodeBlocks,
|
|
65
|
+
// lets limit token usage to 1M
|
|
66
|
+
};
|
|
67
|
+
this.minifiedHtml = null;
|
|
27
68
|
this.response = null;
|
|
69
|
+
this.totalTime = 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
disable() {
|
|
73
|
+
this.isEnabled = false;
|
|
74
|
+
}
|
|
28
75
|
|
|
29
|
-
|
|
76
|
+
connectToEvents() {
|
|
77
|
+
event.dispatcher.on(event.all.result, () => {
|
|
78
|
+
if (this.isEnabled && this.numTokens > 0) {
|
|
79
|
+
const numTokensK = Math.ceil(this.numTokens / 1000);
|
|
80
|
+
const maxTokensK = Math.ceil(this.config.maxTokens / 1000);
|
|
81
|
+
output.print(`AI assistant took ${this.totalTime}s and used ~${numTokensK}K input tokens. Tokens limit: ${maxTokensK}K`);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
30
85
|
|
|
86
|
+
checkRequestFn() {
|
|
31
87
|
if (!this.isEnabled) {
|
|
32
|
-
debug('
|
|
88
|
+
debug('AI assistant is disabled');
|
|
33
89
|
return;
|
|
34
90
|
}
|
|
35
91
|
|
|
36
|
-
|
|
37
|
-
apiKey: process.env.OPENAI_API_KEY,
|
|
38
|
-
});
|
|
92
|
+
if (this.config.request) return;
|
|
39
93
|
|
|
40
|
-
|
|
41
|
-
|
|
94
|
+
const noRequestErrorMessage = `
|
|
95
|
+
No request function is set for AI assistant.
|
|
96
|
+
Please implement your own request function and set it in the config.
|
|
97
|
+
|
|
98
|
+
[!] AI request was decoupled from CodeceptJS. To connect to OpenAI or other AI service, please implement your own request function and set it in the config.
|
|
99
|
+
|
|
100
|
+
Example (connect to OpenAI):
|
|
101
|
+
|
|
102
|
+
ai: {
|
|
103
|
+
request: async (messages) => {
|
|
104
|
+
const OpenAI = require('openai');
|
|
105
|
+
const openai = new OpenAI({ apiKey: process.env['OPENAI_API_KEY'] })
|
|
106
|
+
const response = await openai.chat.completions.create({
|
|
107
|
+
model: 'gpt-3.5-turbo-0125',
|
|
108
|
+
messages,
|
|
109
|
+
});
|
|
110
|
+
return response?.data?.choices[0]?.message?.content;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
`.trim();
|
|
42
114
|
|
|
43
|
-
|
|
44
|
-
return aiInstance || new AiAssistant();
|
|
115
|
+
throw new Error(noRequestErrorMessage);
|
|
45
116
|
}
|
|
46
117
|
|
|
47
118
|
async setHtmlContext(html) {
|
|
@@ -50,98 +121,99 @@ class AiAssistant {
|
|
|
50
121
|
if (this.htmlConfig.simplify) {
|
|
51
122
|
processedHTML = removeNonInteractiveElements(processedHTML, this.htmlConfig);
|
|
52
123
|
}
|
|
124
|
+
|
|
53
125
|
if (this.htmlConfig.minify) processedHTML = await minifyHtml(processedHTML);
|
|
54
126
|
if (this.htmlConfig.maxLength) processedHTML = splitByChunks(processedHTML, this.htmlConfig.maxLength)[0];
|
|
55
127
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
this.html = processedHTML;
|
|
128
|
+
this.minifiedHtml = processedHTML;
|
|
59
129
|
}
|
|
60
130
|
|
|
61
131
|
getResponse() {
|
|
62
132
|
return this.response || '';
|
|
63
133
|
}
|
|
64
134
|
|
|
65
|
-
mockResponse(response) {
|
|
66
|
-
this.mockedResponse = response;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
135
|
async createCompletion(messages) {
|
|
70
|
-
if (!this.
|
|
136
|
+
if (!this.isEnabled) return '';
|
|
71
137
|
|
|
72
|
-
debug(messages);
|
|
138
|
+
debug('Request', messages);
|
|
73
139
|
|
|
74
|
-
|
|
140
|
+
this.checkRequestFn();
|
|
75
141
|
|
|
76
142
|
this.response = null;
|
|
77
143
|
|
|
78
|
-
|
|
79
|
-
const completion = await this.openai.createChatCompletion({
|
|
80
|
-
...this.config,
|
|
81
|
-
messages,
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
this.response = completion?.data?.choices[0]?.message?.content;
|
|
85
|
-
|
|
86
|
-
debug(this.response);
|
|
144
|
+
this.calculateTokens(messages);
|
|
87
145
|
|
|
146
|
+
try {
|
|
147
|
+
const startTime = process.hrtime();
|
|
148
|
+
this.response = await this.config.request(messages);
|
|
149
|
+
const endTime = process.hrtime(startTime);
|
|
150
|
+
const executionTimeInSeconds = endTime[0] + endTime[1] / 1e9;
|
|
151
|
+
|
|
152
|
+
this.totalTime += Math.round(executionTimeInSeconds);
|
|
153
|
+
debug('AI response time', executionTimeInSeconds);
|
|
154
|
+
debug('Response', this.response);
|
|
155
|
+
this.stopWhenReachingTokensLimit();
|
|
88
156
|
return this.response;
|
|
89
157
|
} catch (err) {
|
|
90
158
|
debug(err.response);
|
|
91
159
|
output.print('');
|
|
92
|
-
output.error(`
|
|
93
|
-
output.error(err?.response?.data?.error?.code);
|
|
94
|
-
output.error(err?.response?.data?.error?.message);
|
|
160
|
+
output.error(`AI service error: ${err.message}`);
|
|
161
|
+
if (err?.response?.data?.error?.code) output.error(err?.response?.data?.error?.code);
|
|
162
|
+
if (err?.response?.data?.error?.message) output.error(err?.response?.data?.error?.message);
|
|
163
|
+
this.stopWhenReachingTokensLimit();
|
|
95
164
|
return '';
|
|
96
165
|
}
|
|
97
166
|
}
|
|
98
167
|
|
|
99
|
-
async healFailedStep(
|
|
168
|
+
async healFailedStep(failureContext) {
|
|
100
169
|
if (!this.isEnabled) return [];
|
|
101
|
-
if (!
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const response = await this.createCompletion(messages);
|
|
170
|
+
if (!failureContext.html) throw new Error('No HTML context provided');
|
|
171
|
+
|
|
172
|
+
await this.setHtmlContext(failureContext.html);
|
|
173
|
+
|
|
174
|
+
if (!this.minifiedHtml) {
|
|
175
|
+
debug('HTML context is empty after removing non-interactive elements & minification');
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const response = await this.createCompletion(this.prompts.healStep(this.minifiedHtml, failureContext));
|
|
113
180
|
if (!response) return [];
|
|
114
181
|
|
|
115
|
-
return
|
|
182
|
+
return this.config.response(response);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
calculateTokens(messages) {
|
|
186
|
+
// we implement naive approach for calculating tokens with no extra requests
|
|
187
|
+
// this approach was tested via https://platform.openai.com/tokenizer
|
|
188
|
+
// we need it to display current usage tokens usage so users could analyze effectiveness of AI
|
|
189
|
+
|
|
190
|
+
const inputString = messages.map(m => m.content).join(' ').trim();
|
|
191
|
+
const numWords = (inputString.match(/[^\s\-:=]+/g) || []).length;
|
|
192
|
+
|
|
193
|
+
// 2.5 token is constant for average HTML input
|
|
194
|
+
const tokens = numWords * 2.5;
|
|
195
|
+
|
|
196
|
+
this.numTokens += tokens;
|
|
197
|
+
|
|
198
|
+
return tokens;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
stopWhenReachingTokensLimit() {
|
|
202
|
+
if (this.numTokens < this.config.maxTokens) return;
|
|
203
|
+
|
|
204
|
+
output.print(`AI assistant has reached the limit of ${this.config.maxTokens} tokens in this session. It will be disabled now`);
|
|
205
|
+
this.disable();
|
|
116
206
|
}
|
|
117
207
|
|
|
118
208
|
async writeSteps(input) {
|
|
119
209
|
if (!this.isEnabled) return;
|
|
120
|
-
if (!this.
|
|
210
|
+
if (!this.minifiedHtml) throw new Error('No HTML context provided');
|
|
121
211
|
|
|
122
212
|
const snippets = [];
|
|
123
213
|
|
|
124
|
-
const
|
|
125
|
-
{
|
|
126
|
-
role: 'user',
|
|
127
|
-
content: `I am test engineer writing test in CodeceptJS
|
|
128
|
-
I have opened web page and I want to use CodeceptJS to ${input} on this page
|
|
129
|
-
Provide me valid CodeceptJS code to accomplish it
|
|
130
|
-
Use only locators from this HTML: \n\n${this.html}`,
|
|
131
|
-
},
|
|
132
|
-
{ role: 'user', content: 'Propose only CodeceptJS steps code. Do not include Scenario or Feature into response' },
|
|
133
|
-
|
|
134
|
-
// old prompt
|
|
135
|
-
// { role: 'user', content: 'I want to click button Submit using CodeceptJS on this HTML page: <html><body><button>Submit</button></body></html>' },
|
|
136
|
-
// { role: 'assistant', content: '```js\nI.click("Submit");\n```' },
|
|
137
|
-
// { role: 'user', content: 'I want to click button Submit using CodeceptJS on this HTML page: <html><body><button>Login</button></body></html>' },
|
|
138
|
-
// { role: 'assistant', content: 'No suggestions' },
|
|
139
|
-
// { role: 'user', content: `Now I want to ${input} on this HTML page using CodeceptJS code` },
|
|
140
|
-
// { role: 'user', content: `Provide me with CodeceptJS code to achieve this on THIS page.` },
|
|
141
|
-
];
|
|
142
|
-
const response = await this.createCompletion(messages);
|
|
214
|
+
const response = await this.createCompletion(this.prompts.writeStep(this.minifiedHtml, input));
|
|
143
215
|
if (!response) return;
|
|
144
|
-
snippets.push(...
|
|
216
|
+
snippets.push(...this.config.response(response));
|
|
145
217
|
|
|
146
218
|
debug(snippets[0]);
|
|
147
219
|
|
|
@@ -177,4 +249,4 @@ function parseCodeBlocks(response) {
|
|
|
177
249
|
return modifiedSnippets.filter(snippet => !!snippet);
|
|
178
250
|
}
|
|
179
251
|
|
|
180
|
-
module.exports = AiAssistant;
|
|
252
|
+
module.exports = new AiAssistant();
|
package/lib/cli.js
CHANGED
package/lib/command/generate.js
CHANGED
|
@@ -258,3 +258,37 @@ helpers: {
|
|
|
258
258
|
`);
|
|
259
259
|
});
|
|
260
260
|
};
|
|
261
|
+
|
|
262
|
+
const healTemplate = fs.readFileSync(path.join(__dirname, '../template/heal.js'), 'utf8').toString();
|
|
263
|
+
|
|
264
|
+
module.exports.heal = function (genPath) {
|
|
265
|
+
const testsPath = getTestRoot(genPath);
|
|
266
|
+
|
|
267
|
+
let configFile = path.join(testsPath, `codecept.conf.${extension}`);
|
|
268
|
+
|
|
269
|
+
if (!fileExists(configFile)) {
|
|
270
|
+
configFile = path.join(testsPath, `codecept.conf.${extension}`);
|
|
271
|
+
if (fileExists(configFile)) extension = 'ts';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
output.print('Creating basic heal recipes');
|
|
275
|
+
output.print(`Add your own custom recipes to ./heal.${extension} file`);
|
|
276
|
+
output.print('Require this file in the config file and enable heal plugin:');
|
|
277
|
+
output.print('--------------------------');
|
|
278
|
+
output.print(`
|
|
279
|
+
require('./heal')
|
|
280
|
+
|
|
281
|
+
exports.config = {
|
|
282
|
+
// ...
|
|
283
|
+
plugins: {
|
|
284
|
+
heal: {
|
|
285
|
+
enabled: true
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
`);
|
|
290
|
+
|
|
291
|
+
const healFile = path.join(testsPath, `heal.${extension}`);
|
|
292
|
+
if (!safeFileWrite(healFile, healTemplate)) return;
|
|
293
|
+
output.success(`Heal recipes were created in ${healFile}`);
|
|
294
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// For Node version >=10.5.0, have to use experimental flag
|
|
2
2
|
const { tryOrDefault } = require('../utils');
|
|
3
3
|
const output = require('../output');
|
|
4
|
+
const store = require('../store');
|
|
4
5
|
const event = require('../event');
|
|
5
6
|
const Workers = require('../workers');
|
|
6
7
|
|
|
@@ -96,6 +97,8 @@ module.exports = async function (workerCount, selectedRuns, options) {
|
|
|
96
97
|
});
|
|
97
98
|
|
|
98
99
|
try {
|
|
100
|
+
if (options.verbose || options.debug) store.debugMode = true;
|
|
101
|
+
|
|
99
102
|
if (options.verbose) {
|
|
100
103
|
global.debugMode = true;
|
|
101
104
|
const { getMachineInfo } = require('./info');
|
package/lib/command/run.js
CHANGED
|
@@ -2,16 +2,19 @@ const {
|
|
|
2
2
|
getConfig, printError, getTestRoot, createOutputDir,
|
|
3
3
|
} = require('./utils');
|
|
4
4
|
const Config = require('../config');
|
|
5
|
+
const store = require('../store');
|
|
5
6
|
const Codecept = require('../codecept');
|
|
6
7
|
|
|
7
8
|
module.exports = async function (test, options) {
|
|
8
9
|
// registering options globally to use in config
|
|
9
10
|
// Backward compatibility for --profile
|
|
11
|
+
// TODO: remove in CodeceptJS 4
|
|
10
12
|
process.profile = options.profile;
|
|
11
13
|
|
|
12
14
|
if (options.profile) {
|
|
13
15
|
process.env.profile = options.profile;
|
|
14
16
|
}
|
|
17
|
+
if (options.verbose || options.debug) store.debugMode = true;
|
|
15
18
|
|
|
16
19
|
const configFile = options.config;
|
|
17
20
|
|
package/lib/container.js
CHANGED
|
@@ -8,6 +8,7 @@ const recorder = require('./recorder');
|
|
|
8
8
|
const event = require('./event');
|
|
9
9
|
const WorkerStorage = require('./workerStorage');
|
|
10
10
|
const store = require('./store');
|
|
11
|
+
const ai = require('./ai');
|
|
11
12
|
|
|
12
13
|
let container = {
|
|
13
14
|
helpers: {},
|
|
@@ -45,6 +46,7 @@ class Container {
|
|
|
45
46
|
container.translation = loadTranslation(config.translation || null, config.vocabularies || []);
|
|
46
47
|
container.support = createSupportObjects(config.include || {});
|
|
47
48
|
container.plugins = createPlugins(config.plugins || {}, opts);
|
|
49
|
+
if (opts && opts.ai) ai.enable(config.ai); // enable AI Assistant
|
|
48
50
|
if (config.gherkin) loadGherkinSteps(config.gherkin.steps || []);
|
|
49
51
|
if (opts && typeof opts.timeouts === 'boolean') store.timeouts = opts.timeouts;
|
|
50
52
|
}
|