codeceptjs 4.0.0-beta.6.esm-aria → 4.0.0-beta.8.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 (69) hide show
  1. package/README.md +46 -3
  2. package/bin/codecept.js +9 -0
  3. package/bin/test-server.js +64 -0
  4. package/docs/webapi/click.mustache +5 -1
  5. package/lib/ai.js +66 -102
  6. package/lib/codecept.js +99 -24
  7. package/lib/command/generate.js +33 -1
  8. package/lib/command/init.js +7 -3
  9. package/lib/command/run-workers.js +31 -2
  10. package/lib/command/run.js +15 -0
  11. package/lib/command/workers/runTests.js +331 -58
  12. package/lib/config.js +16 -5
  13. package/lib/container.js +15 -13
  14. package/lib/effects.js +1 -1
  15. package/lib/element/WebElement.js +327 -0
  16. package/lib/event.js +10 -1
  17. package/lib/helper/AI.js +11 -11
  18. package/lib/helper/ApiDataFactory.js +34 -6
  19. package/lib/helper/Appium.js +156 -42
  20. package/lib/helper/GraphQL.js +3 -3
  21. package/lib/helper/GraphQLDataFactory.js +4 -4
  22. package/lib/helper/JSONResponse.js +48 -40
  23. package/lib/helper/Mochawesome.js +24 -2
  24. package/lib/helper/Playwright.js +841 -153
  25. package/lib/helper/Puppeteer.js +263 -67
  26. package/lib/helper/REST.js +21 -0
  27. package/lib/helper/WebDriver.js +116 -26
  28. package/lib/helper/clientscripts/PollyWebDriverExt.js +1 -1
  29. package/lib/helper/extras/PlaywrightReactVueLocator.js +52 -0
  30. package/lib/helper/extras/PlaywrightRestartOpts.js +12 -1
  31. package/lib/helper/network/actions.js +8 -6
  32. package/lib/listener/config.js +11 -3
  33. package/lib/listener/enhancedGlobalRetry.js +110 -0
  34. package/lib/listener/globalTimeout.js +19 -4
  35. package/lib/listener/helpers.js +8 -2
  36. package/lib/listener/retryEnhancer.js +85 -0
  37. package/lib/listener/steps.js +12 -0
  38. package/lib/mocha/asyncWrapper.js +13 -3
  39. package/lib/mocha/cli.js +1 -1
  40. package/lib/mocha/factory.js +3 -0
  41. package/lib/mocha/gherkin.js +1 -1
  42. package/lib/mocha/test.js +6 -0
  43. package/lib/mocha/ui.js +13 -0
  44. package/lib/output.js +62 -18
  45. package/lib/plugin/coverage.js +16 -3
  46. package/lib/plugin/enhancedRetryFailedStep.js +99 -0
  47. package/lib/plugin/htmlReporter.js +3648 -0
  48. package/lib/plugin/retryFailedStep.js +1 -0
  49. package/lib/plugin/stepByStepReport.js +1 -1
  50. package/lib/recorder.js +28 -3
  51. package/lib/result.js +100 -23
  52. package/lib/retryCoordinator.js +207 -0
  53. package/lib/step/base.js +1 -1
  54. package/lib/step/comment.js +2 -2
  55. package/lib/step/meta.js +1 -1
  56. package/lib/template/heal.js +1 -1
  57. package/lib/template/prompts/generatePageObject.js +31 -0
  58. package/lib/template/prompts/healStep.js +13 -0
  59. package/lib/template/prompts/writeStep.js +9 -0
  60. package/lib/test-server.js +334 -0
  61. package/lib/utils/mask_data.js +47 -0
  62. package/lib/utils.js +87 -6
  63. package/lib/workerStorage.js +2 -1
  64. package/lib/workers.js +179 -23
  65. package/package.json +60 -52
  66. package/translations/utils.js +2 -10
  67. package/typings/index.d.ts +19 -7
  68. package/typings/promiseBasedTypes.d.ts +5525 -3759
  69. package/typings/types.d.ts +5791 -3781
