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.
Files changed (188) hide show
  1. package/README.md +89 -119
  2. package/bin/codecept.js +53 -54
  3. package/docs/webapi/clearCookie.mustache +1 -1
  4. package/lib/actor.js +70 -102
  5. package/lib/ai.js +131 -121
  6. package/lib/assert/empty.js +11 -12
  7. package/lib/assert/equal.js +16 -21
  8. package/lib/assert/error.js +2 -2
  9. package/lib/assert/include.js +11 -15
  10. package/lib/assert/throws.js +3 -5
  11. package/lib/assert/truth.js +10 -7
  12. package/lib/assert.js +18 -18
  13. package/lib/codecept.js +112 -101
  14. package/lib/colorUtils.js +48 -50
  15. package/lib/command/check.js +206 -0
  16. package/lib/command/configMigrate.js +13 -14
  17. package/lib/command/definitions.js +24 -36
  18. package/lib/command/dryRun.js +16 -16
  19. package/lib/command/generate.js +38 -39
  20. package/lib/command/gherkin/init.js +36 -38
  21. package/lib/command/gherkin/snippets.js +76 -74
  22. package/lib/command/gherkin/steps.js +21 -18
  23. package/lib/command/info.js +49 -15
  24. package/lib/command/init.js +41 -37
  25. package/lib/command/interactive.js +22 -13
  26. package/lib/command/list.js +11 -10
  27. package/lib/command/run-multiple/chunk.js +50 -47
  28. package/lib/command/run-multiple/collection.js +5 -5
  29. package/lib/command/run-multiple/run.js +3 -3
  30. package/lib/command/run-multiple.js +27 -47
  31. package/lib/command/run-rerun.js +6 -7
  32. package/lib/command/run-workers.js +15 -66
  33. package/lib/command/run.js +8 -8
  34. package/lib/command/utils.js +22 -21
  35. package/lib/command/workers/runTests.js +131 -241
  36. package/lib/config.js +111 -49
  37. package/lib/container.js +589 -244
  38. package/lib/data/context.js +16 -18
  39. package/lib/data/dataScenarioConfig.js +9 -9
  40. package/lib/data/dataTableArgument.js +7 -7
  41. package/lib/data/table.js +6 -12
  42. package/lib/effects.js +307 -0
  43. package/lib/els.js +160 -0
  44. package/lib/event.js +24 -19
  45. package/lib/globals.js +141 -0
  46. package/lib/heal.js +89 -81
  47. package/lib/helper/AI.js +3 -2
  48. package/lib/helper/ApiDataFactory.js +19 -19
  49. package/lib/helper/Appium.js +47 -51
  50. package/lib/helper/FileSystem.js +35 -15
  51. package/lib/helper/GraphQL.js +1 -1
  52. package/lib/helper/GraphQLDataFactory.js +4 -4
  53. package/lib/helper/JSONResponse.js +72 -45
  54. package/lib/helper/Mochawesome.js +14 -11
  55. package/lib/helper/Playwright.js +832 -434
  56. package/lib/helper/Puppeteer.js +393 -292
  57. package/lib/helper/REST.js +32 -27
  58. package/lib/helper/WebDriver.js +320 -219
  59. package/lib/helper/errors/ConnectionRefused.js +6 -6
  60. package/lib/helper/errors/ElementAssertion.js +11 -16
  61. package/lib/helper/errors/ElementNotFound.js +5 -9
  62. package/lib/helper/errors/RemoteBrowserConnectionRefused.js +5 -5
  63. package/lib/helper/extras/Console.js +11 -11
  64. package/lib/helper/extras/PlaywrightLocator.js +110 -0
  65. package/lib/helper/extras/PlaywrightPropEngine.js +18 -18
  66. package/lib/helper/extras/PlaywrightRestartOpts.js +23 -23
  67. package/lib/helper/extras/Popup.js +22 -22
  68. package/lib/helper/extras/React.js +29 -30
  69. package/lib/helper/network/actions.js +33 -48
  70. package/lib/helper/network/utils.js +76 -83
  71. package/lib/helper/scripts/blurElement.js +6 -6
  72. package/lib/helper/scripts/focusElement.js +6 -6
  73. package/lib/helper/scripts/highlightElement.js +9 -9
  74. package/lib/helper/scripts/isElementClickable.js +34 -34
  75. package/lib/helper.js +2 -1
  76. package/lib/history.js +23 -20
  77. package/lib/hooks.js +10 -10
  78. package/lib/html.js +90 -100
  79. package/lib/index.js +48 -21
  80. package/lib/listener/config.js +8 -9
  81. package/lib/listener/emptyRun.js +54 -0
  82. package/lib/listener/exit.js +10 -12
  83. package/lib/listener/{retry.js → globalRetry.js} +10 -10
  84. package/lib/listener/globalTimeout.js +166 -0
  85. package/lib/listener/helpers.js +43 -24
  86. package/lib/listener/mocha.js +4 -5
  87. package/lib/listener/result.js +11 -0
  88. package/lib/listener/steps.js +26 -23
  89. package/lib/listener/store.js +20 -0
  90. package/lib/locator.js +213 -192
  91. package/lib/mocha/asyncWrapper.js +264 -0
  92. package/lib/mocha/bdd.js +167 -0
  93. package/lib/mocha/cli.js +341 -0
  94. package/lib/mocha/factory.js +160 -0
  95. package/lib/{interfaces → mocha}/featureConfig.js +33 -13
  96. package/lib/{interfaces → mocha}/gherkin.js +75 -45
  97. package/lib/mocha/hooks.js +121 -0
  98. package/lib/mocha/index.js +21 -0
  99. package/lib/mocha/inject.js +46 -0
  100. package/lib/{interfaces → mocha}/scenarioConfig.js +32 -8
  101. package/lib/mocha/suite.js +89 -0
  102. package/lib/mocha/test.js +178 -0
  103. package/lib/mocha/types.d.ts +42 -0
  104. package/lib/mocha/ui.js +229 -0
  105. package/lib/output.js +86 -64
  106. package/lib/parser.js +44 -44
  107. package/lib/pause.js +160 -139
  108. package/lib/plugin/analyze.js +403 -0
  109. package/lib/plugin/{autoLogin.js → auth.js} +137 -43
  110. package/lib/plugin/autoDelay.js +19 -15
  111. package/lib/plugin/coverage.js +22 -27
  112. package/lib/plugin/customLocator.js +5 -5
  113. package/lib/plugin/customReporter.js +53 -0
  114. package/lib/plugin/heal.js +49 -17
  115. package/lib/plugin/pageInfo.js +140 -0
  116. package/lib/plugin/pauseOnFail.js +4 -3
  117. package/lib/plugin/retryFailedStep.js +60 -19
  118. package/lib/plugin/screenshotOnFail.js +80 -83
  119. package/lib/plugin/stepByStepReport.js +70 -31
  120. package/lib/plugin/stepTimeout.js +7 -13
  121. package/lib/plugin/subtitles.js +10 -9
  122. package/lib/recorder.js +167 -126
  123. package/lib/rerun.js +94 -50
  124. package/lib/result.js +161 -0
  125. package/lib/secret.js +18 -17
  126. package/lib/session.js +95 -89
  127. package/lib/step/base.js +239 -0
  128. package/lib/step/comment.js +10 -0
  129. package/lib/step/config.js +50 -0
  130. package/lib/step/func.js +46 -0
  131. package/lib/step/helper.js +50 -0
  132. package/lib/step/meta.js +99 -0
  133. package/lib/step/record.js +74 -0
  134. package/lib/step/retry.js +11 -0
  135. package/lib/step/section.js +55 -0
  136. package/lib/step.js +18 -332
  137. package/lib/steps.js +54 -0
  138. package/lib/store.js +37 -5
  139. package/lib/template/heal.js +2 -11
  140. package/lib/timeout.js +60 -0
  141. package/lib/transform.js +8 -8
  142. package/lib/translation.js +32 -18
  143. package/lib/utils.js +354 -250
  144. package/lib/workerStorage.js +16 -16
  145. package/lib/workers.js +366 -282
  146. package/package.json +107 -95
  147. package/translations/de-DE.js +5 -4
  148. package/translations/fr-FR.js +5 -4
  149. package/translations/index.js +23 -9
  150. package/translations/it-IT.js +5 -4
  151. package/translations/ja-JP.js +5 -4
  152. package/translations/nl-NL.js +76 -0
  153. package/translations/pl-PL.js +5 -4
  154. package/translations/pt-BR.js +5 -4
  155. package/translations/ru-RU.js +5 -4
  156. package/translations/utils.js +18 -0
  157. package/translations/zh-CN.js +5 -4
  158. package/translations/zh-TW.js +5 -4
  159. package/typings/index.d.ts +177 -186
  160. package/typings/promiseBasedTypes.d.ts +3573 -5941
  161. package/typings/types.d.ts +4042 -6370
  162. package/lib/cli.js +0 -256
  163. package/lib/helper/ExpectHelper.js +0 -391
  164. package/lib/helper/Nightmare.js +0 -1504
  165. package/lib/helper/Protractor.js +0 -1863
  166. package/lib/helper/SoftExpectHelper.js +0 -381
  167. package/lib/helper/TestCafe.js +0 -1414
  168. package/lib/helper/clientscripts/nightmare.js +0 -213
  169. package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -43
  170. package/lib/helper/testcafe/testControllerHolder.js +0 -42
  171. package/lib/helper/testcafe/testcafe-utils.js +0 -62
  172. package/lib/interfaces/bdd.js +0 -81
  173. package/lib/listener/artifacts.js +0 -19
  174. package/lib/listener/timeout.js +0 -109
  175. package/lib/mochaFactory.js +0 -113
  176. package/lib/plugin/allure.js +0 -15
  177. package/lib/plugin/commentStep.js +0 -136
  178. package/lib/plugin/debugErrors.js +0 -67
  179. package/lib/plugin/eachElement.js +0 -127
  180. package/lib/plugin/fakerTransform.js +0 -49
  181. package/lib/plugin/retryTo.js +0 -127
  182. package/lib/plugin/selenoid.js +0 -384
  183. package/lib/plugin/standardActingHelpers.js +0 -3
  184. package/lib/plugin/tryTo.js +0 -115
  185. package/lib/plugin/wdio.js +0 -249
  186. package/lib/scenario.js +0 -224
  187. package/lib/ui.js +0 -236
  188. 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
