docs/assertions.md
CodeceptJS ships with browser assertions built into the I object — I.see('Welcome'), I.seeElement('.cart'), I.dontSee('Error'). They read like prose, produce clear failure messages, and cover most day-to-day checks with no extra setup.
When the built-ins are not enough — sort order, business math, JSON shapes, domain rules — you have three ways to assert, in order of preference:
I.seeTableIsOrdered('Price', 'desc')I.expectDeepEqualExcluding(order, expected, ['id'])chai, jest, or Node's assertThis page also covers element assertions via WebElement, soft assertions for running many checks in one scenario, and masking secrets in assertion logs.
Every browser helper (Playwright, WebDriver, Puppeteer) exposes the same assertion API. Every positive check has a dontSee* counterpart.
I.see(text) asserts that visible text appears on the page. A second argument scopes the search.
I.see('Order confirmed')
I.see('Total: $42.00', '.cart-summary')
I.dontSee('Out of stock')
seeTextEquals is stricter — the element's text must match exactly.
I.seeTextEquals('Welcome, Miles', 'h1')
seechecks rendered, visible text. For content hidden by CSS, useseeInSourceorseeElementInDOM.
ARIA locators make the strongest assertions — they survive CSS refactors and describe what the user sees.
I.seeElement({ role: 'button', name: 'Submit' })
I.seeElement('.alert-success', '#checkout')
I.dontSeeElement('.spinner')
I.seeElementInDOM('#hidden-token') // in the DOM, possibly invisible
I.dontSeeElementInDOM('.removed-row')
I.seeNumberOfElements('.cart-item', 3)
I.seeNumberOfVisibleElements('.notification', 1)
I.seeInField('Email', '[email protected]')
I.seeCheckboxIsChecked('Accept Terms')
I.seeInCurrentUrl('/dashboard')
I.seeInTitle('Admin — Orders')
I.seeCookie('session')
Reach for a custom assertion when a check is:
I.see calls that obscure intentFour options, from least setup to most:
I (I.expectEqual, I.expectDeepEqual, …). Best for quick, readable one-offs on grabbed data.codeceptjs/assertions — the same factories CodeceptJS uses internally. Zero dependencies, failure messages match I.see style.chai, jest, node:assert. Use when you need a matcher the others do not cover.I.see* method via a helper class. Best for checks that repeat across many tests.ExpectHelper exposes chai's assertions as methods on the I object. Use it for one-off checks on data you have already grabbed — no helper class, no boilerplate.
Install separately:
npm i -D @codeceptjs/expect-helper
Configure:
helpers: {
Playwright: { /* ... */ },
ExpectHelper: {},
}
Use it in scenarios:
Scenario('checkout total matches the sum of line items', async ({ I }) => {
I.amOnPage('/cart')
const prices = await I.grabTextFromAll('.line-item .price')
const total = await I.grabTextFrom('.cart-total')
const sum = prices
.map(p => Number(p.replace(/[^0-9.]/g, '')))
.reduce((a, b) => a + b, 0)
I.expectEqual(Number(total.replace(/[^0-9.]/g, '')), sum)
})
Scenario('API returns the created order shape', async ({ I }) => {
const { data } = await I.sendPostRequest('/api/orders', { items: ['SKU-1'] })
I.expectDeepEqualExcluding(
data,
{ items: ['SKU-1'], status: 'pending', total: 29.99 },
['id', 'createdAt', 'updatedAt'],
)
I.expectMatchesPattern(data.id, /^ord_[a-z0-9]{16}$/)
I.expectLengthOf(data.items, 1)
})
Common methods:
| Method | Purpose |
|---|---|
expectEqual / expectNotEqual | Shallow equality |
expectDeepEqual / expectDeepMembers | Deep equality for objects and arrays |
expectDeepEqualExcluding | Deep equality, ignoring named fields |
expectContain / expectStartsWith / expectEndsWith | Substring / prefix / suffix |
expectMatchesPattern / expectMatchRegex | Regex match |
expectAbove / expectBelow / expectLengthOf | Numeric and length checks |
expectHasProperty / expectEmpty | Object shape |
expectJsonSchema / expectJsonSchemaUsingAJV | Full schema validation |
ExpectHelper calls appear in the step log next to browser steps, so failures read in order with the rest of the scenario. See the full reference.
CodeceptJS ships a small, dependency-free assertion library at codeceptjs/assertions. It powers every built-in I.see* method, and you can use it directly in your own scenarios and helpers. Failure messages render with the same formatting as I.see failures, so reports stay consistent.
import { equals, includes, empty, truth } from 'codeceptjs/assertions'
Each factory takes a subject — the noun that appears in the failure message — and returns an assertion with .assert(actual, expected) (fails on mismatch) and .negate(actual, expected) (the dontSee* direction).
Strict equality, comparing a grabbed value to an expected one:
const total = await I.grabTextFrom('.cart-total')
equals('cart total').assert(total, '$42.00')
// expected cart total "$10.00" to equal "$42.00"
// negate — useful when an action should change a value
const sessionAfter = await I.grabCookie('session')
equals('session id').negate(sessionAfter.value, sessionBefore.value)
// expected session id not to equal "abc123"
Substring or array contains, working on grabbed text or arrays:
const title = await I.grabTitle()
includes('page title').assert('Welcome', title)
// expected page title to include "Welcome"
const resultTitles = await I.grabTextFromAll('.result h3')
includes('search results').assert('miles', resultTitles)
// expected search results to include "miles"
const logs = await I.grabBrowserLogs()
includes('console logs').negate('Uncaught', logs.map(l => l.text()))
// expected console logs not to include "Uncaught"
Empty value or empty array — pairs naturally with grabTextFromAll or grabWebElements:
I.click('Archive all')
const remaining = await I.grabWebElements('.email-row')
empty('inbox').assert(remaining)
// expected inbox '[ELEMENT, ELEMENT]' to be empty
I.click('Submit')
const errors = await I.grabTextFromAll('.field-error')
empty('form errors').assert(errors)
// expected form errors '[Email is required]' to be empty
Truthy value with custom phrasing — the second argument shapes the message:
const cookie = await I.grabCookie('session')
truth('session cookie', 'to be set').assert(cookie)
// expected session cookie to be set
const button = await I.grabWebElement({ role: 'button', name: 'Checkout' })
truth('checkout button', 'to be enabled').assert(await button.isEnabled())
// expected checkout button to be enabled
const stock = Number(await I.grabAttributeFrom('.product', 'data-stock'))
truth('stock level', 'to be positive').assert(stock > 0)
// expected stock level to be positive
For comparisons the four factories do not cover, fall through to chai/jest/node:assert, or wrap the check in a reusable custom assertion helper.
When you need a matcher that ExpectHelper does not cover, or your team already standardises on a library, grab the data and assert however you like. Any library works — grab* methods return plain JavaScript values.
grab*methods always needawait.
Node's built-in assert — zero dependencies:
import { strict as assert } from 'node:assert'
Scenario('profile email matches the logged-in user', async ({ I }) => {
I.amOnPage('/profile')
const email = await I.grabTextFrom('.user-email')
assert.equal(email, '[email protected]')
})
Chai:
import { expect } from 'chai'
Scenario('product list is sorted alphabetically', async ({ I }) => {
I.amOnPage('/catalog')
const names = await I.grabTextFromAll('.product .name')
expect(names).to.deep.equal([...names].sort())
})
Jest's expect (install expect standalone if you are not on Jest):
import { expect } from 'expect'
Scenario('dashboard renders every KPI', async ({ I }) => {
I.amOnPage('/dashboard')
const kpis = await I.grabTextFromAll('.kpi .value')
expect(kpis).toHaveLength(6)
expect(kpis[0]).toMatch(/^\$[\d,]+$/)
})
Failures from these libraries fail the scenario normally, but they do not appear as CodeceptJS steps — the failure shows up in the error output. For checks you want visible in the step log, prefer ExpectHelper or codeceptjs/assertions.
When the same check appears across many tests, wrap it in a custom helper. The assertion lives in one place, has a name that reads like a requirement, and produces a clean failure message.
Scaffold a helper with npx codeceptjs gh, then write a class extending @codeceptjs/helper. Public methods — anything not prefixed with _ — become methods on I. Reach other helpers through this.helpers['<HelperName>']. Inside the helper, use codeceptjs/assertions (or any of the libraries above) — never throw new Error(...) — so failures render as proper assertion errors.
helpers/table_assertions.js
import Helper from '@codeceptjs/helper'
import { equals } from 'codeceptjs/assertions'
class TableAssertions extends Helper {
/**
* @param {string} columnName - text of the column header
* @param {'asc'|'desc'} order
*/
async seeTableIsOrdered(columnName, order = 'asc') {
const { Playwright } = this.helpers
const headers = await Playwright.grabTextFromAll('table thead th')
const col = headers.findIndex(h => h.trim() === columnName) + 1
const cells = await Playwright.grabTextFromAll(`table tbody tr td:nth-child(${col})`)
const sorted = [...cells].sort()
if (order === 'desc') sorted.reverse()
equals(`column "${columnName}" sorted ${order}`).assert(cells.join(','), sorted.join(','))
}
}
export default TableAssertions
Wire it up in codecept.conf.js:
helpers: {
Playwright: { /* ... */ },
TableAssertions: { require: './helpers/table_assertions.js' },
}
Use it:
Scenario('orders table sorts by price on click', ({ I }) => {
I.amOnPage('/orders')
I.click('Price')
I.seeTableIsOrdered('Price', 'asc')
I.click('Price')
I.seeTableIsOrdered('Price', 'desc')
})
Follow the naming convention: positive assertions start with
see*, negative withdontSee*(use.negate()from the same factory). It keeps the custom API consistent with CodeceptJS built-ins.
grabWebElement and grabWebElements return objects with a uniform API across helpers: isVisible(), isEnabled(), getText(), getAttribute(), getBoundingBox(), exists(). See the full WebElement API.
Use WebElement when you need to loop over many elements and assert on each.
Scenario('every todo row has a label and a checkbox', async ({ I }) => {
I.amOnPage('/todos')
const rows = await I.grabWebElements('.todo-item')
I.expectLengthAboveThan(rows, 0)
for (const row of rows) {
const label = await row.getText()
I.expectNotEmpty(label.trim())
const checkbox = await row.$('input[type="checkbox"]')
I.expectTrue(await checkbox.isVisible())
}
})
getBoundingBox() enables layout assertions — confirming a sticky header stays pinned, or a tooltip sits inside the viewport.
const header = await I.grabWebElement('.sticky-header')
const box = await header.getBoundingBox()
I.expectEqual(box.y, 0)
Use a soft assertion when one scenario needs to check many independent facts and you want to see every failure in one run, not just the first.
CodeceptJS provides hopeThat from codeceptjs/effects. It wraps a block of I.* steps:
hopeThat returns true.hopeThat returns false.import { hopeThat } from 'codeceptjs/effects'
Scenario('registration form shows every validation error at once', async ({ I }) => {
I.amOnPage('/register')
I.click('Create Account') // submit empty form
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'))
await hopeThat(() => I.seeElement('.summary-error'))
})
Failures are written to the test log as Unsuccessful assertion > ... and attached to the test as notes for reporters that surface them.
hopeThat does not fail the scenario on its own — each call logs the failure and lets the scenario continue. Call hopeThat.noErrors() once at the end to fail the scenario if any soft assertion failed. It throws a single assertion error listing every recorded failure and clears the state for the next test.
import { hopeThat } from 'codeceptjs/effects'
Scenario('registration form shows every validation error at once', async ({ I }) => {
I.amOnPage('/register')
I.click('Create Account') // submit empty form
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'))
await hopeThat(() => I.seeElement('.summary-error'))
hopeThat.noErrors()
})
If two checks failed, the scenario fails with a single aggregated message like:
expected soft assertions '[expected web application to include "You must accept the terms", expected element (.summary-error) to be visible]' to be empty
| You want to check… | Use |
|---|---|
| Visible text on the page | I.see / I.dontSee |
| An element by role and accessible name | I.seeElement({ role, name }) |
| A form field's current value | I.seeInField / I.seeCheckboxIsChecked |
| URL or page title | I.seeInCurrentUrl / I.seeInTitle |
| A count of matching elements | I.seeNumberOfElements |
| Business logic / JSON shape on grabbed data | ExpectHelper — expectEqual, expectDeepEqualExcluding, expectJsonSchema |
| A lightweight, dependency-free assertion in a scenario | equals, includes, empty, truth from codeceptjs/assertions |
| Per-element state in a loop | grabWebElements + WebElement API |
| A matcher the above do not cover | grab* + chai / jest / node:assert |
| A reusable, project-specific check | Custom helper with see* method using codeceptjs/assertions |
| Many independent checks in one run | hopeThat from codeceptjs/effects |
| Hiding values from logs | secret() |