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