From f572b8e79ad2c722e17045cf2133503f7c624dc4 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Sun, 26 Apr 2026 22:27:29 +0300 Subject: [PATCH 01/18] update docs --- docs/ai.md | 121 ---------- docs/auth.md | 318 +++++++++++++++++++++++++ docs/debugging.md | 32 +++ docs/helpers/AI.md | 102 -------- docs/helpers/OpenAI.md | 70 ------ docs/pageobjects.md | 2 - docs/probe.html | 21 ++ docs/webapi/seeFileDownloaded.mustache | 23 ++ 8 files changed, 394 insertions(+), 295 deletions(-) create mode 100644 docs/auth.md delete mode 100644 docs/helpers/AI.md delete mode 100644 docs/helpers/OpenAI.md create mode 100644 docs/probe.html create mode 100644 docs/webapi/seeFileDownloaded.mustache diff --git a/docs/ai.md b/docs/ai.md index 15a2b0672..1ed707bac 100644 --- a/docs/ai.md +++ b/docs/ai.md @@ -22,9 +22,7 @@ So, instead of asking "write me a test" it can ask "write a test for **this** pa CodeceptJS AI can do the following: - πŸ‹οΈβ€β™€οΈ **assist writing tests** in `pause()` or interactive shell mode -- πŸ“ƒ **generate page objects** in `pause()` or interactive shell mode - πŸš‘ **self-heal failing tests** (can be used on CI) -- πŸ’¬ send arbitrary prompts to AI provider from any tested page attaching its HTML contents ![](/img/fill_form.gif) @@ -385,125 +383,6 @@ Run tests with both AI and analyze enabled: npx codeceptjs run --ai ``` -## Arbitrary Prompts - -What if you want to take AI on the journey of test automation and ask it questions while browsing pages? - -This is possible with the new `AI` helper. Enable it in your config file in `helpers` section: - -```js -// inside codecept.conf -helpers: { - // Playwright, Puppeteer, or WebDrver helper should be enabled too - Playwright: { - }, - - AI: {} -} -``` - -AI helper will be automatically attached to Playwright, WebDriver, or another web helper you use. It includes the following methods: - -- `askGptOnPage` - sends GPT prompt attaching the HTML of the page. Large pages will be split into chunks, according to `chunkSize` config. You will receive responses for all chunks. -- `askGptOnPageFragment` - sends GPT prompt attaching the HTML of the specific element. This method is recommended over `askGptOnPage` as you can reduce the amount of data to be processed. -- `askGptGeneralPrompt` - sends GPT prompt without HTML. -- `askForPageObject` - creates PageObject for you, explained in next section. - -`askGpt` methods won't remove non-interactive elements, so it is recommended to manually control the size of the sent HTML. - -Here are some good use cases for this helper: - -- get page summaries -- inside pause mode navigate through your application and ask to document pages -- etc... - -```js -// use it inside test or inside interactive pause -// pretend you are technical writer asking for documentation -const pageDoc = await I.askGptOnPageFragment('Act as technical writer, describe what is this page for', '#container') -``` - -As of now, those use cases do not apply to test automation but maybe you can apply them to your testing setup. - -## Generate PageObjects - -Last but not the least. AI helper can be used to quickly prototype PageObjects on pages browsed within interactive session. - -![](/img/ai_page_object.png) - -Enable AI helper as explained in previous section and launch shell: - -``` -npx codeceptjs shell --ai -``` - -Also this is availble from `pause()` if AI helper is enabled, - -Ensure that browser is started in window mode, then browse the web pages on your site. -On a page you want to create PageObject execute `askForPageObject()` command. The only required parameter is the name of a page: - -```js -I.askForPageObject('login') -``` - -This command sends request to AI provider should create valid CodeceptJS PageObject. -Run it few times or switch AI provider if response is not satisfactory to you. - -> You can change the style of PageObject and locator preferences by adjusting prompt in a config file - -When completed successfully, page object is saved to **output** directory and loaded into the shell as `page` variable so locators and methods can be checked on the fly. - -If page object has `signInButton` locator you can quickly check it by typing: - -```js -I.click(page.signInButton) -``` - -If page object has `clickForgotPassword` method you can execute it as: - -```js -=> page.clickForgotPassword() -``` - -Here is an example of a session: - -```shell -Page object for login is saved to .../output/loginPage-1718579784751.js -Page object registered for this session as `page` variable -Use `=>page.methodName()` in shell to run methods of page object -Use `click(page.locatorName)` to check locators of page object - - I.=>page.clickSignUp() - I.click(page.signUpLink) - I.=> page.enterPassword('asdasd') - I.=> page.clickSignIn() -``` - -You can improve prompt by passing custom request as a second parameter: - -```js -I.askForPageObject('login', 'implement signIn(username, password) method') -``` - -To generate page object for the part of a page, pass in root locator as third parameter. - -```js -I.askForPageObject('login', '', '#auth') -``` - -In this case, all generated locators, will use `#auth` as their root element. - -Don't aim for perfect PageObjects but find a good enough one, which you can use for writing your tests. -All created page objects are considered temporary, that's why saved to `output` directory. - -Rename created PageObject to remove timestamp and move it from `output` to `pages` folder and include it into codecept.conf file: - -```js - include: { - loginPage: "./pages/loginPage.js", - // ... -``` - ## Advanced Configuration AI prompts and HTML compression can be configured inside `ai` section of `codecept.conf` file: diff --git a/docs/auth.md b/docs/auth.md new file mode 100644 index 000000000..38de6b0ae --- /dev/null +++ b/docs/auth.md @@ -0,0 +1,318 @@ +--- +permalink: /auth +title: Authorization +--- + +# Authorization + +The `auth` plugin logs a user in once and reuses that session for every test that follows. After the first login it stores the cookies (in memory or in a file) and replays them on later tests. If the session expires, the plugin notices and logs in again. + +## Quick Start + +Enable the plugin in `codecept.conf.js` and define one user with `login` and `check` functions: + +```js +plugins: { + auth: { + enabled: true, + users: { + admin: { + login: (I) => { + I.amOnPage('/login') + I.fillField('email', 'admin@site.com') + I.fillField('password', secret('123456')) + I.click('Sign in') + }, + check: (I) => { + I.amOnPage('/') + I.see('Admin', '.navbar') + }, + }, + }, + }, +} +``` + +Inject `login` into a test and call it with the user name: + +```js +Feature('Dashboard') + +Before(({ login }) => { + login('admin') +}) + +Scenario('admin sees the dashboard', ({ I }) => { + I.amOnPage('/dashboard') + I.see('Welcome, Admin') +}) +``` + +## How It Works + +When you call `login('admin')`: + +1. **`restore`** opens a page and applies the saved cookies. +2. **`check`** verifies the user is signed in. If it throws or fails an assertion, the plugin assumes the session is dead. +3. **`login`** runs the sign-in flow when `restore` + `check` fail (or no cookies exist yet). +4. **`fetch`** reads the new cookies and stores them for the next test. + +Defaults cover the common case: `fetch` calls `I.grabCookie()`, `restore` calls `I.amOnPage('/')` then `I.setCookie(cookies)`, and `check` is a no-op. Override any of them when your app needs something different. + +## Configuration + +| Option | Default | Purpose | +| ------------ | --------- | -------------------------------------------------------- | +| `users` | β€” | Map of session names to user definitions. | +| `inject` | `'login'` | Name of the function injected into tests. | +| `saveToFile` | `false` | Write cookies to `/_session.json`. | + +Each user accepts four functions: + +- `login(I)` β€” sign-in flow. Required. +- `check(I, session)` β€” verify the session is still valid. Throw to force a re-login. +- `fetch(I)` β€” return the cookies (or token) to store. Defaults to `I.grabCookie()`. +- `restore(I, session)` β€” replay the stored session. Defaults to `I.amOnPage('/')` + `I.setCookie()`. + +## When to Log In: `Before` vs `BeforeSuite` + +You can call `login()` in either hook. Pick based on how many users a suite touches. + +### `Before` β€” one login per test + +The default and the safe choice. Use it whenever a suite mixes users, or when you are not on Playwright. + +```js +Feature('Mixed users') + +Scenario('admin can ban a user', ({ I, login }) => { + login('admin') + I.amOnPage('/users/42') + I.click('Ban') +}) + +Scenario('regular user cannot see the ban button', ({ I, login }) => { + login('user') + I.amOnPage('/users/42') + I.dontSee('Ban') +}) +``` + +When the user changes between tests, the plugin clears the previous user's cookies before applying the new ones. + +### `BeforeSuite` β€” one login per suite (Playwright only) + +Calling `login()` from `BeforeSuite` lets Playwright load cookies *before* it opens the browser, which removes the extra navigation that `restore` would otherwise need. Use this only when every test in the suite runs as the same user. + +```js +Feature('Admin reports') + +BeforeSuite(({ login }) => { + login('admin') +}) + +Scenario('export sales report', ({ I }) => { + I.amOnPage('/reports/sales') + I.click('Export') +}) + +Scenario('export traffic report', ({ I }) => { + I.amOnPage('/reports/traffic') + I.click('Export') +}) +``` + +> ⚠ If a test inside the suite calls `login()` with a different user, the plugin resets the cookies and signs in again. That cancels the speed-up. When the suite needs more than one user, prefer `Before`. + +## Persisting Sessions to a File + +Set `saveToFile: true` to keep sessions across test runs. The plugin writes one JSON file per user into the output directory and reloads them on the next start. + +```js +plugins: { + auth: { + enabled: true, + saveToFile: true, + users: { admin: { login: (I) => I.loginAsAdmin() } }, + }, +} +``` + +This is most useful while writing tests: you log in once, then iterate without paying the sign-in cost on every run. Delete the JSON file (or let it expire on the server) to force a fresh login. + +## Examples + +### Reuse a `steps_file.js` helper + +Move the sign-in flow into a custom step and call it from the plugin: + +```js +plugins: { + auth: { + enabled: true, + saveToFile: true, + users: { + admin: { + login: (I) => I.loginAdmin(), + check: (I) => { + I.amOnPage('/') + I.see('Admin') + }, + }, + }, + }, +} +``` + +### Multiple users with a custom inject name + +Rename the injected function to `loginAs` for readability: + +```js +plugins: { + auth: { + enabled: true, + inject: 'loginAs', + users: { + user: { + login: (I) => { + I.amOnPage('/login') + I.fillField('email', 'user@site.com') + I.fillField('password', secret('123456')) + I.click('Login') + }, + check: (I) => I.see('User', '.navbar'), + }, + admin: { + login: (I) => { + I.amOnPage('/login') + I.fillField('email', 'admin@site.com') + I.fillField('password', secret('123456')) + I.click('Login') + }, + check: (I) => I.see('Admin', '.navbar'), + }, + }, + }, +} +``` + +Inside a test: + +```js +Before(({ loginAs }) => loginAs('user')) +``` + +### Let the helper keep cookies, skip `fetch`/`restore` + +If your helper already keeps cookies between tests (e.g. WebDriver's `keepCookies: true`), disable `fetch` and `restore` so the plugin only handles the first login: + +```js +helpers: { + WebDriver: { keepCookies: true }, +}, +plugins: { + auth: { + enabled: true, + users: { + admin: { + login: (I) => { + I.amOnPage('/login') + I.fillField('email', 'admin@site.com') + I.fillField('password', secret('123456')) + I.click('Login') + }, + check: (I) => { + I.amOnPage('/dashboard') + I.see('Admin', '.navbar') + }, + fetch: () => {}, + restore: () => {}, + }, + }, + }, +} +``` + +### Sessions stored in local storage + +Override `fetch` and `restore` to read and write a token instead of cookies: + +```js +plugins: { + auth: { + enabled: true, + users: { + admin: { + login: (I) => I.loginAsAdmin(), + check: (I) => I.see('Admin', '.navbar'), + fetch: (I) => I.executeScript(() => localStorage.getItem('session_id')), + restore: (I, session) => { + I.amOnPage('/') + I.executeScript((s) => localStorage.setItem('session_id', s), session) + }, + }, + }, + }, +} +``` + +### Async login + +When `login`, `check`, `restore`, or `fetch` is `async`, the plugin awaits it. Inside your test, `await` the injected function: + +```js +plugins: { + auth: { + enabled: true, + users: { + admin: { + login: async (I) => { + const phrase = await I.grabTextFrom('#phrase') + I.fillField('username', 'admin') + I.fillField('password', secret('password')) + I.fillField('phrase', phrase) + }, + check: (I) => { + I.amOnPage('/') + I.see('Admin') + }, + }, + }, + }, +} +``` + +```js +Scenario('login', async ({ login }) => { + await login('admin') +}) +``` + +### Validate the session inside `check` + +`check` receives the value returned by `fetch` as its second argument. Throw from `check` to force a fresh login: + +```js +plugins: { + auth: { + enabled: true, + users: { + admin: { + login: (I) => I.loginAsAdmin(), + check: (I, session) => { + if (session.profile.email !== 'admin@site.com') { + throw new Error('Wrong user signed in') + } + }, + }, + }, + }, +} +``` + +## Tips + +- **Force a re-login** by throwing inside `check` β€” the plugin treats it as an expired session and runs `login` again. +- **Mask credentials** with `secret()` so passwords never appear in the test output. See [Secrets](/secrets). +- **Switch users mid-test** with `session()` when one scenario needs two browsers signed in as different users. See [Multiple Sessions](/sessions). diff --git a/docs/debugging.md b/docs/debugging.md index 00c8c87fc..81b8cd66a 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -166,6 +166,38 @@ npx codeceptjs run -p pauseOn:url:/checkout/* This is useful when you want to inspect a specific page regardless of which test step navigates there. +## Browser Control + +For ad-hoc overrides of browser helper config without editing `codecept.conf`, use the `browser` plugin via `-p`. Works for Playwright, Puppeteer, WebDriver and Appium in one call. + +Force a visible browser: + +```bash +npx codeceptjs run -p browser:show +``` + +Force headless (also injects `--headless` into WebDriver chrome/firefox capability args): + +```bash +npx codeceptjs run -p browser:hide +``` + +Switch the browser engine for Playwright / Puppeteer / WebDriver / TestCafe in one shot β€” no per-helper config gymnastics: + +```bash +npx codeceptjs run -p browser:browser=firefox +npx codeceptjs run -p browser:browser=webkit:hide +``` + +Pass any other helper config as `key=value`. Values are coerced (`true`/`false` β†’ boolean, digits β†’ Number, otherwise string). Tokens are colon-chained on a single `-p`: + +```bash +npx codeceptjs run -p browser:windowSize=1024x768:video=false +npx codeceptjs run -p browser:hide:video=true +``` + +`browser=` routes through `setBrowser` (so Puppeteer correctly receives `product`, Playwright receives `browser`, etc.); `windowSize=WxH` routes through `setWindowSize` (which also injects `--window-size=W,H` into chromium/chrome args). Anything else is shallow-merged onto every browser helper present in config. + ## IDE Debugging ### VS Code diff --git a/docs/helpers/AI.md b/docs/helpers/AI.md deleted file mode 100644 index 96e0dc607..000000000 --- a/docs/helpers/AI.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -permalink: /helpers/AI -editLink: false -sidebar: auto -title: AI ---- - - - -## AI - -**Extends Helper** - -AI Helper for CodeceptJS. - -This helper class provides integration with the AI GPT-3.5 or 4 language model for generating responses to questions or prompts within the context of web pages. It allows you to interact with the GPT-3.5 model to obtain intelligent responses based on HTML fragments or general prompts. -This helper should be enabled with any web helpers like Playwright or Puppeteer or WebDriver to ensure the HTML context is available. - -Use it only in development mode. It is recommended to run it only inside pause() mode. - -## Configuration - -This helper should be configured in codecept.conf.{js|ts} - -* `chunkSize`: - The maximum number of characters to send to the AI API at once. We split HTML fragments by 8000 chars to not exceed token limit. Increase this value if you use GPT-4. - -### Parameters - -* `config` - -### askForPageObject - -Generates PageObject for current page using AI. - -It saves the PageObject to the output directory. You can review the page object and adjust it as needed and move to pages directory. -Prompt can be customized in a global config file. - -```js -// create page object for whole page -I.askForPageObject('home'); - -// create page object with extra prompt -I.askForPageObject('home', 'implement signIn(username, password) method'); - -// create page object for a specific element -I.askForPageObject('home', null, '.detail'); -``` - -Asks for a page object based on the provided page name, locator, and extra prompt. - -#### Parameters - -* `pageName` **[string][1]** The name of the page to retrieve the object for. -* `extraPrompt` **([string][1] | null)** An optional extra prompt for additional context or information. -* `locator` **([string][1] | null)** An optional locator to find a specific element on the page. - -Returns **[Promise][2]<[Object][3]>** A promise that resolves to the requested page object. - -### askGptGeneralPrompt - -Send a general request to AI and return response. - -#### Parameters - -* `prompt` **[string][1]** - -Returns **[Promise][2]<[string][1]>** A Promise that resolves to the generated response from the GPT model. - -### askGptOnPage - -Asks the AI GPT language model a question based on the provided prompt within the context of the current page's HTML. - -```js -I.askGptOnPage('what does this page do?'); -``` - -#### Parameters - -* `prompt` **[string][1]** The question or prompt to ask the GPT model. - -Returns **[Promise][2]<[string][1]>** A Promise that resolves to the generated responses from the GPT model, joined by newlines. - -### askGptOnPageFragment - -Asks the AI a question based on the provided prompt within the context of a specific HTML fragment on the current page. - -```js -I.askGptOnPageFragment('describe features of this screen', '.screen'); -``` - -#### Parameters - -* `prompt` **[string][1]** The question or prompt to ask the GPT-3.5 model. -* `locator` **[string][1]** The locator or selector used to identify the HTML fragment on the page. - -Returns **[Promise][2]<[string][1]>** A Promise that resolves to the generated response from the GPT model. - -[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String - -[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise - -[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object diff --git a/docs/helpers/OpenAI.md b/docs/helpers/OpenAI.md deleted file mode 100644 index 35a5e9406..000000000 --- a/docs/helpers/OpenAI.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -permalink: /helpers/OpenAI -editLink: false -sidebar: auto -title: OpenAI ---- - - - -## OpenAI - -**Extends Helper** - -OpenAI Helper for CodeceptJS. - -This helper class provides integration with the OpenAI GPT-3.5 or 4 language model for generating responses to questions or prompts within the context of web pages. It allows you to interact with the GPT-3.5 model to obtain intelligent responses based on HTML fragments or general prompts. -This helper should be enabled with any web helpers like Playwright or Puppeteer or WebDrvier to ensure the HTML context is available. - -## Configuration - -This helper should be configured in codecept.json or codecept.conf.js - -- `chunkSize`: - The maximum number of characters to send to the OpenAI API at once. We split HTML fragments by 8000 chars to not exceed token limit. Increase this value if you use GPT-4. - -### Parameters - -- `config` - -### askGptGeneralPrompt - -Send a general request to ChatGPT and return response. - -#### Parameters - -- `prompt` **[string][1]** - -Returns **[Promise][2]<[string][1]>** A Promise that resolves to the generated response from the GPT model. - -### askGptOnPage - -Asks the OpenAI GPT language model a question based on the provided prompt within the context of the current page's HTML. - -```js -I.askGptOnPage('what does this page do?'); -``` - -#### Parameters - -- `prompt` **[string][1]** The question or prompt to ask the GPT model. - -Returns **[Promise][2]<[string][1]>** A Promise that resolves to the generated responses from the GPT model, joined by newlines. - -### askGptOnPageFragment - -Asks the OpenAI GPT-3.5 language model a question based on the provided prompt within the context of a specific HTML fragment on the current page. - -```js -I.askGptOnPageFragment('describe features of this screen', '.screen'); -``` - -#### Parameters - -- `prompt` **[string][1]** The question or prompt to ask the GPT-3.5 model. -- `locator` **[string][1]** The locator or selector used to identify the HTML fragment on the page. - -Returns **[Promise][2]<[string][1]>** A Promise that resolves to the generated response from the GPT model. - -[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String - -[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise diff --git a/docs/pageobjects.md b/docs/pageobjects.md index c82723e47..809b34c63 100644 --- a/docs/pageobjects.md +++ b/docs/pageobjects.md @@ -55,8 +55,6 @@ export default function() { ## PageObject -> CodeceptJS can [generate PageObjects using AI](/ai#generate-pageobjects). It fetches all interactive elements from a page, generates locators and methods page and writes JS code. Generated page object can be tested on the fly within the same browser session. - If an application has different pages (login, admin, etc) you should use a page object. CodeceptJS can generate a template for it with the following command: diff --git a/docs/probe.html b/docs/probe.html new file mode 100644 index 000000000..b22b06a51 --- /dev/null +++ b/docs/probe.html @@ -0,0 +1,21 @@ + + + + +

+
diff --git a/docs/webapi/seeFileDownloaded.mustache b/docs/webapi/seeFileDownloaded.mustache
new file mode 100644
index 000000000..63d61d16e
--- /dev/null
+++ b/docs/webapi/seeFileDownloaded.mustache
@@ -0,0 +1,23 @@
+Checks that a file was downloaded during the current test.
+Downloads are automatically saved to `output/downloads`.
+
+Can be called with different arguments:
+
+- **No argument** β€” asserts that at least one file was downloaded.
+- **Number** β€” asserts that exactly N files were downloaded.
+- **String** β€” asserts that a file with the exact name was downloaded.
+- **Glob pattern** (contains `*`, `?`, `[`) β€” asserts that a file matching the pattern was downloaded.
+- **Regex string** (`/pattern/`) β€” asserts that a file matching the regex was downloaded.
+
+```js
+I.click('Download');
+I.seeFileDownloaded();
+
+I.seeFileDownloaded('report.pdf');
+I.seeFileDownloaded(2);
+I.seeFileDownloaded('*.pdf');
+I.seeFileDownloaded('/report-.+\\.pdf/');
+```
+
+@param {string|number} [arg] filename, number of files, glob pattern, or regex string.
+@returns {void} automatically synchronized promise through #recorder

From 1ae964b555fdad16e4d6e979e1e0b893bc3a8d07 Mon Sep 17 00:00:00 2001
From: DavertMik 
Date: Mon, 27 Apr 2026 01:13:48 +0300
Subject: [PATCH 02/18] updated docs, added browser plugin