package/README.md CHANGED
@@ -10,7 +10,6 @@
10
10
  | 🌐 Web | Playwright | [![Playwright Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/playwright.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/playwright.yml) |
11
11
  | 🌐 Web | Puppeteer | [![Puppeteer Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/puppeteer.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/puppeteer.yml) |
12
12
  | 🌐 Web | WebDriver | [![WebDriver Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/webdriver.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/webdriver.yml) |
13
- | 🌐 Web | TestCafe | [![TestCafe Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/testcafe.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/testcafe.yml) |
14
13
  | 📱 Mobile | Appium | [![Appium Tests - Android](https://github.com/codeceptjs/CodeceptJS/actions/workflows/appium_Android.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/appium_Android.yml) |
15
14
 
16
15
  # CodeceptJS [![Made in Ukraine](https://img.shields.io/badge/made_in-ukraine-ffd700.svg?labelColor=0057b7)](https://stand-with-ukraine.pp.ua)
@@ -43,7 +42,6 @@ CodeceptJS uses **Helper** modules to provide actions to `I` object. Currently,
43
42
  - [**Playwright**](https://github.com/codeceptjs/CodeceptJS/blob/master/docs/helpers/Playwright.md) - is a Node library to automate the Chromium, WebKit and Firefox browsers with a single API.
44
43
  - [**Puppeteer**](https://github.com/codeceptjs/CodeceptJS/blob/master/docs/helpers/Puppeteer.md) - uses Google Chrome's Puppeteer for fast headless testing.
45
44
  - [**WebDriver**](https://github.com/codeceptjs/CodeceptJS/blob/master/docs/helpers/WebDriver.md) - uses [webdriverio](http://webdriver.io/) to run tests via WebDriver or Devtools protocol.
46
- - [**TestCafe**](https://github.com/codeceptjs/CodeceptJS/blob/master/docs/helpers/TestCafe.md) - cheap and fast cross-browser test automation.
47
45
  - [**Appium**](https://github.com/codeceptjs/CodeceptJS/blob/master/docs/helpers/Appium.md) - for **mobile testing** with Appium
48
46
  - [**Detox**](https://github.com/codeceptjs/CodeceptJS/blob/master/docs/helpers/Detox.md) - This is a wrapper on top of Detox library, aimed to unify testing experience for CodeceptJS framework. Detox provides a grey box testing for mobile applications, playing especially well for React Native apps.
49
47
 
@@ -53,7 +51,7 @@ And more to come...
53
51
 
54
52
  CodeceptJS is a successor of [Codeception](http://codeception.com), a popular full-stack testing framework for PHP.
55
53
  With CodeceptJS your scenario-driven functional and acceptance tests will be as simple and clean as they can be.
56
- You don't need to worry about asynchronous nature of NodeJS or about various APIs of Playwright, Selenium, Puppeteer, TestCafe, etc. as CodeceptJS unifies them and makes them work as they are synchronous.
54
+ You don't need to worry about asynchronous nature of NodeJS or about various APIs of Playwright, Selenium, Puppeteer, etc. as CodeceptJS unifies them and makes them work as they are synchronous.
57
55
 
58
56
  ## Features
59
57
 
@@ -64,6 +62,8 @@ You don't need to worry about asynchronous nature of NodeJS or about various API
64
62
  - Also plays nice with TypeScript.
65
63
  - </> Smart locators: use names, labels, matching text, CSS or XPath to locate elements.
66
64
  - 🌐 Interactive debugging shell: pause test at any point and try different commands in a browser.
65
+ - ⚡ **Parallel testing** with dynamic test pooling for optimal load balancing and performance.
66
+ - 📊 **Built-in HTML Reporter** with interactive dashboard, step-by-step execution details, and comprehensive test analytics.
67
67
  - Easily create tests, pageobjects, stepobjects with CLI generators.
68
68
 
69
69
  ## Installation
@@ -233,6 +233,49 @@ Scenario('test title', () => {
233
233
  })
234
234
  ```
235
235
 
236
+ ## HTML Reporter
237
+
238
+ CodeceptJS includes a powerful built-in HTML Reporter that generates comprehensive, interactive test reports with detailed information about your test runs. The HTML reporter is **enabled by default** for all new projects and provides:
239
+
240
+ ### Features
241
+
242
+ - **Interactive Dashboard**: Visual statistics, pie charts, and expandable test details
243
+ - **Step-by-Step Execution**: Shows individual test steps with timing and status indicators
244
+ - **BDD/Gherkin Support**: Full support for feature files with proper scenario formatting
245
+ - **System Information**: Comprehensive environment details including browser versions
246
+ - **Advanced Filtering**: Real-time filtering by status, tags, features, and test types
247
+ - **History Tracking**: Multi-run history with trend visualization
248
+ - **Error Details**: Clean formatting of error messages and stack traces
249
+ - **Artifacts Support**: Display screenshots and other test artifacts
250
+
251
+ ### Visual Examples
252
+
253
+ #### Interactive Test Dashboard
254
+
255
+ The main dashboard provides a complete overview with interactive statistics and pie charts:
256
+
257
+ ![HTML Reporter Dashboard](docs/shared/html-reporter-main-dashboard.png)
258
+
259
+ #### Detailed Test Results
260
+
261
+ Each test shows comprehensive execution details with expandable step information:
262
+
263
+ ![HTML Reporter Test Details](docs/shared/html-reporter-test-details.png)
264
+
265
+ #### Advanced Filtering Capabilities
266
+
267
+ Real-time filtering allows quick navigation through test results:
268
+
269
+ ![HTML Reporter Filtering](docs/shared/html-reporter-filtering.png)
270
+
271
+ #### BDD/Gherkin Support
272
+
273
+ Full support for Gherkin scenarios with proper feature formatting:
274
+
275
+ ![HTML Reporter BDD Details](docs/shared/html-reporter-bdd-details.png)
276
+
277
+ The HTML reporter generates self-contained reports that can be easily shared with your team. Learn more about configuration and features in the [HTML Reporter documentation](https://codecept.io/plugins/#htmlreporter).
278
+
236
279
  ## PageObjects
237
280
 
238
281
  CodeceptJS provides the most simple way to create and use page objects in your test.
package/bin/codecept.js CHANGED
@@ -141,6 +141,12 @@ program.command('generate:helper [path]').alias('gh').description('Generates a n
141
141
 
142
142
  program.command('generate:heal [path]').alias('gr').description('Generates basic heal recipes').action(commandHandlerWithProperty('../lib/command/generate.js', 'heal'))
143
143
 
144
+ program
145
+ .command('generate:prompt <promptName> [path]')
146
+ .alias('gp')
147
+ .description('Generates AI prompt template (writeStep, healStep, generatePageObject)')
148
+ .action(commandHandlerWithProperty('../lib/command/generate.js', 'prompt'))
149
+
144
150
  program
145
151
  .command('run [test]')
146
152
  .description('Executes tests')
@@ -157,6 +163,8 @@ program
157
163
  .option('--tests', 'run only JS test files and skip features')
158
164
  .option('--no-timeouts', 'disable all timeouts')
159
165
  .option('-p, --plugins <k=v,k2=v2,...>', 'enable plugins, comma-separated')
166
+ .option('--shuffle', 'Shuffle the order in which test files run')
167
+ .option('--shard <index/total>', 'run only a fraction of tests (e.g., --shard 1/4)')
160
168
 
161
169
  // mocha options
162
170
  .option('--colors', 'force enabling of colors')
@@ -188,6 +196,7 @@ program
188
196
  .option('-i, --invert', 'inverts --grep matches')
189
197
  .option('-o, --override [value]', 'override current config options')
190
198
  .option('--suites', 'parallel execution of suites not single tests')
199
+ .option('--by <strategy>', 'test distribution strategy: "test" (pre-assign individual tests), "suite" (pre-assign test suites), or "pool" (dynamic distribution for optimal load balancing, recommended)')
191
200
  .option(commandFlags.debug.flag, commandFlags.debug.description)
192
201
  .option(commandFlags.verbose.flag, commandFlags.verbose.description)
193
202
  .option('--features', 'run only *.feature files and skip tests')
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Standalone test server script to replace json-server
5
+ */
6
+
7
+ import path from 'path'
8
+ import { fileURLToPath } from 'url'
9
+ import { dirname } from 'path'
10
+ import TestServer from '../lib/test-server.js'
11
+
12
+ const __filename = fileURLToPath(import.meta.url)
13
+ const __dirname = dirname(__filename)
14
+
15
+ // Parse command line arguments
16
+ const args = process.argv.slice(2)
17
+ let dbFile = path.join(__dirname, '../test/data/rest/db.json')
18
+ let port = 8010
19
+ let host = '0.0.0.0'
20
+ let readOnly = false
21
+
22
+ // Simple argument parsing
23
+ for (let i = 0; i < args.length; i++) {
24
+ const arg = args[i]
25
+
26
+ if (arg === '-p' || arg === '--port') {
27
+ port = parseInt(args[++i])
28
+ } else if (arg === '--host') {
29
+ host = args[++i]
30
+ } else if (arg === '--read-only' || arg === '-r') {
31
+ readOnly = true
32
+ } else if (!arg.startsWith('-')) {
33
+ dbFile = path.resolve(arg)
34
+ }
35
+ }
36
+
37
+ // Create and start server
38
+ const server = new TestServer({ port, host, dbFile, readOnly })
39
+
40
+ console.log(`Starting test server with db file: ${dbFile}`)
41
+ if (readOnly) {
42
+ console.log('Running in READ-ONLY mode - changes will not be persisted to disk')
43
+ }
44
+
45
+ server
46
+ .start()
47
+ .then(() => {
48
+ console.log(`Test server is ready and listening on http://${host}:${port}`)
49
+ })
50
+ .catch(err => {
51
+ console.error('Failed to start test server:', err)
52
+ process.exit(1)
53
+ })
54
+
55
+ // Graceful shutdown
56
+ process.on('SIGINT', () => {
57
+ console.log('\nShutting down test server...')
58
+ server.stop().then(() => process.exit(0))
59
+ })
60
+
61
+ process.on('SIGTERM', () => {
62
+ console.log('\nShutting down test server...')
63
+ server.stop().then(() => process.exit(0))
64
+ })
@@ -3,9 +3,13 @@ If a fuzzy locator is given, the page will be searched for a button, link, or im
3
3
  For buttons, the "value" attribute, "name" attribute, and inner text are searched. For links, the link text is searched.
4
4
  For images, the "alt" attribute and inner text of any parent links are searched.
5
5
 
6
+ If no locator is provided, defaults to clicking the body element (`'//body'`).
7
+
6
8
  The second parameter is a context (CSS or XPath locator) to narrow the search.
7
9
 
8
10
  ```js
11
+ // click body element (default)
12
+ I.click();
9
13
  // simple link
10
14
  I.click('Logout');
11
15
  // button of form
@@ -20,6 +24,6 @@ I.click('Logout', '#nav');
20
24
  I.click({css: 'nav a.login'});
21
25
  ```
22
26
 
23
- @param {CodeceptJS.LocatorOrString} locator clickable link or button located by text, or any element located by CSS|XPath|strict locator.
27
+ @param {CodeceptJS.LocatorOrString} [locator='//body'] (optional, `'//body'` by default) clickable link or button located by text, or any element located by CSS|XPath|strict locator.
24
28
  @param {?CodeceptJS.LocatorOrString | null} [context=null] (optional, `null` by default) element to search in CSS|XPath|Strict locator.
25
29
  @returns {void} automatically synchronized promise through #recorder
package/lib/ai.js CHANGED
@@ -3,6 +3,12 @@ const debug = debugModule('codeceptjs:ai')
3
3
  import output from './output.js'
4
4
  import event from './event.js'
5
5
  import { removeNonInteractiveElements, minifyHtml, splitByChunks } from './html.js'
6
+ import { generateText } from 'ai'
7
+ import { fileURLToPath } from 'url'
8
+ import path from 'path'
9
+ import { fileExists } from './utils.js'
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
6
12
 
7
13
  const defaultHtmlConfig = {
8
14
  maxLength: 50000,
@@ -11,62 +17,31 @@ const defaultHtmlConfig = {
11
17
  html: {},
12
18
  }
13
19
 
14
- const defaultPrompts = {
15
- writeStep: (html, input) => [
16
- {
17
- role: 'user',
18
- content: `I am test engineer writing test in CodeceptJS
19
- I have opened web page and I want to use CodeceptJS to ${input} on this page
20
- Provide me valid CodeceptJS code to accomplish it
21
- Use only locators from this HTML: \n\n${html}`,
22
- },
23
- ],
24
-
25
- healStep: (html, { step, error, prevSteps }) => {
26
- return [
27
- {
28
- role: 'user',
29
- content: `As a test automation engineer I am testing web application using CodeceptJS.
30
- I want to heal a test that fails. Here is the list of executed steps: ${prevSteps.map(s => s.toString()).join(', ')}
31
- Propose how to adjust ${step.toCode()} step to fix the test.
32
- Use locators in order of preference: semantic locator by text, CSS, XPath. Use codeblocks marked with \`\`\`
33
- Here is the error message: ${error.message}
34
- Here is HTML code of a page where the failure has happened: \n\n${html}`,
35
- },
36
- ]
37
- },
38
-
39
- generatePageObject: (html, extraPrompt = '', rootLocator = null) => [
40
- {
41
- role: 'user',
42
- content: `As a test automation engineer I am creating a Page Object for a web application using CodeceptJS.
43
- Here is an sample page object:
44
-
45
- const { I } = inject();
46
-
47
- module.exports = {
48
-
49
- // setting locators
50
- element1: '#selector',
51
- element2: '.selector',
52
- element3: locate().withText('text'),
53
-
54
- // seting methods
55
- doSomethingOnPage(params) {
56
- // ...
57
- },
58
- }
59
-
60
- I want to generate a Page Object for the page I provide.
61
- Write JavaScript code in similar manner to list all locators on the page.
62
- Use locators in order of preference: by text (use locate().withText()), label, CSS, XPath.
63
- Avoid TailwindCSS, Bootstrap or React style formatting classes in locators.
64
- Add methods to to interact with page when needed.
65
- ${extraPrompt}
66
- ${rootLocator ? `All provided elements are inside '${rootLocator}'. Declare it as root variable and for every locator use locate(...).inside(root)` : ''}
67
- Add only locators from this HTML: \n\n${html}`,
68
- },
69
- ],
20
+ async function loadPrompts() {
21
+ const prompts = {}
22
+ const promptNames = ['writeStep', 'healStep', 'generatePageObject']
23
+
24
+ for (const name of promptNames) {
25
+ let promptPath
26
+
27
+ if (global.codecept_dir) {
28
+ promptPath = path.join(global.codecept_dir, `prompts/${name}.js`)
29
+ }
30
+
31
+ if (!promptPath || !fileExists(promptPath)) {
32
+ promptPath = path.join(__dirname, `template/prompts/${name}.js`)
33
+ }
34
+
35
+ try {
36
+ const module = await import(promptPath)
37
+ prompts[name] = module.default || module
38
+ debug(`Loaded prompt ${name} from ${promptPath}`)
39
+ } catch (err) {
40
+ debug(`Failed to load prompt ${name}:`, err.message)
41
+ }
42
+ }
43
+
44
+ return prompts
70
45
  }
71
46
 
72
47
  class AiAssistant {
@@ -78,7 +53,7 @@ class AiAssistant {
78
53
  this.connectToEvents()
79
54
  }
80
55
 
81
- enable(config = {}) {
56
+ async enable(config = {}) {
82
57
  debug('Enabling AI assistant')
83
58
  this.isEnabled = true
84
59
 
@@ -86,7 +61,9 @@ class AiAssistant {
86
61
 
87
62
  this.config = Object.assign(this.config, aiConfig)
88
63
  this.htmlConfig = Object.assign(defaultHtmlConfig, html)
89
- this.prompts = Object.assign(defaultPrompts, prompts)
64
+
65
+ const loadedPrompts = await loadPrompts()
66
+ this.prompts = Object.assign(loadedPrompts, prompts || {})
90
67
 
91
68
  debug('Config', this.config)
92
69
  }
@@ -96,9 +73,8 @@ class AiAssistant {
96
73
  this.isEnabled = false
97
74
  this.config = {
98
75
  maxTokens: 1000000,
99
- request: null,
76
+ model: null,
100
77
  response: parseCodeBlocks,
101
- // lets limit token usage to 1M
102
78
  }
103
79
  this.minifiedHtml = null
104
80
  this.response = null
@@ -114,41 +90,44 @@ class AiAssistant {
114
90
  if (this.isEnabled && this.numTokens > 0) {
115
91
  const numTokensK = Math.ceil(this.numTokens / 1000)
116
92
  const maxTokensK = Math.ceil(this.config.maxTokens / 1000)
117
- output.print(`AI assistant took ${this.totalTime}s and used ~${numTokensK}K input tokens. Tokens limit: ${maxTokensK}K`)
93
+ output.print(`AI assistant took ${this.totalTime}s and used ${numTokensK}K tokens. Tokens limit: ${maxTokensK}K`)
118
94
  }
119
95
  })
120
96
  }
121
97
 
122
- checkRequestFn() {
98
+ checkModel() {
123
99
  if (!this.isEnabled) {
124
100
  debug('AI assistant is disabled')
125
101
  return
126
102
  }
127
103
 
128
- if (this.config.request) return
104
+ if (this.config.model) return
129
105
 
130
- const noRequestErrorMessage = `
131
- No request function is set for AI assistant.
106
+ const noModelErrorMessage = `
107
+ No model is set for AI assistant.
132
108
 
133
- [!] AI request was decoupled from CodeceptJS. To connect to OpenAI or other AI service.
134
- Please implement your own request function and set it in the config.
109
+ [!] Please configure AI model using Vercel AI SDK providers.
135
110
 
136
111
  Example (connect to OpenAI):
137
112
 
113
+ import { openai } from '@ai-sdk/openai';
114
+
138
115
  ai: {
139
- request: async (messages) => {
140
- const OpenAI = require('openai');
141
- const openai = new OpenAI({ apiKey: process.env['OPENAI_API_KEY'] })
142
- const response = await openai.chat.completions.create({
143
- model: 'gpt-4o-mini',
144
- messages,
145
- });
146
- return response?.data?.choices[0]?.message?.content;
147
- }
116
+ model: openai('gpt-4o-mini')
148
117
  }
118
+
119
+ Example (connect to Anthropic):
120
+
121
+ import { anthropic } from '@ai-sdk/anthropic';
122
+
123
+ ai: {
124
+ model: anthropic('claude-3-5-sonnet-20241022')
125
+ }
126
+
127
+ See https://ai-sdk.dev/docs/foundations/providers-and-models for all providers.
149
128
  `.trim()
150
129
 
151
- throw new Error(noRequestErrorMessage)
130
+ throw new Error(noModelErrorMessage)
152
131
  }
153
132
 
154
133
  async setHtmlContext(html) {
@@ -172,28 +151,32 @@ class AiAssistant {
172
151
  if (!this.isEnabled) return ''
173
152
 
174
153
  try {
175
- this.checkRequestFn()
154
+ this.checkModel()
176
155
  debug('Request', messages)
177
156
 
178
157
  this.response = null
179
158
 
180
- this.calculateTokens(messages)
181
159
  const startTime = process.hrtime()
182
- this.response = await this.config.request(messages)
160
+ const result = await generateText({
161
+ model: this.config.model,
162
+ messages,
163
+ })
183
164
  const endTime = process.hrtime(startTime)
184
165
  const executionTimeInSeconds = endTime[0] + endTime[1] / 1e9
185
166
 
167
+ this.response = result.text
168
+ this.numTokens += result.usage.totalTokens
169
+
186
170
  this.totalTime += Math.round(executionTimeInSeconds)
187
171
  debug('AI response time', executionTimeInSeconds)
188
172
  debug('Response', this.response)
173
+ debug('Usage', result.usage)
189
174
  this.stopWhenReachingTokensLimit()
190
175
  return this.response
191
176
  } catch (err) {
192
- debug(err.response)
177
+ debug(err)
193
178
  output.print('')
194
179
  output.error(`AI service error: ${err.message}`)
195
- if (err?.response?.data?.error?.code) output.error(err?.response?.data?.error?.code)
196
- if (err?.response?.data?.error?.message) output.error(err?.response?.data?.error?.message)
197
180
  this.stopWhenReachingTokensLimit()
198
181
  return ''
199
182
  }
@@ -232,25 +215,6 @@ class AiAssistant {
232
215
  return this.config.response(response)
233
216
  }
234
217
 
235
- calculateTokens(messages) {
236
- // we implement naive approach for calculating tokens with no extra requests
237
- // this approach was tested via https://platform.openai.com/tokenizer
238
- // we need it to display current tokens usage so users could analyze effectiveness of AI
239
-
240
- const inputString = messages
241
- .map(m => m.content)
242
- .join(' ')
243
- .trim()
244
- const numWords = (inputString.match(/[^\s\-:=]+/g) || []).length
245
-
246
- // 2.5 token is constant for average HTML input
247
- const tokens = numWords * 2.5
248
-
249
- this.numTokens += tokens
250
-
251
- return tokens
252
- }
253
-
254
218
  stopWhenReachingTokensLimit() {
255
219
  if (this.numTokens < this.config.maxTokens) return
256
220
 
package/lib/codecept.js CHANGED
@@ -1,14 +1,13 @@
1
1
  import { existsSync, readFileSync } from 'fs'
2
2
  import { globSync } from 'glob'
3
+ import shuffle from 'lodash.shuffle'
3
4
  import fsPath from 'path'
4
5
  import { resolve } from 'path'
5
6
  import { fileURLToPath } from 'url'
6
7
  import { dirname } from 'path'
7
- import { createRequire } from 'module'
8
8
 
9
9
  const __filename = fileURLToPath(import.meta.url)
10
10
  const __dirname = dirname(__filename)
11
- const require = createRequire(import.meta.url)
12
11
 
13
12
  import Helper from '@codeceptjs/helper'
14
13
  import container from './container.js'
@@ -19,6 +18,7 @@ import ActorFactory from './actor.js'
19
18
  import output from './output.js'
20
19
  import { emptyFolder } from './utils.js'
21
20
  import { initCodeceptGlobals } from './globals.js'
21
+ import recorder from './recorder.js'
22
22
 
23
23
  import storeListener from './listener/store.js'
24
24
  import stepsListener from './listener/steps.js'
@@ -45,7 +45,7 @@ class Codecept {
45
45
  this.config = Config.create(config)
46
46
  this.opts = opts
47
47
  this.testFiles = new Array(0)
48
- this.requireModules(config.require)
48
+ this.requiringModules = config.require
49
49
  }
50
50
 
51
51
  /**
@@ -53,26 +53,36 @@ class Codecept {
53
53
  *
54
54
  * @param {string[]} requiringModules
55
55
  */
56
- requireModules(requiringModules) {
56
+ async requireModules(requiringModules) {
57
57
  if (requiringModules) {
58
- requiringModules.forEach(requiredModule => {
59
- const isLocalFile = existsSync(requiredModule) || existsSync(`${requiredModule}.js`)
58
+ for (const requiredModule of requiringModules) {
59
+ let modulePath = requiredModule
60
+ const isLocalFile = existsSync(modulePath) || existsSync(`${modulePath}.js`)
60
61
  if (isLocalFile) {
61
- requiredModule = resolve(requiredModule)
62
+ modulePath = resolve(modulePath)
63
+ // For ESM, ensure .js extension for local files
64
+ if (!modulePath.endsWith('.js') && !modulePath.endsWith('.mjs') && !modulePath.endsWith('.cjs')) {
65
+ if (existsSync(`${modulePath}.js`)) {
66
+ modulePath = `${modulePath}.js`
67
+ }
68
+ }
62
69
  }
63
- require(requiredModule)
64
- })
70
+ // Use dynamic import for ESM
71
+ await import(modulePath)
72
+ }
65
73
  }
66
74
  }
67
75
 
68
76
  /**
69
- * Initialize CodeceptJS at specific directory.
70
- * If async initialization is required, pass callback as second parameter.
77
+ * Initialize CodeceptJS at specific dir.
78
+ * Loads config, requires factory methods
71
79
  *
72
80
  * @param {string} dir
73
81
  */
74
82
  async init(dir) {
75
83
  await this.initGlobals(dir)
84
+ // Require modules before initializing
85
+ await this.requireModules(this.requiringModules)
76
86
  // initializing listeners
77
87
  await container.create(this.config, this.opts)
78
88
  // Store container globally for easy access
@@ -93,16 +103,24 @@ class Codecept {
93
103
  * Executes hooks.
94
104
  */
95
105
  async runHooks() {
96
- // default hooks
97
- runHook(storeListener)
98
- runHook(stepsListener)
99
- runHook(configListener)
100
- runHook(resultListener)
101
- runHook(helpersListener)
102
- runHook(globalTimeoutListener)
103
- runHook(globalRetryListener)
104
- runHook(exitListener)
105
- runHook(emptyRunListener)
106
+ // default hooks - dynamic imports for ESM
107
+ const listenerModules = [
108
+ './listener/store.js',
109
+ './listener/steps.js',
110
+ './listener/config.js',
111
+ './listener/result.js',
112
+ './listener/helpers.js',
113
+ './listener/globalTimeout.js',
114
+ './listener/globalRetry.js',
115
+ './listener/retryEnhancer.js',
116
+ './listener/exit.js',
117
+ './listener/emptyRun.js',
118
+ ]
119
+
120
+ for (const modulePath of listenerModules) {
121
+ const module = await import(modulePath)
122
+ runHook(module.default || module)
123
+ }
106
124
 
107
125
  // custom hooks (previous iteration of plugins)
108
126
  this.config.hooks.forEach(hook => runHook(hook))
@@ -111,6 +129,7 @@ class Codecept {
111
129
  /**
112
130
  * Executes bootstrap.
113
131
  *
132
+ * @returns {Promise<void>}
114
133
  */
115
134
  async bootstrap() {
116
135
  return runHook(this.config.bootstrap, 'bootstrap')
@@ -118,7 +137,8 @@ class Codecept {
118
137
 
119
138
  /**
120
139
  * Executes teardown.
121
-
140
+ *
141
+ * @returns {Promise<void>}
122
142
  */
123
143
  async teardown() {
124
144
  return runHook(this.config.teardown, 'teardown')
@@ -171,6 +191,50 @@ class Codecept {
171
191
  })
172
192
  }
173
193
  }
194
+
195
+ if (this.opts.shuffle) {
196
+ this.testFiles = shuffle(this.testFiles)
197
+ }
198
+
199
+ if (this.opts.shard) {
200
+ this.testFiles = this._applySharding(this.testFiles, this.opts.shard)
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Apply sharding to test files based on shard configuration
206
+ *
207
+ * @param {Array<string>} testFiles - Array of test file paths
208
+ * @param {string} shardConfig - Shard configuration in format "index/total" (e.g., "1/4")
209
+ * @returns {Array<string>} - Filtered array of test files for this shard
210
+ */
211
+ _applySharding(testFiles, shardConfig) {
212
+ const shardMatch = shardConfig.match(/^(\d+)\/(\d+)$/)
213
+ if (!shardMatch) {
214
+ throw new Error('Invalid shard format. Expected format: "index/total" (e.g., "1/4")')
215
+ }
216
+
217
+ const shardIndex = parseInt(shardMatch[1], 10)
218
+ const shardTotal = parseInt(shardMatch[2], 10)
219
+
220
+ if (shardTotal < 1) {
221
+ throw new Error('Shard total must be at least 1')
222
+ }
223
+
224
+ if (shardIndex < 1 || shardIndex > shardTotal) {
225
+ throw new Error(`Shard index ${shardIndex} must be between 1 and ${shardTotal}`)
226
+ }
227
+
228
+ if (testFiles.length === 0) {
229
+ return testFiles
230
+ }
231
+
232
+ // Calculate which tests belong to this shard
233
+ const shardSize = Math.ceil(testFiles.length / shardTotal)
234
+ const startIndex = (shardIndex - 1) * shardSize
235
+ const endIndex = Math.min(startIndex + shardSize, testFiles.length)
236
+
237
+ return testFiles.slice(startIndex, endIndex)
174
238
  }
175
239
 
176
240
  /**
@@ -205,15 +269,21 @@ class Codecept {
205
269
  })
206
270
  }
207
271
 
208
- const done = () => {
272
+ const done = async (failures) => {
209
273
  event.emit(event.all.result, container.result())
210
274
  event.emit(event.all.after, this)
275
+ // Wait for any recorder tasks added by event.all.after handlers
276
+ await recorder.promise()
277
+ // Set exit code based on test failures
278
+ if (failures) {
279
+ process.exitCode = 1
280
+ }
211
281
  resolve()
212
282
  }
213
283
 
214
284
  try {
215
285
  event.emit(event.all.before, this)
216
- mocha.run(() => done())
286
+ mocha.run(async (failures) => await done(failures))
217
287
  } catch (e) {
218
288
  output.error(e.stack)
219
289
  reject(e)
@@ -221,6 +291,11 @@ class Codecept {
221
291
  })
222
292
  }
223
293
 
294
+ /**
295
+ * Returns the version string of CodeceptJS.
296
+ *
297
+ * @returns {string} The version string.
298
+ */
224
299
  static version() {
225
300
  return JSON.parse(readFileSync(`${__dirname}/../package.json`, 'utf8')).version
226
301
  }