codeceptjs 4.0.0-beta.2 → 4.0.0-beta.20

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.
Files changed (209) hide show
  1. package/README.md +133 -120
  2. package/bin/codecept.js +107 -96
  3. package/bin/test-server.js +64 -0
  4. package/docs/webapi/clearCookie.mustache +1 -1
  5. package/docs/webapi/click.mustache +5 -1
  6. package/lib/actor.js +71 -103
  7. package/lib/ai.js +159 -188
  8. package/lib/assert/empty.js +22 -24
  9. package/lib/assert/equal.js +30 -37
  10. package/lib/assert/error.js +14 -14
  11. package/lib/assert/include.js +43 -48
  12. package/lib/assert/throws.js +11 -11
  13. package/lib/assert/truth.js +22 -22
  14. package/lib/assert.js +20 -18
  15. package/lib/codecept.js +262 -162
  16. package/lib/colorUtils.js +50 -52
  17. package/lib/command/check.js +206 -0
  18. package/lib/command/configMigrate.js +56 -51
  19. package/lib/command/definitions.js +96 -109
  20. package/lib/command/dryRun.js +77 -79
  21. package/lib/command/generate.js +234 -194
  22. package/lib/command/gherkin/init.js +42 -33
  23. package/lib/command/gherkin/snippets.js +76 -74
  24. package/lib/command/gherkin/steps.js +20 -17
  25. package/lib/command/info.js +74 -38
  26. package/lib/command/init.js +301 -290
  27. package/lib/command/interactive.js +41 -32
  28. package/lib/command/list.js +28 -27
  29. package/lib/command/run-multiple/chunk.js +51 -48
  30. package/lib/command/run-multiple/collection.js +5 -5
  31. package/lib/command/run-multiple/run.js +5 -1
  32. package/lib/command/run-multiple.js +97 -97
  33. package/lib/command/run-rerun.js +19 -25
  34. package/lib/command/run-workers.js +68 -92
  35. package/lib/command/run.js +39 -27
  36. package/lib/command/utils.js +80 -64
  37. package/lib/command/workers/runTests.js +388 -226
  38. package/lib/config.js +109 -50
  39. package/lib/container.js +641 -261
  40. package/lib/data/context.js +60 -61
  41. package/lib/data/dataScenarioConfig.js +47 -47
  42. package/lib/data/dataTableArgument.js +32 -32
  43. package/lib/data/table.js +22 -22
  44. package/lib/effects.js +307 -0
  45. package/lib/element/WebElement.js +327 -0
  46. package/lib/els.js +160 -0
  47. package/lib/event.js +173 -163
  48. package/lib/globals.js +141 -0
  49. package/lib/heal.js +89 -85
  50. package/lib/helper/AI.js +131 -41
  51. package/lib/helper/ApiDataFactory.js +107 -75
  52. package/lib/helper/Appium.js +542 -404
  53. package/lib/helper/FileSystem.js +100 -79
  54. package/lib/helper/GraphQL.js +44 -43
  55. package/lib/helper/GraphQLDataFactory.js +52 -52
  56. package/lib/helper/JSONResponse.js +126 -88
  57. package/lib/helper/Mochawesome.js +54 -29
  58. package/lib/helper/Playwright.js +2547 -1316
  59. package/lib/helper/Puppeteer.js +1578 -1181
  60. package/lib/helper/REST.js +209 -68
  61. package/lib/helper/WebDriver.js +1482 -1342
  62. package/lib/helper/errors/ConnectionRefused.js +6 -6
  63. package/lib/helper/errors/ElementAssertion.js +11 -16
  64. package/lib/helper/errors/ElementNotFound.js +5 -9
  65. package/lib/helper/errors/RemoteBrowserConnectionRefused.js +5 -5
  66. package/lib/helper/extras/Console.js +11 -11
  67. package/lib/helper/extras/PlaywrightLocator.js +110 -0
  68. package/lib/helper/extras/PlaywrightPropEngine.js +18 -18
  69. package/lib/helper/extras/PlaywrightReactVueLocator.js +17 -8
  70. package/lib/helper/extras/PlaywrightRestartOpts.js +25 -11
  71. package/lib/helper/extras/Popup.js +22 -22
  72. package/lib/helper/extras/React.js +27 -28
  73. package/lib/helper/network/actions.js +36 -42
  74. package/lib/helper/network/utils.js +78 -84
  75. package/lib/helper/scripts/blurElement.js +5 -5
  76. package/lib/helper/scripts/focusElement.js +5 -5
  77. package/lib/helper/scripts/highlightElement.js +8 -8
  78. package/lib/helper/scripts/isElementClickable.js +34 -34
  79. package/lib/helper.js +2 -3
  80. package/lib/history.js +23 -19
  81. package/lib/hooks.js +8 -8
  82. package/lib/html.js +94 -104
  83. package/lib/index.js +38 -27
  84. package/lib/listener/config.js +30 -23
  85. package/lib/listener/emptyRun.js +54 -0
  86. package/lib/listener/enhancedGlobalRetry.js +110 -0
  87. package/lib/listener/exit.js +16 -18
  88. package/lib/listener/globalRetry.js +70 -0
  89. package/lib/listener/globalTimeout.js +181 -0
  90. package/lib/listener/helpers.js +76 -51
  91. package/lib/listener/mocha.js +10 -11
  92. package/lib/listener/result.js +11 -0
  93. package/lib/listener/retryEnhancer.js +85 -0
  94. package/lib/listener/steps.js +71 -59
  95. package/lib/listener/store.js +20 -0
  96. package/lib/locator.js +214 -197
  97. package/lib/mocha/asyncWrapper.js +274 -0
  98. package/lib/mocha/bdd.js +167 -0
  99. package/lib/mocha/cli.js +341 -0
  100. package/lib/mocha/factory.js +163 -0
  101. package/lib/mocha/featureConfig.js +89 -0
  102. package/lib/mocha/gherkin.js +231 -0
  103. package/lib/mocha/hooks.js +121 -0
  104. package/lib/mocha/index.js +21 -0
  105. package/lib/mocha/inject.js +46 -0
  106. package/lib/{interfaces → mocha}/scenarioConfig.js +58 -34
  107. package/lib/mocha/suite.js +89 -0
  108. package/lib/mocha/test.js +184 -0
  109. package/lib/mocha/types.d.ts +42 -0
  110. package/lib/mocha/ui.js +242 -0
  111. package/lib/output.js +141 -71
  112. package/lib/parser.js +47 -44
  113. package/lib/pause.js +173 -145
  114. package/lib/plugin/analyze.js +403 -0
  115. package/lib/plugin/{autoLogin.js → auth.js} +178 -79
  116. package/lib/plugin/autoDelay.js +36 -40
  117. package/lib/plugin/coverage.js +131 -78
  118. package/lib/plugin/customLocator.js +22 -21
  119. package/lib/plugin/customReporter.js +53 -0
  120. package/lib/plugin/enhancedRetryFailedStep.js +99 -0
  121. package/lib/plugin/heal.js +101 -110
  122. package/lib/plugin/htmlReporter.js +3648 -0
  123. package/lib/plugin/pageInfo.js +140 -0
  124. package/lib/plugin/pauseOnFail.js +12 -11
  125. package/lib/plugin/retryFailedStep.js +82 -47
  126. package/lib/plugin/screenshotOnFail.js +111 -92
  127. package/lib/plugin/stepByStepReport.js +159 -101
  128. package/lib/plugin/stepTimeout.js +20 -25
  129. package/lib/plugin/subtitles.js +38 -38
  130. package/lib/recorder.js +193 -130
  131. package/lib/rerun.js +94 -49
  132. package/lib/result.js +238 -0
  133. package/lib/retryCoordinator.js +207 -0
  134. package/lib/secret.js +20 -18
  135. package/lib/session.js +95 -89
  136. package/lib/step/base.js +239 -0
  137. package/lib/step/comment.js +10 -0
  138. package/lib/step/config.js +50 -0
  139. package/lib/step/func.js +46 -0
  140. package/lib/step/helper.js +50 -0
  141. package/lib/step/meta.js +99 -0
  142. package/lib/step/record.js +74 -0
  143. package/lib/step/retry.js +11 -0
  144. package/lib/step/section.js +55 -0
  145. package/lib/step.js +18 -329
  146. package/lib/steps.js +54 -0
  147. package/lib/store.js +38 -7
  148. package/lib/template/heal.js +3 -12
  149. package/lib/template/prompts/generatePageObject.js +31 -0
  150. package/lib/template/prompts/healStep.js +13 -0
  151. package/lib/template/prompts/writeStep.js +9 -0
  152. package/lib/test-server.js +334 -0
  153. package/lib/timeout.js +60 -0
  154. package/lib/transform.js +8 -8
  155. package/lib/translation.js +34 -21
  156. package/lib/utils/loaderCheck.js +124 -0
  157. package/lib/utils/mask_data.js +47 -0
  158. package/lib/utils/typescript.js +237 -0
  159. package/lib/utils.js +411 -228
  160. package/lib/workerStorage.js +37 -34
  161. package/lib/workers.js +532 -296
  162. package/package.json +124 -95
  163. package/translations/de-DE.js +5 -3
  164. package/translations/fr-FR.js +5 -4
  165. package/translations/index.js +22 -12
  166. package/translations/it-IT.js +4 -3
  167. package/translations/ja-JP.js +4 -3
  168. package/translations/nl-NL.js +76 -0
  169. package/translations/pl-PL.js +4 -3
  170. package/translations/pt-BR.js +4 -3
  171. package/translations/ru-RU.js +4 -3
  172. package/translations/utils.js +10 -0
  173. package/translations/zh-CN.js +4 -3
  174. package/translations/zh-TW.js +4 -3
  175. package/typings/index.d.ts +546 -185
  176. package/typings/promiseBasedTypes.d.ts +150 -875
  177. package/typings/types.d.ts +547 -992
  178. package/lib/cli.js +0 -249
  179. package/lib/dirname.js +0 -5
  180. package/lib/helper/Expect.js +0 -425
  181. package/lib/helper/ExpectHelper.js +0 -399
  182. package/lib/helper/MockServer.js +0 -223
  183. package/lib/helper/Nightmare.js +0 -1411
  184. package/lib/helper/Protractor.js +0 -1835
  185. package/lib/helper/SoftExpectHelper.js +0 -381
  186. package/lib/helper/TestCafe.js +0 -1410
  187. package/lib/helper/clientscripts/nightmare.js +0 -213
  188. package/lib/helper/testcafe/testControllerHolder.js +0 -42
  189. package/lib/helper/testcafe/testcafe-utils.js +0 -63
  190. package/lib/interfaces/bdd.js +0 -98
  191. package/lib/interfaces/featureConfig.js +0 -69
  192. package/lib/interfaces/gherkin.js +0 -195
  193. package/lib/listener/artifacts.js +0 -19
  194. package/lib/listener/retry.js +0 -68
  195. package/lib/listener/timeout.js +0 -109
  196. package/lib/mochaFactory.js +0 -110
  197. package/lib/plugin/allure.js +0 -15
  198. package/lib/plugin/commentStep.js +0 -136
  199. package/lib/plugin/debugErrors.js +0 -67
  200. package/lib/plugin/eachElement.js +0 -127
  201. package/lib/plugin/fakerTransform.js +0 -49
  202. package/lib/plugin/retryTo.js +0 -121
  203. package/lib/plugin/selenoid.js +0 -371
  204. package/lib/plugin/standardActingHelpers.js +0 -9
  205. package/lib/plugin/tryTo.js +0 -105
  206. package/lib/plugin/wdio.js +0 -246
  207. package/lib/scenario.js +0 -222
  208. package/lib/ui.js +0 -238
  209. 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
+ }