codeceptjs 4.0.0-beta.1 → 4.0.0-beta.10.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 +133 -120
- package/bin/codecept.js +107 -96
- package/bin/test-server.js +64 -0
- package/docs/webapi/clearCookie.mustache +1 -1
- package/docs/webapi/click.mustache +5 -1
- package/lib/actor.js +71 -103
- package/lib/ai.js +159 -188
- package/lib/assert/empty.js +22 -24
- package/lib/assert/equal.js +30 -37
- package/lib/assert/error.js +14 -14
- package/lib/assert/include.js +43 -48
- package/lib/assert/throws.js +11 -11
- package/lib/assert/truth.js +22 -22
- package/lib/assert.js +20 -18
- package/lib/codecept.js +238 -162
- package/lib/colorUtils.js +50 -52
- package/lib/command/check.js +206 -0
- package/lib/command/configMigrate.js +56 -51
- package/lib/command/definitions.js +96 -109
- package/lib/command/dryRun.js +77 -79
- package/lib/command/generate.js +234 -194
- package/lib/command/gherkin/init.js +42 -33
- package/lib/command/gherkin/snippets.js +76 -74
- package/lib/command/gherkin/steps.js +20 -17
- package/lib/command/info.js +74 -38
- package/lib/command/init.js +300 -290
- package/lib/command/interactive.js +41 -32
- package/lib/command/list.js +28 -27
- package/lib/command/run-multiple/chunk.js +51 -48
- package/lib/command/run-multiple/collection.js +5 -5
- package/lib/command/run-multiple/run.js +5 -1
- package/lib/command/run-multiple.js +97 -97
- package/lib/command/run-rerun.js +19 -25
- package/lib/command/run-workers.js +68 -92
- package/lib/command/run.js +39 -27
- package/lib/command/utils.js +80 -64
- package/lib/command/workers/runTests.js +388 -226
- package/lib/config.js +124 -50
- package/lib/container.js +751 -260
- package/lib/data/context.js +60 -61
- package/lib/data/dataScenarioConfig.js +47 -47
- package/lib/data/dataTableArgument.js +32 -32
- package/lib/data/table.js +22 -22
- package/lib/effects.js +307 -0
- package/lib/element/WebElement.js +327 -0
- package/lib/els.js +160 -0
- package/lib/event.js +173 -163
- package/lib/globals.js +141 -0
- package/lib/heal.js +89 -85
- package/lib/helper/AI.js +131 -41
- package/lib/helper/ApiDataFactory.js +107 -75
- package/lib/helper/Appium.js +542 -404
- package/lib/helper/FileSystem.js +100 -79
- package/lib/helper/GraphQL.js +44 -43
- package/lib/helper/GraphQLDataFactory.js +52 -52
- package/lib/helper/JSONResponse.js +126 -88
- package/lib/helper/Mochawesome.js +54 -29
- package/lib/helper/Playwright.js +2547 -1316
- package/lib/helper/Puppeteer.js +1578 -1181
- package/lib/helper/REST.js +209 -68
- package/lib/helper/WebDriver.js +1482 -1342
- 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/PlaywrightReactVueLocator.js +17 -8
- package/lib/helper/extras/PlaywrightRestartOpts.js +25 -11
- package/lib/helper/extras/Popup.js +22 -22
- package/lib/helper/extras/React.js +27 -28
- package/lib/helper/network/actions.js +36 -42
- package/lib/helper/network/utils.js +78 -84
- package/lib/helper/scripts/blurElement.js +5 -5
- package/lib/helper/scripts/focusElement.js +5 -5
- package/lib/helper/scripts/highlightElement.js +8 -8
- package/lib/helper/scripts/isElementClickable.js +34 -34
- package/lib/helper.js +2 -3
- package/lib/history.js +23 -19
- package/lib/hooks.js +8 -8
- package/lib/html.js +94 -104
- package/lib/index.js +38 -27
- package/lib/listener/config.js +30 -23
- package/lib/listener/emptyRun.js +54 -0
- package/lib/listener/enhancedGlobalRetry.js +110 -0
- package/lib/listener/exit.js +16 -18
- package/lib/listener/globalRetry.js +70 -0
- package/lib/listener/globalTimeout.js +181 -0
- package/lib/listener/helpers.js +76 -51
- package/lib/listener/mocha.js +10 -11
- package/lib/listener/result.js +11 -0
- package/lib/listener/retryEnhancer.js +85 -0
- package/lib/listener/steps.js +71 -59
- package/lib/listener/store.js +20 -0
- package/lib/locator.js +214 -197
- package/lib/mocha/asyncWrapper.js +274 -0
- package/lib/mocha/bdd.js +167 -0
- package/lib/mocha/cli.js +341 -0
- package/lib/mocha/factory.js +163 -0
- package/lib/mocha/featureConfig.js +89 -0
- package/lib/mocha/gherkin.js +231 -0
- 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 +58 -34
- package/lib/mocha/suite.js +89 -0
- package/lib/mocha/test.js +184 -0
- package/lib/mocha/types.d.ts +42 -0
- package/lib/mocha/ui.js +242 -0
- package/lib/output.js +141 -71
- package/lib/parser.js +47 -44
- package/lib/pause.js +173 -145
- package/lib/plugin/analyze.js +403 -0
- package/lib/plugin/{autoLogin.js → auth.js} +178 -79
- package/lib/plugin/autoDelay.js +36 -40
- package/lib/plugin/coverage.js +131 -78
- package/lib/plugin/customLocator.js +22 -21
- package/lib/plugin/customReporter.js +53 -0
- package/lib/plugin/enhancedRetryFailedStep.js +99 -0
- package/lib/plugin/heal.js +101 -110
- package/lib/plugin/htmlReporter.js +3648 -0
- package/lib/plugin/pageInfo.js +140 -0
- package/lib/plugin/pauseOnFail.js +12 -11
- package/lib/plugin/retryFailedStep.js +82 -47
- package/lib/plugin/screenshotOnFail.js +111 -92
- package/lib/plugin/stepByStepReport.js +159 -101
- package/lib/plugin/stepTimeout.js +20 -25
- package/lib/plugin/subtitles.js +38 -38
- package/lib/recorder.js +193 -130
- package/lib/rerun.js +94 -49
- package/lib/result.js +238 -0
- package/lib/retryCoordinator.js +207 -0
- package/lib/secret.js +20 -18
- 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 -329
- package/lib/steps.js +54 -0
- package/lib/store.js +38 -7
- package/lib/template/heal.js +3 -12
- package/lib/template/prompts/generatePageObject.js +31 -0
- package/lib/template/prompts/healStep.js +13 -0
- package/lib/template/prompts/writeStep.js +9 -0
- package/lib/test-server.js +334 -0
- package/lib/timeout.js +60 -0
- package/lib/transform.js +8 -8
- package/lib/translation.js +34 -21
- package/lib/utils/mask_data.js +47 -0
- package/lib/utils.js +411 -228
- package/lib/workerStorage.js +37 -34
- package/lib/workers.js +532 -296
- package/package.json +115 -95
- package/translations/de-DE.js +5 -3
- package/translations/fr-FR.js +5 -4
- package/translations/index.js +22 -12
- 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 +10 -0
- package/translations/zh-CN.js +4 -3
- package/translations/zh-TW.js +4 -3
- package/typings/index.d.ts +546 -185
- package/typings/promiseBasedTypes.d.ts +150 -879
- package/typings/types.d.ts +547 -996
- package/lib/cli.js +0 -249
- package/lib/dirname.js +0 -5
- package/lib/helper/Expect.js +0 -425
- package/lib/helper/ExpectHelper.js +0 -399
- package/lib/helper/MockServer.js +0 -223
- package/lib/helper/Nightmare.js +0 -1411
- package/lib/helper/Protractor.js +0 -1835
- package/lib/helper/SoftExpectHelper.js +0 -381
- package/lib/helper/TestCafe.js +0 -1410
- package/lib/helper/clientscripts/nightmare.js +0 -213
- package/lib/helper/testcafe/testControllerHolder.js +0 -42
- package/lib/helper/testcafe/testcafe-utils.js +0 -63
- package/lib/interfaces/bdd.js +0 -98
- package/lib/interfaces/featureConfig.js +0 -69
- package/lib/interfaces/gherkin.js +0 -195
- package/lib/listener/artifacts.js +0 -19
- package/lib/listener/retry.js +0 -68
- package/lib/listener/timeout.js +0 -109
- package/lib/mochaFactory.js +0 -110
- 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 -121
- package/lib/plugin/selenoid.js +0 -371
- package/lib/plugin/standardActingHelpers.js +0 -9
- package/lib/plugin/tryTo.js +0 -105
- package/lib/plugin/wdio.js +0 -246
- package/lib/scenario.js +0 -222
- package/lib/ui.js +0 -238
- package/lib/within.js +0 -70
package/lib/heal.js
CHANGED
|
@@ -1,126 +1,125 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import recorder from './recorder.js'
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
const logger = debug('myapp:server');
|
|
9
|
-
|
|
10
|
-
logger('codeceptjs:heal');
|
|
1
|
+
import debugModule from 'debug'
|
|
2
|
+
const debug = debugModule('codeceptjs:heal')
|
|
3
|
+
import colors from 'chalk'
|
|
4
|
+
import recorder from './recorder.js'
|
|
5
|
+
import output from './output.js'
|
|
6
|
+
import event from './event.js'
|
|
11
7
|
|
|
12
8
|
/**
|
|
13
9
|
* @class
|
|
14
10
|
*/
|
|
15
11
|
class Heal {
|
|
16
12
|
constructor() {
|
|
17
|
-
this.recipes = {}
|
|
18
|
-
this.fixes = []
|
|
19
|
-
this.prepareFns = []
|
|
20
|
-
this.contextName = null
|
|
21
|
-
this.numHealed = 0
|
|
13
|
+
this.recipes = {}
|
|
14
|
+
this.fixes = []
|
|
15
|
+
this.prepareFns = []
|
|
16
|
+
this.contextName = null
|
|
17
|
+
this.numHealed = 0
|
|
22
18
|
}
|
|
23
19
|
|
|
24
20
|
clear() {
|
|
25
|
-
this.recipes = {}
|
|
26
|
-
this.fixes = []
|
|
27
|
-
this.prepareFns = []
|
|
28
|
-
this.contextName = null
|
|
29
|
-
this.numHealed = 0
|
|
21
|
+
this.recipes = {}
|
|
22
|
+
this.fixes = []
|
|
23
|
+
this.prepareFns = []
|
|
24
|
+
this.contextName = null
|
|
25
|
+
this.numHealed = 0
|
|
30
26
|
}
|
|
31
27
|
|
|
32
28
|
addRecipe(name, opts = {}) {
|
|
33
|
-
if (!opts.priority) opts.priority = 0
|
|
29
|
+
if (!opts.priority) opts.priority = 0
|
|
34
30
|
|
|
35
|
-
if (!opts.fn) throw new Error(`Recipe ${name} should have a function 'fn' to execute`)
|
|
31
|
+
if (!opts.fn) throw new Error(`Recipe ${name} should have a function 'fn' to execute`)
|
|
36
32
|
|
|
37
|
-
this.recipes[name] = opts
|
|
33
|
+
this.recipes[name] = opts
|
|
38
34
|
}
|
|
39
35
|
|
|
40
36
|
connectToEvents() {
|
|
41
|
-
event.dispatcher.on(event.suite.before,
|
|
42
|
-
this.contextName = suite.title
|
|
43
|
-
})
|
|
37
|
+
event.dispatcher.on(event.suite.before, suite => {
|
|
38
|
+
this.contextName = suite.title
|
|
39
|
+
})
|
|
44
40
|
|
|
45
|
-
event.dispatcher.on(event.test.started,
|
|
46
|
-
this.contextName = test.fullTitle()
|
|
47
|
-
})
|
|
41
|
+
event.dispatcher.on(event.test.started, test => {
|
|
42
|
+
this.contextName = test.fullTitle()
|
|
43
|
+
})
|
|
48
44
|
|
|
49
45
|
event.dispatcher.on(event.test.finished, () => {
|
|
50
|
-
this.contextName = null
|
|
51
|
-
})
|
|
46
|
+
this.contextName = null
|
|
47
|
+
})
|
|
52
48
|
}
|
|
53
49
|
|
|
54
50
|
hasCorrespondingRecipes(step) {
|
|
55
|
-
return matchRecipes(this.recipes, this.contextName)
|
|
56
|
-
.filter(r => !r.steps || r.steps.includes(step.name))
|
|
57
|
-
.length > 0;
|
|
51
|
+
return matchRecipes(this.recipes, this.contextName).filter(r => !r.steps || r.steps.includes(step.name)).length > 0
|
|
58
52
|
}
|
|
59
53
|
|
|
60
54
|
async getCodeSuggestions(context) {
|
|
61
|
-
const suggestions = []
|
|
62
|
-
const recipes = matchRecipes(this.recipes, this.contextName)
|
|
55
|
+
const suggestions = []
|
|
56
|
+
const recipes = matchRecipes(this.recipes, this.contextName)
|
|
63
57
|
|
|
64
|
-
|
|
58
|
+
debug('Recipes', recipes)
|
|
65
59
|
|
|
66
|
-
const currentOutputLevel = output.level()
|
|
67
|
-
output.level(0)
|
|
60
|
+
const currentOutputLevel = output.level()
|
|
61
|
+
output.level(0)
|
|
68
62
|
|
|
69
|
-
for (const [property, prepareFn] of Object.entries(
|
|
70
|
-
|
|
63
|
+
for (const [property, prepareFn] of Object.entries(
|
|
64
|
+
recipes
|
|
65
|
+
.map(r => r.prepare)
|
|
66
|
+
.filter(p => !!p)
|
|
67
|
+
.reduce((acc, obj) => ({ ...acc, ...obj }), {}),
|
|
68
|
+
)) {
|
|
69
|
+
if (!prepareFn) continue
|
|
71
70
|
|
|
72
|
-
if (context[property]) continue
|
|
73
|
-
context[property] = await prepareFn(
|
|
71
|
+
if (context[property]) continue
|
|
72
|
+
context[property] = await prepareFn(global.inject())
|
|
74
73
|
}
|
|
75
74
|
|
|
76
|
-
output.level(currentOutputLevel)
|
|
75
|
+
output.level(currentOutputLevel)
|
|
77
76
|
|
|
78
77
|
for (const recipe of recipes) {
|
|
79
|
-
let snippets = await recipe.fn(context)
|
|
80
|
-
if (!Array.isArray(snippets)) snippets = [snippets]
|
|
78
|
+
let snippets = await recipe.fn(context)
|
|
79
|
+
if (!Array.isArray(snippets)) snippets = [snippets]
|
|
81
80
|
|
|
82
81
|
suggestions.push({
|
|
83
82
|
name: recipe.name,
|
|
84
83
|
snippets,
|
|
85
|
-
})
|
|
84
|
+
})
|
|
86
85
|
}
|
|
87
86
|
|
|
88
|
-
return suggestions.filter(s => !isBlank(s.snippets))
|
|
87
|
+
return suggestions.filter(s => !isBlank(s.snippets))
|
|
89
88
|
}
|
|
90
89
|
|
|
91
90
|
async healStep(failedStep, error, failureContext = {}) {
|
|
92
|
-
output.debug(`Trying to heal ${failedStep.toCode()} step`)
|
|
91
|
+
output.debug(`Trying to heal ${failedStep.toCode()} step`)
|
|
93
92
|
|
|
94
93
|
Object.assign(failureContext, {
|
|
95
94
|
error,
|
|
96
95
|
step: failedStep,
|
|
97
96
|
prevSteps: failureContext?.test?.steps?.slice(0, -1) || [],
|
|
98
|
-
})
|
|
97
|
+
})
|
|
99
98
|
|
|
100
|
-
const suggestions = await this.getCodeSuggestions(failureContext)
|
|
99
|
+
const suggestions = await this.getCodeSuggestions(failureContext)
|
|
101
100
|
|
|
102
101
|
if (suggestions.length === 0) {
|
|
103
|
-
|
|
104
|
-
throw error
|
|
102
|
+
debug('No healing suggestions found')
|
|
103
|
+
throw error
|
|
105
104
|
}
|
|
106
105
|
|
|
107
|
-
output.debug(`Received ${suggestions.length} suggestion${suggestions.length === 1 ? '' : 's'}`)
|
|
106
|
+
output.debug(`Received ${suggestions.length} suggestion${suggestions.length === 1 ? '' : 's'}`)
|
|
108
107
|
|
|
109
|
-
|
|
108
|
+
debug(suggestions)
|
|
110
109
|
|
|
111
110
|
for (const suggestion of suggestions) {
|
|
112
111
|
for (const codeSnippet of suggestion.snippets) {
|
|
113
112
|
try {
|
|
114
|
-
|
|
115
|
-
recorder.catch(
|
|
116
|
-
|
|
117
|
-
})
|
|
113
|
+
debug('Executing', codeSnippet)
|
|
114
|
+
recorder.catch(e => {
|
|
115
|
+
debug(e)
|
|
116
|
+
})
|
|
118
117
|
|
|
119
118
|
if (typeof codeSnippet === 'string') {
|
|
120
|
-
const I =
|
|
121
|
-
await eval(codeSnippet)
|
|
119
|
+
const I = global.container.support('I')
|
|
120
|
+
await eval(codeSnippet)
|
|
122
121
|
} else if (typeof codeSnippet === 'function') {
|
|
123
|
-
await codeSnippet(
|
|
122
|
+
await codeSnippet(global.container.support())
|
|
124
123
|
}
|
|
125
124
|
|
|
126
125
|
this.fixes.push({
|
|
@@ -128,49 +127,54 @@ class Heal {
|
|
|
128
127
|
test: failureContext?.test,
|
|
129
128
|
step: failedStep,
|
|
130
129
|
snippet: codeSnippet,
|
|
131
|
-
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
if (failureContext?.test) {
|
|
133
|
+
const test = failureContext.test
|
|
134
|
+
let note = `This test was healed by '${suggestion.name}'`
|
|
135
|
+
note += `\n\nReplace the failed code:\n\n`
|
|
136
|
+
note += colors.red(`- ${failedStep.toCode()}\n`)
|
|
137
|
+
note += colors.green(`+ ${codeSnippet}\n`)
|
|
138
|
+
test.addNote('heal', note)
|
|
139
|
+
test.meta.healed = true
|
|
140
|
+
}
|
|
132
141
|
|
|
133
|
-
recorder.add('healed', () => output.print(colors.bold.green(` Code healed successfully by ${suggestion.name}`), colors.gray('(no errors thrown)')))
|
|
134
|
-
this.numHealed
|
|
142
|
+
recorder.add('healed', () => output.print(colors.bold.green(` Code healed successfully by ${suggestion.name}`), colors.gray('(no errors thrown)')))
|
|
143
|
+
this.numHealed++
|
|
135
144
|
// recorder.session.restore();
|
|
136
|
-
return
|
|
145
|
+
return
|
|
137
146
|
} catch (err) {
|
|
138
|
-
|
|
139
|
-
recorder.ignoreErr(err)
|
|
140
|
-
recorder.catchWithoutStop(err)
|
|
141
|
-
await recorder.promise()
|
|
147
|
+
debug('Failed to execute code', err)
|
|
148
|
+
recorder.ignoreErr(err) // healing did not help
|
|
149
|
+
recorder.catchWithoutStop(err)
|
|
150
|
+
await recorder.promise() // wait for all promises to resolve
|
|
142
151
|
}
|
|
143
152
|
}
|
|
144
153
|
}
|
|
145
|
-
output.debug(`Couldn't heal the code for ${failedStep.toCode()}`)
|
|
146
|
-
recorder.throw(error)
|
|
154
|
+
output.debug(`Couldn't heal the code for ${failedStep.toCode()}`)
|
|
155
|
+
recorder.throw(error)
|
|
147
156
|
}
|
|
148
157
|
|
|
149
|
-
static setDefaultHealers() {
|
|
150
|
-
|
|
158
|
+
static async setDefaultHealers() {
|
|
159
|
+
await import('./template/heal.js')
|
|
151
160
|
}
|
|
152
161
|
}
|
|
153
162
|
|
|
154
|
-
const heal = new Heal()
|
|
163
|
+
const heal = new Heal()
|
|
155
164
|
|
|
156
|
-
export
|
|
165
|
+
export default heal
|
|
157
166
|
|
|
158
167
|
function matchRecipes(recipes, contextName) {
|
|
159
168
|
return Object.entries(recipes)
|
|
160
169
|
.filter(([, recipe]) => !contextName || !recipe.grep || new RegExp(recipe.grep).test(contextName))
|
|
161
170
|
.sort(([, a], [, b]) => a.priority - b.priority)
|
|
162
171
|
.map(([name, recipe]) => {
|
|
163
|
-
recipe.name = name
|
|
164
|
-
return recipe
|
|
172
|
+
recipe.name = name
|
|
173
|
+
return recipe
|
|
165
174
|
})
|
|
166
|
-
.filter(r => !!r.fn)
|
|
175
|
+
.filter(r => !!r.fn)
|
|
167
176
|
}
|
|
168
177
|
|
|
169
178
|
function isBlank(value) {
|
|
170
|
-
return (
|
|
171
|
-
value == null
|
|
172
|
-
|| (Array.isArray(value) && value.length === 0)
|
|
173
|
-
|| (typeof value === 'object' && Object.keys(value).length === 0)
|
|
174
|
-
|| (typeof value === 'string' && value.trim() === '')
|
|
175
|
-
);
|
|
179
|
+
return value == null || (Array.isArray(value) && value.length === 0) || (typeof value === 'object' && Object.keys(value).length === 0) || (typeof value === 'string' && value.trim() === '')
|
|
176
180
|
}
|
package/lib/helper/AI.js
CHANGED
|
@@ -1,39 +1,53 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
1
|
+
import HelperModule from '@codeceptjs/helper'
|
|
2
|
+
import ora from 'ora-classic'
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import ai from '../ai.js'
|
|
6
|
+
import Container from '../container.js'
|
|
7
|
+
import { splitByChunks, minifyHtml } from '../html.js'
|
|
8
|
+
import { beautify } from '../utils.js'
|
|
9
|
+
import output from '../output.js'
|
|
10
|
+
import { registerVariable } from '../pause.js'
|
|
11
|
+
|
|
12
|
+
const standardActingHelpers = Container.STANDARD_ACTING_HELPERS
|
|
13
|
+
|
|
14
|
+
const gtpRole = {
|
|
15
|
+
user: 'user',
|
|
16
|
+
}
|
|
6
17
|
|
|
7
18
|
/**
|
|
8
19
|
* AI Helper for CodeceptJS.
|
|
9
20
|
*
|
|
10
21
|
* This helper class provides integration with the AI GPT-3.5 or 4 language model for generating responses to questions or prompts within the context of web pages. It allows you to interact with the GPT-3.5 model to obtain intelligent responses based on HTML fragments or general prompts.
|
|
11
|
-
* This helper should be enabled with any web helpers like Playwright or Puppeteer or
|
|
22
|
+
* This helper should be enabled with any web helpers like Playwright or Puppeteer or WebDriver to ensure the HTML context is available.
|
|
23
|
+
*
|
|
24
|
+
* Use it only in development mode. It is recommended to run it only inside pause() mode.
|
|
12
25
|
*
|
|
13
26
|
* ## Configuration
|
|
14
27
|
*
|
|
15
|
-
* This helper should be configured in codecept.
|
|
28
|
+
* This helper should be configured in codecept.conf.{js|ts}
|
|
16
29
|
*
|
|
17
30
|
* * `chunkSize`: (optional, default: 80000) - The maximum number of characters to send to the AI API at once. We split HTML fragments by 8000 chars to not exceed token limit. Increase this value if you use GPT-4.
|
|
18
31
|
*/
|
|
19
32
|
class AI extends Helper {
|
|
20
33
|
constructor(config) {
|
|
21
|
-
super(config)
|
|
22
|
-
this.aiAssistant =
|
|
34
|
+
super(config)
|
|
35
|
+
this.aiAssistant = ai
|
|
23
36
|
|
|
24
37
|
this.options = {
|
|
25
38
|
chunkSize: 80000,
|
|
26
|
-
}
|
|
27
|
-
this.options = { ...this.options, ...config }
|
|
39
|
+
}
|
|
40
|
+
this.options = { ...this.options, ...config }
|
|
41
|
+
this.aiAssistant.enable(this.config)
|
|
28
42
|
}
|
|
29
43
|
|
|
30
44
|
_beforeSuite() {
|
|
31
|
-
const helpers = Container.helpers()
|
|
45
|
+
const helpers = Container.helpers()
|
|
32
46
|
|
|
33
47
|
for (const helperName of standardActingHelpers) {
|
|
34
48
|
if (Object.keys(helpers).indexOf(helperName) > -1) {
|
|
35
|
-
this.helper = helpers[helperName]
|
|
36
|
-
break
|
|
49
|
+
this.helper = helpers[helperName]
|
|
50
|
+
break
|
|
37
51
|
}
|
|
38
52
|
}
|
|
39
53
|
}
|
|
@@ -50,30 +64,34 @@ class AI extends Helper {
|
|
|
50
64
|
* @returns {Promise<string>} - A Promise that resolves to the generated responses from the GPT model, joined by newlines.
|
|
51
65
|
*/
|
|
52
66
|
async askGptOnPage(prompt) {
|
|
53
|
-
const html = await this.helper.grabSource()
|
|
67
|
+
const html = await this.helper.grabSource()
|
|
54
68
|
|
|
55
|
-
const htmlChunks = splitByChunks(html, this.options.chunkSize)
|
|
69
|
+
const htmlChunks = splitByChunks(html, this.options.chunkSize)
|
|
56
70
|
|
|
57
|
-
if (htmlChunks.length > 1) this.debug(`Splitting HTML into ${htmlChunks.length} chunks`)
|
|
71
|
+
if (htmlChunks.length > 1) this.debug(`Splitting HTML into ${htmlChunks.length} chunks`)
|
|
58
72
|
|
|
59
|
-
const responses = []
|
|
73
|
+
const responses = []
|
|
60
74
|
|
|
61
75
|
for (const chunk of htmlChunks) {
|
|
62
76
|
const messages = [
|
|
63
|
-
{ role:
|
|
64
|
-
{ role:
|
|
65
|
-
]
|
|
77
|
+
{ role: gtpRole.user, content: prompt },
|
|
78
|
+
{ role: gtpRole.user, content: `Within this HTML: ${await minifyHtml(chunk)}` },
|
|
79
|
+
]
|
|
66
80
|
|
|
67
|
-
if (htmlChunks.length > 1)
|
|
81
|
+
if (htmlChunks.length > 1)
|
|
82
|
+
messages.push({
|
|
83
|
+
role: 'user',
|
|
84
|
+
content: 'If action is not possible on this page, do not propose anything, I will send another HTML fragment',
|
|
85
|
+
})
|
|
68
86
|
|
|
69
|
-
const response = await this.
|
|
87
|
+
const response = await this._processAIRequest(messages)
|
|
70
88
|
|
|
71
|
-
|
|
89
|
+
output.print(response)
|
|
72
90
|
|
|
73
|
-
responses.push(response)
|
|
91
|
+
responses.push(response)
|
|
74
92
|
}
|
|
75
93
|
|
|
76
|
-
return responses.join('\n\n')
|
|
94
|
+
return responses.join('\n\n')
|
|
77
95
|
}
|
|
78
96
|
|
|
79
97
|
/**
|
|
@@ -89,36 +107,108 @@ class AI extends Helper {
|
|
|
89
107
|
* @returns {Promise<string>} - A Promise that resolves to the generated response from the GPT model.
|
|
90
108
|
*/
|
|
91
109
|
async askGptOnPageFragment(prompt, locator) {
|
|
92
|
-
const html = await this.helper.grabHTMLFrom(locator)
|
|
110
|
+
const html = await this.helper.grabHTMLFrom(locator)
|
|
93
111
|
|
|
94
112
|
const messages = [
|
|
95
|
-
{ role:
|
|
96
|
-
{ role:
|
|
97
|
-
]
|
|
113
|
+
{ role: gtpRole.user, content: prompt },
|
|
114
|
+
{ role: gtpRole.user, content: `Within this HTML: ${await minifyHtml(html)}` },
|
|
115
|
+
]
|
|
98
116
|
|
|
99
|
-
const response = await this.
|
|
117
|
+
const response = await this._processAIRequest(messages)
|
|
100
118
|
|
|
101
|
-
|
|
119
|
+
output.print(response)
|
|
102
120
|
|
|
103
|
-
return response
|
|
121
|
+
return response
|
|
104
122
|
}
|
|
105
123
|
|
|
106
124
|
/**
|
|
107
|
-
* Send a general request to
|
|
125
|
+
* Send a general request to AI and return response.
|
|
108
126
|
* @param {string} prompt
|
|
109
127
|
* @returns {Promise<string>} - A Promise that resolves to the generated response from the GPT model.
|
|
110
128
|
*/
|
|
111
129
|
async askGptGeneralPrompt(prompt) {
|
|
112
|
-
const messages = [
|
|
113
|
-
|
|
114
|
-
|
|
130
|
+
const messages = [{ role: gtpRole.user, content: prompt }]
|
|
131
|
+
|
|
132
|
+
const response = await this._processAIRequest(messages)
|
|
133
|
+
|
|
134
|
+
output.print(response)
|
|
135
|
+
|
|
136
|
+
return response
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Generates PageObject for current page using AI.
|
|
141
|
+
*
|
|
142
|
+
* It saves the PageObject to the output directory. You can review the page object and adjust it as needed and move to pages directory.
|
|
143
|
+
* Prompt can be customized in a global config file.
|
|
144
|
+
*
|
|
145
|
+
* ```js
|
|
146
|
+
* // create page object for whole page
|
|
147
|
+
* I.askForPageObject('home');
|
|
148
|
+
*
|
|
149
|
+
* // create page object with extra prompt
|
|
150
|
+
* I.askForPageObject('home', 'implement signIn(username, password) method');
|
|
151
|
+
*
|
|
152
|
+
* // create page object for a specific element
|
|
153
|
+
* I.askForPageObject('home', null, '.detail');
|
|
154
|
+
* ```
|
|
155
|
+
*
|
|
156
|
+
* Asks for a page object based on the provided page name, locator, and extra prompt.
|
|
157
|
+
*
|
|
158
|
+
* @async
|
|
159
|
+
* @param {string} pageName - The name of the page to retrieve the object for.
|
|
160
|
+
* @param {string|null} [extraPrompt=null] - An optional extra prompt for additional context or information.
|
|
161
|
+
* @param {string|null} [locator=null] - An optional locator to find a specific element on the page.
|
|
162
|
+
* @returns {Promise<Object>} A promise that resolves to the requested page object.
|
|
163
|
+
*/
|
|
164
|
+
async askForPageObject(pageName, extraPrompt = null, locator = null) {
|
|
165
|
+
const spinner = ora(' Processing AI request...').start()
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const html = locator ? await this.helper.grabHTMLFrom(locator) : await this.helper.grabSource()
|
|
169
|
+
await this.aiAssistant.setHtmlContext(html)
|
|
170
|
+
const response = await this.aiAssistant.generatePageObject(extraPrompt, locator)
|
|
171
|
+
spinner.stop()
|
|
172
|
+
|
|
173
|
+
if (!response[0]) {
|
|
174
|
+
output.error('No response from AI')
|
|
175
|
+
return ''
|
|
176
|
+
}
|
|
115
177
|
|
|
116
|
-
|
|
178
|
+
const code = beautify(response[0])
|
|
117
179
|
|
|
118
|
-
|
|
180
|
+
output.print('----- Generated PageObject ----')
|
|
181
|
+
output.print(code)
|
|
182
|
+
output.print('-------------------------------')
|
|
183
|
+
|
|
184
|
+
const fileName = path.join(output_dir, `${pageName}Page-${Date.now()}.js`)
|
|
185
|
+
|
|
186
|
+
output.print(output.styles.bold(`Page object for ${pageName} is saved to ${output.styles.bold(fileName)}`))
|
|
187
|
+
fs.writeFileSync(fileName, code)
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
registerVariable('page', require(fileName))
|
|
191
|
+
output.success('Page object registered for this session as `page` variable')
|
|
192
|
+
output.print('Use `=>page.methodName()` in shell to run methods of page object')
|
|
193
|
+
output.print('Use `click(page.locatorName)` to check locators of page object')
|
|
194
|
+
} catch (err) {
|
|
195
|
+
output.error('Error while registering page object')
|
|
196
|
+
output.error(err.message)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return code
|
|
200
|
+
} catch (e) {
|
|
201
|
+
spinner.stop()
|
|
202
|
+
throw Error(`Something went wrong! ${e.message}`)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
119
205
|
|
|
120
|
-
|
|
206
|
+
async _processAIRequest(messages) {
|
|
207
|
+
const spinner = ora(' Processing AI request...').start()
|
|
208
|
+
const response = await this.aiAssistant.createCompletion(messages)
|
|
209
|
+
spinner.stop()
|
|
210
|
+
return response
|
|
121
211
|
}
|
|
122
212
|
}
|
|
123
213
|
|
|
124
|
-
export default AI
|
|
214
|
+
export default AI
|