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
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import debugFactory from 'debug'
|
|
2
|
+
const debug = debugFactory('codeceptjs:analyze')
|
|
3
|
+
import { isMainThread } from 'node:worker_threads'
|
|
4
|
+
import figures from 'figures'
|
|
5
|
+
const { arrowRight } = figures
|
|
6
|
+
import Container from '../container.js'
|
|
7
|
+
// Container already imported correctly above
|
|
8
|
+
import store from '../store.js'
|
|
9
|
+
|
|
10
|
+
import aiModule from '../ai.js'
|
|
11
|
+
const ai = aiModule.default || aiModule
|
|
12
|
+
import colors from 'chalk'
|
|
13
|
+
import ora from 'ora'
|
|
14
|
+
import event from '../event.js'
|
|
15
|
+
|
|
16
|
+
import output from '../output.js'
|
|
17
|
+
|
|
18
|
+
import { ansiRegExp, base64EncodeFile, markdownToAnsi } from '../utils.js'
|
|
19
|
+
|
|
20
|
+
const MAX_DATA_LENGTH = 5000
|
|
21
|
+
|
|
22
|
+
const defaultConfig = {
|
|
23
|
+
clusterize: 5,
|
|
24
|
+
analyze: 2,
|
|
25
|
+
vision: false,
|
|
26
|
+
categories: [
|
|
27
|
+
'Browser connection error / browser crash',
|
|
28
|
+
'Network errors (server error, timeout, etc)',
|
|
29
|
+
'HTML / page elements (not found, not visible, etc)',
|
|
30
|
+
'Navigation errors (404, etc)',
|
|
31
|
+
'Code errors (syntax error, JS errors, etc)',
|
|
32
|
+
'Library & framework errors (CodeceptJS internal errors, user-defined libraries, etc)',
|
|
33
|
+
'Data errors (password incorrect, no options in select, invalid format, etc)',
|
|
34
|
+
'Assertion failures',
|
|
35
|
+
'Other errors',
|
|
36
|
+
],
|
|
37
|
+
prompts: {
|
|
38
|
+
clusterize: (tests, config) => {
|
|
39
|
+
const serializedFailedTests = tests
|
|
40
|
+
.map((test, index) => {
|
|
41
|
+
if (!test || !test.err) return
|
|
42
|
+
return `
|
|
43
|
+
#${index + 1}: ${serializeTest(test)}
|
|
44
|
+
${serializeError(test.err).slice(0, MAX_DATA_LENGTH / tests.length)}`.trim()
|
|
45
|
+
})
|
|
46
|
+
.join('\n\n--------\n\n')
|
|
47
|
+
|
|
48
|
+
const messages = [
|
|
49
|
+
{
|
|
50
|
+
role: 'user',
|
|
51
|
+
content: `
|
|
52
|
+
I am test analyst analyzing failed tests in CodeceptJS testing framework.
|
|
53
|
+
|
|
54
|
+
Please analyze the following failed tests and classify them into groups by their cause.
|
|
55
|
+
If there is no groups detected, say: "No common groups found".
|
|
56
|
+
|
|
57
|
+
Provide a short description of the group and a list of failed tests that belong to this group.
|
|
58
|
+
Use percent sign to indicate the percentage of failed tests in the group if this percentage is greater than 30%.
|
|
59
|
+
|
|
60
|
+
Here are failed tests:
|
|
61
|
+
|
|
62
|
+
${serializedFailedTests}
|
|
63
|
+
|
|
64
|
+
Common categories of failures by order of priority:
|
|
65
|
+
|
|
66
|
+
${config.categories.join('\n- ')}
|
|
67
|
+
|
|
68
|
+
If there is no groups of tests, say: "No patterns found"
|
|
69
|
+
Preserve error messages but cut them if they are too long.
|
|
70
|
+
Respond clearly and directly, without introductory words or phrases like 'Of course,' 'Here is the answer,' etc.
|
|
71
|
+
Do not list more than 3 errors in the group.
|
|
72
|
+
If you identify that all tests in the group have the same tag, add this tag to the group report, otherwise ignore TAG section.
|
|
73
|
+
If you identify that all tests in the group have the same suite, add this suite to the group report, otherwise ignore SUITE section.
|
|
74
|
+
Pick different emojis for each group.
|
|
75
|
+
Order groups by the number of tests in the group.
|
|
76
|
+
If group has one test, skip that group.
|
|
77
|
+
|
|
78
|
+
Provide list of groups in following format:
|
|
79
|
+
|
|
80
|
+
_______________________________
|
|
81
|
+
|
|
82
|
+
## Group <group_number> <emoji>
|
|
83
|
+
|
|
84
|
+
* SUMMARY <summary_of_errors>
|
|
85
|
+
* CATEGORY <category_of_failure>
|
|
86
|
+
* URL <url_of_failure_if_any>
|
|
87
|
+
* ERROR <error_message_1>, <error_message_2>, ...
|
|
88
|
+
* STEP <step_of_failure> (use CodeceptJS format I.click(), I.see(), etc; if all failures happend on the same step)
|
|
89
|
+
* SUITE <suite_title>, <suite_title> (if SUITE is present, and if all tests in the group have the same suite or suites)
|
|
90
|
+
* TAG <tag> (if TAG is present, and if all tests in the group have the same tag)
|
|
91
|
+
* AFFECTED TESTS (<total number of tests>):
|
|
92
|
+
x <test1 title>
|
|
93
|
+
x <test2 title>
|
|
94
|
+
x <test3 title>
|
|
95
|
+
x ...
|
|
96
|
+
`,
|
|
97
|
+
},
|
|
98
|
+
]
|
|
99
|
+
return messages
|
|
100
|
+
},
|
|
101
|
+
analyze: (test, config) => {
|
|
102
|
+
const testMessage = serializeTest(test)
|
|
103
|
+
const errorMessage = serializeError(test.err)
|
|
104
|
+
|
|
105
|
+
const messages = [
|
|
106
|
+
{
|
|
107
|
+
role: 'user',
|
|
108
|
+
content: [
|
|
109
|
+
{
|
|
110
|
+
type: 'text',
|
|
111
|
+
text: `
|
|
112
|
+
I am qa engineer analyzing failed tests in CodeceptJS testing framework.
|
|
113
|
+
Please analyze the following failed test and error its error and explain it.
|
|
114
|
+
|
|
115
|
+
Pick one of the categories of failures and explain it.
|
|
116
|
+
|
|
117
|
+
Categories of failures in order of priority:
|
|
118
|
+
|
|
119
|
+
${config.categories.join('\n- ')}
|
|
120
|
+
|
|
121
|
+
Here is the test and error:
|
|
122
|
+
|
|
123
|
+
------- TEST -------
|
|
124
|
+
${testMessage}
|
|
125
|
+
|
|
126
|
+
------- ERROR -------
|
|
127
|
+
${errorMessage}
|
|
128
|
+
|
|
129
|
+
------ INSTRUCTIONS ------
|
|
130
|
+
|
|
131
|
+
Do not get to details, be concise.
|
|
132
|
+
If there is failed step, just write it in STEPS section.
|
|
133
|
+
If you have suggestions for the test, write them in SUMMARY section.
|
|
134
|
+
Do not be too technical in SUMMARY section.
|
|
135
|
+
Inside SUMMARY write exact values, if you have suggestions, explain which information you used to suggest.
|
|
136
|
+
Be concise, each section should not take more than one sentence.
|
|
137
|
+
|
|
138
|
+
Response format:
|
|
139
|
+
|
|
140
|
+
* SUMMARY <explanation_of_failure>
|
|
141
|
+
* ERROR <error_message_1>, <error_message_2>, ...
|
|
142
|
+
* CATEGORY <category_of_failure>
|
|
143
|
+
* STEPS <step_of_failure>
|
|
144
|
+
* URL <url_of_failure_if_any>
|
|
145
|
+
|
|
146
|
+
Do not add any other sections or explanations. Only CATEGORY, SUMMARY, STEPS.
|
|
147
|
+
${config.vision ? 'Also a screenshot of the page is attached to the prompt.' : ''}
|
|
148
|
+
`,
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
if (config.vision && test.artifacts.screenshot) {
|
|
155
|
+
debug('Adding screenshot to prompt')
|
|
156
|
+
messages[0].content.push({
|
|
157
|
+
type: 'image_url',
|
|
158
|
+
image_url: {
|
|
159
|
+
url: 'data:image/png;base64,' + base64EncodeFile(test.artifacts.screenshot),
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return messages
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
*
|
|
171
|
+
* Uses AI to analyze test failures and provide insights
|
|
172
|
+
*
|
|
173
|
+
* This plugin analyzes failed tests using AI to provide detailed explanations and group similar failures.
|
|
174
|
+
* When enabled with --ai flag, it generates reports after test execution.
|
|
175
|
+
*
|
|
176
|
+
* #### Usage
|
|
177
|
+
*
|
|
178
|
+
* ```js
|
|
179
|
+
* // in codecept.conf.js
|
|
180
|
+
* exports.config = {
|
|
181
|
+
* plugins: {
|
|
182
|
+
* analyze: {
|
|
183
|
+
* enabled: true,
|
|
184
|
+
* clusterize: 5,
|
|
185
|
+
* analyze: 2,
|
|
186
|
+
* vision: false
|
|
187
|
+
* }
|
|
188
|
+
* }
|
|
189
|
+
* }
|
|
190
|
+
* ```
|
|
191
|
+
*
|
|
192
|
+
* #### Configuration
|
|
193
|
+
*
|
|
194
|
+
* * `clusterize` (number) - minimum number of failures to trigger clustering analysis. Default: 5
|
|
195
|
+
* * `analyze` (number) - maximum number of individual test failures to analyze in detail. Default: 2
|
|
196
|
+
* * `vision` (boolean) - enables visual analysis of test screenshots. Default: false
|
|
197
|
+
* * `categories` (array) - list of failure categories for classification. Defaults to:
|
|
198
|
+
* - Browser connection error / browser crash
|
|
199
|
+
* - Network errors (server error, timeout, etc)
|
|
200
|
+
* - HTML / page elements (not found, not visible, etc)
|
|
201
|
+
* - Navigation errors (404, etc)
|
|
202
|
+
* - Code errors (syntax error, JS errors, etc)
|
|
203
|
+
* - Library & framework errors
|
|
204
|
+
* - Data errors (password incorrect, invalid format, etc)
|
|
205
|
+
* - Assertion failures
|
|
206
|
+
* - Other errors
|
|
207
|
+
* * `prompts` (object) - customize AI prompts for analysis
|
|
208
|
+
* - `clusterize` - prompt for clustering analysis
|
|
209
|
+
* - `analyze` - prompt for individual test analysis
|
|
210
|
+
*
|
|
211
|
+
* #### Features
|
|
212
|
+
*
|
|
213
|
+
* * Groups similar failures when number of failures >= clusterize value
|
|
214
|
+
* * Provides detailed analysis of individual failures
|
|
215
|
+
* * Analyzes screenshots if vision=true and screenshots are available
|
|
216
|
+
* * Classifies failures into predefined categories
|
|
217
|
+
* * Suggests possible causes and solutions
|
|
218
|
+
*
|
|
219
|
+
* @param {Object} config - Plugin configuration
|
|
220
|
+
* @returns {void}
|
|
221
|
+
*/
|
|
222
|
+
export default function (config = {}) {
|
|
223
|
+
config = Object.assign(defaultConfig, config)
|
|
224
|
+
|
|
225
|
+
event.dispatcher.on(event.workers.before, () => {
|
|
226
|
+
if (!ai.isEnabled) return
|
|
227
|
+
console.log('Enabled AI analysis')
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
event.dispatcher.on(event.all.result, async result => {
|
|
231
|
+
if (!isMainThread) return // run only on main thread
|
|
232
|
+
if (!ai.isEnabled) {
|
|
233
|
+
console.log('AI is disabled, no analysis will be performed. Run tests with --ai flag to enable it.')
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
printReport(result)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
event.dispatcher.on(event.workers.result, async result => {
|
|
241
|
+
if (!result.hasFailed) {
|
|
242
|
+
console.log('Everything is fine, skipping AI analysis')
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!ai.isEnabled) {
|
|
247
|
+
console.log('AI is disabled, no analysis will be performed. Run tests with --ai flag to enable it.')
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
printReport(result)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
async function printReport(result) {
|
|
255
|
+
const failedTestsAndErrors = result.tests.filter(t => t.err)
|
|
256
|
+
|
|
257
|
+
if (!failedTestsAndErrors.length) return
|
|
258
|
+
|
|
259
|
+
debug(failedTestsAndErrors.map(t => serializeTest(t) + '\n' + serializeError(t.err)))
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
if (failedTestsAndErrors.length >= config.clusterize) {
|
|
263
|
+
const response = await clusterize(failedTestsAndErrors)
|
|
264
|
+
printHeader()
|
|
265
|
+
console.log(response)
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
output.plugin('analyze', `Analyzing first ${config.analyze} failed tests...`)
|
|
270
|
+
|
|
271
|
+
// we pick only unique errors to not repeat answers
|
|
272
|
+
const uniqueErrors = failedTestsAndErrors.filter((item, index, array) => {
|
|
273
|
+
return array.findIndex(t => t.err?.message === item.err?.message) === index
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
for (let i = 0; i < config.analyze; i++) {
|
|
277
|
+
if (!uniqueErrors[i]) break
|
|
278
|
+
|
|
279
|
+
const response = await analyze(uniqueErrors[i])
|
|
280
|
+
if (!response) {
|
|
281
|
+
break
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
printHeader()
|
|
285
|
+
console.log()
|
|
286
|
+
console.log('--------------------------------')
|
|
287
|
+
console.log(arrowRight, colors.bold.white(uniqueErrors[i].fullTitle()), config.vision ? '👀' : '')
|
|
288
|
+
console.log()
|
|
289
|
+
console.log()
|
|
290
|
+
console.log(response)
|
|
291
|
+
console.log()
|
|
292
|
+
}
|
|
293
|
+
} catch (err) {
|
|
294
|
+
console.error('Error analyzing failed tests', err)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!Object.keys(container.plugins()).includes('pageInfo')) {
|
|
298
|
+
console.log('To improve analysis, enable pageInfo plugin to get more context for failed tests.')
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
let hasPrintedHeader = false
|
|
303
|
+
|
|
304
|
+
function printHeader() {
|
|
305
|
+
if (!hasPrintedHeader) {
|
|
306
|
+
console.log()
|
|
307
|
+
console.log(colors.bold.white('🪄 AI REPORT:'))
|
|
308
|
+
hasPrintedHeader = true
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function clusterize(failedTestsAndErrors) {
|
|
313
|
+
const spinner = ora('Clusterizing failures...').start()
|
|
314
|
+
const prompt = config.prompts.clusterize(failedTestsAndErrors, config)
|
|
315
|
+
try {
|
|
316
|
+
const response = await ai.createCompletion(prompt)
|
|
317
|
+
spinner.stop()
|
|
318
|
+
return formatResponse(response)
|
|
319
|
+
} catch (err) {
|
|
320
|
+
spinner.stop()
|
|
321
|
+
console.error('Error clusterizing failures', err.message)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function analyze(failedTestAndError) {
|
|
326
|
+
const spinner = ora('Analyzing failure...').start()
|
|
327
|
+
const prompt = config.prompts.analyze(failedTestAndError, config)
|
|
328
|
+
try {
|
|
329
|
+
const response = await ai.createCompletion(prompt)
|
|
330
|
+
spinner.stop()
|
|
331
|
+
return formatResponse(response)
|
|
332
|
+
} catch (err) {
|
|
333
|
+
spinner.stop()
|
|
334
|
+
console.error('Error analyzing failure:', err.message)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function serializeError(error) {
|
|
340
|
+
if (typeof error === 'string') {
|
|
341
|
+
return error
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (!error) return
|
|
345
|
+
|
|
346
|
+
let errorMessage = 'ERROR: ' + error.message
|
|
347
|
+
|
|
348
|
+
if (error.inspect) {
|
|
349
|
+
errorMessage = 'ERROR: ' + error.inspect()
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (error.stack) {
|
|
353
|
+
errorMessage +=
|
|
354
|
+
'\n' +
|
|
355
|
+
error.stack
|
|
356
|
+
.replace(global.codecept_dir || '', '.')
|
|
357
|
+
.split('\n')
|
|
358
|
+
.map(line => line.replace(ansiRegExp(), ''))
|
|
359
|
+
.slice(0, 5)
|
|
360
|
+
.join('\n')
|
|
361
|
+
}
|
|
362
|
+
if (error.steps) {
|
|
363
|
+
errorMessage += '\n STEPS: ' + error.steps.map(s => s.toCode()).join('\n')
|
|
364
|
+
}
|
|
365
|
+
return errorMessage
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function serializeTest(test) {
|
|
369
|
+
if (!test.uid) return
|
|
370
|
+
|
|
371
|
+
let testMessage = 'TEST TITLE: ' + test.title
|
|
372
|
+
|
|
373
|
+
if (test.suite) {
|
|
374
|
+
testMessage += '\n SUITE: ' + test.suite.title
|
|
375
|
+
}
|
|
376
|
+
if (test.parent) {
|
|
377
|
+
testMessage += '\n SUITE: ' + test.parent.title
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (test.steps?.length) {
|
|
381
|
+
const failedSteps = test.steps
|
|
382
|
+
if (failedSteps.length) testMessage += '\n STEP: ' + failedSteps.map(s => s.toCode()).join('; ')
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const pageInfo = test.notes.find(n => n.type === 'pageInfo')
|
|
386
|
+
if (pageInfo) {
|
|
387
|
+
testMessage += '\n PAGE INFO: ' + pageInfo.text
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return testMessage
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function formatResponse(response) {
|
|
394
|
+
return response
|
|
395
|
+
.replace(/<think>([\s\S]*?)<\/think>/g, store.debugMode ? colors.cyan('$1') : '')
|
|
396
|
+
.split('\n')
|
|
397
|
+
.map(line => line.trim())
|
|
398
|
+
.filter(line => !/^[A-Z\s]+$/.test(line))
|
|
399
|
+
.map(line => markdownToAnsi(line))
|
|
400
|
+
.map(line => line.replace(/^x /gm, ` ${colors.red.bold('x')} `))
|
|
401
|
+
.join('\n')
|
|
402
|
+
.trim()
|
|
403
|
+
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { fileExists, isAsyncFunction } from '../utils.js'
|
|
4
|
+
import CommentStep from '../step/comment.js'
|
|
5
|
+
import Section from '../step/section.js'
|
|
6
|
+
import container from '../container.js'
|
|
7
|
+
import store from '../store.js'
|
|
8
|
+
import event from '../event.js'
|
|
9
|
+
import recorder from '../recorder.js'
|
|
10
|
+
import output from '../output.js'
|
|
9
11
|
|
|
10
12
|
const defaultUser = {
|
|
11
|
-
fetch:
|
|
13
|
+
fetch: I => I.grabCookie(),
|
|
12
14
|
check: () => {},
|
|
13
15
|
restore: (I, cookies) => {
|
|
14
16
|
I.amOnPage('/') // open a page
|
|
@@ -72,7 +74,7 @@ const defaultConfig = {
|
|
|
72
74
|
* #### Example: Simple login
|
|
73
75
|
*
|
|
74
76
|
* ```js
|
|
75
|
-
*
|
|
77
|
+
* auth: {
|
|
76
78
|
* enabled: true,
|
|
77
79
|
* saveToFile: true,
|
|
78
80
|
* inject: 'login',
|
|
@@ -93,7 +95,7 @@ const defaultConfig = {
|
|
|
93
95
|
* #### Example: Multiple users
|
|
94
96
|
*
|
|
95
97
|
* ```js
|
|
96
|
-
*
|
|
98
|
+
* auth: {
|
|
97
99
|
* enabled: true,
|
|
98
100
|
* saveToFile: true,
|
|
99
101
|
* inject: 'loginAs', // use `loginAs` instead of login
|
|
@@ -140,7 +142,7 @@ const defaultConfig = {
|
|
|
140
142
|
* }
|
|
141
143
|
* },
|
|
142
144
|
* plugins: {
|
|
143
|
-
*
|
|
145
|
+
* auth: {
|
|
144
146
|
* users: {
|
|
145
147
|
* admin: {
|
|
146
148
|
* login: (I) => {
|
|
@@ -167,7 +169,7 @@ const defaultConfig = {
|
|
|
167
169
|
*
|
|
168
170
|
* ```js
|
|
169
171
|
* plugins: {
|
|
170
|
-
*
|
|
172
|
+
* auth: {
|
|
171
173
|
* admin: {
|
|
172
174
|
* login: (I) => I.loginAsAdmin(),
|
|
173
175
|
* check: (I) => I.see('Admin', '.navbar'),
|
|
@@ -183,18 +185,18 @@ const defaultConfig = {
|
|
|
183
185
|
* }
|
|
184
186
|
* ```
|
|
185
187
|
*
|
|
186
|
-
* #### Tips: Using async function in the
|
|
188
|
+
* #### Tips: Using async function in the auth
|
|
187
189
|
*
|
|
188
|
-
* If you use async functions in the
|
|
190
|
+
* If you use async functions in the auth plugin, login function should be used with `await` keyword.
|
|
189
191
|
*
|
|
190
192
|
* ```js
|
|
191
|
-
*
|
|
193
|
+
* auth: {
|
|
192
194
|
* enabled: true,
|
|
193
195
|
* saveToFile: true,
|
|
194
196
|
* inject: 'login',
|
|
195
197
|
* users: {
|
|
196
198
|
* admin: {
|
|
197
|
-
* login: async (I) => { // If you use async function in the
|
|
199
|
+
* login: async (I) => { // If you use async function in the auth plugin
|
|
198
200
|
* const phrase = await I.grabTextFrom('#phrase')
|
|
199
201
|
* I.fillField('username', 'admin'),
|
|
200
202
|
* I.fillField('password', 'password')
|
|
@@ -220,13 +222,13 @@ const defaultConfig = {
|
|
|
220
222
|
* Instead of asserting on page elements for the current user in `check`, you can use the `session` you saved in `fetch`
|
|
221
223
|
*
|
|
222
224
|
* ```js
|
|
223
|
-
*
|
|
225
|
+
* auth: {
|
|
224
226
|
* enabled: true,
|
|
225
227
|
* saveToFile: true,
|
|
226
228
|
* inject: 'login',
|
|
227
229
|
* users: {
|
|
228
230
|
* admin: {
|
|
229
|
-
* login: async (I) => { // If you use async function in the
|
|
231
|
+
* login: async (I) => { // If you use async function in the auth plugin
|
|
230
232
|
* const phrase = await I.grabTextFrom('#phrase')
|
|
231
233
|
* I.fillField('username', 'admin'),
|
|
232
234
|
* I.fillField('password', 'password')
|
|
@@ -250,10 +252,10 @@ const defaultConfig = {
|
|
|
250
252
|
*
|
|
251
253
|
*
|
|
252
254
|
*/
|
|
253
|
-
|
|
255
|
+
export default function (config) {
|
|
254
256
|
config = Object.assign(defaultConfig, config)
|
|
255
257
|
Object.keys(config.users).map(
|
|
256
|
-
|
|
258
|
+
u =>
|
|
257
259
|
(config.users[u] = {
|
|
258
260
|
...defaultUser,
|
|
259
261
|
...config.users[u],
|
|
@@ -262,25 +264,47 @@ module.exports = function (config) {
|
|
|
262
264
|
|
|
263
265
|
if (config.saveToFile) {
|
|
264
266
|
// loading from file
|
|
265
|
-
|
|
266
|
-
const fileName = path.join(global.output_dir, `${name}_session.json`)
|
|
267
|
-
if (!fileExists(fileName)) continue
|
|
268
|
-
const data = fs.readFileSync(fileName).toString()
|
|
269
|
-
try {
|
|
270
|
-
store[`${name}_session`] = JSON.parse(data)
|
|
271
|
-
} catch (err) {
|
|
272
|
-
throw new Error(`Could not load session from ${fileName}\n${err}`)
|
|
273
|
-
}
|
|
274
|
-
debug(`Loaded user session for ${name}`)
|
|
275
|
-
}
|
|
267
|
+
loadCookiesFromFile(config)
|
|
276
268
|
}
|
|
277
269
|
|
|
278
|
-
const loginFunction = async
|
|
279
|
-
const userSession = config.users[name]
|
|
270
|
+
const loginFunction = async name => {
|
|
280
271
|
const I = container.support('I')
|
|
272
|
+
const userSession = config.users[name]
|
|
273
|
+
|
|
274
|
+
if (!userSession) {
|
|
275
|
+
throw new Error(`User '${name}' was not configured for authorization in auth plugin. Add it to the plugin config`)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const test = store.currentTest
|
|
279
|
+
|
|
280
|
+
// we are in BeforeSuite hook
|
|
281
|
+
if (!test) {
|
|
282
|
+
enableAuthBeforeEachTest(name)
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const section = new Section(`I am logged in as ${name}`)
|
|
287
|
+
|
|
288
|
+
if (config.saveToFile && !store[`${name}_session`]) {
|
|
289
|
+
loadCookiesFromFile(config)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (isPlaywrightSession() && test?.opts?.cookies) {
|
|
293
|
+
if (test.opts.user == name) {
|
|
294
|
+
output.debug(`Cookies already loaded for ${name}`)
|
|
295
|
+
|
|
296
|
+
alreadyLoggedIn(name)
|
|
297
|
+
return
|
|
298
|
+
} else {
|
|
299
|
+
output.debug(`Cookies already loaded for ${test.opts.user}, but not for ${name}`)
|
|
300
|
+
await I.deleteCookie()
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
section.start()
|
|
305
|
+
|
|
281
306
|
const cookies = store[`${name}_session`]
|
|
282
|
-
const shouldAwait =
|
|
283
|
-
isAsyncFunction(userSession.login) || isAsyncFunction(userSession.restore) || isAsyncFunction(userSession.check)
|
|
307
|
+
const shouldAwait = isAsyncFunction(userSession.login) || isAsyncFunction(userSession.restore) || isAsyncFunction(userSession.check)
|
|
284
308
|
|
|
285
309
|
const loginAndSave = async () => {
|
|
286
310
|
if (shouldAwait) {
|
|
@@ -289,13 +313,14 @@ module.exports = function (config) {
|
|
|
289
313
|
userSession.login(I)
|
|
290
314
|
}
|
|
291
315
|
|
|
316
|
+
section.end()
|
|
292
317
|
const cookies = await userSession.fetch(I)
|
|
293
318
|
if (!cookies) {
|
|
294
|
-
debug("Cannot save user session with empty cookies from auto login's fetch method")
|
|
319
|
+
output.debug("Cannot save user session with empty cookies from auto login's fetch method")
|
|
295
320
|
return
|
|
296
321
|
}
|
|
297
322
|
if (config.saveToFile) {
|
|
298
|
-
debug(`Saved user session into file for ${name}`)
|
|
323
|
+
output.debug(`Saved user session into file for ${name}`)
|
|
299
324
|
fs.writeFileSync(path.join(global.output_dir, `${name}_session.json`), JSON.stringify(cookies))
|
|
300
325
|
}
|
|
301
326
|
store[`${name}_session`] = cookies
|
|
@@ -311,18 +336,20 @@ module.exports = function (config) {
|
|
|
311
336
|
userSession.restore(I, cookies)
|
|
312
337
|
userSession.check(I, cookies)
|
|
313
338
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
debug(
|
|
339
|
+
section.end()
|
|
340
|
+
recorder.session.catch(err => {
|
|
341
|
+
output.debug(`Failed auto login for ${name} due to ${err}`)
|
|
342
|
+
output.debug('Logging in again')
|
|
317
343
|
recorder.session.start('auto login')
|
|
318
344
|
return loginAndSave()
|
|
319
345
|
.then(() => {
|
|
320
346
|
recorder.add(() => recorder.session.restore('auto login'))
|
|
321
|
-
recorder.catch(() => debug('continue'))
|
|
347
|
+
recorder.catch(() => output.debug('continue'))
|
|
322
348
|
})
|
|
323
|
-
.catch(
|
|
349
|
+
.catch(err => {
|
|
324
350
|
recorder.session.restore('auto login')
|
|
325
351
|
recorder.session.restore('check login')
|
|
352
|
+
section.end()
|
|
326
353
|
recorder.throw(err)
|
|
327
354
|
})
|
|
328
355
|
})
|
|
@@ -333,8 +360,75 @@ module.exports = function (config) {
|
|
|
333
360
|
return recorder.promise()
|
|
334
361
|
}
|
|
335
362
|
|
|
363
|
+
function enableAuthBeforeEachTest(name) {
|
|
364
|
+
const suite = store.currentSuite
|
|
365
|
+
if (!suite) return
|
|
366
|
+
|
|
367
|
+
output.debug(`enabling auth as ${name} for each test of suite ${suite.title}`)
|
|
368
|
+
|
|
369
|
+
// we are setting test opts so they can be picked up by Playwright if it starts browser for this test
|
|
370
|
+
suite.eachTest(test => {
|
|
371
|
+
// preload from store
|
|
372
|
+
if (store[`${name}_session`]) {
|
|
373
|
+
test.opts.cookies = store[`${name}_session`]
|
|
374
|
+
test.opts.user = name
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (!config.saveToFile) return
|
|
379
|
+
const cookieFile = path.join(global.output_dir, `${name}_session.json`)
|
|
380
|
+
|
|
381
|
+
if (!fileExists(cookieFile)) {
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const context = fs.readFileSync(cookieFile).toString()
|
|
386
|
+
test.opts.cookies = JSON.parse(context)
|
|
387
|
+
test.opts.user = name
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
function runLoginFunctionForTest(test) {
|
|
391
|
+
if (!suite.tests.includes(test)) return
|
|
392
|
+
// let's call this function to ensure that authorization happened
|
|
393
|
+
// if no cookies, it will login and save them
|
|
394
|
+
loginFunction(name)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// we are in BeforeSuite hook
|
|
398
|
+
event.dispatcher.on(event.test.started, runLoginFunctionForTest)
|
|
399
|
+
event.dispatcher.on(event.suite.after, () => {
|
|
400
|
+
event.dispatcher.off(event.test.started, runLoginFunctionForTest)
|
|
401
|
+
})
|
|
402
|
+
}
|
|
403
|
+
|
|
336
404
|
// adding this to DI container
|
|
337
405
|
const support = {}
|
|
338
406
|
support[config.inject] = loginFunction
|
|
339
407
|
container.append({ support })
|
|
408
|
+
|
|
409
|
+
return loginFunction
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function loadCookiesFromFile(config) {
|
|
413
|
+
for (const name in config.users) {
|
|
414
|
+
const fileName = path.join(global.output_dir, `${name}_session.json`)
|
|
415
|
+
if (!fileExists(fileName)) continue
|
|
416
|
+
const data = fs.readFileSync(fileName).toString()
|
|
417
|
+
try {
|
|
418
|
+
store[`${name}_session`] = JSON.parse(data)
|
|
419
|
+
} catch (err) {
|
|
420
|
+
throw new Error(`Could not load session from ${fileName}\n${err}`)
|
|
421
|
+
}
|
|
422
|
+
output.debug(`Loaded user session for ${name}`)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function isPlaywrightSession() {
|
|
427
|
+
return !!container.helpers('Playwright')
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function alreadyLoggedIn(name) {
|
|
431
|
+
const step = new CommentStep('am logged in as')
|
|
432
|
+
step.actor = 'I'
|
|
433
|
+
return step.addToRecorder([name])
|
|
340
434
|
}
|