codeceptjs 4.0.0-beta.3 → 4.0.0-beta.5
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 +134 -119
- package/bin/codecept.js +12 -2
- package/bin/test-server.js +53 -0
- package/docs/webapi/clearCookie.mustache +1 -1
- package/lib/actor.js +66 -102
- package/lib/ai.js +130 -121
- package/lib/assert/empty.js +3 -5
- package/lib/assert/equal.js +4 -7
- package/lib/assert/include.js +4 -6
- package/lib/assert/throws.js +2 -4
- package/lib/assert/truth.js +2 -2
- package/lib/codecept.js +141 -86
- package/lib/command/check.js +201 -0
- package/lib/command/configMigrate.js +2 -4
- package/lib/command/definitions.js +8 -26
- package/lib/command/dryRun.js +30 -35
- package/lib/command/generate.js +10 -14
- package/lib/command/gherkin/snippets.js +75 -73
- package/lib/command/gherkin/steps.js +1 -1
- package/lib/command/info.js +42 -8
- package/lib/command/init.js +13 -12
- package/lib/command/interactive.js +10 -2
- package/lib/command/list.js +1 -1
- package/lib/command/run-multiple/chunk.js +48 -45
- package/lib/command/run-multiple.js +12 -35
- package/lib/command/run-workers.js +21 -58
- package/lib/command/utils.js +5 -6
- package/lib/command/workers/runTests.js +263 -222
- package/lib/container.js +386 -238
- package/lib/data/context.js +10 -13
- package/lib/data/dataScenarioConfig.js +8 -8
- package/lib/data/dataTableArgument.js +6 -6
- package/lib/data/table.js +5 -11
- package/lib/effects.js +223 -0
- package/lib/element/WebElement.js +327 -0
- package/lib/els.js +158 -0
- package/lib/event.js +21 -17
- package/lib/heal.js +88 -80
- package/lib/helper/AI.js +2 -1
- package/lib/helper/ApiDataFactory.js +4 -7
- package/lib/helper/Appium.js +50 -57
- package/lib/helper/FileSystem.js +3 -3
- package/lib/helper/GraphQLDataFactory.js +4 -4
- package/lib/helper/JSONResponse.js +75 -37
- package/lib/helper/Mochawesome.js +31 -9
- package/lib/helper/Nightmare.js +37 -58
- package/lib/helper/Playwright.js +267 -272
- package/lib/helper/Protractor.js +56 -87
- package/lib/helper/Puppeteer.js +247 -264
- package/lib/helper/REST.js +29 -17
- package/lib/helper/TestCafe.js +22 -47
- package/lib/helper/WebDriver.js +157 -368
- package/lib/helper/extras/PlaywrightPropEngine.js +2 -2
- package/lib/helper/extras/Popup.js +22 -22
- package/lib/helper/network/utils.js +1 -1
- package/lib/helper/testcafe/testcafe-utils.js +27 -28
- package/lib/listener/emptyRun.js +55 -0
- package/lib/listener/exit.js +7 -10
- package/lib/listener/{retry.js → globalRetry.js} +5 -5
- package/lib/listener/globalTimeout.js +165 -0
- package/lib/listener/helpers.js +15 -15
- package/lib/listener/mocha.js +1 -1
- package/lib/listener/result.js +12 -0
- package/lib/listener/retryEnhancer.js +85 -0
- package/lib/listener/steps.js +32 -18
- package/lib/listener/store.js +20 -0
- package/lib/locator.js +1 -1
- package/lib/mocha/asyncWrapper.js +231 -0
- package/lib/{interfaces → mocha}/bdd.js +3 -3
- package/lib/mocha/cli.js +308 -0
- package/lib/mocha/factory.js +104 -0
- package/lib/{interfaces → mocha}/featureConfig.js +32 -12
- package/lib/{interfaces → mocha}/gherkin.js +26 -28
- package/lib/mocha/hooks.js +112 -0
- package/lib/mocha/index.js +12 -0
- package/lib/mocha/inject.js +29 -0
- package/lib/{interfaces → mocha}/scenarioConfig.js +31 -7
- package/lib/mocha/suite.js +82 -0
- package/lib/mocha/test.js +181 -0
- package/lib/mocha/types.d.ts +42 -0
- package/lib/mocha/ui.js +232 -0
- package/lib/output.js +93 -65
- package/lib/pause.js +160 -138
- package/lib/plugin/analyze.js +396 -0
- package/lib/plugin/auth.js +435 -0
- package/lib/plugin/autoDelay.js +8 -8
- package/lib/plugin/autoLogin.js +3 -338
- package/lib/plugin/commentStep.js +6 -1
- package/lib/plugin/coverage.js +10 -22
- package/lib/plugin/customLocator.js +3 -3
- package/lib/plugin/customReporter.js +52 -0
- package/lib/plugin/eachElement.js +1 -1
- package/lib/plugin/fakerTransform.js +1 -1
- package/lib/plugin/heal.js +36 -9
- package/lib/plugin/htmlReporter.js +1947 -0
- package/lib/plugin/pageInfo.js +140 -0
- package/lib/plugin/retryFailedStep.js +17 -18
- package/lib/plugin/retryTo.js +2 -113
- package/lib/plugin/screenshotOnFail.js +17 -58
- package/lib/plugin/selenoid.js +15 -35
- package/lib/plugin/standardActingHelpers.js +4 -1
- package/lib/plugin/stepByStepReport.js +56 -17
- package/lib/plugin/stepTimeout.js +5 -12
- package/lib/plugin/subtitles.js +4 -4
- package/lib/plugin/tryTo.js +3 -102
- package/lib/plugin/wdio.js +8 -10
- package/lib/recorder.js +155 -124
- package/lib/rerun.js +43 -42
- package/lib/result.js +161 -0
- package/lib/secret.js +1 -2
- package/lib/step/base.js +239 -0
- package/lib/step/comment.js +10 -0
- package/lib/step/config.js +50 -0
- package/lib/step/func.js +46 -0
- package/lib/step/helper.js +50 -0
- package/lib/step/meta.js +99 -0
- package/lib/step/record.js +74 -0
- package/lib/step/retry.js +11 -0
- package/lib/step/section.js +55 -0
- package/lib/step.js +21 -332
- package/lib/steps.js +50 -0
- package/lib/store.js +37 -5
- package/lib/template/heal.js +2 -11
- package/lib/test-server.js +323 -0
- package/lib/timeout.js +66 -0
- package/lib/utils.js +351 -218
- package/lib/within.js +75 -55
- package/lib/workerStorage.js +2 -1
- package/lib/workers.js +386 -277
- package/package.json +81 -75
- package/translations/de-DE.js +5 -3
- package/translations/fr-FR.js +5 -4
- package/translations/index.js +1 -0
- package/translations/it-IT.js +4 -3
- package/translations/ja-JP.js +4 -3
- package/translations/nl-NL.js +76 -0
- package/translations/pl-PL.js +4 -3
- package/translations/pt-BR.js +4 -3
- package/translations/ru-RU.js +4 -3
- package/translations/utils.js +9 -0
- package/translations/zh-CN.js +4 -3
- package/translations/zh-TW.js +4 -3
- package/typings/index.d.ts +197 -187
- package/typings/promiseBasedTypes.d.ts +53 -903
- package/typings/types.d.ts +372 -1042
- package/lib/cli.js +0 -257
- package/lib/helper/ExpectHelper.js +0 -391
- package/lib/helper/MockServer.js +0 -221
- package/lib/helper/SoftExpectHelper.js +0 -381
- package/lib/listener/artifacts.js +0 -19
- package/lib/listener/timeout.js +0 -109
- package/lib/mochaFactory.js +0 -113
- package/lib/plugin/debugErrors.js +0 -67
- package/lib/scenario.js +0 -224
- package/lib/ui.js +0 -236
package/lib/actor.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
const Step = require('./step')
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
1
|
+
const Step = require('./step')
|
|
2
|
+
const MetaStep = require('./step/meta')
|
|
3
|
+
const recordStep = require('./step/record')
|
|
4
|
+
const container = require('./container')
|
|
5
|
+
const { methodsOfObject } = require('./utils')
|
|
6
|
+
const { TIMEOUT_ORDER } = require('./timeout')
|
|
7
|
+
const event = require('./event')
|
|
8
|
+
const store = require('./store')
|
|
9
|
+
const output = require('./output')
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* @interface
|
|
@@ -21,13 +22,13 @@ class Actor {
|
|
|
21
22
|
* ⚠️ returns a promise which is synchronized internally by recorder
|
|
22
23
|
*/
|
|
23
24
|
async say(msg, color = 'cyan') {
|
|
24
|
-
const step = new Step('say', 'say')
|
|
25
|
-
step.status = 'passed'
|
|
25
|
+
const step = new Step('say', 'say')
|
|
26
|
+
step.status = 'passed'
|
|
26
27
|
return recordStep(step, [msg]).then(() => {
|
|
27
28
|
// this is backward compatibility as this event may be used somewhere
|
|
28
|
-
event.emit(event.step.comment, msg)
|
|
29
|
-
output.say(msg, `${color}`)
|
|
30
|
-
})
|
|
29
|
+
event.emit(event.step.comment, msg)
|
|
30
|
+
output.say(msg, `${color}`)
|
|
31
|
+
})
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/**
|
|
@@ -38,14 +39,16 @@ class Actor {
|
|
|
38
39
|
* @inner
|
|
39
40
|
*/
|
|
40
41
|
limitTime(timeout) {
|
|
41
|
-
if (!store.timeouts) return this
|
|
42
|
+
if (!store.timeouts) return this
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
output.log(`Timeout to ${step}: ${timeout}s`);
|
|
45
|
-
step.setTimeout(timeout * 1000, Step.TIMEOUT_ORDER.codeLimitTime);
|
|
46
|
-
});
|
|
44
|
+
console.log('I.limitTime() is deprecated, use step.timeout() instead')
|
|
47
45
|
|
|
48
|
-
|
|
46
|
+
event.dispatcher.prependOnceListener(event.step.before, step => {
|
|
47
|
+
output.log(`Timeout to ${step}: ${timeout}s`)
|
|
48
|
+
step.setTimeout(timeout * 1000, TIMEOUT_ORDER.codeLimitTime)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
return this
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
/**
|
|
@@ -55,11 +58,10 @@ class Actor {
|
|
|
55
58
|
* @inner
|
|
56
59
|
*/
|
|
57
60
|
retry(opts) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return this;
|
|
61
|
+
console.log('I.retry() is deprecated, use step.retry() instead')
|
|
62
|
+
const retryStep = require('./step/retry')
|
|
63
|
+
retryStep(opts)
|
|
64
|
+
return this
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
67
|
|
|
@@ -70,92 +72,54 @@ class Actor {
|
|
|
70
72
|
* @ignore
|
|
71
73
|
*/
|
|
72
74
|
module.exports = function (obj = {}) {
|
|
73
|
-
|
|
74
|
-
store.actor = new Actor();
|
|
75
|
-
}
|
|
76
|
-
const actor = store.actor;
|
|
77
|
-
|
|
78
|
-
const translation = container.translation();
|
|
79
|
-
|
|
80
|
-
if (Object.keys(obj).length > 0) {
|
|
81
|
-
Object.keys(obj)
|
|
82
|
-
.forEach(action => {
|
|
83
|
-
const actionAlias = translation.actionAliasFor(action);
|
|
75
|
+
const actor = container.actor() || new Actor()
|
|
84
76
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
ms.actor = translation.I;
|
|
90
|
-
}
|
|
91
|
-
ms.setContext(actor);
|
|
92
|
-
actor[action] = actor[actionAlias] = ms.run.bind(ms, currentMethod);
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const helpers = container.helpers();
|
|
77
|
+
// load all helpers once container initialized
|
|
78
|
+
container.started(() => {
|
|
79
|
+
const translation = container.translation()
|
|
80
|
+
const helpers = container.helpers()
|
|
97
81
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
.forEach((helper) => {
|
|
82
|
+
// add methods from enabled helpers
|
|
83
|
+
Object.values(helpers).forEach(helper => {
|
|
101
84
|
methodsOfObject(helper, 'Helper')
|
|
102
85
|
.filter(method => method !== 'constructor' && method[0] !== '_')
|
|
103
|
-
.forEach(
|
|
104
|
-
const actionAlias = translation.actionAliasFor(action)
|
|
86
|
+
.forEach(action => {
|
|
87
|
+
const actionAlias = translation.actionAliasFor(action)
|
|
105
88
|
if (!actor[action]) {
|
|
106
89
|
actor[action] = actor[actionAlias] = function () {
|
|
107
|
-
const step = new Step(helper, action)
|
|
90
|
+
const step = new Step(helper, action)
|
|
108
91
|
if (translation.loaded) {
|
|
109
|
-
step.name = actionAlias
|
|
110
|
-
step.actor = translation.I
|
|
92
|
+
step.name = actionAlias
|
|
93
|
+
step.actor = translation.I
|
|
111
94
|
}
|
|
112
95
|
// add methods to promise chain
|
|
113
|
-
return recordStep(step, Array.from(arguments))
|
|
114
|
-
}
|
|
96
|
+
return recordStep(step, Array.from(arguments))
|
|
97
|
+
}
|
|
115
98
|
}
|
|
116
|
-
})
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
recorder.add('step passed', () => {
|
|
144
|
-
step.endTime = Date.now();
|
|
145
|
-
event.emit(event.step.passed, step, val);
|
|
146
|
-
event.emit(event.step.finished, step);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
recorder.catchWithoutStop((err) => {
|
|
150
|
-
step.status = 'failed';
|
|
151
|
-
step.endTime = Date.now();
|
|
152
|
-
event.emit(event.step.failed, step);
|
|
153
|
-
event.emit(event.step.finished, step);
|
|
154
|
-
throw err;
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
recorder.add('return result', () => val);
|
|
158
|
-
// run async after step hooks
|
|
159
|
-
|
|
160
|
-
return recorder.promise();
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// add translated custom steps from actor
|
|
103
|
+
Object.keys(obj).forEach(key => {
|
|
104
|
+
const actionAlias = translation.actionAliasFor(key)
|
|
105
|
+
if (!actor[actionAlias]) {
|
|
106
|
+
actor[actionAlias] = actor[key]
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
container.append({
|
|
111
|
+
support: {
|
|
112
|
+
I: actor,
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
// store.actor = actor;
|
|
117
|
+
// add custom steps from actor
|
|
118
|
+
Object.keys(obj).forEach(key => {
|
|
119
|
+
const ms = new MetaStep('I', key)
|
|
120
|
+
ms.setContext(actor)
|
|
121
|
+
actor[key] = ms.run.bind(ms, obj[key])
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
return actor
|
|
161
125
|
}
|
package/lib/ai.js
CHANGED
|
@@ -1,40 +1,44 @@
|
|
|
1
|
-
const debug = require('debug')('codeceptjs:ai')
|
|
2
|
-
const output = require('./output')
|
|
3
|
-
const event = require('./event')
|
|
4
|
-
const { removeNonInteractiveElements, minifyHtml, splitByChunks } = require('./html')
|
|
1
|
+
const debug = require('debug')('codeceptjs:ai')
|
|
2
|
+
const output = require('./output')
|
|
3
|
+
const event = require('./event')
|
|
4
|
+
const { removeNonInteractiveElements, minifyHtml, splitByChunks } = require('./html')
|
|
5
5
|
|
|
6
6
|
const defaultHtmlConfig = {
|
|
7
7
|
maxLength: 50000,
|
|
8
8
|
simplify: true,
|
|
9
9
|
minify: true,
|
|
10
10
|
html: {},
|
|
11
|
-
}
|
|
11
|
+
}
|
|
12
12
|
|
|
13
13
|
const defaultPrompts = {
|
|
14
|
-
writeStep: (html, input) => [
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
writeStep: (html, input) => [
|
|
15
|
+
{
|
|
16
|
+
role: 'user',
|
|
17
|
+
content: `I am test engineer writing test in CodeceptJS
|
|
17
18
|
I have opened web page and I want to use CodeceptJS to ${input} on this page
|
|
18
19
|
Provide me valid CodeceptJS code to accomplish it
|
|
19
20
|
Use only locators from this HTML: \n\n${html}`,
|
|
20
|
-
|
|
21
|
+
},
|
|
21
22
|
],
|
|
22
23
|
|
|
23
24
|
healStep: (html, { step, error, prevSteps }) => {
|
|
24
|
-
return [
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
return [
|
|
26
|
+
{
|
|
27
|
+
role: 'user',
|
|
28
|
+
content: `As a test automation engineer I am testing web application using CodeceptJS.
|
|
27
29
|
I want to heal a test that fails. Here is the list of executed steps: ${prevSteps.map(s => s.toString()).join(', ')}
|
|
28
30
|
Propose how to adjust ${step.toCode()} step to fix the test.
|
|
29
31
|
Use locators in order of preference: semantic locator by text, CSS, XPath. Use codeblocks marked with \`\`\`
|
|
30
32
|
Here is the error message: ${error.message}
|
|
31
33
|
Here is HTML code of a page where the failure has happened: \n\n${html}`,
|
|
32
|
-
|
|
34
|
+
},
|
|
35
|
+
]
|
|
33
36
|
},
|
|
34
37
|
|
|
35
|
-
generatePageObject: (html, extraPrompt = '', rootLocator = null) => [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
generatePageObject: (html, extraPrompt = '', rootLocator = null) => [
|
|
39
|
+
{
|
|
40
|
+
role: 'user',
|
|
41
|
+
content: `As a test automation engineer I am creating a Page Object for a web application using CodeceptJS.
|
|
38
42
|
Here is an sample page object:
|
|
39
43
|
|
|
40
44
|
const { I } = inject();
|
|
@@ -60,72 +64,73 @@ module.exports = {
|
|
|
60
64
|
${extraPrompt}
|
|
61
65
|
${rootLocator ? `All provided elements are inside '${rootLocator}'. Declare it as root variable and for every locator use locate(...).inside(root)` : ''}
|
|
62
66
|
Add only locators from this HTML: \n\n${html}`,
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
}
|
|
65
70
|
|
|
66
71
|
class AiAssistant {
|
|
67
72
|
constructor() {
|
|
68
|
-
this.totalTime = 0
|
|
69
|
-
this.numTokens = 0
|
|
73
|
+
this.totalTime = 0
|
|
74
|
+
this.numTokens = 0
|
|
70
75
|
|
|
71
|
-
this.reset()
|
|
72
|
-
this.connectToEvents()
|
|
76
|
+
this.reset()
|
|
77
|
+
this.connectToEvents()
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
enable(config = {}) {
|
|
76
|
-
debug('Enabling AI assistant')
|
|
77
|
-
this.isEnabled = true
|
|
81
|
+
debug('Enabling AI assistant')
|
|
82
|
+
this.isEnabled = true
|
|
78
83
|
|
|
79
|
-
const { html, prompts, ...aiConfig } = config
|
|
84
|
+
const { html, prompts, ...aiConfig } = config
|
|
80
85
|
|
|
81
|
-
this.config = Object.assign(this.config, aiConfig)
|
|
82
|
-
this.htmlConfig = Object.assign(defaultHtmlConfig, html)
|
|
83
|
-
this.prompts = Object.assign(defaultPrompts, prompts)
|
|
86
|
+
this.config = Object.assign(this.config, aiConfig)
|
|
87
|
+
this.htmlConfig = Object.assign(defaultHtmlConfig, html)
|
|
88
|
+
this.prompts = Object.assign(defaultPrompts, prompts)
|
|
84
89
|
|
|
85
|
-
debug('Config', this.config)
|
|
90
|
+
debug('Config', this.config)
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
reset() {
|
|
89
|
-
this.numTokens = 0
|
|
90
|
-
this.isEnabled = false
|
|
94
|
+
this.numTokens = 0
|
|
95
|
+
this.isEnabled = false
|
|
91
96
|
this.config = {
|
|
92
97
|
maxTokens: 1000000,
|
|
93
98
|
request: null,
|
|
94
99
|
response: parseCodeBlocks,
|
|
95
100
|
// lets limit token usage to 1M
|
|
96
|
-
}
|
|
97
|
-
this.minifiedHtml = null
|
|
98
|
-
this.response = null
|
|
99
|
-
this.totalTime = 0
|
|
101
|
+
}
|
|
102
|
+
this.minifiedHtml = null
|
|
103
|
+
this.response = null
|
|
104
|
+
this.totalTime = 0
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
disable() {
|
|
103
|
-
this.isEnabled = false
|
|
108
|
+
this.isEnabled = false
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
connectToEvents() {
|
|
107
112
|
event.dispatcher.on(event.all.result, () => {
|
|
108
113
|
if (this.isEnabled && this.numTokens > 0) {
|
|
109
|
-
const numTokensK = Math.ceil(this.numTokens / 1000)
|
|
110
|
-
const maxTokensK = Math.ceil(this.config.maxTokens / 1000)
|
|
111
|
-
output.print(`AI assistant took ${this.totalTime}s and used ~${numTokensK}K input tokens. Tokens limit: ${maxTokensK}K`)
|
|
114
|
+
const numTokensK = Math.ceil(this.numTokens / 1000)
|
|
115
|
+
const maxTokensK = Math.ceil(this.config.maxTokens / 1000)
|
|
116
|
+
output.print(`AI assistant took ${this.totalTime}s and used ~${numTokensK}K input tokens. Tokens limit: ${maxTokensK}K`)
|
|
112
117
|
}
|
|
113
|
-
})
|
|
118
|
+
})
|
|
114
119
|
}
|
|
115
120
|
|
|
116
121
|
checkRequestFn() {
|
|
117
122
|
if (!this.isEnabled) {
|
|
118
|
-
debug('AI assistant is disabled')
|
|
119
|
-
return
|
|
123
|
+
debug('AI assistant is disabled')
|
|
124
|
+
return
|
|
120
125
|
}
|
|
121
126
|
|
|
122
|
-
if (this.config.request) return
|
|
127
|
+
if (this.config.request) return
|
|
123
128
|
|
|
124
129
|
const noRequestErrorMessage = `
|
|
125
|
-
No request function is set for AI assistant.
|
|
126
|
-
Please implement your own request function and set it in the config.
|
|
130
|
+
No request function is set for AI assistant.
|
|
127
131
|
|
|
128
|
-
[!] AI request was decoupled from CodeceptJS. To connect to OpenAI or other AI service
|
|
132
|
+
[!] AI request was decoupled from CodeceptJS. To connect to OpenAI or other AI service.
|
|
133
|
+
Please implement your own request function and set it in the config.
|
|
129
134
|
|
|
130
135
|
Example (connect to OpenAI):
|
|
131
136
|
|
|
@@ -134,82 +139,80 @@ class AiAssistant {
|
|
|
134
139
|
const OpenAI = require('openai');
|
|
135
140
|
const openai = new OpenAI({ apiKey: process.env['OPENAI_API_KEY'] })
|
|
136
141
|
const response = await openai.chat.completions.create({
|
|
137
|
-
model: 'gpt-
|
|
142
|
+
model: 'gpt-4o-mini',
|
|
138
143
|
messages,
|
|
139
144
|
});
|
|
140
145
|
return response?.data?.choices[0]?.message?.content;
|
|
141
146
|
}
|
|
142
147
|
}
|
|
143
|
-
`.trim()
|
|
148
|
+
`.trim()
|
|
144
149
|
|
|
145
|
-
throw new Error(noRequestErrorMessage)
|
|
150
|
+
throw new Error(noRequestErrorMessage)
|
|
146
151
|
}
|
|
147
152
|
|
|
148
153
|
async setHtmlContext(html) {
|
|
149
|
-
let processedHTML = html
|
|
154
|
+
let processedHTML = html
|
|
150
155
|
|
|
151
156
|
if (this.htmlConfig.simplify) {
|
|
152
|
-
processedHTML = removeNonInteractiveElements(processedHTML, this.htmlConfig)
|
|
157
|
+
processedHTML = removeNonInteractiveElements(processedHTML, this.htmlConfig)
|
|
153
158
|
}
|
|
154
159
|
|
|
155
|
-
if (this.htmlConfig.minify) processedHTML = await minifyHtml(processedHTML)
|
|
156
|
-
if (this.htmlConfig.maxLength) processedHTML = splitByChunks(processedHTML, this.htmlConfig.maxLength)[0]
|
|
160
|
+
if (this.htmlConfig.minify) processedHTML = await minifyHtml(processedHTML)
|
|
161
|
+
if (this.htmlConfig.maxLength) processedHTML = splitByChunks(processedHTML, this.htmlConfig.maxLength)[0]
|
|
157
162
|
|
|
158
|
-
this.minifiedHtml = processedHTML
|
|
163
|
+
this.minifiedHtml = processedHTML
|
|
159
164
|
}
|
|
160
165
|
|
|
161
166
|
getResponse() {
|
|
162
|
-
return this.response || ''
|
|
167
|
+
return this.response || ''
|
|
163
168
|
}
|
|
164
169
|
|
|
165
170
|
async createCompletion(messages) {
|
|
166
|
-
if (!this.isEnabled) return ''
|
|
167
|
-
|
|
168
|
-
debug('Request', messages);
|
|
169
|
-
|
|
170
|
-
this.checkRequestFn();
|
|
171
|
-
|
|
172
|
-
this.response = null;
|
|
173
|
-
|
|
174
|
-
this.calculateTokens(messages);
|
|
171
|
+
if (!this.isEnabled) return ''
|
|
175
172
|
|
|
176
173
|
try {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
this.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
174
|
+
this.checkRequestFn()
|
|
175
|
+
debug('Request', messages)
|
|
176
|
+
|
|
177
|
+
this.response = null
|
|
178
|
+
|
|
179
|
+
this.calculateTokens(messages)
|
|
180
|
+
const startTime = process.hrtime()
|
|
181
|
+
this.response = await this.config.request(messages)
|
|
182
|
+
const endTime = process.hrtime(startTime)
|
|
183
|
+
const executionTimeInSeconds = endTime[0] + endTime[1] / 1e9
|
|
184
|
+
|
|
185
|
+
this.totalTime += Math.round(executionTimeInSeconds)
|
|
186
|
+
debug('AI response time', executionTimeInSeconds)
|
|
187
|
+
debug('Response', this.response)
|
|
188
|
+
this.stopWhenReachingTokensLimit()
|
|
189
|
+
return this.response
|
|
187
190
|
} catch (err) {
|
|
188
|
-
debug(err.response)
|
|
189
|
-
output.print('')
|
|
190
|
-
output.error(`AI service error: ${err.message}`)
|
|
191
|
-
if (err?.response?.data?.error?.code) output.error(err?.response?.data?.error?.code)
|
|
192
|
-
if (err?.response?.data?.error?.message) output.error(err?.response?.data?.error?.message)
|
|
193
|
-
this.stopWhenReachingTokensLimit()
|
|
194
|
-
return ''
|
|
191
|
+
debug(err.response)
|
|
192
|
+
output.print('')
|
|
193
|
+
output.error(`AI service error: ${err.message}`)
|
|
194
|
+
if (err?.response?.data?.error?.code) output.error(err?.response?.data?.error?.code)
|
|
195
|
+
if (err?.response?.data?.error?.message) output.error(err?.response?.data?.error?.message)
|
|
196
|
+
this.stopWhenReachingTokensLimit()
|
|
197
|
+
return ''
|
|
195
198
|
}
|
|
196
199
|
}
|
|
197
200
|
|
|
198
201
|
async healFailedStep(failureContext) {
|
|
199
|
-
if (!this.isEnabled) return []
|
|
200
|
-
if (!failureContext.html) throw new Error('No HTML context provided')
|
|
202
|
+
if (!this.isEnabled) return []
|
|
203
|
+
if (!failureContext.html) throw new Error('No HTML context provided')
|
|
201
204
|
|
|
202
|
-
await this.setHtmlContext(failureContext.html)
|
|
205
|
+
await this.setHtmlContext(failureContext.html)
|
|
203
206
|
|
|
204
207
|
if (!this.minifiedHtml) {
|
|
205
|
-
debug('HTML context is empty after removing non-interactive elements & minification')
|
|
206
|
-
return []
|
|
208
|
+
debug('HTML context is empty after removing non-interactive elements & minification')
|
|
209
|
+
return []
|
|
207
210
|
}
|
|
208
211
|
|
|
209
|
-
const response = await this.createCompletion(this.prompts.healStep(this.minifiedHtml, failureContext))
|
|
210
|
-
if (!response) return []
|
|
212
|
+
const response = await this.createCompletion(this.prompts.healStep(this.minifiedHtml, failureContext))
|
|
213
|
+
if (!response) return []
|
|
211
214
|
|
|
212
|
-
return this.config.response(response)
|
|
215
|
+
return this.config.response(response)
|
|
213
216
|
}
|
|
214
217
|
|
|
215
218
|
/**
|
|
@@ -219,13 +222,13 @@ class AiAssistant {
|
|
|
219
222
|
* @returns
|
|
220
223
|
*/
|
|
221
224
|
async generatePageObject(extraPrompt = null, locator = null) {
|
|
222
|
-
if (!this.isEnabled) return []
|
|
223
|
-
if (!this.minifiedHtml) throw new Error('No HTML context provided')
|
|
225
|
+
if (!this.isEnabled) return []
|
|
226
|
+
if (!this.minifiedHtml) throw new Error('No HTML context provided')
|
|
224
227
|
|
|
225
|
-
const response = await this.createCompletion(this.prompts.generatePageObject(this.minifiedHtml, locator, extraPrompt))
|
|
226
|
-
if (!response) return []
|
|
228
|
+
const response = await this.createCompletion(this.prompts.generatePageObject(this.minifiedHtml, locator, extraPrompt))
|
|
229
|
+
if (!response) return []
|
|
227
230
|
|
|
228
|
-
return this.config.response(response)
|
|
231
|
+
return this.config.response(response)
|
|
229
232
|
}
|
|
230
233
|
|
|
231
234
|
calculateTokens(messages) {
|
|
@@ -233,66 +236,72 @@ class AiAssistant {
|
|
|
233
236
|
// this approach was tested via https://platform.openai.com/tokenizer
|
|
234
237
|
// we need it to display current tokens usage so users could analyze effectiveness of AI
|
|
235
238
|
|
|
236
|
-
const inputString = messages
|
|
237
|
-
|
|
239
|
+
const inputString = messages
|
|
240
|
+
.map(m => m.content)
|
|
241
|
+
.join(' ')
|
|
242
|
+
.trim()
|
|
243
|
+
const numWords = (inputString.match(/[^\s\-:=]+/g) || []).length
|
|
238
244
|
|
|
239
245
|
// 2.5 token is constant for average HTML input
|
|
240
|
-
const tokens = numWords * 2.5
|
|
246
|
+
const tokens = numWords * 2.5
|
|
241
247
|
|
|
242
|
-
this.numTokens += tokens
|
|
248
|
+
this.numTokens += tokens
|
|
243
249
|
|
|
244
|
-
return tokens
|
|
250
|
+
return tokens
|
|
245
251
|
}
|
|
246
252
|
|
|
247
253
|
stopWhenReachingTokensLimit() {
|
|
248
|
-
if (this.numTokens < this.config.maxTokens) return
|
|
254
|
+
if (this.numTokens < this.config.maxTokens) return
|
|
249
255
|
|
|
250
|
-
output.print(`AI assistant has reached the limit of ${this.config.maxTokens} tokens in this session. It will be disabled now`)
|
|
251
|
-
this.disable()
|
|
256
|
+
output.print(`AI assistant has reached the limit of ${this.config.maxTokens} tokens in this session. It will be disabled now`)
|
|
257
|
+
this.disable()
|
|
252
258
|
}
|
|
253
259
|
|
|
254
260
|
async writeSteps(input) {
|
|
255
|
-
if (!this.isEnabled) return
|
|
256
|
-
if (!this.minifiedHtml) throw new Error('No HTML context provided')
|
|
261
|
+
if (!this.isEnabled) return
|
|
262
|
+
if (!this.minifiedHtml) throw new Error('No HTML context provided')
|
|
257
263
|
|
|
258
|
-
const snippets = []
|
|
264
|
+
const snippets = []
|
|
259
265
|
|
|
260
|
-
const response = await this.createCompletion(this.prompts.writeStep(this.minifiedHtml, input))
|
|
261
|
-
if (!response) return
|
|
262
|
-
snippets.push(...this.config.response(response))
|
|
266
|
+
const response = await this.createCompletion(this.prompts.writeStep(this.minifiedHtml, input))
|
|
267
|
+
if (!response) return
|
|
268
|
+
snippets.push(...this.config.response(response))
|
|
263
269
|
|
|
264
|
-
debug(snippets[0])
|
|
270
|
+
debug(snippets[0])
|
|
265
271
|
|
|
266
|
-
return snippets[0]
|
|
272
|
+
return snippets[0]
|
|
267
273
|
}
|
|
268
274
|
}
|
|
269
275
|
|
|
270
276
|
function parseCodeBlocks(response) {
|
|
271
277
|
// Regular expression pattern to match code snippets
|
|
272
|
-
const codeSnippetPattern = /```(?:javascript|js|typescript|ts)?\n([\s\S]+?)\n```/g
|
|
278
|
+
const codeSnippetPattern = /```(?:javascript|js|typescript|ts)?\n([\s\S]+?)\n```/g
|
|
273
279
|
|
|
274
280
|
// Array to store extracted code snippets
|
|
275
|
-
const codeSnippets = []
|
|
281
|
+
const codeSnippets = []
|
|
276
282
|
|
|
277
|
-
response = response
|
|
283
|
+
response = response
|
|
284
|
+
.split('\n')
|
|
285
|
+
.map(line => line.trim())
|
|
286
|
+
.join('\n')
|
|
278
287
|
|
|
279
288
|
// Iterate over matches and extract code snippets
|
|
280
|
-
let match
|
|
289
|
+
let match
|
|
281
290
|
while ((match = codeSnippetPattern.exec(response)) !== null) {
|
|
282
|
-
codeSnippets.push(match[1])
|
|
291
|
+
codeSnippets.push(match[1])
|
|
283
292
|
}
|
|
284
293
|
|
|
285
294
|
// Remove "Scenario", "Feature", and "require()" lines
|
|
286
295
|
const modifiedSnippets = codeSnippets.map(snippet => {
|
|
287
|
-
const lines = snippet.split('\n')
|
|
296
|
+
const lines = snippet.split('\n')
|
|
288
297
|
|
|
289
|
-
const filteredLines = lines.filter(line => !line.includes('I.amOnPage') && !line.startsWith('Scenario') && !line.startsWith('Feature') && !line.includes('= require('))
|
|
298
|
+
const filteredLines = lines.filter(line => !line.includes('I.amOnPage') && !line.startsWith('Scenario') && !line.startsWith('Feature') && !line.includes('= require('))
|
|
290
299
|
|
|
291
|
-
return filteredLines.join('\n')
|
|
300
|
+
return filteredLines.join('\n')
|
|
292
301
|
// remove snippets that move from current url
|
|
293
|
-
})
|
|
302
|
+
}) // .filter(snippet => !line.includes('I.amOnPage'));
|
|
294
303
|
|
|
295
|
-
return modifiedSnippets.filter(snippet => !!snippet)
|
|
304
|
+
return modifiedSnippets.filter(snippet => !!snippet)
|
|
296
305
|
}
|
|
297
306
|
|
|
298
|
-
module.exports = new AiAssistant()
|
|
307
|
+
module.exports = new AiAssistant()
|
package/lib/assert/empty.js
CHANGED
|
@@ -5,7 +5,7 @@ const output = require('../output')
|
|
|
5
5
|
|
|
6
6
|
class EmptinessAssertion extends Assertion {
|
|
7
7
|
constructor(params) {
|
|
8
|
-
super(
|
|
8
|
+
super(value => {
|
|
9
9
|
if (Array.isArray(value)) {
|
|
10
10
|
return value.length === 0
|
|
11
11
|
}
|
|
@@ -22,9 +22,7 @@ class EmptinessAssertion extends Assertion {
|
|
|
22
22
|
const err = new AssertionFailedError(this.params, "{{customMessage}}expected {{subject}} '{{value}}' {{type}}")
|
|
23
23
|
|
|
24
24
|
err.cliMessage = () => {
|
|
25
|
-
const msg = err.template
|
|
26
|
-
.replace('{{value}}', output.colors.bold('{{value}}'))
|
|
27
|
-
.replace('{{subject}}', output.colors.bold('{{subject}}'))
|
|
25
|
+
const msg = err.template.replace('{{value}}', output.colors.bold('{{value}}')).replace('{{subject}}', output.colors.bold('{{subject}}'))
|
|
28
26
|
return template(msg, this.params)
|
|
29
27
|
}
|
|
30
28
|
return err
|
|
@@ -39,5 +37,5 @@ class EmptinessAssertion extends Assertion {
|
|
|
39
37
|
|
|
40
38
|
module.exports = {
|
|
41
39
|
Assertion: EmptinessAssertion,
|
|
42
|
-
empty:
|
|
40
|
+
empty: subject => new EmptinessAssertion({ subject }),
|
|
43
41
|
}
|
package/lib/assert/equal.js
CHANGED
|
@@ -18,10 +18,7 @@ class EqualityAssertion extends Assertion {
|
|
|
18
18
|
getException() {
|
|
19
19
|
const params = this.params
|
|
20
20
|
params.jar = template(params.jar, params)
|
|
21
|
-
const err = new AssertionFailedError(
|
|
22
|
-
params,
|
|
23
|
-
'{{customMessage}}expected {{jar}} "{{expected}}" {{type}} "{{actual}}"',
|
|
24
|
-
)
|
|
21
|
+
const err = new AssertionFailedError(params, '{{customMessage}}expected {{jar}} "{{expected}}" {{type}} "{{actual}}"')
|
|
25
22
|
err.showDiff = false
|
|
26
23
|
if (typeof err.cliMessage === 'function') {
|
|
27
24
|
err.message = err.cliMessage()
|
|
@@ -42,8 +39,8 @@ class EqualityAssertion extends Assertion {
|
|
|
42
39
|
|
|
43
40
|
module.exports = {
|
|
44
41
|
Assertion: EqualityAssertion,
|
|
45
|
-
equals:
|
|
46
|
-
urlEquals:
|
|
42
|
+
equals: jar => new EqualityAssertion({ jar }),
|
|
43
|
+
urlEquals: baseUrl => {
|
|
47
44
|
const assert = new EqualityAssertion({ jar: 'url of current page' })
|
|
48
45
|
assert.comparator = function (expected, actual) {
|
|
49
46
|
if (expected.indexOf('http') !== 0) {
|
|
@@ -53,5 +50,5 @@ module.exports = {
|
|
|
53
50
|
}
|
|
54
51
|
return assert
|
|
55
52
|
},
|
|
56
|
-
fileEquals:
|
|
53
|
+
fileEquals: file => new EqualityAssertion({ file, jar: 'contents of {{file}}' }),
|
|
57
54
|
}
|