CodeceptJS is a modern end to end testing framework with a special BDD-style syntax. The tests are written as a linear scenario of the user’s action on a site.
Feature('CodeceptJS demo')
Scenario('check Welcome page on site',({I})=>{
I.amOnPage('/')
I.see('Welcome')
})
Tests are written as ES modules using modern JavaScript syntax.
Each test is described inside a Scenario function with the I object passed into it.
The I object is an actor, an abstraction for a testing user. The I is a proxy object for currently enabled Helpers.
CodeceptJS delegates all test commands to helper backends. Tests written with the I object (the actor) don’t directly execute actions. Instead, CodeceptJS routes them through configurable helpers:
Playwright - Chromium, Firefox, WebKit automation
WebDriver - Native browser automation via WebDriver Protocol
Appium - Mobile testing on iOS/Android
Puppeteer - Chromium automation via DevTools Protocol
All helpers share the same API, so it’s easy to switch backends. However, due to backend differences and limitations, they aren’t guaranteed to be compatible with each other. For example, you can set request headers in Playwright or Puppeteer, but not in WebDriver.
Pick one helper to define how your tests execute. If requirements change, it’s straightforward to migrate to another.
Tests appear synchronous but all actions are wrapped in promises and chained together in a global promise chain. This means:
You usually don’t need await for regular actions - commands are automatically queued
Each I.* command is appended to the promise chain
Setup, teardown, and all test steps execute in sequence
// These execute in order WITHOUT await
I.amOnPage('/')
I.click('Login')
I.see('Welcome')
Behind the scenes, this is equivalent to:
Promise.resolve()
.then(()=>I.amOnPage('/'))
.then(()=>I.click('Login'))
.then(()=>I.see('Welcome'))
When You DO Need await:
Use await only with grab actions (methods that retrieve data from the page):
Scenario('use data from page',async({I})=>{
I.fillField('email','user@example.com')
I.click('Generate Password')
// grab actions return data - use await here
const password =awaitI.grabTextFrom('#password')
I.fillField('password', password)
I.click('Login')
})
Also use await with imported functions and page object methods, as they may contain async operations that aren’t part of the promise chain (e.g., await loginPage.login() if it contains I.grab operations inside).
Rule: If an action starts with grab, or if calling an imported function/page object method, you must await it. Regular actions (I.click(), I.fillField(), I.see()) don’t need await.
Tests are written from a user’s perspective. There is an actor (represented as I) which contains actions taken from helpers. A test is written as a sequence of actions performed by an actor:
I.amOnPage('/')
I.click('Login')
I.see('Please Login','h1')
// ...
A complete test file looks like this:
// suite declaration, like describe() in other frameworks
Feature('User Authentication')
// before each hook
Before(({I})=>{
I.amOnPage('/')
})
// a test
Scenario('user can login with valid credentials',({I})=>{
I.click('Login')
I.fillField('email','user@example.com')
I.fillField('password','password123')
I.click('Sign In')
I.see('Welcome, User')
})
// after each hook
After(({I})=>{
// ...
})
CodeceptJS doesn’t allow nested suites or multiple suites in one file.
When an URL doesn’t start with a protocol (http:// or https://) it is considered to be a relative URL and will be appended to the URL which was initially set-up in the config.
It is recommended to use a relative URL and keep the base URL in the config file, so you can easily switch between development, stage, and production environments.
Use form methods to interact with inputs, selects, checkboxes, and other form elements. Fields can be located by label, name, CSS, XPath, or aria-label:
// Fill fields - by label, name, CSS, or aria-label
I.fillField('Email','user@test.com')
I.fillField('My Address','Home Sweet Home')// matches aria-label or aria-labelledby
I.fillField('LoginForm[username]','davert')// by field name attribute
I.fillField('Password',secret('123456'))// use secret() for sensitive data
// Use context (3rd parameter) to narrow search to specific form
CodeceptJS provides built-in browser assertions instead of generic expect() calls. This keeps tests readable and produces clear failure messages without extra assertion libraries.
I.see(text) - checks that text is visible on the page
I.seeElement(locator) - checks that element exists and is visible in DOM
All assertions have a dontSee / dontSee* counterpart
// Text visibility
I.see('Welcome, Miles')
I.see('Error','.alert')// with context
I.dontSee('Loading...')
// Element presence
I.seeElement({ role:'button', name:'Submit'})
I.seeElement('.success-message','#checkout')// with context
I.dontSeeElement('.error')
I.seeElementInDOM('#hidden-input')// in DOM but may be invisible
Grabbers retrieve data from the page for use in subsequent steps. They are the equivalent of Playwright’s textContent(), Cypress’s cy.get().invoke(), or WebDriver’s getText() — but integrated into the CodeceptJS promise chain.
Grabbers always require await since they return data out of the known promise chain.
CodeceptJS automatically waits for elements before clicking, filling, and most other interactions — so explicit waits are rarely needed. Failed steps are also automatically retried.
Use wait* methods when you need to explicitly wait for a UI change, such as a modal appearing, a spinner hiding, or a value updating:
I.waitForVisible('.modal')// wait for modal to appear
I.waitForInvisible('.spinner')// wait for spinner to hide
I.waitForText('Success',5,'.alert')// wait for text in element (5s timeout)
I.waitForEnabled('#submit-btn')// wait for button to become enabled
I.waitForElement('.results li',10)// wait for results to load
I.wait(2)// explicit pause in seconds (last resort)
Example usage inside scenario:
Scenario('submit and wait for confirmation',({I})=>{
By default tests run headless (no browser window). To open a browser during test execution set show: true in helper config, or use @codeceptjs/configure:
Configuration is set in codecept.conf.js. The two most important settings are the helper (which browser engine to use) and the base URL of your application:
exportconst config ={
helpers:{
Playwright:{
url:'http://localhost:3000',// base URL for I.amOnPage('/')
show:!process.env.CI,// show browser locally, headless on CI