### Chapter: index (docs/index.md)

---
layout: Landing
sidebar: false
actionText: Quickstart
actionLink: /quickstart
---

::: slot pause
## Write a Test with Interactive Pause

Open a browser on an empty page and pause execution.
Type in commands to complete the test scenario.

Successful commands will be saved into a file.

```js
Scenario('Checkout test', ({ I }) => {
  I.amOnPage('/checkout');
  pause();
})
```
Copy commands from a file into a test. A test is ready!
:::

::: slot write
## Write Tests from UI

With CodeceptUI you can write your tests without closing a browser at all.

Write initial commands and execute a test. An interactive pause will be started when test finishes.

Share one browser accross test runs to save time on opening a browser.
:::


::: slot autocomplete
## Powered with IntelliSense

Use auto-completion writing a test fast.

We use TypeScript type definitions that are automatically updated for custom steps and page objects.

Writing a test in Visual Studio Code is as easy as picking a correct action and putting a parameter. It's really that nice!

:::


::: slot ui

## Watch & Run Tests from UI

We have a flexible interactive web runner which allows you to watch, debug, and write your tests in a web mode.

Features:

* Toggle headless/window mode with one click
* See HTML snapshot of each step
* Works with WebDriver, Puppeteer, Playwright
* Shows step-by-step execution
* Integrated with your local IDE

:::


::: slot run

## Print a Test

Each executed step will be printed on screen when running with `--steps`
```js
Scenario('Checkout test', ({ I }) => {
  I.amOnPage('/checkout');
  I.fillField('First name', 'davert');
  I.fillField('#lastName', 'mik');
  I.fillField('Promo code', '123345')
  //...
})
```

:::

::: slot code

## Realworld Example

Can we use it for long scenarios? Sure!

```js
import { faker } from '@faker-js/faker'                                     // Use 3rd-party JS code

Feature('Store');

Scenario('Create a new store', async ({ I, login, SettingsPage }) => {
  const storeName = faker.lorem.slug();
  login('customer');                                          // Login customer from saved cookies
  SettingsPage.open();                                        // Use Page objects
  I.dontSee(storeName, '.settings');                          // Assert text not present inside an element (located by CSS)
  I.click('Add', '.settings');                                // Click link by text inside element (located by CSS)
  I.fillField('Store Name', storeName);                       // Fill fields by labels or placeholders
  I.fillField('Email', faker.internet.email());
  I.fillField('Telephone', faker.phone.phoneNumberFormat());
  I.selectInDropdown('Status', 'Active');                     // Use custom methods
  I.click('Create', step.retry(2));                           // Retry flaky step
  I.waitInUrl('/settings/setup/stores');                      // Explicit waiter
  I.see(storeName, '.settings');                              // Assert text present inside an element (located by CSS)
  const storeId = await I.grabTextFrom('#store-id');          // Use await to get information from browser
  I.say(`Created a store with ${storeId}`);                   // Print custom comments
}).tag('stores');`;

```
:::

### Chapter: quickstart (docs/quickstart.md)

---
permalink: quickstart
title: Quickstart
layout: Section
sidebar: true
---

# Quickstart

Install CodeceptJS into your project:

```
npm install codeceptjs playwright --save-dev
```

Then install the browser binaries:

```
npx playwright install --with-deps
```

The `--with-deps` flag also installs required system dependencies for the browsers.

> Prefer WebDriver or Appium? See [installation options](/installation/) for all supported helpers.

---

### Init

Initialize CodeceptJS to set up the config file and test directory:

```
npx codeceptjs init
```

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** | 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

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 | 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`

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.

---

### Write Your First Test

Open the generated test file. It will look like this:

```js
Feature('My First Test');

Scenario('test something', ({ I }) => {

});
```

Add a simple scenario:

```js
Feature('My First Test');

Scenario('test something', ({ I }) => {
  I.amOnPage('https://github.com');
  I.see('GitHub');
});
```

---

### Run Tests

```
npx codeceptjs run
```

Expected output:

```bash
My First Test --
  test something
     I am on page "https://github.com"
     I see "GitHub"
 ✓ OK
```