---
 .gitignore                       |   3 +-
 docs/ai.md                       |   8 +-
 docs/installation.md             |   2 +-
 docs/migration-4.md              | 450 +++++++++++++++++++++++++++++++
 docs/probe.html                  |  21 --
 docs/quickstart.md               | 118 +++-----
 lib/config.js                    |  18 ++
 lib/container.js                 |  37 ++-
 lib/helper/AI.js                 | 214 ---------------
 lib/plugin/browser.js            | 173 ++++++++++++
 package.json                     |   6 +-
 test/unit/plugin/browser_test.js | 140 ++++++++++
 typings/index.d.ts               |   3 -
 13 files changed, 869 insertions(+), 324 deletions(-)
 create mode 100644 docs/migration-4.md
 delete mode 100644 docs/probe.html
 delete mode 100644 lib/helper/AI.js
 create mode 100644 lib/plugin/browser.js
 create mode 100644 test/unit/plugin/browser_test.js

diff --git a/.gitignore b/.gitignore
index 1439146bc..dd3afd1f8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,4 +27,5 @@ yarn.lock
 /.vs
 typings/types.d.ts
 typings/promiseBasedTypes.d.ts
-reflection/
\ No newline at end of file
+reflection/
+skills/
diff --git a/docs/ai.md b/docs/ai.md
index 1ed707bac..6471de539 100644
--- a/docs/ai.md
+++ b/docs/ai.md
@@ -58,7 +58,7 @@ import { openai } from '@ai-sdk/openai'
 export default {
   // ... other config
   ai: {
-    model: openai('gpt-4o-mini'),
+    model: openai('gpt-5'),
   },
 }
 ```
@@ -92,7 +92,7 @@ import { openai } from '@ai-sdk/openai'
 
 export default {
   ai: {
-    model: openai('gpt-4o-mini'),
+    model: openai('gpt-5'),
     // or use gpt-4o, gpt-3.5-turbo, etc.
   },
 }
@@ -119,8 +119,8 @@ import { anthropic } from '@ai-sdk/anthropic'
 
 export default {
   ai: {
-    model: anthropic('claude-3-5-sonnet-20241022'),
-    // or use claude-3-opus-20240229, claude-3-haiku-20240307, etc.
+    model: anthropic('claude-sonnet-4-6'),
+    // or use claude-opus-4-7, claude-haiku-4-5, etc.
   },
 }
 ```
diff --git a/docs/installation.md b/docs/installation.md
index de9098aa7..318e412e6 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -64,7 +64,7 @@ CodeceptJS v4.x supports ECMAScript Modules (ESM) format. To use ESM:
 1. Add `"type": "module"` to your `package.json`
 2. Update import syntax in configuration files to use ESM format
 
-For detailed migration instructions and important behavioral changes, see the **[ESM Migration Guide](esm-migration.md)**.
+For detailed migration instructions and important behavioral changes, see the **[3.x β†’ 4.x Migration Guide](migration-4.md)**.
 
 ## WebDriver
 
