docs/tutorial.md
CodeceptJS 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, 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 — a public page with a billing and payment form. By the end you will have a clean test and a reusable page object.
You need Node.js (and npm) installed. Check with:
node --version
npm --version
Create a new folder, then install CodeceptJS together with Playwright:
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:
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:
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',
},
},
}
Open Checkout_test.js:
Feature('Checkout');
Scenario('test something', ({ I }) => {
});
A test lives inside a Scenario block. Let's open the checkout page:
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...
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 URLI.click(locator) — click an elementI.fillField(field, value) — type into an inputI.selectOption(select, option) — choose an option in a dropdownI.checkOption(locator) — tick a checkbox or radioI.see(text) — assert that text is visibleI.seeInField(field, value) — assert an input holds a valueCodeceptJS 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.
Most actions accept a locator. CodeceptJS supports several strategies — prefer the readable ones:
// 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', '[email protected]');
Best practice: prefer labels and ARIA locators (
{ role, name }). They survive styling changes and document intent. Fall back to CSS/XPath only when needed.
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:
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', '[email protected]'); // 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.I.seeInField. On a real shop you would assert a confirmation, e.g. I.see('Your order has been placed').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:
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.');
});
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 chainSet a breakpoint to inspect the page interactively by adding pause() to the scenario:
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.
The browser is shown locally and runs headless on CI thanks to setHeadlessWhen(process.env.CI). To force it either way for one run:
npx codeceptjs run --headless
Once the test is stable, run the whole suite:
npx codeceptjs run
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:
npx codeceptjs gpo
Call it Checkout. It is created in ./pages/Checkout.js and registered in codecept.conf.js under include:
export const config = {
// ...
include: {
checkoutPage: './pages/Checkout.js',
},
}
Page objects are classes. Move the form interactions into named methods:
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 destructureIbefore 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:
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.
When you have many tests, run them in parallel using Node workers:
npx codeceptjs run-workers 3
From here, explore:
pause(), the interactive shell, and AI-assisted debuggingIf 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.
</content></invoke>