codeceptjs 4.0.0-beta.7.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.
- package/README.md +46 -3
- package/bin/codecept.js +9 -0
- package/bin/test-server.js +64 -0
- package/docs/webapi/click.mustache +5 -1
- package/lib/ai.js +66 -102
- package/lib/codecept.js +99 -24
- package/lib/command/generate.js +33 -1
- package/lib/command/init.js +7 -3
- package/lib/command/run-workers.js +31 -2
- package/lib/command/run.js +15 -0
- package/lib/command/workers/runTests.js +331 -58
- package/lib/config.js +16 -5
- package/lib/container.js +15 -13
- package/lib/effects.js +1 -1
- package/lib/element/WebElement.js +327 -0
- package/lib/event.js +10 -1
- package/lib/helper/AI.js +11 -11
- package/lib/helper/ApiDataFactory.js +34 -6
- package/lib/helper/Appium.js +156 -42
- package/lib/helper/GraphQL.js +3 -3
- package/lib/helper/GraphQLDataFactory.js +4 -4
- package/lib/helper/JSONResponse.js +48 -40
- package/lib/helper/Mochawesome.js +24 -2
- package/lib/helper/Playwright.js +841 -153
- package/lib/helper/Puppeteer.js +263 -67
- package/lib/helper/REST.js +21 -0
- package/lib/helper/WebDriver.js +105 -16
- package/lib/helper/clientscripts/PollyWebDriverExt.js +1 -1
- package/lib/helper/extras/PlaywrightReactVueLocator.js +52 -0
- package/lib/helper/extras/PlaywrightRestartOpts.js +12 -1
- package/lib/helper/network/actions.js +8 -6
- package/lib/listener/config.js +11 -3
- package/lib/listener/enhancedGlobalRetry.js +110 -0
- package/lib/listener/globalTimeout.js +19 -4
- package/lib/listener/helpers.js +8 -2
- package/lib/listener/retryEnhancer.js +85 -0
- package/lib/listener/steps.js +12 -0
- package/lib/mocha/asyncWrapper.js +13 -3
- package/lib/mocha/cli.js +1 -1
- package/lib/mocha/factory.js +3 -0
- package/lib/mocha/gherkin.js +1 -1
- package/lib/mocha/test.js +6 -0
- package/lib/mocha/ui.js +13 -0
- package/lib/output.js +62 -18
- package/lib/plugin/coverage.js +16 -3
- package/lib/plugin/enhancedRetryFailedStep.js +99 -0
- package/lib/plugin/htmlReporter.js +3648 -0
- package/lib/plugin/retryFailedStep.js +1 -0
- package/lib/plugin/stepByStepReport.js +1 -1
- package/lib/recorder.js +28 -3
- package/lib/result.js +100 -23
- package/lib/retryCoordinator.js +207 -0
- package/lib/step/base.js +1 -1
- package/lib/step/comment.js +2 -2
- package/lib/step/meta.js +1 -1
- package/lib/template/heal.js +1 -1
- package/lib/template/prompts/generatePageObject.js +31 -0
- package/lib/template/prompts/healStep.js +13 -0
- package/lib/template/prompts/writeStep.js +9 -0
- package/lib/test-server.js +334 -0
- package/lib/utils/mask_data.js +47 -0
- package/lib/utils.js +87 -6
- package/lib/workerStorage.js +2 -1
- package/lib/workers.js +179 -23
- package/package.json +58 -47
- package/typings/index.d.ts +19 -7
- package/typings/promiseBasedTypes.d.ts +5525 -3759
- package/typings/types.d.ts +5791 -3781
package/README.md
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
| 🌐 Web | Playwright | [](https://github.com/codeceptjs/CodeceptJS/actions/workflows/playwright.yml) |
|
|
11
11
|
| 🌐 Web | Puppeteer | [](https://github.com/codeceptjs/CodeceptJS/actions/workflows/puppeteer.yml) |
|
|
12
12
|
| 🌐 Web | WebDriver | [](https://github.com/codeceptjs/CodeceptJS/actions/workflows/webdriver.yml) |
|
|
13
|
-
| 🌐 Web | TestCafe | [](https://github.com/codeceptjs/CodeceptJS/actions/workflows/testcafe.yml) |
|
|
14
13
|
| 📱 Mobile | Appium | [](https://github.com/codeceptjs/CodeceptJS/actions/workflows/appium_Android.yml) |
|
|
15
14
|
|
|
16
15
|
# CodeceptJS [](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,
|
|
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
|
+

|
|
258
|
+
|
|
259
|
+
#### Detailed Test Results
|
|
260
|
+
|
|
261
|
+
Each test shows comprehensive execution details with expandable step information:
|
|
262
|
+
|
|
263
|
+

|
|
264
|
+
|
|
265
|
+
#### Advanced Filtering Capabilities
|
|
266
|
+
|
|
267
|
+
Real-time filtering allows quick navigation through test results:
|
|
268
|
+
|
|
269
|
+

|
|
270
|
+
|
|
271
|
+
#### BDD/Gherkin Support
|
|
272
|
+
|
|
273
|
+
Full support for Gherkin scenarios with proper feature formatting:
|
|
274
|
+
|
|
275
|
+

|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
104
|
+
if (this.config.model) return
|
|
129
105
|
|
|
130
|
-
const
|
|
131
|
-
No
|
|
106
|
+
const noModelErrorMessage = `
|
|
107
|
+
No model is set for AI assistant.
|
|
132
108
|
|
|
133
|
-
[!]
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
59
|
-
|
|
58
|
+
for (const requiredModule of requiringModules) {
|
|
59
|
+
let modulePath = requiredModule
|
|
60
|
+
const isLocalFile = existsSync(modulePath) || existsSync(`${modulePath}.js`)
|
|
60
61
|
if (isLocalFile) {
|
|
61
|
-
|
|
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
|
-
|
|
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
|
|
70
|
-
*
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
}
|