diff --git a/docs/migration-4.md b/docs/migration-4.md
new file mode 100644
index 000000000..fa354c4c1
--- /dev/null
+++ b/docs/migration-4.md
@@ -0,0 +1,450 @@
+---
+permalink: /migration-4
+title: Migrating from 3.x to 4.x
+---
+
+# Migrating from 3.x to 4.x
+
+CodeceptJS 4.x is a major release. It moves the codebase from CommonJS to native ESM, drops several long-deprecated helpers and plugins, replaces legacy plugins with first-class APIs, and bumps most third-party dependencies.
+
+This guide tells you exactly what to change in your project to upgrade.
+
+## 1. Update Node and Package
+
+CodeceptJS 4.x supports Node 16+, but Node 20 or newer is recommended.
+
+```bash
+npm install codeceptjs@4
+```
+
+If you write tests in TypeScript, install `tsx`:
+
+```bash
+npm install --save-dev tsx
+```
+
+> 4.x replaces `ts-node/esm` with `tsx`. `ts-node/esm` is no longer recommended and emits a warning.
+
+## 2. Switch Your Project to ESM
+
+CodeceptJS 4.x ships as native ESM (`"type": "module"`). **Convert your project to ESM**.
+Add to your `package.json`:
+
+```json
+{
+  "type": "module"
+}
+```
+
+Then convert your config, page objects, and custom helpers to ESM (sections below).
+
+
+### Convert Custom Helpers
+
+3.x:
+
+```js
+const Helper = require('@codeceptjs/helper')
+
+class MyHelper extends Helper {
+  doSomething() { /* ... */ }
+}
+
+module.exports = MyHelper
+```
+
+4.x:
+
+```js
+import Helper from '@codeceptjs/helper'
+
+class MyHelper extends Helper {
+  doSomething() { /* ... */ }
+}
+
+export default MyHelper
+```
+
+### Convert Page Objects
+
+Replace `module.exports = { ... }` with `export default { ... }`.
+
+Page objects gain new lifecycle hooks in 4.x: `_before`, `_after`, `_afterSuite`. They run automatically around suites that include the page object.
+
+### Convert Programmatic Usage
+
+3.x:
+
+```js
+const { codecept, container, event } = require('codeceptjs')
+```
+
+4.x:
+
+```js
+import codeceptjs, { container, event } from 'codeceptjs'
+```
+
+`Container.create()` and `Config.load()` are now **async**. Await them:
+
+```js
+const config = await Config.load('./codecept.conf.js')
+await Container.create(config, opts)
+```
+
+## 3. Remove Helpers That No Longer Exist
+
+| Removed helper | What to do |
+|----------------|------------|
+| `Nightmare` | Switch to `Playwright`, `Puppeteer`, or `WebDriver`. |
+| `Protractor` | Switch to `Playwright` or `WebDriver`. |
+| `TestCafe` | Switch to `Playwright`. |
+| `AI` | Use the top-level `ai:` config option and the new `aiTrace` plugin. |
+
+`Container.STANDARD_ACTING_HELPERS` no longer lists `TestCafe`.
+
+## 4. Replace or Remove Plugins
+
+| Removed plugin | Replacement |
+|----------------|-------------|
+| `autoLogin` | **`auth` plugin** β€” see [Authorization](/auth). |
+| `tryTo` | `import { tryTo } from 'codeceptjs/effects'` |
+| `retryTo` | `import { retryTo } from 'codeceptjs/effects'` |
+| `eachElement` | `import { eachElement } from 'codeceptjs/els'` |
+| `commentStep` | `import step from 'codeceptjs/steps'` then `step.section('name')` / `step.endSection()` |
+| `fakerTransform` | Import `@faker-js/faker` directly in tests. |
+| `enhancedRetryFailedStep` | Merged into `retryFailedStep`. Rename in config. |
+| `allure` | Use [@testomatio/reporter](https://testomat.io) or Mochawesome. |
+| `htmlReporter` | Use an external reporter. |
+| `wdio` | Configure WebdriverIO services directly in `helpers.WebDriver`. |
+| `selenoid` | Run Selenoid externally. |
+| `standardActingHelpers` | No longer needed; the list lives in core. |
+
+### `autoLogin` β†’ `auth`
+
+3.x:
+
+```js
+plugins: {
+  autoLogin: {
+    enabled: true,
+    saveToFile: true,
+    inject: 'login',
+    users: { admin: { login, check, fetch } },
+  },
+}
+```
+
+4.x:
+
+```js
+plugins: {
+  auth: {
+    enabled: true,
+    users: {
+      admin: {
+        login: (I) => { /* ... */ },
+        check: (I) => { /* ... */ },
+      },
+    },
+  },
+}
+```
+
+Inject `login` and call `login('admin')` β€” same as before.
+
+### New Plugins You Can Enable
+
+- **`aiTrace`** β€” captures failure traces (DOM, console, network, screenshots) for AI debugging. See [AI Trace](/aitrace).
+- **`pauseOn`** β€” pauses execution on a chosen event or on failure. See [Debugging](/debugging).
+
+## 5. Update Removed and Changed APIs
+
+### AI Config Now Uses Vercel AI SDK
+
+3.x required a hand-written `request` function that called your provider's SDK directly. 4.x replaces this with [Vercel AI SDK](https://ai-sdk.dev) β€” pass a `model` and CodeceptJS handles the calls.
+
+Install the SDK and the provider package you want:
+
+```bash
+npm install ai @ai-sdk/openai
+# or @ai-sdk/anthropic, @ai-sdk/google, @ai-sdk/mistral, @ai-sdk/groq, @ai-sdk/xai, @ai-sdk/azure, @ai-sdk/cohere
+```
+
+3.x:
+
+```js
+ai: {
+  request: async messages => {
+    const OpenAI = require('openai')
+    const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
+    const completion = await openai.chat.completions.create({
+      model: 'gpt-3.5-turbo',
+      messages,
+    })
+    return completion?.choices[0]?.message?.content
+  },
+}
+```
+
+4.x:
+
+```js
+import { openai } from '@ai-sdk/openai'
+
+export default {
+  ai: {
+    model: openai('gpt-5'),
+  },
+}
+```
+
+The same shape works for every supported provider β€” swap `openai('gpt-5')` for `anthropic('claude-sonnet-4-6')`, `google('gemini-1.5-flash')`, etc. API keys still come from environment variables (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GOOGLE_GENERATIVE_AI_API_KEY`, ...).
+
+The `request` function is no longer supported. Delete it from your config.
+
+See [Testing with AI](/ai) for the full provider list and prompt customization.
+
+### JSON Schema Validation: Joi β†’ Zod
+
+`I.seeResponseMatchesJsonSchema()` (from the `JSONResponse` helper) now validates with [Zod](https://zod.dev) instead of [Joi](https://joi.dev). Joi is gone from the dependency tree; Zod is bundled.
+
+Rewrite your schemas:
+
+3.x:
+
+```js
+const Joi = require('joi')
+
+I.seeResponseMatchesJsonSchema(Joi.object().keys({
+  name: Joi.string().required(),
+  email: Joi.string().email().required(),
+  age: Joi.number().integer().min(0),
+}))
+```
+
+4.x:
+
+```js
+import { z } from 'zod'
+
+I.seeResponseMatchesJsonSchema(z.object({
+  name: z.string(),
+  email: z.string().email(),
+  age: z.number().int().min(0),
+}))
+```
+
+Or pass a callback that receives `z`:
+
+```js
+I.seeResponseMatchesJsonSchema(z => z.object({
+  name: z.string(),
+  id: z.number(),
+}))
+```
+
+Common rewrites:
+
+| Joi | Zod |
+|-----|-----|
+| `Joi.object().keys({...})` | `z.object({...})` |
+| `Joi.string().required()` | `z.string()` (required by default) |
+| `Joi.string().email()` | `z.string().email()` |
+| `Joi.number().integer()` | `z.number().int()` |
+| `Joi.array().items(...)` | `z.array(...)` |
+| `Joi.string().optional()` | `z.string().optional()` |
+| `Joi.date()` | `z.string().datetime()` or `z.date()` |
+| `Joi.alternatives().try(a, b)` | `z.union([a, b])` |
+
+Uninstall `joi` from your project if you only used it for CodeceptJS schemas:
+
+```bash
+npm uninstall joi
+```
+
+### `restart: 'browser'` removed (Playwright)
+
+Use one of:
+
+- `restart: 'session'` β€” reset session per test (default)
+- `restart: 'context'` β€” new browser context per test
+- `restart: 'keep'` β€” keep one browser across tests
+
+### Custom Locator Strategy removed (Playwright)
+
+The `customLocators` strategy registration in Playwright config is removed. Use the `customLocator` plugin or built-in ARIA locators (`{ role: 'button', name: 'Submit' }`).
+
+### `I.retry()` is deprecated
+
+Use the step options API:
+
+```js
+import step from 'codeceptjs/steps'
+
+I.click('Submit', step.retry(3))
+I.fillField('Email', 'a@b.c', step.timeout(10))
+I.click('Add', step.opts({ elementIndex: 2 }))
+```
+
+### Effects and Assertions Are Subpath Imports
+
+```js
+import { within, tryTo, retryTo, hopeThat } from 'codeceptjs/effects'
+import { hopeThat } from 'codeceptjs/assertions'
+import { eachElement, element, expectElement } from 'codeceptjs/els'
+import step from 'codeceptjs/steps'
+import store from 'codeceptjs/store'
+```
+
+`tryTo` and `hopeThat` now return `Promise`. The 3.x generic `Promise` signature is gone.
+
+`hopeThat.noErrors()` is new β€” call it once at the end of a scenario to fail the test if any soft assertion failed.
+
+### Globals Are Deprecated β€” `noGlobals: true` Is the New Default
+
+Up to 3.x, almost everything was global: `Feature`, `Scenario`, `Before`, `pause`, `within`, `session`, `secret`, `Helper`, `actor`, `inject`, `share`, `locate`, `DataTable`, `Given`/`When`/`Then`, `codecept_dir`, `output_dir`.
+
+In 4.x:
+
+- `npx codeceptjs init` writes `noGlobals: true` into new configs.
+- Projects without `noGlobals` set keep the old behavior but print a deprecation warning on every run:
+
+  > Global functions are deprecated. Use `import { Helper, pause, within, session } from "codeceptjs"` instead. Set `noGlobals: true` in config to disable globals.
+
+To silence the warning, set `noGlobals: true`:
+
+```js
+// codecept.conf.js
+export const config = {
+  noGlobals: true,
+  // ...
+}
+```
+
+What changes when `noGlobals: true`:
+
+| Symbol | With `noGlobals: true` |
+|--------|------------------------|
+| `Feature`, `Scenario`, `xFeature`, `xScenario`, `BeforeSuite`, `AfterSuite`, `Before`, `After`, `Background`, `BeforeAll`, `AfterAll` | **Still work in test files** β€” Mocha injects these into the test context. No import needed. |
+| `inject()`, `share()` | **Still global.** No package export β€” keep using them as globals. |
+| `codecept_dir`, `output_dir` | **Still global** (kept for backward compatibility with external plugins). |
+| `pause`, `within`, `session`, `secret`, `locate`, `dataTable`, `actor`, `codeceptjs` | Import from `codeceptjs`. |
+| `Helper` (base class) | Import from `@codeceptjs/helper`. |
+| `Given`, `When`, `Then`, `And`, `DefineParameterType` (BDD step definitions) | Available as globals **inside Gherkin step definition files** (CodeceptJS scope-injects them while loading the step files). No import needed. |
+
+Imports for the new style:
+
+```js
+import { pause, within, session, secret, locate, dataTable, actor } from 'codeceptjs'
+import Helper from '@codeceptjs/helper'
+```
+
+Test files written for 3.x keep working until you flip the flag.
+
+### `wait*` Methods Resolve Relative URLs
+
+`waitInUrl`, `waitUrlEquals`, and `waitCurrentPathEquals` now resolve a relative path against the helper's configured `url` before comparing. In 3.x a literal substring match against `window.location.href` would fail for relative paths.
+
+```js
+// helpers: { Playwright: { url: 'https://app.example.com' } }
+
+I.waitUrlEquals('/dashboard')   // matches https://app.example.com/dashboard
+I.waitInUrl('/users')           // matches any URL containing /users
+```
+
+`waitUrlEquals` error messages now include the actual URL the page was on when the wait timed out β€” easier to diagnose `/dashboard` vs `/dashboard?session=expired`.
+
+## 6. Adopt New Behaviors
+
+### Strict Mode
+
+Playwright, Puppeteer, and WebDriver helpers support `strict: true`. Any locator that matches more than one element throws `MultipleElementsFound` instead of silently picking the first match.
+
+```js
+helpers: {
+  Playwright: { url: '...', strict: true },
+}
+```
+
+Per-step alternative: `I.click('a', step.opts({ exact: true }))`.
+
+The error includes a `fetchDetails()` method that prints XPaths and HTML for every match.
+
+### Element Index
+
+Pick a specific match without writing a more specific locator:
+
+```js
+I.click('a', step.opts({ elementIndex: 2 }))
+I.click('a', step.opts({ elementIndex: 'last' }))
+I.fillField('input', 'x', step.opts({ elementIndex: -1 }))
+```
+
+### Unfocused Element Detection
+
+`I.type()` and `I.pressKey()` throw `NonFocusedType` if no element has focus. Click or focus the field first.
+
+### Context Parameter on Form Methods
+
+`appendField`, `clearField`, `attachFile`, and `moveCursorTo` accept an optional second context argument, matching `fillField` and `click`.
+
+### Other New Methods
+
+- `I.seeCurrentPathEquals(path)` / `I.dontSeeCurrentPathEquals(path)` β€” compare the path ignoring query strings.
+- `I.waitCurrentPathEquals(path, sec?)` β€” wait until the path matches.
+- `I.seeFileDownloaded(name)`
+- `I.clickXY(locator?, x, y)` β€” click at coordinates, either page-relative or element-relative.
+- `I.grabAriaSnapshot(locator?)` β€” capture an accessibility-tree snapshot for the page or a region (Playwright).
+- `I.grabWebElement(locator)` / `I.grabWebElements(locator)` β€” return helper-agnostic `WebElement` wrappers.
+- `attachFile` β€” supports drag-and-drop dropzones.
+- `fillField` β€” supports rich text editors (CKEditor, ProseMirror, etc.).
+- BDD: `But` keyword is recognized.
+
+## 7. Update Dependency Versions
+
+If your project depends on these directly, check for breakage:
+
+| Package | 3.x | 4.x |
+|---------|-----|-----|
+| `chai` | ^4 | ^6 (ESM-only) |
+| `chai-as-promised` | 7 | 8 (ESM-only) |
+| `@cucumber/gherkin` | 35 | 38 |
+| `@cucumber/messages` | 29 | 32 |
+| `chokidar` | 4 | 5 |
+| `commander` | 11 | 14 |
+| `@faker-js/faker` | 9 | 10 |
+| `webdriverio` | 9.12 | 9.23 |
+| `puppeteer` | 24.15 | 24.36 |
+| `electron` | 38 | 40 |
+| `typescript` | 5.8 | 5.9 |
+| `testcafe` | 3.7.2 | **removed** |
+| `inquirer-test` | 2.0.1 | **removed** |
+| `joi` | 18 | **removed** β€” use `zod` |
+| `zod` | β€” | added (^4) β€” schema validation in `JSONResponse` |
+| `tsx` | β€” | added as optional peer |
+| `@modelcontextprotocol/sdk` | β€” | added |
+| `@testomatio/reporter` | β€” | added |
+
+## 8. New Capabilities Worth Knowing
+
+You don't need these to upgrade, but they unlock new workflows:
+
+- **MCP server** β€” `bin/mcp-server.js` (also installed as `codeceptjs-mcp`) exposes CodeceptJS to AI agents through Model Context Protocol. See [MCP](/mcp).
+- **WebElement wrapper** β€” `grabWebElements()` returns helper-agnostic `WebElement` instances with a unified API.
+- **ARIA-first locators** β€” `{ role: 'button', name: 'Submit' }` works in Playwright, Puppeteer, and WebDriver. The `role` type is now first-class in `Locator`. See [Locators](/locators#aria-locators).
+- **Locator DSL** β€” `locate(...)` gains `.withClass()`, `.not()` negation, raw-predicate helpers, and a `role` selector type.
+- **Workers** β€” the `event` dispatcher fires inside worker processes, so listeners and plugins observe parallel runs the same way they observe single-process runs.
+- **Path normalization** β€” file-path handling is normalized cross-platform; tests authored on Windows run unchanged on Linux/CI.
+- **Test metadata** β€” the `Scenario` callback receives a `test` object with `test.tags`, `test.artifacts`, `test.meta`, and `test.notes` for custom reporting.
+- **Security** β€” the `emptyFolder` utility (used by output cleanup) no longer shells out via `rm -rf`, closing a command-injection vector ([#5191](https://github.com/codeceptjs/CodeceptJS/pull/5191)).
+
+## 9. Verify the Upgrade
+
+1. `npx codeceptjs check` β€” surfaces config issues.
+2. `npx codeceptjs run --debug` on a small smoke suite. Confirm the run starts and steps execute.
+3. `npx codeceptjs run --workers 2` β€” confirm parallel execution.
+4. TypeScript users: run with `tsx` installed and confirm error stack traces point at `.ts` files.
+5. If you removed `autoLogin`: confirm sessions restore under the `auth` plugin.
+6. If you used `tryTo` / `retryTo` / `eachElement` plugins: grep your tests for the old globals and switch to subpath imports.
+7. CI: bump the Node version to 20+ if you were on 18 or below.
diff --git a/docs/probe.html b/docs/probe.html
deleted file mode 100644
index b22b06a51..000000000
--- a/docs/probe.html
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-

-
diff --git a/docs/quickstart.md b/docs/quickstart.md
index 69cb9e5f2..593dfe4b5 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -5,101 +5,72 @@ layout: Section
 sidebar: true
 ---
 
-::: slot sidebar
-
-#### Use WebDriver for classical Selenium setup
-
-
-This gives you access to rich Selenium ecosystem and cross-browser support for majority of browsers and devices.
-
-
-
-Start with WebDriver »
-
- WebDriver support is implemented via [webdriverio](https://webdriver.io) library 
-
----
-
-* [Mobile Testing with Appium Β»](/mobile)
-
-:::
-
 # Quickstart
 
-
-Use [CodeceptJS all-in-one installer](https://github.com/codeceptjs/create-codeceptjs) to get CodeceptJS, a demo project, and Playwright.
+Install CodeceptJS into your project:
 
 ```
-npx create-codeceptjs .
+npm install codeceptjs playwright --save-dev
 ```
 
-If you prefer not to use Playwright see other [installation options](/installation/).
-
-![Installation](/img/codeceptinstall.gif)
-
-> To install codeceptjs into a different folder, like `tests` use `npx create-codeceptjs tests`
-
-After CodeceptJS is installed, try running **demo tests** using this commands:
+Then install the browser binaries:
 
-* `npm run codeceptjs:demo` - executes demo tests in window mode
-* `npm run codeceptjs:demo:headless` - executes demo tests in headless mode
-* `npm run codeceptjs:demo:ui` - open CodeceptJS UI to list and run demo tests. 
+```
+npx playwright install --with-deps
+```
 
-[CodeceptJS UI](/ui) application:
+The `--with-deps` flag also installs required system dependencies for the browsers.
 
-![](https://user-images.githubusercontent.com/220264/93860826-4d5fbc80-fcc8-11ea-99dc-af816f3db466.png)
+> Prefer WebDriver or Appium? See [installation options](/installation/) for all supported helpers.
 
 ---
 
 ### Init
 
-To start a new project initialize CodeceptJS to create main config file: `codecept.conf.js`.
+Initialize CodeceptJS to set up the config file and test directory:
 
 ```
 npx codeceptjs init
 ```
 
-Answer questions, agree on defaults:
+This command walks you through a short setup wizard and creates `codecept.conf.js`, a sample test file, and any required browser binaries.
 
+Answer the questions, accepting defaults to get started quickly:
 
 | Question | Default Answer  | Alternative
 |---|---|---|
 | Do you plan to write tests in TypeScript?  | **n** (No)  | or [learn how to use TypeScript](/typescript)
 | Where are your tests located? | `**./*_test.js` | or any glob pattern like `**.spec.js`
-| What helpers do you want to use? | **Playwright** | Which helper to use for: [web testing](https://codecept.io/basics/#architecture), [mobile testing](https://codecept.io/mobile/), [API testing](https://codecept.io/api/)
-| Where should logs, screenshots, and reports to be stored? | `./output` | path to store artifacts and temporary files 
-| Do you want to enable localization for tests? | **n** English (no localization) | or write [localized tests](https://codecept.io/translation/) in your language
-  
-
-Sample output:
+| What helpers do you want to use? | **Playwright** | See options for [web testing](https://codecept.io/basics/#architecture), [mobile testing](https://codecept.io/mobile/), [API testing](https://codecept.io/api/)
+| Where should logs, screenshots, and reports be stored? | `./output` | path to store artifacts and temporary files
 
-```js
-? Do you plan to write tests in TypeScript? 'No'
-? Where are your tests located? '**./*_test.js'
-? What helpers do you want to use? 'Playwright'
-? Where should logs, screenshots, and reports to be stored? '**./output**'
-? Do you want to enable localization for tests? 'English (no localization)'
-```
-
-For Playwright helper provide a website to be tested and browser to be used:
+For Playwright, you'll also be asked about the site and browser:
 
 | Question | Default Answer  | Alternative
 |---|---|---|
-| Base url of site to be tested | http://localhost | Base URL of website you plan to test. Use http://github.com or [sample checkout page](https://getbootstrap.com/docs/5.2/examples/checkout/) if you just want to play around
-| Show browser window | **y** Yes | or run browser in **headless mode** 
-| Browser in which testing will be performed | **chromium** | or run tests in firefox, webkit (which is opensource version of Safari) or launch electron app
+| Base url of site to be tested | http://localhost | URL of the site you plan to test
+| Show browser window | **y** Yes | or run in **headless mode**
+| Browser | **chromium** | or `firefox`, `webkit` (open-source Safari), or `electron`
 
-```js
-? [Playwright] Base url of site to be tested 'http://mysite.com'
-? [Playwright] Show browser window 'Yes'
-? [Playwright] Browser in which testing will be performed. Possible options: chromium, firefox, webkit or electron 'chromium'
+Sample output:
 
 ```
+? Do you plan to write tests in TypeScript? No
+? Where are your tests located? **./*_test.js
+? What helpers do you want to use? Playwright
+? Where should logs, screenshots, and reports be stored? ./output
+? [Playwright] Base url of site to be tested http://localhost
+? [Playwright] Show browser window Yes
+? [Playwright] Browser in which testing will be performed chromium
+```
+
+When asked, create your first feature and test file.
+
+---
 
-Create first feature and test when asked
+### Write Your First Test
 
-Open a newly created file in your favorite JavaScript editor. 
-The file should look like this:
+Open the generated test file. It will look like this:
 
 ```js
 Feature('My First Test');
@@ -108,7 +79,8 @@ Scenario('test something', ({ I }) => {
 
 });
 ```
-Write a simple test scenario:
+
+Add a simple scenario:
 
 ```js
 Feature('My First Test');
@@ -119,13 +91,15 @@ Scenario('test something', ({ I }) => {
 });
 ```
 
-Run a test:
+---
+
+### Run Tests
 
 ```
 npx codeceptjs run
 ```
 
-The output should be similar to this:
+Expected output:
 
 ```bash
 My First Test --
@@ -135,18 +109,12 @@ My First Test --
  βœ“ OK
 ```
 
-To quickly execute tests use following npm scripts:
-
-After CodeceptJS is installed, try running **demo tests** using this commands:
-
-* `npm run codeceptjs` - executes tests in window mode
-* `npm run codeceptjs:headless` - executes tests in headless mode
-* `npm run codeceptjs:ui` - open CodeceptJS UI to list and run tests. 
+Run in headless mode:
 
-More commands available in [CodeceptJS CLI runner](https://codecept.io/commands/).
+```
+npx codeceptjs run --headless
+```
 
+See all available commands in the [CLI reference](https://codecept.io/commands/).
 
 > [β–Ά Next: CodeceptJS Basics](/basics/)
-
-> [β–Ά Next: CodeceptJS with Playwright](/playwright/)
-
diff --git a/lib/config.js b/lib/config.js
index 0b3372e32..7f54dfe20 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -124,6 +124,24 @@ class Config {
     hooks.push(fn)
   }
 
+  /**
+   * Number of registered config hooks. Useful for snapshotting before a phase
+   * (e.g. plugin loading) and re-running only the hooks added during it.
+   * @return {number}
+   */
+  static hooksCount() {
+    return hooks.length
+  }
+
+  /**
+   * Run hooks in `[fromIndex, end)` against the given config object, mutating it.
+   * @param {number} fromIndex
+   * @param {Object} cfg
+   */
+  static runHooksFrom(fromIndex, cfg) {
+    for (let i = fromIndex; i < hooks.length; i++) hooks[i](cfg)
+  }
+
   /**
    * Appends values to current config
    *
diff --git a/lib/container.js b/lib/container.js
index faa1bde06..108cf331e 100644
--- a/lib/container.js
+++ b/lib/container.js
@@ -15,6 +15,7 @@ import store from './store.js'
 import Result from './result.js'
 import ai from './ai.js'
 import actorFactory from './actor.js'
+import Config from './config.js'
 
 let asyncHelperPromise
 
@@ -76,6 +77,7 @@ class Container {
     container.translation = await loadTranslation(config.translation || null, config.vocabularies || [])
     container.proxySupportConfig = config.include || {}
     container.proxySupport = createSupportObjects(container.proxySupportConfig)
+    const hooksBeforePlugins = Config.hooksCount()
     container.plugins = await createPlugins(config.plugins || {}, opts)
     container.result = new Result()
 
@@ -121,6 +123,18 @@ class Container {
     // Wait for all async helpers to finish loading and populate the actor
     await asyncHelperPromise
 
+    // If plugins registered any Config hooks during their boot, run them now
+    // and re-apply the (possibly mutated) helper config to already-instantiated helpers.
+    if (Config.hooksCount() > hooksBeforePlugins) {
+      Config.runHooksFrom(hooksBeforePlugins, config)
+      for (const name of Object.keys(container.helpers)) {
+        const helper = container.helpers[name]
+        if (helper && typeof helper._setConfig === 'function' && config.helpers && config.helpers[name]) {
+          helper._setConfig(config.helpers[name])
+        }
+      }
+    }
+
     if (opts && opts.ai) ai.enable(config.ai) // enable AI Assistant
     if (config.gherkin) await loadGherkinStepsAsync(config.gherkin.steps || [])
     if (opts && typeof opts.timeouts === 'boolean') store.timeouts = opts.timeouts
@@ -748,12 +762,24 @@ async function createPlugins(config, options = {}) {
 }
 
 async function loadGherkinStepsAsync(paths) {
+  // Import BDD module to access step file tracking functions and step DSL
+  const bddModule = await import('./mocha/bdd.js')
+
   global.Before = fn => event.dispatcher.on(event.test.started, fn)
   global.After = fn => event.dispatcher.on(event.test.finished, fn)
   global.Fail = fn => event.dispatcher.on(event.test.failed, fn)
 
-  // Import BDD module to access step file tracking functions
-  const bddModule = await import('./mocha/bdd.js')
+  // Scope-inject Given/When/Then/And while loading step files so they work
+  // with noGlobals: true. When noGlobals: false, globals.js has already set
+  // them as permanent globals β€” skip to avoid deleting them at the end.
+  const injectStepDsl = !!store.noGlobals
+  if (injectStepDsl) {
+    global.Given = bddModule.Given
+    global.When = bddModule.When
+    global.Then = bddModule.Then
+    global.And = bddModule.And
+    global.DefineParameterType = bddModule.defineParameterType
+  }
 
   // If gherkin.steps is string, then this will iterate through that folder and send all step def js files to loadSupportObject
   // If gherkin.steps is Array, it will go the old way
@@ -781,6 +807,13 @@ async function loadGherkinStepsAsync(paths) {
   delete global.Before
   delete global.After
   delete global.Fail
+  if (injectStepDsl) {
+    delete global.Given
+    delete global.When
+    delete global.Then
+    delete global.And
+    delete global.DefineParameterType
+  }
 }
 
 function loadGherkinSteps(paths) {
diff --git a/lib/helper/AI.js b/lib/helper/AI.js
deleted file mode 100644
index 8d709449c..000000000
--- a/lib/helper/AI.js
+++ /dev/null
@@ -1,214 +0,0 @@
-import HelperModule from '@codeceptjs/helper'
-import ora from 'ora-classic'
-import fs from 'fs'
-import path from 'path'
-import ai from '../ai.js'
-import Container from '../container.js'
-import { splitByChunks, minifyHtml } from '../html.js'
-import { beautify } from '../utils.js'
-import output from '../output.js'
-import { registerVariable } from '../pause.js'
-
-const standardActingHelpers = Container.STANDARD_ACTING_HELPERS
-
-const gtpRole = {
-  user: 'user',
-}
-
-/**
- * AI Helper for CodeceptJS.
- *
- * This helper class provides integration with the AI GPT-3.5 or 4 language model for generating responses to questions or prompts within the context of web pages. It allows you to interact with the GPT-3.5 model to obtain intelligent responses based on HTML fragments or general prompts.
- * This helper should be enabled with any web helpers like Playwright or Puppeteer or WebDriver to ensure the HTML context is available.
- *
- * Use it only in development mode. It is recommended to run it only inside pause() mode.
- *
- * ## Configuration
- *
- * This helper should be configured in codecept.conf.{js|ts}
- *
- * * `chunkSize`: (optional, default: 80000) - The maximum number of characters to send to the AI API at once. We split HTML fragments by 8000 chars to not exceed token limit. Increase this value if you use GPT-4.
- */
-class AI extends Helper {
-  constructor(config) {
-    super(config)
-    this.aiAssistant = ai
-
-    this.options = {
-      chunkSize: 80000,
-    }
-    this.options = { ...this.options, ...config }
-    this.aiAssistant.enable(this.config)
-  }
-
-  _beforeSuite() {
-    const helpers = Container.helpers()
-
-    for (const helperName of standardActingHelpers) {
-      if (Object.keys(helpers).indexOf(helperName) > -1) {
-        this.helper = helpers[helperName]
-        break
-      }
-    }
-  }
-
-  /**
-   * Asks the AI GPT language model a question based on the provided prompt within the context of the current page's HTML.
-   *
-   * ```js
-   * I.askGptOnPage('what does this page do?');
-   * ```
-   *
-   * @async
-   * @param {string} prompt - The question or prompt to ask the GPT model.
-   * @returns {Promise} - A Promise that resolves to the generated responses from the GPT model, joined by newlines.
-   */
-  async askGptOnPage(prompt) {
-    const html = await this.helper.grabSource()
-
-    const htmlChunks = splitByChunks(html, this.options.chunkSize)
-
-    if (htmlChunks.length > 1) this.debug(`Splitting HTML into ${htmlChunks.length} chunks`)
-
-    const responses = []
-
-    for (const chunk of htmlChunks) {
-      const messages = [
-        { role: gtpRole.user, content: prompt },
-        { role: gtpRole.user, content: `Within this HTML: ${await minifyHtml(chunk)}` },
-      ]
-
-      if (htmlChunks.length > 1)
-        messages.push({
-          role: 'user',
-          content: 'If action is not possible on this page, do not propose anything, I will send another HTML fragment',
-        })
-
-      const response = await this._processAIRequest(messages)
-
-      output.print(response)
-
-      responses.push(response)
-    }
-
-    return responses.join('\n\n')
-  }
-
-  /**
-   * Asks the AI a question based on the provided prompt within the context of a specific HTML fragment on the current page.
-   *
-   * ```js
-   * I.askGptOnPageFragment('describe features of this screen', '.screen');
-   * ```
-   *
-   * @async
-   * @param {string} prompt - The question or prompt to ask the GPT-3.5 model.
-   * @param {string} locator - The locator or selector used to identify the HTML fragment on the page.
-   * @returns {Promise} - A Promise that resolves to the generated response from the GPT model.
-   */
-  async askGptOnPageFragment(prompt, locator) {
-    const html = await this.helper.grabHTMLFrom(locator)
-
-    const messages = [
-      { role: gtpRole.user, content: prompt },
-      { role: gtpRole.user, content: `Within this HTML: ${await minifyHtml(html)}` },
-    ]
-
-    const response = await this._processAIRequest(messages)
-
-    output.print(response)
-
-    return response
-  }
-
-  /**
-   * Send a general request to AI and return response.
-   * @param {string} prompt
-   * @returns {Promise} - A Promise that resolves to the generated response from the GPT model.
-   */
-  async askGptGeneralPrompt(prompt) {
-    const messages = [{ role: gtpRole.user, content: prompt }]
-
-    const response = await this._processAIRequest(messages)
-
-    output.print(response)
-
-    return response
-  }
-
-  /**
-   * Generates PageObject for current page using AI.
-   *
-   * It saves the PageObject to the output directory. You can review the page object and adjust it as needed and move to pages directory.
-   * Prompt can be customized in a global config file.
-   *
-   * ```js
-   * // create page object for whole page
-   * I.askForPageObject('home');
-   *
-   * // create page object with extra prompt
-   * I.askForPageObject('home', 'implement signIn(username, password) method');
-   *
-   * // create page object for a specific element
-   * I.askForPageObject('home', null, '.detail');
-   * ```
-   *
-   * Asks for a page object based on the provided page name, locator, and extra prompt.
-   *
-   * @async
-   * @param {string} pageName - The name of the page to retrieve the object for.
-   * @param {string|null} [extraPrompt=null] - An optional extra prompt for additional context or information.
-   * @param {string|null} [locator=null] - An optional locator to find a specific element on the page.
-   * @returns {Promise} A promise that resolves to the requested page object.
-   */
-  async askForPageObject(pageName, extraPrompt = null, locator = null) {
-    const spinner = ora(' Processing AI request...').start()
-
-    try {
-      const html = locator ? await this.helper.grabHTMLFrom(locator) : await this.helper.grabSource()
-      await this.aiAssistant.setHtmlContext(html)
-      const response = await this.aiAssistant.generatePageObject(extraPrompt, locator)
-      spinner.stop()
-
-      if (!response[0]) {
-        output.error('No response from AI')
-        return ''
-      }
-
-      const code = beautify(response[0])
-
-      output.print('----- Generated PageObject ----')
-      output.print(code)
-      output.print('-------------------------------')
-
-      const fileName = path.join(output_dir, `${pageName}Page-${Date.now()}.js`)
-
-      output.print(output.styles.bold(`Page object for ${pageName} is saved to ${output.styles.bold(fileName)}`))
-      fs.writeFileSync(fileName, code)
-
-      try {
-        registerVariable('page', require(fileName))
-        output.success('Page object registered for this session as `page` variable')
-        output.print('Use `=>page.methodName()` in shell to run methods of page object')
-        output.print('Use `click(page.locatorName)` to check locators of page object')
-      } catch (err) {
-        output.error('Error while registering page object')
-        output.error(err.message)
-      }
-
-      return code
-    } catch (e) {
-      spinner.stop()
-      throw Error(`Something went wrong! ${e.message}`)
-    }
-  }
-
-  async _processAIRequest(messages) {
-    const spinner = ora(' Processing AI request...').start()
-    const response = await this.aiAssistant.createCompletion(messages)
-    spinner.stop()
-    return response
-  }
-}
-
-export default AI
diff --git a/lib/plugin/browser.js b/lib/plugin/browser.js
new file mode 100644
index 000000000..b7033bde0
--- /dev/null
+++ b/lib/plugin/browser.js
@@ -0,0 +1,173 @@
+import output from '../output.js'
+import Config from '../config.js'
+
+const BROWSER_HELPERS = ['Playwright', 'Puppeteer', 'WebDriver', 'Appium']
+
+const PUPPETEER_BROWSERS = ['chrome', 'firefox']
+const PLAYWRIGHT_BROWSERS = ['chromium', 'webkit', 'firefox']
+
+/**
+ * Overrides browser helper config from the command line. Works for all browser helpers
+ * (Playwright, Puppeteer, WebDriver, Appium) without touching `codecept.conf`.
+ *
+ * Enable it via `-p` option with one or more colon-chained args:
+ *
+ * ```
+ * npx codeceptjs run -p browser:show
+ * npx codeceptjs run -p browser:hide
+ * npx codeceptjs run -p browser:browser=firefox
+ * npx codeceptjs run -p browser:windowSize=1024x768:video=false
+ * npx codeceptjs run -p browser:hide:browser=webkit:windowSize=800x600
+ * ```
+ *
+ * #### Args
+ *
+ * * **show** β€” force visible browser
+ * * **hide** β€” force headless (also injects `--headless` into WebDriver chrome/firefox capability args)
+ * * **`=`** β€” sets `helpers.. = `. Three keys
+ *   get per-helper translation:
+ *     * `browser=` β€” Puppeteer receives `product`, Playwright receives `browser`,
+ *       WebDriver receives `browser`. Validated per helper.
+ *     * `windowSize=WxH` β€” sets `windowSize` on each helper, plus `--window-size=W,H`
+ *       chromium/chrome args for Playwright/Puppeteer.
+ *     * `show=true|false` β€” sets `show` on Playwright/Puppeteer; injects/strips
+ *       `--headless` in WebDriver chrome/firefox capability args.
+ *
+ * Values are coerced: `true`/`false` β†’ boolean, numbers β†’ Number, otherwise string.
+ * Keys whose value is `undefined` are skipped.
+ */
+export default function (config = {}) {
+  const args = config._args || []
+  if (!args.length) return
+
+  const opts = {}
+  for (const arg of args) {
+    if (!arg) continue
+    if (arg === 'show') {
+      opts.show = true
+      continue
+    }
+    if (arg === 'hide') {
+      opts.show = false
+      continue
+    }
+    const eq = arg.indexOf('=')
+    if (eq < 0) {
+      output.error(`browser plugin: unknown arg "${arg}"`)
+      continue
+    }
+    opts[arg.slice(0, eq)] = coerce(arg.slice(eq + 1))
+  }
+
+  if (Object.keys(opts).length === 0) return
+
+  Config.addHook(cfg => applyToHelpers(cfg, opts))
+
+  const summary = Object.entries(opts).map(([k, v]) => `${k}=${v}`).join(', ')
+  output.debug(`browser plugin: applied ${summary}`)
+}
+
+function applyToHelpers(cfg, opts) {
+  if (!cfg.helpers) return
+  const { browser, show, windowSize, ...rest } = opts
+
+  for (const name of BROWSER_HELPERS) {
+    const helper = cfg.helpers[name]
+    if (!helper) continue
+
+    if (browser !== undefined && browser !== null && browser !== '') {
+      applyBrowser(name, helper, browser)
+    }
+    if (show === true) applyHeaded(name, helper)
+    else if (show === false) applyHeadless(name, helper)
+    if (windowSize) applyWindowSize(name, helper, String(windowSize))
+
+    for (const k of Object.keys(rest)) {
+      if (rest[k] !== undefined) helper[k] = rest[k]
+    }
+  }
+}
+
+function applyBrowser(helperName, helper, browser) {
+  if (helperName === 'Puppeteer') {
+    if (!PUPPETEER_BROWSERS.includes(browser)) {
+      throw new Error(`Browser ${browser} is not supported by Puppeteer engine`)
+    }
+    helper.product = browser
+    return
+  }
+  if (helperName === 'Playwright') {
+    if (!PLAYWRIGHT_BROWSERS.includes(browser)) {
+      throw new Error(`Browser ${browser} is not supported by Playwright engine`)
+    }
+    helper.browser = browser
+    return
+  }
+  helper.browser = browser
+}
+
+function applyHeaded(helperName, helper) {
+  if (helperName === 'Playwright' || helperName === 'Puppeteer') {
+    helper.show = true
+    return
+  }
+  if (helperName === 'WebDriver') {
+    stripHeadlessArgs(helper, 'desiredCapabilities')
+    stripHeadlessArgs(helper, 'capabilities')
+  }
+}
+
+function applyHeadless(helperName, helper) {
+  if (helperName === 'Playwright' || helperName === 'Puppeteer') {
+    helper.show = false
+    return
+  }
+  if (helperName === 'WebDriver') {
+    if (helper.browser === 'chrome') {
+      injectHeadlessArgs(helper, 'chromeOptions', ['--headless', '--disable-gpu'])
+    } else if (helper.browser === 'firefox') {
+      injectHeadlessArgs(helper, 'firefoxOptions', ['--headless'])
+    }
+  }
+}
+
+function applyWindowSize(helperName, helper, windowSize) {
+  if (!/^\d+x\d+$/.test(windowSize)) return
+  helper.windowSize = windowSize
+  const [w, h] = windowSize.split('x')
+
+  if (helperName === 'Playwright') {
+    helper.chromium = helper.chromium || {}
+    helper.chromium.args = (helper.chromium.args || []).concat(['--no-sandbox', `--window-size=${w},${h}`])
+    helper.chromium.defaultViewport = null
+    return
+  }
+  if (helperName === 'Puppeteer') {
+    helper.chrome = helper.chrome || {}
+    helper.chrome.args = (helper.chrome.args || []).concat(['--no-sandbox', `--window-size=${w},${h}`])
+    helper.chrome.defaultViewport = null
+  }
+}
+
+function injectHeadlessArgs(helper, optsKey, args) {
+  helper.desiredCapabilities = helper.desiredCapabilities || {}
+  helper.desiredCapabilities[optsKey] = helper.desiredCapabilities[optsKey] || {}
+  helper.desiredCapabilities[optsKey].args = (helper.desiredCapabilities[optsKey].args || []).concat(args)
+}
+
+function stripHeadlessArgs(helper, capsKey) {
+  const caps = helper[capsKey]
+  if (!caps) return
+  for (const optsKey of ['chromeOptions', 'firefoxOptions']) {
+    if (caps[optsKey] && Array.isArray(caps[optsKey].args)) {
+      caps[optsKey].args = caps[optsKey].args.filter(a => a !== '--headless')
+    }
+  }
+}
+
+function coerce(v) {
+  if (v === 'true') return true
+  if (v === 'false') return false
+  if (/^-?\d+(\.\d+)?$/.test(v)) return Number(v)
+  return v
+}
diff --git a/package.json b/package.json
index c25a15410..dbc6430df 100644
--- a/package.json
+++ b/package.json
@@ -87,7 +87,7 @@
     "publish-beta": "./runok.cjs publish:next-beta-version"
   },
   "dependencies": {
-    "@codeceptjs/configure": "1.0.6",
+    "@codeceptjs/configure": "^4.0.0-beta.2",
     "@codeceptjs/helper": "2.0.4",
     "@cucumber/cucumber-expressions": "18",
     "@cucumber/gherkin": "38.0.0",
@@ -115,7 +115,6 @@
     "html-minifier-terser": "7.2.0",
     "inquirer": "^8.2.7",
     "invisi-data": "^1.0.0",
-    "joi": "18.0.2",
     "js-beautify": "1.15.4",
     "lodash.clonedeep": "4.5.0",
     "lodash.merge": "4.6.2",
@@ -131,7 +130,8 @@
     "promise-retry": "1.1.1",
     "resq": "1.11.0",
     "sprintf-js": "1.1.3",
-    "uuid": "11.1.0"
+    "uuid": "11.1.0",
+    "zod": "^4.1.11"
   },
   "optionalDependencies": {
     "@codeceptjs/detox-helper": "1.1.13"
diff --git a/test/unit/plugin/browser_test.js b/test/unit/plugin/browser_test.js
new file mode 100644
index 000000000..8a6e07601
--- /dev/null
+++ b/test/unit/plugin/browser_test.js
@@ -0,0 +1,140 @@
+import { expect } from 'chai'
+import browser from '../../../lib/plugin/browser.js'
+import Config from '../../../lib/config.js'
+
+function applyAndCreate(args, base = {}) {
+  Config.reset()
+  browser({ _args: args })
+  return Config.create(base)
+}
+
+describe('browser plugin', () => {
+  beforeEach(() => Config.reset())
+
+  it('does nothing when no args passed', () => {
+    const cfg = applyAndCreate([], { helpers: { Playwright: { show: true } } })
+    expect(cfg.helpers.Playwright.show).to.equal(true)
+  })
+
+  describe('show / hide flags', () => {
+    it('show forces headed for Playwright + Puppeteer', () => {
+      const cfg = applyAndCreate(['show'], {
+        helpers: { Playwright: { show: false }, Puppeteer: { show: false } },
+      })
+      expect(cfg.helpers.Playwright.show).to.equal(true)
+      expect(cfg.helpers.Puppeteer.show).to.equal(true)
+    })
+
+    it('hide forces headless for Playwright + Puppeteer', () => {
+      const cfg = applyAndCreate(['hide'], {
+        helpers: { Playwright: { show: true }, Puppeteer: { show: true } },
+      })
+      expect(cfg.helpers.Playwright.show).to.equal(false)
+      expect(cfg.helpers.Puppeteer.show).to.equal(false)
+    })
+
+    it('hide injects --headless into WebDriver chrome capability args', () => {
+      const cfg = applyAndCreate(['hide'], {
+        helpers: { WebDriver: { browser: 'chrome' } },
+      })
+      const args = cfg.helpers.WebDriver.desiredCapabilities.chromeOptions.args
+      expect(args).to.include('--headless')
+    })
+
+    it('show strips --headless from WebDriver chrome capability args', () => {
+      const cfg = applyAndCreate(['show'], {
+        helpers: {
+          WebDriver: { browser: 'chrome', desiredCapabilities: { chromeOptions: { args: ['--headless', '--disable-gpu'] } } },
+        },
+      })
+      const args = cfg.helpers.WebDriver.desiredCapabilities.chromeOptions.args
+      expect(args).not.to.include('--headless')
+      expect(args).to.include('--disable-gpu')
+    })
+  })
+
+  describe('windowSize', () => {
+    it('windowSize=WxH sets windowSize across browser helpers and chrome args', () => {
+      const cfg = applyAndCreate(['windowSize=800x600'], {
+        helpers: { Playwright: {}, Puppeteer: {}, WebDriver: {} },
+      })
+      expect(cfg.helpers.Playwright.windowSize).to.equal('800x600')
+      expect(cfg.helpers.Playwright.chromium.args).to.include('--window-size=800,600')
+      expect(cfg.helpers.Puppeteer.windowSize).to.equal('800x600')
+      expect(cfg.helpers.WebDriver.windowSize).to.equal('800x600')
+    })
+  })
+
+  describe('generic key=value passthrough', () => {
+    it('coerces booleans and applies to every browser helper present', () => {
+      const cfg = applyAndCreate(['video=false'], {
+        helpers: { Playwright: {}, Puppeteer: {}, WebDriver: {}, Appium: {} },
+      })
+      expect(cfg.helpers.Playwright.video).to.equal(false)
+      expect(cfg.helpers.Puppeteer.video).to.equal(false)
+      expect(cfg.helpers.WebDriver.video).to.equal(false)
+      expect(cfg.helpers.Appium.video).to.equal(false)
+    })
+
+    it('coerces numbers', () => {
+      const cfg = applyAndCreate(['waitForTimeout=5000'], {
+        helpers: { Playwright: {} },
+      })
+      expect(cfg.helpers.Playwright.waitForTimeout).to.equal(5000)
+    })
+
+    it('keeps strings as strings', () => {
+      const cfg = applyAndCreate(['url=http://staging.test'], {
+        helpers: { Playwright: {} },
+      })
+      expect(cfg.helpers.Playwright.url).to.equal('http://staging.test')
+    })
+
+    it('skips helpers not present in config without errors', () => {
+      const cfg = applyAndCreate(['video=true'], {
+        helpers: { Playwright: {} }, // Puppeteer/WebDriver absent
+      })
+      expect(cfg.helpers.Playwright.video).to.equal(true)
+      expect(cfg.helpers.Puppeteer).to.equal(undefined)
+    })
+  })
+
+  describe('browser engine selection', () => {
+    it('browser=firefox routes through setBrowser, Puppeteer gets product', () => {
+      const cfg = applyAndCreate(['browser=firefox'], {
+        helpers: { Puppeteer: {}, Playwright: {} },
+      })
+      expect(cfg.helpers.Puppeteer.product).to.equal('firefox')
+      expect(cfg.helpers.Puppeteer.browser).to.equal(undefined)
+      expect(cfg.helpers.Playwright.browser).to.equal('firefox')
+    })
+
+    it('browser=webkit + show=false combine cleanly', () => {
+      const cfg = applyAndCreate(['hide', 'browser=webkit'], {
+        helpers: { Playwright: { show: true } },
+      })
+      expect(cfg.helpers.Playwright.browser).to.equal('webkit')
+      expect(cfg.helpers.Playwright.show).to.equal(false)
+    })
+  })
+
+  describe('combined args', () => {
+    it('applies show + windowSize + key=value in a single call', () => {
+      const cfg = applyAndCreate(['show', 'windowSize=1024x768', 'video=false'], {
+        helpers: { Playwright: { show: false }, Puppeteer: { show: false }, WebDriver: { browser: 'chrome' } },
+      })
+      expect(cfg.helpers.Playwright.show).to.equal(true)
+      expect(cfg.helpers.Playwright.windowSize).to.equal('1024x768')
+      expect(cfg.helpers.Playwright.video).to.equal(false)
+      expect(cfg.helpers.Puppeteer.show).to.equal(true)
+      expect(cfg.helpers.Puppeteer.windowSize).to.equal('1024x768')
+      expect(cfg.helpers.WebDriver.windowSize).to.equal('1024x768')
+    })
+  })
+
+  describe('unknown arg', () => {
+    it('does not throw when an arg has no value and is not a flag', () => {
+      expect(() => applyAndCreate(['weirdtoken'], { helpers: { Playwright: {} } })).not.to.throw()
+    })
+  })
+})
diff --git a/typings/index.d.ts b/typings/index.d.ts
index e06a8620e..97db06b22 100644
--- a/typings/index.d.ts
+++ b/typings/index.d.ts
@@ -211,9 +211,6 @@ declare namespace CodeceptJS {
        */
       JSONResponse?: any
 
-      /** Enable AI features for development purposes */
-      AI?: any
-
       [key: string]: any
     }
     /**

From 94f4ab6413e7230d741218873c9e150f2a0cb21e Mon Sep 17 00:00:00 2001
From: DavertMik 
Date: Mon, 27 Apr 2026 01:47:42 +0300
Subject: [PATCH 03/18] docs: 4.x migration guide and ESM-only code samples
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- Add docs/migration-4.md covering ESM switch, removed helpers
  (Nightmare/Protractor/TestCafe/AI/SoftExpect), removed plugins
  (autoLogin/tryTo/retryTo/eachElement/allure/htmlReporter/wdio/etc.),
  Vercel AI SDK, Joi β†’ Zod, restart=browser removal, Custom Locator
  Strategy removal, within β†’ effect, noGlobals: true default,
  wait* relative URL resolution, strict mode, elementIndex,
  CLI plugin args, workers events, TypeScript loader changes.
- Document hopeThat (soft assertions) in docs/effects.md as the
  replacement for SoftExpectHelper; clarify it works with any
  assertion library.
- Convert ~70 CJS code samples across 19 docs to ESM
  (require/module.exports/exports.config β†’ import/export default).
- Fix dynamic require in pageobjects.md β†’ static import.
- Update Anthropic example in ai.md to claude-sonnet-4-6 and
  OpenAI example to gpt-5.

Co-Authored-By: Claude Opus 4.7 (1M context) 
---
 docs/ai.md             |  9 ++---
 docs/api.md            |  8 ++--
 docs/basics.md         |  9 +++++
 docs/bdd.md            |  6 +--
 docs/best.md           | 25 +++++++-----
 docs/commands.md       | 41 +++++++++++++++++++
 docs/configuration.md  | 42 ++++++++++++++++---
 docs/custom-helpers.md | 25 ++++++------
 docs/data.md           | 27 +++++--------
 docs/effects.md        | 43 ++++++++++++++++----
 docs/hooks.md          | 43 ++++++++++----------
 docs/index.md          |  2 +-
 docs/internal-api.md   | 30 +++++++-------
 docs/migration-4.md    | 92 ++++++++++++++++++++++++++++++++++++++++++
 docs/pageobjects.md    |  8 ++--
 docs/parallel.md       | 14 +++----
 docs/playwright.md     | 34 +++++++++-------
 docs/plugins.md        |  4 +-
 docs/puppeteer.md      |  2 +-
 docs/reports.md        |  2 +-
 docs/secrets.md        |  4 +-
 docs/translation.md    |  4 +-
 docs/tutorial.md       |  4 +-
 docs/webdriver.md      | 27 ++++++++-----
 24 files changed, 356 insertions(+), 149 deletions(-)

diff --git a/docs/ai.md b/docs/ai.md
index 6471de539..60a4f8a19 100644
--- a/docs/ai.md
+++ b/docs/ai.md
@@ -230,10 +230,9 @@ npx codeceptjs generate:heal
 Heal recipes should be included into `codecept.conf.js` or `codecept.conf.ts` config file:
 
 ```js
+import './heal.js'
 
-require('./heal')
-
-exports.config = {
+export const config = {
   // ... your codeceptjs config
 ```
 
@@ -491,8 +490,8 @@ It is recommended to try HTML processing on one of your web pages before launchi
 To do that open the common page of your application and using DevTools copy the outerHTML of `` element. Don't use `Page Source` for that, as it may not include dynamically added HTML elements. Save this HTML into a file and create a NodeJS script:
 
 ```js
-const { removeNonInteractiveElements } = require('codeceptjs/lib/html')
-const fs = require('fs')
+import { removeNonInteractiveElements } from 'codeceptjs/lib/html'
+import fs from 'fs'
 
 const htmlOpts = {
   interactiveElements: ['a', 'input', 'button', 'select', 'textarea', 'label', 'option'],
diff --git a/docs/api.md b/docs/api.md
index fe037ee54..cc99b93ff 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -119,12 +119,12 @@ Or you can use the browser cookies if you are running browser session.
 In this case use `setSharedCookies()` from `@codeceptjs/configure` package:
 
 ```js
-const { setSharedCookies } = require('@codeceptjs/configure');
+import { setSharedCookies } from '@codeceptjs/configure'
 
-// add this before exports.config
-setSharedCookies();
+// call before exporting config
+setSharedCookies()
 
-exports.config = {
+export const config = {
   // ...
   helpers: {  
     // also works with Playwright or Puppeteer
diff --git a/docs/basics.md b/docs/basics.md
index d42febdff..c6b7121f3 100644
--- a/docs/basics.md
+++ b/docs/basics.md
@@ -387,6 +387,15 @@ import { setHeadlessWhen } from '@codeceptjs/configure'
 setHeadlessWhen(process.env.CI)  // headless only on CI, show browser locally
 ```
 
+For a single run without editing config, use the `browser` plugin:
+
+```sh
+npx codeceptjs run -p browser:show   # force visible browser
+npx codeceptjs run -p browser:hide   # force headless
+```
+
+See [Plugin Arguments](/commands#plugin-arguments).
+
 
 ## Configuration
 
diff --git a/docs/bdd.md b/docs/bdd.md
index 9807ac664..98cc3c5f4 100644
--- a/docs/bdd.md
+++ b/docs/bdd.md
@@ -291,7 +291,7 @@ Examples of tables using:
     | Chuck | Norris  |
 ```
 ```js
-const { DataTableArgument } = require('codeceptjs');
+import { dataTableArgument as DataTableArgument } from 'codeceptjs';
 //...
 Given('I have a short employees card', (table) => {
   const dataTableArgument = new DataTableArgument(table);
@@ -309,7 +309,7 @@ Given('I have a short employees card', (table) => {
     | Harry | Potter  | Seeker   |
 ```
 ```js
-const { DataTableArgument } = require('codeceptjs');
+import { dataTableArgument as DataTableArgument } from 'codeceptjs';
 //...
 Given('I have an employee card', (table) => {
   const dataTableArgument = new DataTableArgument(table);
@@ -327,7 +327,7 @@ Given('I have an employee card', (table) => {
     | position | Seeker |
 ```
 ```js
-const { DataTableArgument } = require('codeceptjs');
+import { dataTableArgument as DataTableArgument } from 'codeceptjs';
 //...
 Given('I have a formatted employee card', (table) => {
   const dataTableArgument = new DataTableArgument(table);
diff --git a/docs/best.md b/docs/best.md
index dfb1a95f5..3fd83ede9 100644
--- a/docs/best.md
+++ b/docs/best.md
@@ -100,8 +100,8 @@ class CheckoutForm {
   }
 
 }
-module.exports = new CheckoutForm();
-module.exports.CheckoutForm = CheckoutForm; // for inheritance
+export default new CheckoutForm();
+export { CheckoutForm }; // for inheritance
 ```
 
 * for components that are repeated accross a website (widgets) but don't belong to any page, use component objects. They are the same as page objects but focused only aroung one element:
@@ -143,8 +143,8 @@ class DatePicker {
 }
 
 
-module.exports = new DatePicker();
-module.exports.DatePicker = DatePicker; // for inheritance
+export default new DatePicker();
+export { DatePicker }; // for inheritance
 ```
 
 ## Configuration
@@ -156,7 +156,7 @@ module.exports.DatePicker = DatePicker; // for inheritance
 * use `.env` files and dotenv package to load sensitive data
 
 ```js
-require('dotenv').config({ path: '.env' });
+import 'dotenv/config'
 ```
 
 * move similar parts in those configs by moving them to modules and putting them to `config` dir
@@ -164,7 +164,7 @@ require('dotenv').config({ path: '.env' });
 
 ```js
 // inside config/components.js
-module.exports = {
+export default {
     DatePicker: "./components/datePicker",
     Dropdown: "./components/dropdown",
 }
@@ -173,10 +173,13 @@ module.exports = {
 include them like this:
 
 ```js
+import pages from './config/pages.js'
+import components from './config/components.js'
+
   include: {
       I: './steps_file',
-      ...require('./config/pages'), // require POs and DOs for module
-      ...require('./config/components'), // require all components
+      ...pages, // import POs and DOs for module
+      ...components, // import all components
   },
 ```
 
@@ -215,9 +218,9 @@ include: {
 * When you need to customize access to API and go beyond what ApiDataFactory provides, implement DAO:
 
 ```js
-const { faker } = require('@faker-js/faker');
+import { faker } from '@faker-js/faker';
+import { output } from 'codeceptjs';
 const { I } = inject();
-const { output } = require('codeceptjs');
 
 class InterfaceData {
 
@@ -233,5 +236,5 @@ class InterfaceData {
   }
 }
 
-module.exports = new InterfaceData;
+export default new InterfaceData;
 ```
diff --git a/docs/commands.md b/docs/commands.md
index bc554864c..8eaa66b61 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -100,6 +100,47 @@ Display complete debug output including scheduled promises
 DEBUG=codeceptjs:* npx codeceptjs run
 ```
 
+## Plugin Arguments
+
+`run`, `run-workers`, `run-multiple`, `run-rerun` and `dry-run` accept a `-p` (`--plugins`) flag to enable plugins on the command line, with optional arguments per plugin. Tokens are colon-chained per plugin, comma-separated across plugins:
+
+```sh
+npx codeceptjs run -p                        # enable plugin
+npx codeceptjs run -p ::         # enable + pass args
+npx codeceptjs run -p ,:    # multiple plugins
+```
+
+Plugins listed via `-p` are activated even when their config has `enabled: false` (or no `enabled` flag). This is the supported way to switch a plugin on for a single run without editing `codecept.conf`.
+
+A few examples:
+
+```sh
+npx codeceptjs run -p pauseOnFail                  # pause on first failure
+npx codeceptjs run -p pauseOn:step                 # pause before every step
+npx codeceptjs run -p pauseOn:url:/checkout/*      # pause on URL match
+npx codeceptjs run -p stepByStepReport             # produce a step-by-step HTML report
+```
+
+### Browser Control
+
+The built-in `browser` plugin overrides browser-helper config from the CLI β€” works for Playwright, Puppeteer, WebDriver and Appium without editing `codecept.conf`.
+
+```sh
+npx codeceptjs run -p browser:show                       # force visible browser
+npx codeceptjs run -p browser:hide                       # force headless
+npx codeceptjs run -p browser:browser=firefox            # switch engine
+npx codeceptjs run -p browser:windowSize=1024x768        # set viewport
+npx codeceptjs run -p browser:hide:browser=webkit:windowSize=800x600
+```
+
+Tokens after `browser:` are either flags (`show`, `hide`) or `key=value` pairs. Three keys get per-helper translation:
+
+- `browser=` β€” Puppeteer receives `product`, Playwright/WebDriver receive `browser`. Validated per helper (`chromium`/`webkit`/`firefox` for Playwright, `chrome`/`firefox` for Puppeteer).
+- `show=true|false` (or the `show`/`hide` flag) β€” sets `show` on Playwright/Puppeteer; injects/strips `--headless` in WebDriver chrome/firefox capability args.
+- `windowSize=WxH` β€” sets `windowSize` on every helper; also adds `--window-size=W,H` to chromium/chrome args for Playwright/Puppeteer.
+
+Anything else (`-p browser:video=false:waitForTimeout=10000`) is shallow-merged onto every browser helper present in config. Values are coerced (`true`/`false` β†’ boolean, digits β†’ Number, otherwise string).
+
 ## Run Workers
 
 Run tests in parallel threads. CodeceptJS supports different distribution strategies for optimal performance.
diff --git a/docs/configuration.md b/docs/configuration.md
index 73ce85bf0..5decbed77 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -119,7 +119,7 @@ Create `codecept.conf.js` file and make it export `config` property.
 See the config example:
 
 ```js
-exports.config = {
+export const config = {
   helpers: {
     WebDriver: {
       // load variables from the environment and provide defaults
@@ -158,21 +158,51 @@ codeceptjs run --config=./path/to/my/config.js
 
 > πŸ“Ί [Watch this material](https://www.youtube.com/watch?v=onBnfo_rJa4&t=4s) on YouTube
 
-[`@codeceptjs/configure` package](https://github.com/codeceptjs/configure) contains shared recipes for common configuration patterns. This allows to set meta-configuration, independent from a current helper enabled.
+[`@codeceptjs/configure`](https://github.com/codeceptjs/configure) ships with CodeceptJS as a dependency and contains shared recipes for common configuration patterns. It lets you set meta-configuration that's independent of the active helper.
 
-Install it and enable to easily switch to headless/window mode, change window size, etc.
+Toggle headless/headed mode, change window size, etc.:
 
 ```js
-const { setHeadlessWhen, setWindowSize } = require('@codeceptjs/configure')
+import { setHeadlessWhen, setWindowSize } from '@codeceptjs/configure'
 
 setHeadlessWhen(process.env.CI)
 setWindowSize(1600, 1200)
 
-exports.config = {
+export const config = {
   // ...
 }
 ```
 
+For one-shot bundles use `setBrowserConfig` β€” pass any subset of `{ browser, show, windowSize, url, ... }` and the right per-helper translation happens automatically (Puppeteer receives `product` for `browser`, WebDriver gets `--headless` injected, etc.). Keys whose value is `undefined` are skipped, so unset env vars don't clobber existing config:
+
+```js
+import { setBrowserConfig } from '@codeceptjs/configure'
+
+setBrowserConfig({
+  browser: process.env.BROWSER,        // optional engine override
+  show: !process.env.HEADLESS,         // headed unless HEADLESS is set
+  windowSize: '1280x720',
+  url: process.env.URL,                // overrides helper.url when set
+})
+```
+
+`setCommonPlugins()` enables a curated set of plugins and registers a few more as discoverable (so they can be activated ad-hoc via [`-p` plugin arguments](/commands#plugin-arguments) without editing config):
+
+```js
+import { setCommonPlugins } from '@codeceptjs/configure'
+
+setCommonPlugins()
+```
+
+| Plugin             | Default        | Notes                                                                          |
+| :----------------- | :------------- | :----------------------------------------------------------------------------- |
+| `retryFailedStep`  | enabled        | Retry steps that fail with transient errors                                    |
+| `eachElement`      | enabled        | Provides `eachElement` effect                                                  |
+| `screenshotOnFail` | enabled        | Capture a screenshot when a test fails                                         |
+| `pauseOnFail`      | registered     | Activate via `-p pauseOnFail`                                                  |
+| `browser`          | registered     | CLI overrides for browser helpers β€” `-p browser:show`, `-p browser:browser=firefox`, see [commands](/commands#browser-control) |
+| `aiTrace`          | registered     | Capture AI traces β€” `-p aiTrace`                                               |
+
 ## Profile
 
 Using `process.env.profile` you can change the config dynamically.
@@ -186,7 +216,7 @@ codeceptjs run --profile firefox
 ```
 
 ```js
-exports.config = {
+export const config = {
   helpers: {
     WebDriver: {
       url: 'http://localhost:3000',
diff --git a/docs/custom-helpers.md b/docs/custom-helpers.md
index 7c054a153..3628f5f63 100644
--- a/docs/custom-helpers.md
+++ b/docs/custom-helpers.md
@@ -34,7 +34,7 @@ Helpers are classes inherited from [corresponding abstract class](https://github
 Created helper file should look like this:
 
 ```js
-const Helper = require('@codeceptjs/helper')
+import Helper from '@codeceptjs/helper'
 
 class MyHelper extends Helper {
   // before/after hooks
@@ -51,14 +51,14 @@ class MyHelper extends Helper {
   // use: this.helpers['helperName']
 }
 
-module.exports = MyHelper
+export default MyHelper
 ```
 
 When the helper is enabled in config all methods of a helper class are available in `I` object.
 For instance, if we add a new method to helper class:
 
 ```js
-const Helper = require('@codeceptjs/helper')
+import Helper from '@codeceptjs/helper'
 
 class MyHelper extends Helper {
   doAwesomeThings() {
@@ -202,8 +202,9 @@ This can be done inside a helper using the global [promise recorder](/hooks/#api
 Example: Retrying rendering errors in Puppeteer.
 
 ```js
+import { recorder } from 'codeceptjs'
+
 _before() {
-  const recorder = require('codeceptjs').recorder;
   recorder.retry({
     retries: 2,
     when: err => err.message.indexOf('Cannot find context with specified id') > -1,
@@ -217,7 +218,7 @@ Retry rules are available in array `recorder.retries`. The last retry rule can b
 
 ## Using Typescript
 
-With Typescript, just simply replacing `module.exports` with `export` for autocompletion.
+With Typescript, just simply replacing `export default` with `export` for autocompletion.
 
 ## Helper Examples
 
@@ -226,7 +227,7 @@ With Typescript, just simply replacing `module.exports` with `export` for autoco
 In this example we take the power of Playwright to change geolocation in our tests:
 
 ```js
-const Helper = require('@codeceptjs/helper')
+import Helper from '@codeceptjs/helper'
 
 class MyHelper extends Helper {
   async setGeoLocation(longitude, latitude) {
@@ -242,10 +243,10 @@ class MyHelper extends Helper {
 Next example demonstrates how to use WebDriver library to create your own test action. Method `seeAuthentication` will use `browser` instance of WebDriver to get access to cookies. Standard NodeJS assertion library will be used (you can use any).
 
 ```js
-const Helper = require('@codeceptjs/helper')
+import Helper from '@codeceptjs/helper'
 
 // use any assertion library you like
-const assert = require('assert')
+import assert from 'assert'
 
 class MyHelper extends Helper {
   /**
@@ -271,7 +272,7 @@ class MyHelper extends Helper {
   }
 }
 
-module.exports = MyHelper
+export default MyHelper
 ```
 
 ### Puppeteer Example
@@ -281,8 +282,8 @@ Puppeteer has [nice and elegant API](https://github.com/puppeteer/puppeteer/blob
 Let's see how we can use [emulate](https://github.com/puppeteer/puppeteer/blob/main/docs/api/puppeteer.page.emulate.md) function to emulate iPhone browser in a test.
 
 ```js
-const Helper = require('@codeceptjs/helper')
-const puppeteer = require('puppeteer')
+import Helper from '@codeceptjs/helper'
+import puppeteer from 'puppeteer'
 const iPhone = puppeteer.devices['iPhone 6']
 
 class MyHelper extends Helper {
@@ -292,5 +293,5 @@ class MyHelper extends Helper {
   }
 }
 
-module.exports = MyHelper
+export default MyHelper
 ```
diff --git a/docs/data.md b/docs/data.md
index cdb85548a..1b1558ab7 100644
--- a/docs/data.md
+++ b/docs/data.md
@@ -289,10 +289,10 @@ See the example providing a factory for User generation:
 
 ```js
 // factories/post.js
-var Factory = require('rosie').Factory
-var faker = require('@faker-js/faker')
+import { Factory } from 'rosie'
+import faker from '@faker-js/faker'
 
-module.exports = new Factory().attr('name', () => faker.person.findName()).attr('email', () => faker.internet.email())
+export default new Factory().attr('name', () => faker.person.findName()).attr('email', () => faker.internet.email())
 ```
 
 Next is to configure helper to match factories with API:
@@ -341,10 +341,10 @@ See the example providing a factory for User generation:
 
 ```js
 // factories/post.js
-var Factory = require('rosie').Factory
-var faker = require('@faker-js/faker')
+import { Factory } from 'rosie'
+import faker from '@faker-js/faker'
 
-module.exports = new Factory((buildObj) => {
+export default new Factory((buildObj) => {
   return {
     input: { ...buildObj },
   }
@@ -393,23 +393,16 @@ By doing this we can make requests within the current browser session without a
 
 > Sharing browser session with ApiDataFactory or GraphQLDataFactory can be especially useful when you test Single Page Applications
 
-Since CodeceptJS 2.3.3 there is a simple way to enable shared session for browser and data helpers.
-Install [`@codeceptjs/configure`](https://github.com/codeceptjs/configure) package:
-
-```
-npm i @codeceptjs/configure --save
-```
-
-Import `setSharedCookies` function and call it inside a config:
+CodeceptJS bundles [`@codeceptjs/configure`](https://github.com/codeceptjs/configure), which exposes `setSharedCookies` for this case. Call it before exporting your config:
 
 ```js
 // in codecept.conf.js
-const { setSharedCookies } = require('@codeceptjs/configure')
+import { setSharedCookies } from '@codeceptjs/configure'
 
 // share cookies between browser helpers and REST/GraphQL
 setSharedCookies()
 
-exports.config = {}
+export const config = {}
 ```
 
 Without `setSharedCookies` you will need to update the config manually, so a data helper could receive cookies from a browser to make a request. If you would like to configure this process manually, here is an example of doing so:
@@ -418,7 +411,7 @@ Without `setSharedCookies` you will need to update the config manually, so a dat
 
 let cookies; // share cookies
 
-exports.config = {
+export const config = {
 helpers: {
   ApiDataFactory: {
     endpoint: 'http://local.app/api',
diff --git a/docs/effects.md b/docs/effects.md
index e4618d047..89eb8cde2 100644
--- a/docs/effects.md
+++ b/docs/effects.md
@@ -12,14 +12,10 @@ Effects are functions that can modify scenario flow. They provide ways to handle
 Effects can be imported directly from CodeceptJS:
 
 ```js
-// ESM
-import { tryTo, retryTo, within } from 'codeceptjs/effects'
-
-// CommonJS
-const { tryTo, retryTo, within } = require('codeceptjs/effects')
+import { tryTo, retryTo, hopeThat, within } from 'codeceptjs/effects'
 ```
 
-> πŸ“ Note: Prior to v3.7, `tryTo` and `retryTo` were available globally via plugins. This behavior is deprecated and will be removed in v4.0.
+> πŸ“ Note: Prior to v4, `tryTo` and `retryTo` were enabled via plugins (`tryTo`, `retryTo`) that registered them as globals. Those plugins are removed in v4 β€” import effects from `codeceptjs/effects` instead.
 
 ## tryTo
 
@@ -47,6 +43,40 @@ If the steps inside `tryTo` fail:
 - `tryTo` returns `false`
 - Auto-retries are disabled inside `tryTo` blocks
 
+## hopeThat
+
+`hopeThat` is the soft-assertion effect. It wraps a block of steps; if any step inside fails, the failure is recorded as a note on the test and `hopeThat` returns `false`, but the scenario keeps running. Call `hopeThat.noErrors()` once at the end to fail the scenario if any soft assertion failed.
+
+```js
+import { hopeThat } from 'codeceptjs/effects'
+
+Scenario('form shows every validation error', ({ I }) => {
+  I.amOnPage('/signup')
+  I.click('Submit')
+
+  await hopeThat(() => I.see('Email is required', '#email-error'))
+  await hopeThat(() => I.see('Password is required', '#password-error'))
+  await hopeThat(() => I.see('You must accept the terms', '#terms-error'))
+
+  hopeThat.noErrors()  // throws once, listing every recorded failure
+})
+```
+
+`hopeThat` returns `Promise` β€” `true` on success, `false` on caught failure β€” which is handy for branching:
+
+```js
+const cookieAccepted = await hopeThat(() => I.click('Accept cookies'))
+if (!cookieAccepted) I.say('No cookie banner')
+```
+
+> πŸ’‘ In 3.x, soft assertions were provided by `SoftExpectHelper` (`I.softAssert`, `I.softExpectEqual`, `I.flushSoftAssertions`). That helper is gone in 4.x β€” use `hopeThat()` and `hopeThat.noErrors()` instead. `hopeThat` works with **any** assertion you can write inside a step: built-in `I.see*`, custom-helper assertions, `expect()` from your own assertion library, plain `assert` from Node β€” anything that throws on failure.
+
+The same `hopeThat` is also re-exported from `codeceptjs/assertions` if you prefer that subpath:
+
+```js
+import { hopeThat } from 'codeceptjs/assertions'
+```
+
 ## retryTo
 
 The `retryTo` effect allows you to retry a set of steps multiple times until they succeed. This is useful for handling flaky elements or conditions that may need multiple attempts.
@@ -147,4 +177,3 @@ const success = await tryTo(async () => {
 })
 ```
 
-This documentation covers the main effects functionality while providing practical examples and important notes about deprecation and future changes. Let me know if you'd like me to expand any section or add more examples!
diff --git a/docs/hooks.md b/docs/hooks.md
index 2f55ef864..5d2e28715 100644
--- a/docs/hooks.md
+++ b/docs/hooks.md
@@ -21,7 +21,7 @@ const defaultConfig = {
   someDefaultOption: true
 }
 
-module.exports = function(config) {
+export default function(config) {
   config = Object.assign(defaultConfig, config);
   // do stuff
 }
@@ -63,9 +63,9 @@ Let's say we need to populate database for a group of tests.
 
 ```js
 // populate database for slow tests
-const event = require('codeceptjs').event;
+import { event } from 'codeceptjs';
 
-module.exports = function() {
+export default function() {
 
   event.dispatcher.on(event.test.before, function (test) {
 
@@ -84,9 +84,9 @@ If you want to share bootstrap script or run multiple bootstraps, it's a good id
 Plugin can also execute JS before tests but you need to use internal APIs to synchronize promises.
 
 ```js
-const { recorder } = require('codeceptjs');
+import { recorder } from 'codeceptjs';
 
-module.exports = function(options) {
+export default function(options) {
 
   event.dispatcher.on(event.all.before, function () {
     recorder.startUnlessRunning(); // start recording promises
@@ -101,7 +101,7 @@ module.exports = function(options) {
 
 **Use local CodeceptJS installation to get access to `codeceptjs` module**
 
-CodeceptJS provides an API which can be loaded via `require('codeceptjs')` when CodeceptJS is installed locally.
+CodeceptJS provides an API which can be loaded via `import codeceptjs, { recorder, event, output } from 'codeceptjs'` when CodeceptJS is installed locally.
 These internal objects are available:
 
 * [`codecept`](https://github.com/codeceptjs/CodeceptJS/blob/master/lib/codecept.js): test runner class
@@ -123,9 +123,9 @@ CodeceptJS provides a module with [event dispatcher and set of predefined events
 It can be required from codeceptjs package if it is installed locally.
 
 ```js
-const event = require('codeceptjs').event;
+import { event } from 'codeceptjs';
 
-module.exports = function() {
+export default function() {
 
   event.dispatcher.on(event.test.before, function (test) {
 
@@ -198,13 +198,12 @@ To inject asynchronous functions in a test or before/after a test you can subscr
 Provide a function description as a first parameter, function should return a promise:
 
 ```js
-const event = require('codeceptjs').event;
-const recorder = require('codeceptjs').recorder;
-module.exports = function() {
+import { event, recorder } from 'codeceptjs';
+import request from 'request';
 
-  event.dispatcher.on(event.test.before, function (test) {
+export default function() {
 
-    const request = require('request');
+  event.dispatcher.on(event.test.before, function (test) {
 
     recorder.add('create fixture data via API', function() {
       return new Promise((doneFn, errFn) => {
@@ -239,7 +238,7 @@ Output module provides 4 verbosity levels. Depending on the mode you can have di
 It is recommended to avoid `console.log` and use output.* methods for printing.
 
 ```js
-const output = require('codeceptjs').output;
+import { output } from 'codeceptjs';
 
 output.print('This is basic information');
 output.debug('This is debug information');
@@ -252,7 +251,7 @@ CodeceptJS has a dependency injection container with Helpers and Support objects
 They can be retrieved from the container:
 
 ```js
-let container = require('codeceptjs').container;
+import { container } from 'codeceptjs';
 
 // get object with all helpers
 let helpers = container.helpers();
@@ -273,14 +272,15 @@ let plugins = container.plugins();
 New objects can also be added to container in runtime:
 
 ```js
-let container = require('codeceptjs').container;
+import { container } from 'codeceptjs';
+import UserPage from './pages/user.js';
 
 container.append({
   helpers: { // add helper
     MyHelper: new MyHelper({ config1: 'val1' });
   },
   support: { // add page object
-    UserPage: require('./pages/user');
+    UserPage,
   }
 })
 ```
@@ -293,13 +293,12 @@ let mocha = container.mocha();
 
 ### Config
 
-CodeceptJS config can be accessed from `require('codeceptjs').config.get()`:
+CodeceptJS config can be accessed from `import { config } from 'codeceptjs'` then `config.get()`:
 
 ```js
+import { config } from 'codeceptjs';
 
-let config = require('codeceptjs').config.get();
-
-if (config.myKey == 'value') {
+if (config.get().myKey == 'value') {
   // run hook
 }
 ```
@@ -313,7 +312,7 @@ CodeceptJS can be imported and used in custom runners.
 To initialize Codecept you need to create Config and Container objects.
 
 ```js
-const { codecept: Codecept } = require('codeceptjs');
+import { codecept as Codecept } from 'codeceptjs';
 
 const config = { helpers: { WebDriver: { browser: 'chrome', url: 'http://localhost' } } };
 const opts = { steps: true };
diff --git a/docs/index.md b/docs/index.md
index 9e45553b9..1d986df90 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -86,7 +86,7 @@ Scenario('Checkout test', ({ I }) => {
 Can we use it for long scenarios? Sure!
 
 ```js
-const { faker } = require('@faker-js/faker');                               // Use 3rd-party JS code
+import { faker } from '@faker-js/faker'                                     // Use 3rd-party JS code
 
 Feature('Store');
 
diff --git a/docs/internal-api.md b/docs/internal-api.md
index aa0846036..f8827b5ca 100644
--- a/docs/internal-api.md
+++ b/docs/internal-api.md
@@ -8,11 +8,11 @@ title: Internal API
 In this guide we will overview the internal API of CodeceptJS.
 This knowledge is required for customization, writing plugins, etc.
 
-CodeceptJS provides an API which can be loaded via `require('codeceptjs')` when CodeceptJS is installed locally. Otherwise, you can load codeceptjs API via global `codeceptjs` object:
+CodeceptJS provides an API which can be loaded via `import codeceptjs, { recorder, event, output } from 'codeceptjs'` when CodeceptJS is installed locally. Otherwise, you can load codeceptjs API via global `codeceptjs` object:
 
 ```js
 // via module
-const { recorder, event, output } = require('codeceptjs');
+import { recorder, event, output } from 'codeceptjs';
 // or using global object
 const { recorder, event, output } = codeceptjs;
 ```
@@ -37,7 +37,7 @@ CodeceptJS has a dependency injection container with helpers and support objects
 They can be retrieved from the container:
 
 ```js
-const { container } = require('codeceptjs');
+import { container } from 'codeceptjs';
 
 // get object with all helpers
 const helpers = container.helpers();
@@ -58,14 +58,15 @@ const plugins = container.plugins();
 New objects can also be added to container in runtime:
 
 ```js
-const { container } = require('codeceptjs');
+import { container } from 'codeceptjs';
+import UserPage from './pages/user.js';
 
 container.append({
   helpers: { // add helper
     MyHelper: new MyHelper({ config1: 'val1' });
   },
   support: { // add page object
-    UserPage: require('./pages/user');
+    UserPage,
   }
 })
 ```
@@ -85,9 +86,9 @@ CodeceptJS provides a module with an [event dispatcher and set of predefined eve
 It can be required from codeceptjs package if it is installed locally.
 
 ```js
-const { event } = require('codeceptjs');
+import { event } from 'codeceptjs';
 
-module.exports = function() {
+export default function() {
 
   event.dispatcher.on(event.test.before, function (test) {
 
@@ -135,14 +136,13 @@ To inject asynchronous functions in a test or before/after a test you can subscr
 Provide a function in the first parameter, a function must be async or must return a promise:
 
 ```js
-const { event, recorder } = require('codeceptjs');
+import { event, recorder } from 'codeceptjs';
+import request from 'request';
 
-module.exports = function() {
+export default function() {
 
   event.dispatcher.on(event.test.before, function (test) {
 
-    const request = require('request');
-
     recorder.add('create fixture data via API', function() {
       return new Promise((doneFn, errFn) => {
         request({
@@ -162,10 +162,10 @@ module.exports = function() {
 
 ### Config
 
-CodeceptJS config can be accessed from `require('codeceptjs').config.get()`:
+CodeceptJS config can be accessed from `import { config } from 'codeceptjs'` then `config.get()`:
 
 ```js
-const { config } = require('codeceptjs');
+import { config } from 'codeceptjs';
 
 // config object has access to all values of the current config file
 
@@ -187,7 +187,7 @@ Output module provides four verbosity levels. Depending on the mode you can have
 It is recommended to avoid `console.log` and use output.* methods for printing.
 
 ```js
-const output = require('codeceptjs').output;
+import { output } from 'codeceptjs';
 
 output.print('This is basic information');
 output.debug('This is debug information');
@@ -230,7 +230,7 @@ Whenever you execute tests with `--verbose` option you will see registered event
 You can run CodeceptJS tests from your script.
 
 ```js
-const { codecept: Codecept } = require('codeceptjs');
+import { codecept as Codecept } from 'codeceptjs';
 
 // define main config
 const config = { 
diff --git a/docs/migration-4.md b/docs/migration-4.md
index fa354c4c1..145428755 100644
--- a/docs/migration-4.md
+++ b/docs/migration-4.md
@@ -100,9 +100,43 @@ await Container.create(config, opts)
 | `Protractor` | Switch to `Playwright` or `WebDriver`. |
 | `TestCafe` | Switch to `Playwright`. |
 | `AI` | Use the top-level `ai:` config option and the new `aiTrace` plugin. |
+| `SoftExpectHelper` | Use the `hopeThat` effect instead β€” see below. |
 
 `Container.STANDARD_ACTING_HELPERS` no longer lists `TestCafe`.
 
+### `SoftExpectHelper` β†’ `hopeThat`
+
+3.x shipped a `SoftExpectHelper` (`I.softAssert`, `I.softExpectEqual`, `I.flushSoftAssertions`, etc.) for soft assertions. It is gone in 4.x. Use the `hopeThat` effect β€” it works with **any** assertion that throws (built-in `I.see*`, your custom helper, `expect` from chai/jest, Node's `assert`).
+
+3.x:
+
+```js
+helpers: { SoftExpectHelper: {} }
+
+// in scenario
+I.softExpectEqual(user.name, 'jon')
+I.softExpectContain(emails, 'jon@doe.com')
+I.flushSoftAssertions()
+```
+
+4.x:
+
+```js
+import { hopeThat } from 'codeceptjs/effects'
+
+await hopeThat(() => assert.strictEqual(user.name, 'jon'))
+await hopeThat(() => assert.ok(emails.includes('jon@doe.com')))
+hopeThat.noErrors()
+```
+
+Each `hopeThat()` call records the failure as a note on the test and lets the scenario continue; `hopeThat.noErrors()` throws once at the end with every recorded failure if any happened. See [Effects: hopeThat](/effects#hopethat).
+
+### Custom Assertion Libraries
+
+No code changes are required for chai, expect, jest-style matchers, or Node's `assert` β€” just import them in your test files. With `noGlobals: true`, they work the same as before.
+
+Heads up on chai: 3.x pinned `chai@4`; 4.x devDep is `chai@6`, which is **ESM-only** and drops some legacy APIs. If you import chai in your tests, switch to `import { expect } from 'chai'` and verify your matchers still resolve.
+
 ## 4. Replace or Remove Plugins
 
 | Removed plugin | Replacement |
@@ -287,6 +321,21 @@ I.fillField('Email', 'a@b.c', step.timeout(10))
 I.click('Add', step.opts({ elementIndex: 2 }))
 ```
 
+### `within` Is Now an Effect
+
+In 3.x, `within(...)` was a global statement available everywhere. In 4.x it's an effect alongside `tryTo`, `retryTo`, and `hopeThat`. Under `noGlobals: true` you must import it:
+
+```js
+import { within } from 'codeceptjs/effects'
+
+await within('.signup-form', () => {
+  I.fillField('Email', 'a@b.c')
+  I.click('Submit')
+})
+```
+
+`within` returns a Promise β€” `await` it whenever you need its return value or want subsequent steps to wait for it. The same applies to `session`, which moved from global to a regular import (`import { session } from 'codeceptjs'`).
+
 ### Effects and Assertions Are Subpath Imports
 
 ```js
@@ -401,6 +450,49 @@ I.fillField('input', 'x', step.opts({ elementIndex: -1 }))
 - `fillField` β€” supports rich text editors (CKEditor, ProseMirror, etc.).
 - BDD: `But` keyword is recognized.
 
+### CLI Plugin Arguments
+
+`-p` accepts colon-chained arguments, so plugins can be enabled and configured from the command line without editing config:
+
+```bash
+npx codeceptjs run -p pauseOn:fail              # pause on every failure
+npx codeceptjs run -p pauseOn:url:/checkout/*   # pause when URL matches
+npx codeceptjs run -p browser:show              # force visible browser
+npx codeceptjs run -p browser:browser=firefox:windowSize=1024x768
+npx codeceptjs run -p plugin1,plugin2:arg       # multiple plugins
+```
+
+The `browser` plugin is new in 4.x β€” it overrides the active browser helper (Playwright, Puppeteer, WebDriver, Appium) from the CLI, useful for ad-hoc local runs and CI matrices. See [Commands](/commands).
+
+The old `-p all` magic keyword is gone (it conflicted with the colon syntax). Enable specific plugins explicitly: `-p pluginA,pluginB`.
+
+### Workers: Events and Plugin Scope
+
+Two notable changes for parallel runs:
+
+- **Event dispatcher fires inside workers.** In 3.x, listeners attached to `event.dispatcher` only saw events from the main process. In 4.x, plugins and listeners observe per-test events inside each worker, so things like custom reporters and screenshot hooks work the same in single-process and worker modes ([#5464](https://github.com/codeceptjs/CodeceptJS/pull/5464)).
+- **`runInParent` / `runInMain` plugin option.** Set to `false` on plugins that should only run inside worker children (default is `true`). Useful for plugins that aggregate per-worker state from the parent.
+
+```js
+plugins: {
+  myReporter: {
+    enabled: true,
+    runInParent: false,   // only run in worker children
+  },
+}
+```
+
+### TypeScript Improvements
+
+If you write tests in TypeScript, 4.x is significantly better:
+
+- **`tsx` loader** instead of `ts-node/esm` β€” faster startup, better ESM compatibility. Install `tsx` (optional peer dep). `ts-node/esm` still works but emits a deprecation warning.
+- **Error stack traces** point at `.ts` source lines, not transpiled output.
+- **`__dirname` / `__filename`** are injected for TypeScript files that use them (ESM normally hides these globals).
+- **Path aliases** from `tsconfig.json` (`paths`) are resolved at runtime β€” `import x from '@/utils'` works without extra runtime config.
+- **`codecept.conf.ts`** supports top-level `await` via dynamic imports.
+- **`steps_file.ts`** and TypeScript support objects load correctly across files.
+
 ## 7. Update Dependency Versions
 
 If your project depends on these directly, check for breakage:
diff --git a/docs/pageobjects.md b/docs/pageobjects.md
index 809b34c63..9776a26b3 100644
--- a/docs/pageobjects.md
+++ b/docs/pageobjects.md
@@ -358,13 +358,13 @@ export default new SearchPage();
 Inject the page object into a specific scenario:
 
 ```js
+import searchPage from './pages/searchPage.js'
+
 Scenario('user searches for products', ({ I, searchPage }) => {
   I.amOnPage('/');
   searchPage.search('laptop');
   I.see('Search Results');
-}).injectDependencies({ 
-  searchPage: require('./pages/searchPage.js') 
-});
+}).injectDependencies({ searchPage });
 ```
 
 **Use cases:**
@@ -381,7 +381,7 @@ Plain object page objects are still supported for backward compatibility:
 ```js
 const { I } = inject();
 
-module.exports = {
+export default {
   fields: {
     email: '#user_basic_email',
     password: '#user_basic_password'
diff --git a/docs/parallel.md b/docs/parallel.md
index 41c40b5cc..257307415 100644
--- a/docs/parallel.md
+++ b/docs/parallel.md
@@ -178,9 +178,9 @@ npx codeceptjs run-workers 3 --by pool   # βœ“ Best for mixed test execution tim
 ## Test stats with Parallel Execution by Workers
 
 ```js
-const { event } = require('codeceptjs');
+import { event } from 'codeceptjs';
 
-module.exports = function() {
+export default function() {
 
   event.dispatcher.on(event.workers.result, function (result) {
 
@@ -271,9 +271,9 @@ FAIL  | 7 passed, 1 failed, 1 skipped   // 2s
 CodeceptJS also exposes the env var `process.env.RUNS_WITH_WORKERS` when running tests with `run-workers` command so that you could handle the events better in your plugins/helpers
 
 ```js
-const { event } = require('codeceptjs')
+import { event } from 'codeceptjs'
 
-module.exports = function () {
+export default function () {
   // this event would trigger the  `_publishResultsToTestrail` when running `run-workers` command
   event.dispatcher.on(event.workers.result, async () => {
     await _publishResultsToTestrail()
@@ -294,7 +294,7 @@ To run tests in parallel across multiple browsers, modify your `codecept.conf.js
 Start with modifying the `codecept.conf.js` file. Add multiple key inside the config which will be used to configure multiple profiles.
 
 ```
-exports.config = {
+export const config = {
   helpers: {
     WebDriver: {
       url: 'http://localhost:3000',
@@ -369,7 +369,7 @@ Create a placeholder in file:
 
 ```js
 #!/usr/bin/env node
-const { Workers, event } = require('codeceptjs')
+import { Workers, event } from 'codeceptjs'
 // here will go magic
 ```
 
@@ -530,7 +530,7 @@ For complex scenarios where you need to initialize shared data before tests run,
 
 ```js
 // inside codecept.conf.js
-exports.config = {
+export const config = {
   bootstrap() {
     // Initialize shared data container
     share({ userData: null, config: { retries: 3 } })
diff --git a/docs/playwright.md b/docs/playwright.md
index 36d0c08a7..03a79bf71 100644
--- a/docs/playwright.md
+++ b/docs/playwright.md
@@ -174,7 +174,7 @@ Scenario('create todo item', ({ I }) => {
 If you need to get element's value inside a test you can use `grab*` methods. They should be used with `await` operator inside `async` function:
 
 ```js
-const assert = require('assert')
+import assert from 'assert'
 Scenario('get value of current tasks', async ({ I }) => {
   I.fillField('.todo', 'my first item')
   I.pressKey('Enter')
@@ -246,7 +246,7 @@ To use this functionality, all you need to do is set the browser to `electron` i
 `main.js` - main Electron application file
 
 ```js
-const { app, BrowserWindow } = require('electron')
+import { app, BrowserWindow } from 'electron'
 
 function createWindow() {
   const window = new BrowserWindow({ width: 800, height: 600 })
@@ -259,14 +259,15 @@ app.whenReady().then(createWindow)
 `codecept.conf.js` - CodeceptJS configuration file
 
 ```js
-const path = require('path')
+import path from 'path'
+import electron from 'electron'
 
-exports.config = {
+export const config = {
   helpers: {
     Playwright: {
       browser: 'electron',
       electron: {
-        executablePath: require('electron'),
+        executablePath: electron,
         args: [path.join(__dirname, 'main.js')],
       },
     },
@@ -283,14 +284,15 @@ Sometimes, the Electron app is built with [electron-forge](https://www.electronf
 `codecept.conf.js` - CodeceptJS configuration file
 
 ```js
-const path = require('path')
+import path from 'path'
+import electron from 'electron'
 
-exports.config = {
+export const config = {
   helpers: {
     Playwright: {
       browser: 'electron',
       electron: {
-        executablePath: require('electron'),
+        executablePath: electron,
         args: [path.join(__dirname, '.webpack/main/index.js')],
       },
     },
@@ -319,7 +321,7 @@ Device emulation can be enabled in CodeceptJS globally in a config or per sessio
 Playwright contains a [list of predefined devices](https://github.com/microsoft/playwright/blob/master/src/server/deviceDescriptors.js) to emulate, for instance this is how you can enable iPhone 6 emulation for all tests:
 
 ```js
-const { devices } = require('playwright');
+import { devices } from 'playwright';
 
 helpers: {
   Playwright: {
@@ -344,7 +346,7 @@ helpers: {
 To enable device emulation for a specific test, create an additional browser session and pass in config as a second parameter:
 
 ```js
-const { devices } = require('playwright')
+import { devices } from 'playwright'
 
 Scenario('website looks nice on iPhone', () => {
   session('mobile user', devices['iPhone 6'], () => {
@@ -448,7 +450,7 @@ To master request intercepting [use `route` object](https://playwright.dev/docs/
 Playwright may record videos for failed tests. This can be enabled in a config with `video: true` option:
 
 ```js
-exports.config = {
+export const config = {
   helpers: {
     Playwright: {
       // ...
@@ -491,7 +493,7 @@ Inside a trace you get screenshots, DOM snapshots, console logs, network request
 Enable trace with `trace: true` option in a config:
 
 ```js
-exports.config = {
+export const config = {
   helpers: {
     Playwright: {
       // ...
@@ -646,7 +648,7 @@ Minimal examples:
 helpers: { Playwright: { url: 'http://localhost', browser: 'chromium', storageState: 'authState.json' } }
 
 // Inline object
-const state = require('./authState.json');
+import state from './authState.json' with { type: 'json' };
 helpers: { Playwright: { url: 'http://localhost', browser: 'chromium', storageState: state } }
 ```
 
@@ -663,12 +665,14 @@ Scenario('Dashboard (authenticated)', { cookies: authCookies }, ({ I }) => {
 Helper snippet:
 
 ```js
+import fs from 'fs'
+
 // Grab current state as object
 const state = await I.grabStorageState()
 // Persist manually (sensitive file!)
-require('fs').writeFileSync('authState.json', JSON.stringify(state))
+fs.writeFileSync('authState.json', JSON.stringify(state))
 
 // Include IndexedDB (Playwright >= 1.51) if your app relies on it (e.g. Firebase Auth persistence)
 const stateWithIDB = await I.grabStorageState({ indexedDB: true })
-require('fs').writeFileSync('authState-with-idb.json', JSON.stringify(stateWithIDB))
+fs.writeFileSync('authState-with-idb.json', JSON.stringify(stateWithIDB))
 ```
diff --git a/docs/plugins.md b/docs/plugins.md
index 8db17f3fe..36d67b9b2 100644
--- a/docs/plugins.md
+++ b/docs/plugins.md
@@ -18,7 +18,7 @@ When enabled with --ai flag, it generates reports after test execution.
 
 ```js
 // in codecept.conf.js
-exports.config = {
+export const config = {
   plugins: {
     analyze: {
       enabled: true,
@@ -77,7 +77,7 @@ Enable this plugin in your config:
 
 ```js
 // in codecept.conf.js
-exports.config = {
+export const config = {
   plugins: {
     aiTrace: {
       enabled: true
diff --git a/docs/puppeteer.md b/docs/puppeteer.md
index 54c362860..e2272badf 100644
--- a/docs/puppeteer.md
+++ b/docs/puppeteer.md
@@ -169,7 +169,7 @@ Scenario('create todo item', ({ I }) => {
 If you need to get element's value inside a test you can use `grab*` methods. They should be used with `await` operator inside `async` function:
 
 ```js
-const assert = require('assert')
+import assert from 'assert'
 Scenario('get value of current tasks', async ({ I }) => {
   I.fillField('.todo', 'my first item')
   I.pressKey('Enter')
diff --git a/docs/reports.md b/docs/reports.md
index 54f55a17a..b3ecf5939 100644
--- a/docs/reports.md
+++ b/docs/reports.md
@@ -251,7 +251,7 @@ npm install @testomatio/reporter --save-dev
 Add the `testomatio` plugin to your `codecept.conf.js`:
 
 ```js
-exports.config = {
+export const config = {
   plugins: {
     testomatio: {
       enabled: true,
diff --git a/docs/secrets.md b/docs/secrets.md
index 95fc2fe9a..686529ee3 100644
--- a/docs/secrets.md
+++ b/docs/secrets.md
@@ -55,7 +55,7 @@ Enable basic masking with predefined patterns:
 
 ```js
 // codecept.conf.js
-exports.config = {
+export const config = {
   // ... other config
   maskSensitiveData: true,
 }
@@ -75,7 +75,7 @@ Define your own masking patterns:
 
 ```js
 // codecept.conf.js
-exports.config = {
+export const config = {
   // ... other config
   maskSensitiveData: {
     enabled: true,
diff --git a/docs/translation.md b/docs/translation.md
index a9e2bf7ac..f842e08d0 100644
--- a/docs/translation.md
+++ b/docs/translation.md
@@ -201,7 +201,7 @@ Scenario('ログむンできる', ({ 私は }) => {
 To add localized aliases to more actions create a new JSON or JavaScript file returning an object with following fields:
 
 ```js
-module.exports = {
+export default {
   actions: {
     // add action aliases, translating method name to your language
     rightClick: 'Rechtsklick'
@@ -223,7 +223,7 @@ Then enable this vocabulary file in codecept conf:
 Create translation file like this:
 
 ```js
-module.exports = {
+export default {
   I: 'Ya',
   contexts: {
     Feature: 'Feature',
diff --git a/docs/tutorial.md b/docs/tutorial.md
index 053271413..f117628ac 100644
--- a/docs/tutorial.md
+++ b/docs/tutorial.md
@@ -207,7 +207,7 @@ Now open this file:
 ```js
 const { I } = inject();
 
-module.exports = {
+export default {
 
   // insert your locators and methods here
 }
@@ -218,7 +218,7 @@ Feels really empty. What should we do about it? Should we write more code? No, w
 ```js
 const { I } = inject();
 
-module.exports = {
+export default {
 
   fillShippingAddress(name, address, city, state, zip) {
     I.fillField('Name', name);
diff --git a/docs/webdriver.md b/docs/webdriver.md
index a7d6ec3d8..d9f10f00e 100644
--- a/docs/webdriver.md
+++ b/docs/webdriver.md
@@ -39,7 +39,7 @@ npm i @wdio/selenium-standalone-service --save-dev
 Enable it in config inside plugins section:
 
 ```js
-exports.config = {
+export const config = {
   // ...
   // inside condecept.conf.js
   plugins: {
@@ -160,15 +160,22 @@ CodeceptJS has [Selenoid plugin](/plugins#selenoid) which can automagically load
 
 ### Headless Mode
 
-It is recommended to use `@codeceptjs/configure` package to easily toggle headless mode for WebDriver:
+The bundled `@codeceptjs/configure` toggles headless mode for WebDriver β€” for chrome/firefox it injects `--headless` into the matching capability args automatically:
 
 ```js
 // inside codecept.conf.js
-const { setHeadlessWhen, setWindowSize } = require('@codeceptjs/configure');
+import { setHeadlessWhen, setWindowSize } from '@codeceptjs/configure'
 
-setHeadlessWhen(process.env.HEADLESS); // enables headless mode when HEADLESS environment variable exists
+setHeadlessWhen(process.env.HEADLESS) // enables headless mode when HEADLESS env var is set
+setWindowSize(1280, 720)
+```
+
+For one-off runs without editing config, the [`browser` plugin](/commands#browser-control) does the same from the CLI:
+
+```sh
+npx codeceptjs run -p browser:hide                      # force headless
+npx codeceptjs run -p browser:hide:windowSize=1280x720  # combined
 ```
-This requires `@codeceptjs/configure` package to be installed.
 
 Alternatively, you can enable headless mode manually via desired capabilities.
 
@@ -352,7 +359,7 @@ WebDriver helper supports standard [CSS/XPath and text locators](/locators) as w
 If you need to get element's value inside a test you can use `grab*` methods. They should be used with `await` operator inside `async` function:
 
 ```js
-const assert = require('assert');
+import assert from 'assert';
 Scenario('get value of current tasks', async ({ I }) => {
   I.fillField('.todo', 'my first item');
   I.pressKey('Enter')
@@ -414,7 +421,7 @@ By default, they will wait for 1 second. This number can be changed in WebDriver
 
 ```js
 // inside codecept.conf.js
-exports.config = {
+export const config = {
   helpers: {
     WebDriver: {
       // WebDriver config goes here
@@ -434,7 +441,7 @@ Add `smartWait: 5000` to wait for additional 5s.
 
 ```js
 // inside codecept.conf.js
-exports.config = {
+export const config = {
   helpers: {
     WebDriver: {
       // WebDriver config goes here
@@ -509,7 +516,7 @@ With `autoLogin` plugin you can save cookies into a file and reuse same session
 CodeceptJS allows to use several browser windows inside a test. Sometimes we are testing the functionality of websites that we cannot control, such as a closed-source managed package, and there are popups that either remain open for configuring data on the screen, or close as a result of clicking a window. We can use these functions in order to gain more control over which page is being tested with Codecept at any given time. For example:
 
 ```js
-const assert = require('assert');
+import assert from 'assert';
 
 Scenario('should open main page of configured site, open a popup, switch to main page, then switch to popup, close popup, and go back to main page', async ({ I }) => {
     I.amOnPage('/');
@@ -637,7 +644,7 @@ npx codeceptjs gh
 Name a new helper "Web". Now each method of a created class can be added to I object. Be sure to enable this helper in config:
 
 ```js
-exports.config = {
+export const config = {
   helpers: {
     WebDriver: { /* WebDriver config goes here */ },
     WebHelper: {

From bb1312d348395f0f032050b263a02b24f660c212 Mon Sep 17 00:00:00 2001
From: DavertMik 
Date: Mon, 27 Apr 2026 01:50:33 +0300
Subject: [PATCH 04/18] docs: fix faker import + clarify codeceptjs
 internal-api intro

- data.md: faker is a named export; restore `import { faker }`
  and update the install hint from the legacy `faker` package
  to `@faker-js/faker`.
- internal-api.md: lead with the named-import form and demote
  the `codeceptjs` global to a footnote (it only exists when
  `noGlobals: false`).

Co-Authored-By: Claude Opus 4.7 (1M context) 
---
 docs/data.md         | 10 +++++-----
 docs/internal-api.md |  9 ++++-----
 2 files changed, 9 insertions(+), 10 deletions(-)

diff --git a/docs/data.md b/docs/data.md
index 1b1558ab7..7a57c7cff 100644
--- a/docs/data.md
+++ b/docs/data.md
@@ -258,13 +258,13 @@ Just define how many items of any kind you need and the data factory helper will
 
 To make this work some preparations are required.
 
-At first, you need data generation libraries which are [Rosie](https://github.com/rosiejs/rosie) and [Faker](https://www.npmjs.com/package/faker). Faker can generate random names, emails, texts, and Rosie uses them
+At first, you need data generation libraries which are [Rosie](https://github.com/rosiejs/rosie) and [Faker](https://fakerjs.dev). Faker can generate random names, emails, texts, and Rosie uses them
 to generate objects using factories.
 
 Install rosie and faker to create a first factory:
 
-```js
-npm i rosie faker --save-dev
+```sh
+npm i rosie @faker-js/faker --save-dev
 ```
 
 Then create a module which will export a factory for an entity.
@@ -290,7 +290,7 @@ See the example providing a factory for User generation:
 ```js
 // factories/post.js
 import { Factory } from 'rosie'
-import faker from '@faker-js/faker'
+import { faker } from '@faker-js/faker'
 
 export default new Factory().attr('name', () => faker.person.findName()).attr('email', () => faker.internet.email())
 ```
@@ -342,7 +342,7 @@ See the example providing a factory for User generation:
 ```js
 // factories/post.js
 import { Factory } from 'rosie'
-import faker from '@faker-js/faker'
+import { faker } from '@faker-js/faker'
 
 export default new Factory((buildObj) => {
   return {
diff --git a/docs/internal-api.md b/docs/internal-api.md
index f8827b5ca..7aa760399 100644
--- a/docs/internal-api.md
+++ b/docs/internal-api.md
@@ -8,15 +8,14 @@ title: Internal API
 In this guide we will overview the internal API of CodeceptJS.
 This knowledge is required for customization, writing plugins, etc.
 
-CodeceptJS provides an API which can be loaded via `import codeceptjs, { recorder, event, output } from 'codeceptjs'` when CodeceptJS is installed locally. Otherwise, you can load codeceptjs API via global `codeceptjs` object:
+CodeceptJS exposes its internal API as named exports of the `codeceptjs` package. Import only what you need:
 
 ```js
-// via module
-import { recorder, event, output } from 'codeceptjs';
-// or using global object
-const { recorder, event, output } = codeceptjs;
+import { recorder, event, output, container, config } from 'codeceptjs'
 ```
 
+> Older code may have relied on a global `codeceptjs` object (`const { recorder } = codeceptjs`). That global only exists under `noGlobals: false` (the deprecated 3.x default) β€” prefer named imports.
+
 These internal objects are available:
 
 * [`codecept`](https://github.com/Codeception/CodeceptJS/blob/master/lib/codecept.js): test runner class

From f0440fc84ce9830c365b92e9c8356e54145ca72c Mon Sep 17 00:00:00 2001
From: DavertMik 
Date: Mon, 27 Apr 2026 02:45:36 +0300
Subject: [PATCH 05/18] fix(ci): npm link codeceptjs in CI; drop dead joi types
 reference
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* `@codeceptjs/configure` and `@codeceptjs/expect-helper` both do
  `require('codeceptjs')` from inside their own packages. End users
  installing codeceptjs into a project's node_modules resolve fine β€”
  but in this repo (where the project IS codeceptjs) Node has nothing
  at node_modules/codeceptjs to resolve to, so those imports throw and
  helper/plugin loading fails on every CI run. Add `npm link && npm
  link codeceptjs` after install in playwright/puppeteer/webdriver/
  plugin/dtslint/test workflows. Documented in CLAUDE.md.

* `lib/plugin/browser.js`: route through `setBrowserConfig` from
  `@codeceptjs/configure` instead of duplicating the helper-mutation
  logic inline. The configure dep is the right primitive β€” removing
  the duplicate, single source of truth.

* `typings/index.d.ts`: drop `/// `. joi is
  not used in any public type, the directive was leftover from a prior
  incarnation, and it broke `dtslint` (which looks for `@types/joi`,
  doesn't exist) and the def-runner test (failed-resolution alignment
  produced an undefined `resolutionDiagnostics` access in newer TS).

* Docs: `docs/configuration.md` setCommonPlugins table aligned with
  4.x plugin set (no eachElement, pauseOn instead of pauseOnFail);
  `docs/plugins.md` regen with aiTrace section.

Co-Authored-By: Claude Opus 4.7 (1M context) 
---
 .github/workflows/dtslint.yml    |   5 +
 .github/workflows/playwright.yml |   5 +
 .github/workflows/plugin.yml     |   5 +
 .github/workflows/puppeteer.yml  |   5 +
 .github/workflows/test.yml       |  12 ++
 .github/workflows/webdriver.yml  |   5 +
 CLAUDE.md                        |  10 ++
 docs/configuration.md            |   5 +-
 docs/plugins.md                  | 222 ++++++++++++-------------------
 lib/plugin/browser.js            | 122 ++---------------
 typings/index.d.ts               |   1 -
 11 files changed, 141 insertions(+), 256 deletions(-)

diff --git a/.github/workflows/dtslint.yml b/.github/workflows/dtslint.yml
index 5a7053711..f354da042 100644
--- a/.github/workflows/dtslint.yml
+++ b/.github/workflows/dtslint.yml
@@ -28,5 +28,10 @@ jobs:
         env:
           PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true
           PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
+      # @codeceptjs/configure & @codeceptjs/expect-helper do `require('codeceptjs')`;
+      # in this repo (where the project IS codeceptjs) Node has nothing at
+      # node_modules/codeceptjs to resolve to. `npm link` fills the gap.
+      # End users don't need this.
+      - run: npm link && npm link codeceptjs
       - run: npm run def
       - run: npm run dtslint
diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
index fe4559d9e..613951f08 100644
--- a/.github/workflows/playwright.yml
+++ b/.github/workflows/playwright.yml
@@ -40,6 +40,11 @@ jobs:
           npm i --force
         env:
           PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
+      # @codeceptjs/configure & @codeceptjs/expect-helper do `require('codeceptjs')`;
+      # in this repo (where the project IS codeceptjs) Node has nothing at
+      # node_modules/codeceptjs to resolve to. `npm link` fills the gap.
+      # End users don't need this.
+      - run: npm link && npm link codeceptjs
       - name: Allow Release info Change
         run: |
           sudo apt-get update --allow-releaseinfo-change
diff --git a/.github/workflows/plugin.yml b/.github/workflows/plugin.yml
index 747ea1dec..99ad6c612 100644
--- a/.github/workflows/plugin.yml
+++ b/.github/workflows/plugin.yml
@@ -40,6 +40,11 @@ jobs:
           npm i --force
         env:
           PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
+      # @codeceptjs/configure & @codeceptjs/expect-helper do `require('codeceptjs')`;
+      # in this repo (where the project IS codeceptjs) Node has nothing at
+      # node_modules/codeceptjs to resolve to. `npm link` fills the gap.
+      # End users don't need this.
+      - run: npm link && npm link codeceptjs
       - name: Allow Release info Change
         run: |
           sudo apt-get update --allow-releaseinfo-change
diff --git a/.github/workflows/puppeteer.yml b/.github/workflows/puppeteer.yml
index 957e9779e..48cde07a9 100644
--- a/.github/workflows/puppeteer.yml
+++ b/.github/workflows/puppeteer.yml
@@ -40,6 +40,11 @@ jobs:
           npm i --force && npm i puppeteer --force
         env:
           PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true
+      # @codeceptjs/configure & @codeceptjs/expect-helper do `require('codeceptjs')`;
+      # in this repo (where the project IS codeceptjs) Node has nothing at
+      # node_modules/codeceptjs to resolve to. `npm link` fills the gap.
+      # End users don't need this.
+      - run: npm link && npm link codeceptjs
       - name: Start mock server
         run: nohup npm run mock-server:start &
       - name: Wait for mock server
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 3d878b43c..eda0a782e 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -32,6 +32,12 @@ jobs:
         env:
           PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true
           PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
+      # @codeceptjs/configure does `import 'codeceptjs'`; in this repo (where
+      # the project IS codeceptjs) Node has nothing at node_modules/codeceptjs
+      # to resolve to. `npm link` fills in that gap so configure shares our
+      # Config singleton. End users don't need this β€” they install codeceptjs
+      # into their project's node_modules normally.
+      - run: npm link && npm link codeceptjs
       - name: Start mock server
         run: nohup npm run mock-server:start &
       - name: Wait for mock server
@@ -72,6 +78,12 @@ jobs:
         env:
           PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true
           PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
+      # @codeceptjs/configure does `import 'codeceptjs'`; in this repo (where
+      # the project IS codeceptjs) Node has nothing at node_modules/codeceptjs
+      # to resolve to. `npm link` fills in that gap so configure shares our
+      # Config singleton. End users don't need this β€” they install codeceptjs
+      # into their project's node_modules normally.
+      - run: npm link && npm link codeceptjs
       - name: Start mock server
         run: nohup npm run mock-server:start &
       - name: Wait for mock server
diff --git a/.github/workflows/webdriver.yml b/.github/workflows/webdriver.yml
index 9700c13e6..ec4ee7d73 100644
--- a/.github/workflows/webdriver.yml
+++ b/.github/workflows/webdriver.yml
@@ -41,6 +41,11 @@ jobs:
         env:
           PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true
           PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
+      # @codeceptjs/configure & @codeceptjs/expect-helper do `require('codeceptjs')`;
+      # in this repo (where the project IS codeceptjs) Node has nothing at
+      # node_modules/codeceptjs to resolve to. `npm link` fills the gap.
+      # End users don't need this.
+      - run: npm link && npm link codeceptjs
       - name: Start mock server
         run: nohup npm run mock-server:start &
       - name: Wait for mock server
diff --git a/CLAUDE.md b/CLAUDE.md
index 909502386..59913a025 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -7,6 +7,16 @@ This project is migrating from 3.x to ESM replacing all require() calls with imp
 Each time unsure what to do refer to the same file implementation in **3.x branch**
 3.x branch contains stable version of all core classes that work
 
+## Local Development Setup
+
+After `npm install`, run once:
+
+```
+npm link && npm link codeceptjs
+```
+
+This makes `node_modules/codeceptjs` resolve back to the project root. Required because `@codeceptjs/configure` (a runtime dep) does `import 'codeceptjs'` β€” and in this repo the project itself IS codeceptjs, so without the link Node has nothing at `node_modules/codeceptjs` to resolve to. CI runs the same two commands automatically (see `.github/workflows/test.yml`). End users don't need this β€” they install codeceptjs into their own project's node_modules normally.
+
 ## Running Tests
 
 Focus on acceptance tests:
diff --git a/docs/configuration.md b/docs/configuration.md
index 5decbed77..8aa05c3ad 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -197,12 +197,13 @@ setCommonPlugins()
 | Plugin             | Default        | Notes                                                                          |
 | :----------------- | :------------- | :----------------------------------------------------------------------------- |
 | `retryFailedStep`  | enabled        | Retry steps that fail with transient errors                                    |
-| `eachElement`      | enabled        | Provides `eachElement` effect                                                  |
 | `screenshotOnFail` | enabled        | Capture a screenshot when a test fails                                         |
-| `pauseOnFail`      | registered     | Activate via `-p pauseOnFail`                                                  |
+| `pauseOn`          | registered     | Pause on failure / step / file / URL β€” `-p pauseOn:fail`, `-p pauseOn:step`, `-p pauseOn:file:tests/login_test.js`, `-p pauseOn:url:/checkout/*` |
 | `browser`          | registered     | CLI overrides for browser helpers β€” `-p browser:show`, `-p browser:browser=firefox`, see [commands](/commands#browser-control) |
 | `aiTrace`          | registered     | Capture AI traces β€” `-p aiTrace`                                               |
 
+> `eachElement`, `tryTo`, and `retryTo` are no longer plugins in 4.x β€” import them from `codeceptjs/effects`.
+
 ## Profile
 
 Using `process.env.profile` you can change the config dynamically.
diff --git a/docs/plugins.md b/docs/plugins.md
index 36d67b9b2..6e25cdf35 100644
--- a/docs/plugins.md
+++ b/docs/plugins.md
@@ -7,6 +7,38 @@ title: Plugins
 
 
 
+## aiTrace
+
+Generates AI-friendly trace files for debugging with AI agents.
+This plugin creates a markdown file with test execution logs and links to all artifacts
+(screenshots, HTML, ARIA snapshots, browser logs, HTTP requests) for each step.
+
+#### Configuration
+
+```js
+"plugins": {
+   "aiTrace": {
+     "enabled": true
+   }
+ }
+```
+
+Possible config options:
+
+*   `deleteSuccessful`: delete traces for successfully executed tests. Default: false.
+*   `fullPageScreenshots`: should full page screenshots be used. Default: false.
+*   `output`: a directory where traces should be stored. Default: `output`.
+*   `captureHTML`: capture HTML for each step. Default: true.
+*   `captureARIA`: capture ARIA snapshot for each step. Default: true.
+*   `captureBrowserLogs`: capture browser console logs. Default: true.
+*   `captureHTTP`: capture HTTP requests (requires `trace` or `recordHar` enabled in helper config). Default: true.
+*   `captureDebugOutput`: capture CodeceptJS debug output. Default: true.
+*   `ignoreSteps`: steps to ignore in trace. Array of RegExps is expected.
+
+### Parameters
+
+*   `config` **any** 
+
 ## analyze
 
 Uses AI to analyze test failures and provide insights
@@ -18,7 +50,7 @@ When enabled with --ai flag, it generates reports after test execution.
 
 ```js
 // in codecept.conf.js
-export const config = {
+exports.config = {
   plugins: {
     analyze: {
       enabled: true,
@@ -63,118 +95,6 @@ export const config = {
 
 Returns **void** 
 
-## aiTrace
-
-Generates AI-friendly trace files for debugging with AI agents like Claude Code.
-
-When a test fails, you need to understand what went wrong. This plugin automatically captures comprehensive information about test execution - screenshots, HTML, ARIA snapshots, console logs, and HTTP requests - and organizes it in a format optimized for AI analysis.
-
-The generated trace files are structured markdown documents that AI agents can easily parse to understand test context and provide debugging insights.
-
-#### Usage
-
-Enable this plugin in your config:
-
-```js
-// in codecept.conf.js
-export const config = {
-  plugins: {
-    aiTrace: {
-      enabled: true
-    }
-  }
-}
-```
-
-#### Configuration
-
-*   `deleteSuccessful` (boolean) - delete traces for successfully executed tests. Default: false.
-*   `fullPageScreenshots` (boolean) - should full page screenshots be used. Default: false.
-*   `output` (string) - a directory where traces should be stored. Default: `output`.
-*   `captureHTML` (boolean) - capture HTML for each step. Default: true.
-*   `captureARIA` (boolean) - capture ARIA snapshot for each step. Default: true.
-*   `captureBrowserLogs` (boolean) - capture browser console logs. Default: true.
-*   `captureHTTP` (boolean) - capture HTTP requests (requires `trace` or `recordHar` enabled in helper config). Default: true.
-*   `captureDebugOutput` (boolean) - capture CodeceptJS debug output. Default: true.
-*   `ignoreSteps` (array) - steps to ignore in trace. Array of RegExps is expected. Default: [].
-
-#### Artifacts Created
-
-For each test, a `trace_` directory is created with:
-
-*   **trace.md** - AI-friendly markdown file with test execution history
-*   **0000_step_name_screenshot.png** - screenshot for each step (named with step description)
-*   **0000_step_name_page.html** - full HTML of the page at each step
-*   **0000_step_name_aria.txt** - ARIA accessibility snapshot (AI-readable structure)
-*   **0000_step_name_console.json** - browser console logs
-
-Artifact files include step names for easier identification (e.g., `0000_I_see_Product_screenshot.png`).
-
-When HAR or trace recording is enabled in your helper config, links to those files are also included.
-
-#### Example Output
-
-```markdown
-file: /path/to/test.js
-name: My test scenario
-time: 3.45s
----
-
-I am on page "https://example.com"
-  > navigated to https://example.com/
-  > [HTML](./0000_I_am_on_page_https_example.com_page.html)
-  > [ARIA Snapshot](./0000_I_am_on_page_https_example.com_aria.txt)
-  > [Screenshot](./0000_I_am_on_page_https_example.com_screenshot.png)
-  > [Browser Logs](./0000_I_am_on_page_https_example.com_console.json) (7 entries)
-  > HTTP: see [HAR file](../har/...) for network requests
-
-I see "Welcome"
-  > navigated to https://example.com/
-  > [HTML](./0001_I_see_Welcome_page.html)
-  > [ARIA Snapshot](./0001_I_see_Welcome_aria.txt)
-  > [Screenshot](./0001_I_see_Welcome_screenshot.png)
-  > [Browser Logs](./0001_I_see_Welcome_console.json) (0 entries)
-```
-
-Files are named with step descriptions for easier identification.
-
-#### Best Practices
-
-**Save disk space** - Only keep traces for failed tests:
-
-```js
-aiTrace: {
-  enabled: true,
-  deleteSuccessful: true
-}
-```
-
-**Ignore noise** - Don't capture logs for `grab` and `wait` steps:
-
-```js
-aiTrace: {
-  enabled: true,
-  ignoreSteps: [/^grab/, /^wait/]
-}
-```
-
-**Reduce file sizes** - Capture only what you need:
-
-```js
-aiTrace: {
-  enabled: true,
-  captureHTML: false,        // Skip HTML (saves ~500KB per step)
-  captureARIA: true,         // Keep ARIA (only ~16KB)
-  captureBrowserLogs: false  // Skip console logs
-}
-```
-
-### Parameters
-
-*   `config` **[Object][1]** Plugin configuration (optional, default `{}`)
-
-Returns **void** 
-
 ## auth
 
 Logs user in for the first test and reuses session for next tests.
@@ -448,13 +368,35 @@ Possible config options:
 
 *   `config`  
 
-## consolidateWorkerJsonResults
+## browser
+
+Overrides browser helper config from the command line. Works for all browser helpers
+(Playwright, Puppeteer, WebDriver, Appium) without touching `codecept.conf`.
+
+Enable it via `-p` option with one or more colon-chained args:
+
+    npx codeceptjs run -p browser:show
+    npx codeceptjs run -p browser:hide
+    npx codeceptjs run -p browser:browser=firefox
+    npx codeceptjs run -p browser:windowSize=1024x768:video=false
+    npx codeceptjs run -p browser:hide:browser=webkit:windowSize=800x600
+
+#### Args
+
+*   **show** β€” force visible browser
+*   **hide** β€” force headless (also injects `--headless` into WebDriver chrome/firefox capability args)
+*   **`=`** β€” set `helpers.. = `. Three keys
+    get per-helper translation via `setBrowserConfig`:
+    *   `browser=` β€” Puppeteer receives `product`, Playwright receives `browser`
+    *   `windowSize=WxH` β€” also adds `--window-size=W,H` chromium/chrome args
+    *   `show=true|false` β€” toggles `show` on Playwright/Puppeteer; injects/strips
+        `--headless` in WebDriver chrome/firefox capability args
 
-Consolidates JSON reports from multiple workers into a single HTML report
+Values are coerced: `true`/`false` β†’ boolean, numbers β†’ Number, otherwise string.
 
 ### Parameters
 
-*   `config`  
+*   `config`   (optional, default `{}`)
 
 ## coverage
 
@@ -602,17 +544,6 @@ Sample custom reporter for CodeceptJS.
 
 *   `config`  
 
-## enhancedRetryFailedStep
-
-Enhanced retryFailedStep plugin that coordinates with other retry mechanisms
-
-This plugin provides step-level retries and coordinates with global retry settings
-to avoid conflicts and provide predictable behavior.
-
-### Parameters
-
-*   `config`  
-
 ## heal
 
 Self-healing tests with AI.
@@ -661,6 +592,30 @@ Additional config options:
 
 *   `config`   (optional, default `{}`)
 
+## pauseOn
+
+Pauses test execution in different modes. Unlike `pauseOnFail`, this plugin supports
+multiple triggers for pausing and is controlled via CLI arguments.
+
+Enable it via `-p` option with a mode:
+
+    npx codeceptjs run -p pauseOn:fail
+    npx codeceptjs run -p pauseOn:step
+    npx codeceptjs run -p pauseOn:file:tests/login_test.js
+    npx codeceptjs run -p pauseOn:file:tests/login_test.js:43
+    npx codeceptjs run -p pauseOn:url:/users/*
+
+#### Modes
+
+*   **fail** β€” pause when a step fails (same as `pauseOnFail` plugin)
+*   **step** β€” pause before first step, use `next` to advance step-by-step
+*   **file** β€” pause when execution reaches a specific file (and optionally line)
+*   **url** β€” pause when the browser URL matches a pattern (supports `*` wildcards)
+
+### Parameters
+
+*   `config`   (optional, default `{}`)
+
 ## pauseOnFail
 
 Automatically launches [interactive pause][5] when a test fails.
@@ -679,10 +634,6 @@ Enable it manually on each run via `-p` option:
 
     npx codeceptjs run -p pauseOnFail
 
-## reportData
-
-TypeScript: Explicitly type reportData arrays as any\[] to avoid 'never' errors
-
 ## retryFailedStep
 
 Retries each failed step in a test.
@@ -719,6 +670,7 @@ Run tests with plugin enabled:
 *   `ignoredSteps` - an array for custom steps to ignore on retry. Use it to append custom steps to ignored list.
     You can use step names or step prefixes ending with `*`. As such, `wait*` will match all steps starting with `wait`.
     To append your own steps to ignore list - copy and paste a default steps list. Regexp values are accepted as well.
+*   `deferToScenarioRetries` - when enabled (default), step retries are automatically disabled if scenario retries are configured to avoid excessive total retries.
 
 #### Example
 
@@ -750,14 +702,6 @@ Scenario('scenario tite', { disableRetryFailedStep: true }, () => {
 
 *   `config`  
 
-## safeJsonStringify
-
-Safely serialize data to JSON, handling circular references
-
-### Parameters
-
-*   `data`  
-
 ## screenshotOnFail
 
 Creates screenshot on failure. Screenshot is saved into `output` directory.
diff --git a/lib/plugin/browser.js b/lib/plugin/browser.js
index b7033bde0..c91c4031d 100644
--- a/lib/plugin/browser.js
+++ b/lib/plugin/browser.js
@@ -1,10 +1,5 @@
+import { setBrowserConfig } from '@codeceptjs/configure'
 import output from '../output.js'
-import Config from '../config.js'
-
-const BROWSER_HELPERS = ['Playwright', 'Puppeteer', 'WebDriver', 'Appium']
-
-const PUPPETEER_BROWSERS = ['chrome', 'firefox']
-const PLAYWRIGHT_BROWSERS = ['chromium', 'webkit', 'firefox']
 
 /**
  * Overrides browser helper config from the command line. Works for all browser helpers
@@ -24,17 +19,14 @@ const PLAYWRIGHT_BROWSERS = ['chromium', 'webkit', 'firefox']
  *
  * * **show** β€” force visible browser
  * * **hide** β€” force headless (also injects `--headless` into WebDriver chrome/firefox capability args)
- * * **`=`** β€” sets `helpers.. = `. Three keys
- *   get per-helper translation:
- *     * `browser=` β€” Puppeteer receives `product`, Playwright receives `browser`,
- *       WebDriver receives `browser`. Validated per helper.
- *     * `windowSize=WxH` β€” sets `windowSize` on each helper, plus `--window-size=W,H`
- *       chromium/chrome args for Playwright/Puppeteer.
- *     * `show=true|false` β€” sets `show` on Playwright/Puppeteer; injects/strips
- *       `--headless` in WebDriver chrome/firefox capability args.
+ * * **`=`** β€” set `helpers.. = `. Three keys
+ *   get per-helper translation via `setBrowserConfig`:
+ *     * `browser=` β€” Puppeteer receives `product`, Playwright receives `browser`
+ *     * `windowSize=WxH` β€” also adds `--window-size=W,H` chromium/chrome args
+ *     * `show=true|false` β€” toggles `show` on Playwright/Puppeteer; injects/strips
+ *       `--headless` in WebDriver chrome/firefox capability args
  *
  * Values are coerced: `true`/`false` β†’ boolean, numbers β†’ Number, otherwise string.
- * Keys whose value is `undefined` are skipped.
  */
 export default function (config = {}) {
   const args = config._args || []
@@ -61,110 +53,12 @@ export default function (config = {}) {
 
   if (Object.keys(opts).length === 0) return
 
-  Config.addHook(cfg => applyToHelpers(cfg, opts))
+  setBrowserConfig(opts)
 
   const summary = Object.entries(opts).map(([k, v]) => `${k}=${v}`).join(', ')
   output.debug(`browser plugin: applied ${summary}`)
 }
 
-function applyToHelpers(cfg, opts) {
-  if (!cfg.helpers) return
-  const { browser, show, windowSize, ...rest } = opts
-
-  for (const name of BROWSER_HELPERS) {
-    const helper = cfg.helpers[name]
-    if (!helper) continue
-
-    if (browser !== undefined && browser !== null && browser !== '') {
-      applyBrowser(name, helper, browser)
-    }
-    if (show === true) applyHeaded(name, helper)
-    else if (show === false) applyHeadless(name, helper)
-    if (windowSize) applyWindowSize(name, helper, String(windowSize))
-
-    for (const k of Object.keys(rest)) {
-      if (rest[k] !== undefined) helper[k] = rest[k]
-    }
-  }
-}
-
-function applyBrowser(helperName, helper, browser) {
-  if (helperName === 'Puppeteer') {
-    if (!PUPPETEER_BROWSERS.includes(browser)) {
-      throw new Error(`Browser ${browser} is not supported by Puppeteer engine`)
-    }
-    helper.product = browser
-    return
-  }
-  if (helperName === 'Playwright') {
-    if (!PLAYWRIGHT_BROWSERS.includes(browser)) {
-      throw new Error(`Browser ${browser} is not supported by Playwright engine`)
-    }
-    helper.browser = browser
-    return
-  }
-  helper.browser = browser
-}
-
-function applyHeaded(helperName, helper) {
-  if (helperName === 'Playwright' || helperName === 'Puppeteer') {
-    helper.show = true
-    return
-  }
-  if (helperName === 'WebDriver') {
-    stripHeadlessArgs(helper, 'desiredCapabilities')
-    stripHeadlessArgs(helper, 'capabilities')
-  }
-}
-
-function applyHeadless(helperName, helper) {
-  if (helperName === 'Playwright' || helperName === 'Puppeteer') {
-    helper.show = false
-    return
-  }
-  if (helperName === 'WebDriver') {
-    if (helper.browser === 'chrome') {
-      injectHeadlessArgs(helper, 'chromeOptions', ['--headless', '--disable-gpu'])
-    } else if (helper.browser === 'firefox') {
-      injectHeadlessArgs(helper, 'firefoxOptions', ['--headless'])
-    }
-  }
-}
-
-function applyWindowSize(helperName, helper, windowSize) {
-  if (!/^\d+x\d+$/.test(windowSize)) return
-  helper.windowSize = windowSize
-  const [w, h] = windowSize.split('x')
-
-  if (helperName === 'Playwright') {
-    helper.chromium = helper.chromium || {}
-    helper.chromium.args = (helper.chromium.args || []).concat(['--no-sandbox', `--window-size=${w},${h}`])
-    helper.chromium.defaultViewport = null
-    return
-  }
-  if (helperName === 'Puppeteer') {
-    helper.chrome = helper.chrome || {}
-    helper.chrome.args = (helper.chrome.args || []).concat(['--no-sandbox', `--window-size=${w},${h}`])
-    helper.chrome.defaultViewport = null
-  }
-}
-
-function injectHeadlessArgs(helper, optsKey, args) {
-  helper.desiredCapabilities = helper.desiredCapabilities || {}
-  helper.desiredCapabilities[optsKey] = helper.desiredCapabilities[optsKey] || {}
-  helper.desiredCapabilities[optsKey].args = (helper.desiredCapabilities[optsKey].args || []).concat(args)
-}
-
-function stripHeadlessArgs(helper, capsKey) {
-  const caps = helper[capsKey]
-  if (!caps) return
-  for (const optsKey of ['chromeOptions', 'firefoxOptions']) {
-    if (caps[optsKey] && Array.isArray(caps[optsKey].args)) {
-      caps[optsKey].args = caps[optsKey].args.filter(a => a !== '--headless')
-    }
-  }
-}
-
 function coerce(v) {
   if (v === 'true') return true
   if (v === 'false') return false
diff --git a/typings/index.d.ts b/typings/index.d.ts
index 97db06b22..18f5ef065 100644
--- a/typings/index.d.ts
+++ b/typings/index.d.ts
@@ -4,7 +4,6 @@
 /// 
 /// 
 /// 
-/// 
 /// 
 
 declare namespace CodeceptJS {

From 68af6a1b925dbd74ac5be58ec6824023f8f4f59d Mon Sep 17 00:00:00 2001
From: DavertMik 
Date: Mon, 27 Apr 2026 12:37:09 +0300
Subject: [PATCH 06/18] refactor(browser plugin): optional configure import +
 cleaner arg parser
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* `@codeceptjs/configure` is now imported dynamically. If it's not
  installed (e.g. user pinned a stripped-down dep set), the plugin
  prints a one-line hint and skips the override instead of crashing.
* Arg parsing rewritten as small composable functions: `parseArgs` β†’
  `parseArg` β†’ `parseValue`. Uses `String.split('=')` instead of
  manual `indexOf`/`slice`. The hot loop is now a `reduce`.
* Drop number coercion β€” values stay as strings; helpers (and
  setBrowserConfig's regex parsers) coerce as needed. `true`/`false`
  still become real booleans for boolean-typed helper options.

Co-Authored-By: Claude Opus 4.7 (1M context) 
---
 lib/plugin/browser.js            | 67 ++++++++++++++++++--------------
 test/unit/plugin/browser_test.js | 66 ++++++++++++++++---------------
 2 files changed, 72 insertions(+), 61 deletions(-)

diff --git a/lib/plugin/browser.js b/lib/plugin/browser.js
index c91c4031d..6ab2e7230 100644
--- a/lib/plugin/browser.js
+++ b/lib/plugin/browser.js
@@ -1,4 +1,3 @@
-import { setBrowserConfig } from '@codeceptjs/configure'
 import output from '../output.js'
 
 /**
@@ -26,42 +25,52 @@ import output from '../output.js'
  *     * `show=true|false` β€” toggles `show` on Playwright/Puppeteer; injects/strips
  *       `--headless` in WebDriver chrome/firefox capability args
  *
- * Values are coerced: `true`/`false` β†’ boolean, numbers β†’ Number, otherwise string.
+ * Values stay as strings. `true` / `false` are coerced to booleans.
+ *
+ * Requires `@codeceptjs/configure` to be installed; if missing, the plugin
+ * logs a hint and skips the override.
  */
-export default function (config = {}) {
-  const args = config._args || []
-  if (!args.length) return
+export default async function (config = {}) {
+  const opts = parseArgs(config._args || [])
+  if (Object.keys(opts).length === 0) return
 
-  const opts = {}
-  for (const arg of args) {
-    if (!arg) continue
-    if (arg === 'show') {
-      opts.show = true
-      continue
-    }
-    if (arg === 'hide') {
-      opts.show = false
-      continue
-    }
-    const eq = arg.indexOf('=')
-    if (eq < 0) {
-      output.error(`browser plugin: unknown arg "${arg}"`)
-      continue
-    }
-    opts[arg.slice(0, eq)] = coerce(arg.slice(eq + 1))
-  }
+  const configure = await tryImportConfigure()
+  if (!configure) return
 
-  if (Object.keys(opts).length === 0) return
+  configure.setBrowserConfig(opts)
+  output.debug(`browser plugin: applied ${formatOpts(opts)}`)
+}
+
+async function tryImportConfigure() {
+  try {
+    return await import('@codeceptjs/configure')
+  } catch (err) {
+    output.error("browser plugin: '@codeceptjs/configure' is not installed; CLI overrides are skipped. Run `npm i @codeceptjs/configure` to enable.")
+    return null
+  }
+}
 
-  setBrowserConfig(opts)
+function parseArgs(args) {
+  return args.filter(Boolean).reduce((acc, arg) => Object.assign(acc, parseArg(arg)), {})
+}
 
-  const summary = Object.entries(opts).map(([k, v]) => `${k}=${v}`).join(', ')
-  output.debug(`browser plugin: applied ${summary}`)
+function parseArg(arg) {
+  if (arg === 'show') return { show: true }
+  if (arg === 'hide') return { show: false }
+  if (arg.includes('=')) {
+    const [key, ...rest] = arg.split('=')
+    return { [key]: parseValue(rest.join('=')) }
+  }
+  output.error(`browser plugin: unknown arg "${arg}"`)
+  return {}
 }
 
-function coerce(v) {
+function parseValue(v) {
   if (v === 'true') return true
   if (v === 'false') return false
-  if (/^-?\d+(\.\d+)?$/.test(v)) return Number(v)
   return v
 }
+
+function formatOpts(opts) {
+  return Object.entries(opts).map(([k, v]) => `${k}=${v}`).join(', ')
+}
diff --git a/test/unit/plugin/browser_test.js b/test/unit/plugin/browser_test.js
index 8a6e07601..84f594a50 100644
--- a/test/unit/plugin/browser_test.js
+++ b/test/unit/plugin/browser_test.js
@@ -2,47 +2,47 @@ import { expect } from 'chai'
 import browser from '../../../lib/plugin/browser.js'
 import Config from '../../../lib/config.js'
 
-function applyAndCreate(args, base = {}) {
+async function applyAndCreate(args, base = {}) {
   Config.reset()
-  browser({ _args: args })
+  await browser({ _args: args })
   return Config.create(base)
 }
 
 describe('browser plugin', () => {
   beforeEach(() => Config.reset())
 
-  it('does nothing when no args passed', () => {
-    const cfg = applyAndCreate([], { helpers: { Playwright: { show: true } } })
+  it('does nothing when no args passed', async () => {
+    const cfg = await applyAndCreate([], { helpers: { Playwright: { show: true } } })
     expect(cfg.helpers.Playwright.show).to.equal(true)
   })
 
   describe('show / hide flags', () => {
-    it('show forces headed for Playwright + Puppeteer', () => {
-      const cfg = applyAndCreate(['show'], {
+    it('show forces headed for Playwright + Puppeteer', async () => {
+      const cfg = await applyAndCreate(['show'], {
         helpers: { Playwright: { show: false }, Puppeteer: { show: false } },
       })
       expect(cfg.helpers.Playwright.show).to.equal(true)
       expect(cfg.helpers.Puppeteer.show).to.equal(true)
     })
 
-    it('hide forces headless for Playwright + Puppeteer', () => {
-      const cfg = applyAndCreate(['hide'], {
+    it('hide forces headless for Playwright + Puppeteer', async () => {
+      const cfg = await applyAndCreate(['hide'], {
         helpers: { Playwright: { show: true }, Puppeteer: { show: true } },
       })
       expect(cfg.helpers.Playwright.show).to.equal(false)
       expect(cfg.helpers.Puppeteer.show).to.equal(false)
     })
 
-    it('hide injects --headless into WebDriver chrome capability args', () => {
-      const cfg = applyAndCreate(['hide'], {
+    it('hide injects --headless into WebDriver chrome capability args', async () => {
+      const cfg = await applyAndCreate(['hide'], {
         helpers: { WebDriver: { browser: 'chrome' } },
       })
       const args = cfg.helpers.WebDriver.desiredCapabilities.chromeOptions.args
       expect(args).to.include('--headless')
     })
 
-    it('show strips --headless from WebDriver chrome capability args', () => {
-      const cfg = applyAndCreate(['show'], {
+    it('show strips --headless from WebDriver chrome capability args', async () => {
+      const cfg = await applyAndCreate(['show'], {
         helpers: {
           WebDriver: { browser: 'chrome', desiredCapabilities: { chromeOptions: { args: ['--headless', '--disable-gpu'] } } },
         },
@@ -54,8 +54,8 @@ describe('browser plugin', () => {
   })
 
   describe('windowSize', () => {
-    it('windowSize=WxH sets windowSize across browser helpers and chrome args', () => {
-      const cfg = applyAndCreate(['windowSize=800x600'], {
+    it('windowSize=WxH sets windowSize across browser helpers and chrome args', async () => {
+      const cfg = await applyAndCreate(['windowSize=800x600'], {
         helpers: { Playwright: {}, Puppeteer: {}, WebDriver: {} },
       })
       expect(cfg.helpers.Playwright.windowSize).to.equal('800x600')
@@ -66,8 +66,8 @@ describe('browser plugin', () => {
   })
 
   describe('generic key=value passthrough', () => {
-    it('coerces booleans and applies to every browser helper present', () => {
-      const cfg = applyAndCreate(['video=false'], {
+    it('coerces booleans and applies to every browser helper present', async () => {
+      const cfg = await applyAndCreate(['video=false'], {
         helpers: { Playwright: {}, Puppeteer: {}, WebDriver: {}, Appium: {} },
       })
       expect(cfg.helpers.Playwright.video).to.equal(false)
@@ -76,23 +76,23 @@ describe('browser plugin', () => {
       expect(cfg.helpers.Appium.video).to.equal(false)
     })
 
-    it('coerces numbers', () => {
-      const cfg = applyAndCreate(['waitForTimeout=5000'], {
+    it('keeps numbers as strings (helpers coerce as needed)', async () => {
+      const cfg = await applyAndCreate(['waitForTimeout=5000'], {
         helpers: { Playwright: {} },
       })
-      expect(cfg.helpers.Playwright.waitForTimeout).to.equal(5000)
+      expect(cfg.helpers.Playwright.waitForTimeout).to.equal('5000')
     })
 
-    it('keeps strings as strings', () => {
-      const cfg = applyAndCreate(['url=http://staging.test'], {
+    it('keeps strings as strings', async () => {
+      const cfg = await applyAndCreate(['url=http://staging.test'], {
         helpers: { Playwright: {} },
       })
       expect(cfg.helpers.Playwright.url).to.equal('http://staging.test')
     })
 
-    it('skips helpers not present in config without errors', () => {
-      const cfg = applyAndCreate(['video=true'], {
-        helpers: { Playwright: {} }, // Puppeteer/WebDriver absent
+    it('skips helpers not present in config without errors', async () => {
+      const cfg = await applyAndCreate(['video=true'], {
+        helpers: { Playwright: {} },
       })
       expect(cfg.helpers.Playwright.video).to.equal(true)
       expect(cfg.helpers.Puppeteer).to.equal(undefined)
@@ -100,8 +100,8 @@ describe('browser plugin', () => {
   })
 
   describe('browser engine selection', () => {
-    it('browser=firefox routes through setBrowser, Puppeteer gets product', () => {
-      const cfg = applyAndCreate(['browser=firefox'], {
+    it('browser=firefox routes through setBrowser, Puppeteer gets product', async () => {
+      const cfg = await applyAndCreate(['browser=firefox'], {
         helpers: { Puppeteer: {}, Playwright: {} },
       })
       expect(cfg.helpers.Puppeteer.product).to.equal('firefox')
@@ -109,8 +109,8 @@ describe('browser plugin', () => {
       expect(cfg.helpers.Playwright.browser).to.equal('firefox')
     })
 
-    it('browser=webkit + show=false combine cleanly', () => {
-      const cfg = applyAndCreate(['hide', 'browser=webkit'], {
+    it('browser=webkit + show=false combine cleanly', async () => {
+      const cfg = await applyAndCreate(['hide', 'browser=webkit'], {
         helpers: { Playwright: { show: true } },
       })
       expect(cfg.helpers.Playwright.browser).to.equal('webkit')
@@ -119,8 +119,8 @@ describe('browser plugin', () => {
   })
 
   describe('combined args', () => {
-    it('applies show + windowSize + key=value in a single call', () => {
-      const cfg = applyAndCreate(['show', 'windowSize=1024x768', 'video=false'], {
+    it('applies show + windowSize + key=value in a single call', async () => {
+      const cfg = await applyAndCreate(['show', 'windowSize=1024x768', 'video=false'], {
         helpers: { Playwright: { show: false }, Puppeteer: { show: false }, WebDriver: { browser: 'chrome' } },
       })
       expect(cfg.helpers.Playwright.show).to.equal(true)
@@ -133,8 +133,10 @@ describe('browser plugin', () => {
   })
 
   describe('unknown arg', () => {
-    it('does not throw when an arg has no value and is not a flag', () => {
-      expect(() => applyAndCreate(['weirdtoken'], { helpers: { Playwright: {} } })).not.to.throw()
+    it('does not throw when an arg has no value and is not a flag', async () => {
+      let threw = false
+      try { await applyAndCreate(['weirdtoken'], { helpers: { Playwright: {} } }) } catch { threw = true }
+      expect(threw).to.equal(false)
     })
   })
 })

From ccbccbfd07c45d02feffb29613431267cee35b2c Mon Sep 17 00:00:00 2001
From: DavertMik 
Date: Mon, 27 Apr 2026 16:03:23 +0300
Subject: [PATCH 07/18] deps: pin @codeceptjs/expect-helper to ^4.0.0-beta.3

Both `@codeceptjs/configure@^4.0.0-beta.2` and
`@codeceptjs/expect-helper@^4.0.0-beta.3` are now native ESM with
codeceptjs declared as an optional peer dep, matching CodeceptJS 4.x.
Caret ranges pick up future 4.x betas, RCs, and the eventual stable
release without further package.json edits.

Co-Authored-By: Claude Opus 4.7 (1M context) 
---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index dbc6430df..7b0eab682 100644
--- a/package.json
+++ b/package.json
@@ -138,7 +138,7 @@
   },
   "devDependencies": {
     "@apollo/server": "^5",
-    "@codeceptjs/expect-helper": "^1.0.2",
+    "@codeceptjs/expect-helper": "^4.0.0-beta.3",
     "@codeceptjs/mock-request": "0.3.1",
     "@eslint/eslintrc": "3.3.3",
     "@eslint/js": "9.39.2",

From 45f3a7ca4b05dbad0bda126372a6d1442a222a9a Mon Sep 17 00:00:00 2001
From: DavertMik 
Date: Mon, 27 Apr 2026 18:19:10 +0300
Subject: [PATCH 08/18] docs(locators): elevate semantic+context as the default
 pattern
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Reframe semantic locators (`I.click('Save', '.header')`) as the
recommended default for stable scenarios, not a prototyping
shortcut. Combined with a context, they read like prose, survive
ARIA/CSS refactors, and disambiguate duplicate labels β€” so they're
more precise than ARIA or CSS used alone.

- Intro: lead with the "semantic + context" recipe.
- Locator-types table: split semantic into "with context" (default)
  and "no context" (unique label / prototyping); document the
  combined pattern as a first-class type.
- Semantic section: front-load the "pair with a context" guidance
  and drop the "switch to strict locators once stable" line.
- Context section: explain why scoping every action is the
  default, not the special case.

Co-Authored-By: Claude Opus 4.7 (1M context) 
---
 docs/locators.md | 61 ++++++++++++++++++++++++++++++++++++------------
 1 file changed, 46 insertions(+), 15 deletions(-)

diff --git a/docs/locators.md b/docs/locators.md
index 0dc2b96d1..d00ca22f9 100644
--- a/docs/locators.md
+++ b/docs/locators.md
@@ -10,9 +10,17 @@ Locators tell CodeceptJS which element on the page a step acts on. Every action
 CodeceptJS accepts locators in two forms:
 
 - **Strict locator** β€” an object whose single key names the strategy: `{ css: 'button' }`, `{ role: 'button', name: 'Submit' }`, `{ xpath: '//td[1]' }`, `{ id: 'email' }`. The strategy is explicit, so the helper runs exactly one query.
-- **Fuzzy locator** β€” a plain string. CodeceptJS guesses the strategy from shape (`#foo` β†’ id, `//td` β†’ xpath, `.row` β†’ css) and falls back to semantic matching (labels, button text, placeholders). Convenient, but slower and sometimes ambiguous.
+- **Semantic locator** β€” a plain string like `'Sign In'` or `'Email'`. CodeceptJS matches it against labels, button text, placeholders, and `aria-*` attributes the way a user would read the page.
 
-Prefer strict locators in stable test suites. Reach for fuzzy strings when prototyping.
+Both are idiomatic. The strongest pattern in CodeceptJS β€” readable, resilient, and unambiguous β€” is a **semantic locator scoped to a context**:
+
+```js
+I.click('Save', '.header')
+I.fillField('Search', 'Item 1', '.topbar')
+I.click({ role: 'button', name: 'Submit' }, '#login-form')
+```
+
+The context narrows the search to one region of the page, and the semantic string says what the user actually clicks. This is **more precise than ARIA or CSS alone** because it combines structural scope with human-readable intent.
 
 Supported strategies: `css`, `xpath`, `id`, `name`, `role`, `frame`, `shadow`, `pw`. Shadow DOM and React selectors have their own pages β€” see [Shadow DOM](/shadow) and [React](/react). Playwright-specific locators (`_react`, `_vue`, `data-testid`) use the `pw` strategy: `{ pw: '_react=Button[name="Save"]' }`.
 
@@ -20,10 +28,11 @@ Supported strategies: `css`, `xpath`, `id`, `name`, `role`, `frame`, `shadow`, `
 
 | Type | Example | Strengths | Weaknesses | Reach for it when |
 |------|---------|-----------|------------|-------------------|
+| **Semantic + context** | `I.click('Save', '.header')` | Reads like prose; survives CSS and ARIA refactors; the context disambiguates duplicates | Needs a stable region to scope into | **Default for stable suites.** Anywhere a label, button text, or placeholder identifies the element |
 | **ARIA role** | `{ role: 'button', name: 'Save' }` | Survives markup changes; matches how users and screen readers identify elements; exposes accessibility gaps | Needs correct ARIA roles and accessible names; slower than CSS | The app follows accessibility guidelines and you want tests that mirror user intent |
+| **Semantic (no context)** | `'Sign In'`, `'Email'` | No locator to maintain; reads like prose | Ambiguous when the same label appears more than once on the page | A label is unique on the page, or you are prototyping |
 | **CSS** | `{ css: '.btn-save' }` or `.btn-save` | Fast; familiar to every web developer; composes with class, attribute, and pseudo-selectors | Couples tests to styling; breaks on CSS refactors; cannot match by visible text | A stable class, id, or data-attribute exists on the target |
 | **XPath** | `{ xpath: '//table//tr[2]/td[last()]' }` | Walks the tree in any direction (`ancestor`, `following-sibling`); matches visible text | Verbose; slow; harder to read than CSS | You need text matching or axis navigation that CSS cannot express |
-| **Semantic (fuzzy)** | `'Sign In'`, `'Email'` | No locator to maintain; reads like prose | Several lookups per call; ambiguous when labels repeat | Writing a quick scenario or prototyping |
 | **ID / name** | `#email`, `{ name: 'user[email]' }` | Shortest possible locator; unambiguous | Requires an `id` or `name` attribute to exist | Forms and elements with stable ids |
 | **Accessibility id** | `~login-button` | Works in both web (`aria-label`) and mobile | Mobile apps need to expose the id | Cross-platform web and mobile tests |
 | **Custom (`$foo`)** | `$register_button` | Encodes team convention (`data-qa`, `data-test`) in two characters | Needs the [customLocator plugin](/plugins#customlocator) | Your team uses dedicated test attributes |
@@ -97,26 +106,41 @@ Long XPath expressions become unreadable fast. The [`locate()` builder](#combini
 
 ## Semantic locators
 
-When you pass a plain string to a form or click action, CodeceptJS tries several strategies in order: links, buttons, labels, placeholders, `aria-label`.
+A plain string is a semantic locator. CodeceptJS reads it the way a user would: as a button label, a link, a field name, a placeholder, or an `aria-label`.
 
 ```js
-I.click('Sign In')                      // matches ,