Run in headless mode:

```
npx codeceptjs run --p browser:hide
```

See all available commands in the [CLI reference](https://codecept.io/commands/).

> [▶ Next: CodeceptJS Basics](/basics/)

### Chapter: api (docs/api.md)

---
permalink: /api
title: API Testing
---

## API Testing

CodeceptJS provides a way to write tests in declarative manner for REST and GraphQL APIs. 

Take a look:

```js
I.sendGetRequest('/users/1');
// returns { "user": { "name": "jon" }, "projects": [] }
I.seeResponseCodeIsSuccessful();
I.seeResponseContainsKeys(['user', 'projects']);
I.seeResponseContainsJson({ user: { name: 'jon' } });
I.seeResponseMatchesJsonSchema($ => {
  return $.object(
    user: $.object({
      name: $.string(),
    }),
    projects: $.array()
  )
});
```
In this code we checked API request for:

* status code
* data inclusion
* data structure

These are the things you should generally test your APIs for.

> 🤓 It is recommended to check only invariable parts of responses. Check for required fields and only values you control. For instance, it is not recommended to check id fields, date fields, as they can be frequently changed.

## Installation

Install CodeceptJS if it is not installed yet.

```
npm i codeceptjs --save-dev
```

Initialize CodeceptJS and select REST or GraphQL helper when asked for a helper:

```
npx codeceptjs init
```

## Configuration

Ensure that inside `codecept.conf.js` in helpers section `REST` or `GraphQL` helpers are enabled.

* If you use `REST` helper add `JSONResponse` helper below with no extra config:

```js
// inside codecept.conf.js
// ...
  helpers: {
    REST: {
      endpoint: 'http://localhost:3000/api'
    },
    // .. add JSONResponse helper here
    JSONResponse: {}
  }
```
* If you use `GraphQL` helper add `JSONResponse` helper, configuring it to use GraphQL for requests:

```js
  helpers: {
    GraphQL: {
      endpoint: 'http://localhost:3000/graphql'
    },
    // .. add JSONResponse helper here
    JSONResponse: {
      requestHelper: 'GraphQL',
    }
  }
```

Originally, REST and GraphQL helpers were not designed for API testing. 
They were used to perform API requests for browser tests. As so, they lack assertion methods to API responses.

[`JSONResponse`](/helpers/JSONResponse/) helper adds response assertions. 

> 💡 In CodeceptJS assertions start with `see` prefix. Learn more about assertions by [opening reference for JSONResponse](/helpers/JSONResponse/) helper.

Generate TypeScript definitions to get auto-completions for JSONResponse:

```
npx codeceptjs def
```

After helpers were configured and typings were generated, you can start writing first API test. By default, CodeceptJS saves tests in `tests` directory and uses `*_test.js` suffix. The `init` command created the first test for you to start.

> Check [API Examples](https://github.com/codeceptjs/api-examples) to see tests implementations.

## Requests

[REST](/helpers/REST/) or [GraphQL](/helpers/GraphQL/) helpers implement methods for making API requests.
Both helpers send requests via HTTP protocol from CodeceptJS process. 
For most cases, you will need to have authentication. It can be passed via headers, which can be added to helper's configuration in `codecept.conf.js`. 

```js
helpers: {
  REST: {
    defaultHeaders: {
      // use Bearer Authorization
      'Authorization': 'Bearer 11111',
      'Content-Type': 'application/json',
      'Accept': 'application/json',
    },
  }
}
```

Or you can use the browser cookies if you are running browser session.
In this case use `setSharedCookies()` from `@codeceptjs/configure` package:

```js
import { setSharedCookies } from '@codeceptjs/configure'

// call before exporting config
setSharedCookies()

export const config = {
  // ...
  helpers: {  
    // also works with Playwright or Puppeteer
    WebDriver: {
      //... 
    },

    REST: { 
      // ...
    }
  }
}
```

### REST

REST helper can send GET/POST/PATCH/etc requests to REST API endpoint:

* [`I.sendGetRequest()`](/helpers/REST#sendGetRequest)
* [`I.sendPostRequest()`](/helpers/REST#sendPostRequest)
* [`I.sendPutRequest()`](/helpers/REST#sendPutRequest)
* [`I.sendPatchRequest()`](/helpers/REST#sendPatchRequest)
* [`I.sendDeleteRequest()`](/helpers/REST#sendDeleteRequest)
* [`I.sendDeleteRequestWithPayload()`](/helpers/REST#sendDeleteRequestWithPayload)
* ...

Authentication headers can be set in [helper's config](https://codecept.io/helpers/REST/#configuration) or per test with headers or special methods like `I.amBearerAuthenticated`.

Example:

```js
Feature('Users endpoint')

Scenario('create user', ({ I }) => {
    // this way we pass Bearer token
  I.amBearerAuthenticated(secret('token-is-here'));
  // for custom authorization with headers use
  // I.haveRequestHeaders method

  // here we send a POST request
  const response = await I.sendPostRequest('/users', {
    name: 'joe',
    email: 'joe@mail.com'
  });
  // usually we won't need direct access to response object for API testing 
  // but you can obtain it from request

  // check the last request was successful
  // this method introduced by JSONResponse helper
  I.seeResponseCodeIsSuccessful();
})
```

### GraphQL

GraphQL have request format different then in REST API, but the response format is the same.
It's plain old JSON. This why `JSONResponse` helper works for both API types.
Configure authorization headers in `codecept.conf.js` and make your first query:  

```js
Feature('Users endpoint')

Scenario('get user by query', ({ I }) => {
  // make GraphQL query or mutation
  const resp = await I.sendQuery('{ user(id: 0) { id name email }}');
  I.seeResponseCodeIsSuccessful();

  // GraphQL always returns key data as part of response
  I.seeResponseContainsKeys(['data']);

  // check data for partial inclusion
  I.seeResponseContainsJson({
    data: {
      user: {
        name: 'john doe',
        email: 'johnd@mutex.com',
      },
    },
  });
});
```

GraphQL helper has two methods available:

* [`I.sendQuery()`](/helpers/GraphQL#sendQuery)
* [`I.sendMutation()`](/helpers/GraphQL#sendMutation)

## Assertions

`JSONResponse` provides set of assertions for responses in JSON format. These assertions were designed to check only invariable parts of responses. So instead of checking that response equals to the one provided, we will check for data inclusion and structure matching.

For most of cases, you won't need to perform assertions by accessing `response` object directly. All assretions are performed under hood inside `JSONResponse` module. It is recommended to keep it that way, to keep tests readable and make test log to contain all assertions.

```js
Scenario('I make API call', ({ I }) => {
  // request was made by REST 
  // or by GraphQL helper

  // check that response code is 2xx
  I.seeResponseCodeIsSuccessful();

  // check that response contains keys
  I.seeResponseContainsKeys(['data', 'pages', 'meta']);
});
```

### Response Status Codes

[Response status codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) can be checked to be equal to some value or to be in a specific range. 
To check that response code is `200` call `I.seeResponseCodeIs`:

```js
I.seeResponseCodeIs(200);
```
But because other response codes in 2xx range are also valid responses, you can use `seeResponseCodeIsSuccessful()` which will match 200 (OK), 201 (Created), 206 (Partial Content) and others. Methods to check 3xx, 4xx, 5xx response statuses also available.

```js
// matches 200, 201, 202, ... 206
I.seeResponseCodeIsSuccessful();

// matches 300...308
I.seeResponseCodeIsRedirection();

// matches 400..451
I.seeResponseCodeIsClientError();

// matches 500-511
I.seeResponseCodeIsServerError();
```

### Structure

The most basic thing to check in response is existence of keys in JSON object. Use [`I.seeResponseContainsKeys()`](/helpers/JSONResponse#seeResponseContainsKeys) method for it:

```js
// response is { "name": "joe", "email": "joe@joe.com" }
I.seeResponseContainsKeys(['name', 'email']);
```

> ℹ️ If response is an array, it will check that every element in array have provided keys

However, this is a very naive approach. It won't work for arrays or nested objects.
To check complex JSON structures `JSONResponse` helper uses [`Zod`](https://zod.dev) library.
It has rich API to validate JSON by the schema defined using JavaScript.

```js
// import zod library,
// it is installed with CodeceptJS
import { z } from 'zod';

// create schema definition using Zod API
const schema = z.object({
  email: z.string().email(),
  phone: z.string().regex(/^\d{3}-\d{3}-\d{4}$/),
  birthday: z.string().datetime().max(new Date('2004-01-01'))
});

// check that response matches that schema
I.seeResponseMatchesJsonSchema(schema);
```

> 📋 **Migration Note**: CodeceptJS has migrated from Joi to Zod v4 for JSON schema validation.
> If you have existing tests using Joi, please update them:
> * Replace `const Joi = require('joi')` with `import { z } from 'zod'`
> * Replace `Joi.object().keys({...})` with `z.object({...})`
> * Replace `Joi.string().email()` with `z.string().email()`
> * Replace `Joi.date()` with appropriate `z.string()` or `z.date()` types
> * See [Zod documentation](https://zod.dev) for complete API reference

### Data Inclusion

To check that response contains expected data use `I.seeResponseContainsJson` method. 
It will check the response data for partial match. 

```js
I.seeResponseContainsJson({
  user: {
    email: 'user@user.com'
  }
})
```

> ℹ️ If response is an array, it will check that at least one element in array matches JSON

To perform arbitrary assertions on a response object use `seeResponseValidByCallback`. 
It allows you to do any kind of assertions by using `expect` from [`chai`](https://www.chaijs.com) library.

```js
I.seeResponseValidByCallback(({ data, status, expect }) => {
  // we receive data and expect to combine them for good assertion
  expect(data.users.length).to.be.gte(10);
})
```

## Extending JSONResponse

To add more assertions it is recommended to create a custom helper.
Inside it you can get access to latest JSON response:

```js
// inside a custom helper
makeSomeCustomAssertion() {
  const response = this.helpers.JSONResponse.response;
}
```

### Chapter: tutorial (docs/tutorial.md)

---
permalink: /tutorial
title: CodeceptJS Complete Tutorial
---

# Tutorial: Testing a Checkout Page

**[CodeceptJS](https://codecept.io) is a popular open-source end-to-end testing framework** for JavaScript. It is designed to make web tests readable and easy to maintain by writing them as a linear scenario of user actions. By default it drives the browser with **[Playwright](https://playwright.dev)**, but the same tests can run via WebDriver, Puppeteer, or Appium without changes.

In this tutorial we write a real, runnable test for the **[Bootstrap checkout example](https://getbootstrap.com/docs/4.0/examples/checkout/)** — a public page with a billing and payment form. By the end you will have a clean test and a reusable page object.

## Install CodeceptJS

You need Node.js (and npm) installed. Check with:

```bash
node --version
npm --version
```

Create a new folder, then install CodeceptJS together with Playwright:

```bash
npm init -y
npm install codeceptjs playwright --save-dev
npx playwright install --with-deps
```

`npx playwright install` downloads the Chromium, Firefox, and WebKit browsers; `--with-deps` also installs the system libraries they need.

Now scaffold the project:

```bash
npx codeceptjs init
```

`init` runs a short wizard. Accept the defaults — when asked for the **base URL** enter `https://getbootstrap.com`, and name the first test **Checkout**. This creates:

```
.
├── codecept.conf.js
├── package.json
└── Checkout_test.js
```

`codecept.conf.js` holds the project configuration. Because CodeceptJS 4.x uses **ES modules**, the config and tests use `import`/`export` syntax — `init` sets `"type": "module"` in `package.json` for you.

Open `codecept.conf.js`. The two settings that matter here are the helper and the base URL:

```js
import { setHeadlessWhen } from '@codeceptjs/configure'

// show the browser locally, run headless on CI
setHeadlessWhen(process.env.CI)

export const config = {
  tests: './*_test.js',
  output: './output',
  helpers: {
    Playwright: {
      url: 'https://getbootstrap.com',
      browser: 'chromium',
    },
  },
}
```

## Your First Test

Open `Checkout_test.js`:

```js
Feature('Checkout');

Scenario('test something', ({ I }) => {
});
```

A test lives inside a `Scenario` block. Let's open the checkout page:

```js
Feature('Checkout');

Scenario('test something', ({ I }) => {
  I.amOnPage('/docs/4.0/examples/checkout/');
});
```

`I.amOnPage()` navigates the browser. Because the path is relative, it is appended to the base URL from the config — keep the base URL in config so you can switch between staging and production without touching tests.

But you may be wondering...

### What is `I`?

In CodeceptJS the `I` object is the **actor** — it represents the user performing actions. It exposes methods (called *actions*) that simulate interactions with the app:

- `I.amOnPage(url)` — navigate to a URL
- `I.click(locator)` — click an element
- `I.fillField(field, value)` — type into an input
- `I.selectOption(select, option)` — choose an option in a dropdown
- `I.checkOption(locator)` — tick a checkbox or radio
- `I.see(text)` — assert that text is visible
- `I.seeInField(field, value)` — assert an input holds a value

CodeceptJS **waits automatically** before clicking, filling, and most other actions, so you rarely need explicit waits. Steps also write themselves into a promise chain, so you usually **don't need `await`** for regular actions — only for `grab*` actions and page object methods that return data.

### Locating Elements

Most actions accept a locator. CodeceptJS supports several strategies — prefer the readable ones:

```js
// by visible text / label
I.click('Continue to checkout');
I.fillField('First name', 'John');

// by ARIA role and accessible name (resilient to CSS changes)
I.click({ role: 'button', name: 'Continue to checkout' });

// by CSS or XPath, when nothing semantic is available
I.fillField('#email', 'john@example.com');
```

> **Best practice:** prefer labels and ARIA locators (`{ role, name }`). They survive styling changes and document intent. Fall back to CSS/XPath only when needed.

## Writing the Checkout Test

The Bootstrap checkout form has billing fields, country/state selects, and a payment section. CodeceptJS finds inputs by their visible `<label>`, so the test reads like the form:

```js
Feature('Checkout');

Scenario('fill in the checkout form', ({ I }) => {
  I.amOnPage('/docs/4.0/examples/checkout/');
  I.see('Checkout form');

  // billing address — fields located by their labels
  I.fillField('First name', 'John');
  I.fillField('Last name', 'Doe');
  I.fillField('Username', 'johndoe');
  I.fillField('#email', 'john@example.com'); // label has "(Optional)", use CSS
  I.fillField('Address', '123 Main St.');
  I.selectOption('Country', 'United States');
  I.selectOption('State', 'California');
  I.fillField('Zip', '10001');

  // shipping / preferences
  I.checkOption('Shipping address is the same as my billing address');
  I.checkOption('Save this information for next time');

  // payment — "Credit card" is selected by default
  I.click('Credit card');
  I.fillField('Name on card', 'John Doe');
  I.fillField('Credit card number', secret('4111 1111 1111 1111'));

  // verify the form holds what we entered
  I.seeInField('First name', 'John');
  I.seeInField('Address', '123 Main St.');
  I.click('Continue to checkout');
});
```

A few things worth noting:

- **`secret()`** wraps the card number so it is masked (`****`) in logs and reports. Use it for any sensitive value — see [Secrets](/secrets).
- Never use a real card number. Payment providers like Stripe publish [test card numbers](https://docs.stripe.com/testing) for exactly this.
- This is a static demo page with no backend, so we verify by reading field values back with `I.seeInField`. On a real shop you would assert a confirmation, e.g. `I.see('Your order has been placed')`.

### A Negative Scenario

Good test suites cover failures too. The form validates on submit — submitting it empty shows error messages. CodeceptJS doesn't allow multiple scenarios in one file's suite to nest, but you can add as many `Scenario` blocks as you like:

```js
Scenario('shows validation errors on empty submit', ({ I }) => {
  I.amOnPage('/docs/4.0/examples/checkout/');
  I.click('Continue to checkout');
  I.see('Valid first name is required.');
});
```

### Running the Test

```bash
npx codeceptjs run --steps
```

`--steps` prints every step as it runs. Useful flags while developing:

- `--steps` — print each step
- `--debug` — steps plus extra debug output (recommended while writing tests)
- `--verbose` — everything, including the promise chain

Set a breakpoint to inspect the page interactively by adding `pause()` to the scenario:

```js
Scenario('fill in the checkout form', ({ I }) => {
  I.amOnPage('/docs/4.0/examples/checkout/');
  I.fillField('First name', 'John');
  pause(); // test stops here; type steps live in the browser
});
```

In the pause shell you can type `I.click('...')`, inspect the page, and find better locators. See [Debugging](/debugging).

The browser is shown locally and runs headless on CI thanks to `setHeadlessWhen(process.env.CI)`. To force it either way for one run:

```bash
npx codeceptjs run --headless
```

Once the test is stable, run the whole suite:

```bash
npx codeceptjs run
```

## Refactoring with a Page Object

What if more tests need to fill this form? Copy-pasting steps doesn't scale. The **Page Object** pattern keeps locators and interactions in one reusable place.

Generate one:

```bash
npx codeceptjs gpo
```

Call it `Checkout`. It is created in `./pages/Checkout.js` and registered in `codecept.conf.js` under `include`:

```js
export const config = {
  // ...
  include: {
    checkoutPage: './pages/Checkout.js',
  },
}
```

Page objects are **classes**. Move the form interactions into named methods:

```js
const { I } = inject();

class CheckoutPage {
  url = '/docs/4.0/examples/checkout/'

  open() {
    I.amOnPage(this.url);
    I.see('Checkout form');
  }

  fillBillingAddress({ firstName, lastName, username, address, country, state, zip }) {
    I.fillField('First name', firstName);
    I.fillField('Last name', lastName);
    I.fillField('Username', username);
    I.fillField('Address', address);
    I.selectOption('Country', country);
    I.selectOption('State', state);
    I.fillField('Zip', zip);
  }

  payWithCard(name, number) {
    I.click('Credit card');
    I.fillField('Name on card', name);
    I.fillField('Credit card number', secret(number));
  }

  submit() {
    I.click('Continue to checkout');
  }
}

export default CheckoutPage
```

> `inject()` returns a lazy proxy, so it's safe to destructure `I` before the class. Export the **class** — CodeceptJS auto-instantiates it. (Plain-object page objects still work but classes support lifecycle hooks and inheritance.)

The test now reads at the business level. Inject `checkoutPage` by the name you set in the config:

```js
Feature('Checkout');

Scenario('complete a checkout', ({ I, checkoutPage }) => {
  checkoutPage.open();
  checkoutPage.fillBillingAddress({
    firstName: 'John',
    lastName: 'Doe',
    username: 'johndoe',
    address: '123 Main St.',
    country: 'United States',
    state: 'California',
    zip: '10001',
  });
  checkoutPage.payWithCard('John Doe', '4111 1111 1111 1111');
  checkoutPage.submit();

  I.seeInField('First name', 'John');
});
```

Shorter, intention-revealing, and every other checkout test can reuse the same methods. As coverage grows, add methods to the page object instead of duplicating steps.

## Going Further

When you have many tests, run them in parallel using Node workers:

```bash
npx codeceptjs run-workers 3
```

From here, explore:

- [Locators](/locators) — every locating strategy in depth
- [Page Objects](/pageobjects) — fragments, step objects, lifecycle hooks
- [Data-driven tests](/data) — run one scenario over many inputs
- [Debugging](/debugging) — `pause()`, the interactive shell, and AI-assisted debugging
- [Continuous Integration](/continuous-integration) — running the suite on CI

## Summary

If you are just starting with test automation, CodeceptJS lets you describe tests in near-natural language and handles waiting and retries for you. If you already know JavaScript, page objects and dependency injection keep your suite focused on business behavior — which is what keeps tests stable and maintainable as the app grows.

> [▶ Next: CodeceptJS Basics](/basics/)
</content>
</invoke>