- const fs = require('fs')
2
- const path = require('path')
3
- const { fileExists } = require('../utils')
4
- const container = require('../container')
5
- const store = require('../store')
6
- const recorder = require('../recorder')
7
- const { debug } = require('../output')
8
- const isAsyncFunction = require('../utils').isAsyncFunction
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: (I) => I.grabCookie(),
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
- * autoLogin: {
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
- * autoLogin: {
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
- * autoLogin: {
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
- * autoLogin: {
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 autoLogin
188
+ * #### Tips: Using async function in the auth
187
189
  *
188
- * If you use async functions in the autoLogin plugin, login function should be used with `await` keyword.
190
+ * If you use async functions in the auth plugin, login function should be used with `await` keyword.
189
191
  *
190
192
  * ```js
191
- * autoLogin: {
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 autoLogin plugin
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
- * autoLogin: {
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 autoLogin plugin
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
- module.exports = function (config) {
255
+ export default function (config) {
254
256
  config = Object.assign(defaultConfig, config)
255
257
  Object.keys(config.users).map(
256
- (u) =>
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
- for (const name in config.users) {
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 (name) => {
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
- recorder.session.catch((err) => {
315
- debug(`Failed auto login for ${name} due to ${err}`)
316
- debug('Logging in again')
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((err) => {
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
  }