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/.mocharc.cjs b/.mocharc.cjs new file mode 100644 index 000000000..3326a37d8 --- /dev/null +++ b/.mocharc.cjs @@ -0,0 +1,6 @@ +const path = require('path') + +module.exports = { + require: [path.join(__dirname, 'test', 'support', 'setup.mjs')], + extension: ['js', 'mjs'], +} diff --git a/.mocharc.mjs b/.mocharc.mjs deleted file mode 100644 index 22fcee554..000000000 --- a/.mocharc.mjs +++ /dev/null @@ -1,10 +0,0 @@ -import path from 'path' -import { fileURLToPath } from 'url' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) - -export default { - require: [path.join(__dirname, 'test', 'support', 'setup.mjs')], - extension: ['js', 'mjs'], -} \ No newline at end of file diff --git a/docs/ai.md b/docs/ai.md index 15a2b0672..60a4f8a19 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) @@ -60,7 +58,7 @@ import { openai } from '@ai-sdk/openai' export default { // ... other config ai: { - model: openai('gpt-4o-mini'), + model: openai('gpt-5'), }, } ``` @@ -94,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. }, } @@ -121,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. }, } ``` @@ -232,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 ``` @@ -385,125 +382,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: @@ -612,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/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/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..8aa05c3ad 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,52 @@ 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 | +| `screenshotOnFail` | enabled | Capture a screenshot when a test fails | +| `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. @@ -186,7 +217,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..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. @@ -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/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/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/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/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/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/internal-api.md b/docs/internal-api.md index aa0846036..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 `require('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 -const { recorder, event, output } = require('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 @@ -37,7 +36,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 +57,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 +85,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 +135,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 +161,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 +186,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 +229,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/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 ,