Skip to content

Assertions

CodeceptJS ships with browser assertions built into the I objectI.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:

  1. Reusable custom assertions in a helper: I.seeTableIsOrdered('Price', 'desc')
  2. Quick assertions with ExpectHelper on grabbed data: I.expectDeepEqualExcluding(order, expected, ['id'])
  3. Bring your own librarychai, jest, or Node’s assert

This 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')

see checks rendered, visible text. For content hidden by CSS, use seeInSource or seeElementInDOM.

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', 'miles@davis.com')
I.seeCheckboxIsChecked('Accept Terms')
I.seeInCurrentUrl('/dashboard')
I.seeInTitle('Admin — Orders')
I.seeCookie('session')

Reach for a custom assertion when a check is:

  • Repeated across tests — “is this table sorted?”, “does this response match our Order schema?”
  • Domain-specific — “is the cart total equal to the sum of its items plus tax?”
  • Structural, not textual — shape of a JSON payload, ordering of a list, layout of elements
  • Too noisy with built-ins — chains of I.see calls that obscure intent

Four options, from least setup to most:

  • Expect Helper — chai matchers exposed on I (I.expectEqual, I.expectDeepEqual, …). Best for quick, readable one-offs on grabbed data.
  • Built-in codeceptjs/assertions — the same factories CodeceptJS uses internally. Zero dependencies, failure messages match I.see style.
  • External librarychai, jest, node:assert. Use when you need a matcher the others do not cover.
  • Reusable custom assertions — wrap any of the above into your own 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:

Terminal window
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:

MethodPurpose
expectEqual / expectNotEqualShallow equality
expectDeepEqual / expectDeepMembersDeep equality for objects and arrays
expectDeepEqualExcludingDeep equality, ignoring named fields
expectContain / expectStartsWith / expectEndsWithSubstring / prefix / suffix
expectMatchesPattern / expectMatchRegexRegex match
expectAbove / expectBelow / expectLengthOfNumeric and length checks
expectHasProperty / expectEmptyObject shape
expectJsonSchema / expectJsonSchemaUsingAJVFull 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 need await.

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, 'miles@davis.com')
})

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 with dontSee* (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:

  • If the steps inside succeed, hopeThat returns true.
  • If any step inside fails, the failure is logged to the report as a note, the scenario keeps running, and 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 pageI.see / I.dontSee
An element by role and accessible nameI.seeElement({ role, name })
A form field’s current valueI.seeInField / I.seeCheckboxIsChecked
URL or page titleI.seeInCurrentUrl / I.seeInTitle
A count of matching elementsI.seeNumberOfElements
Business logic / JSON shape on grabbed dataExpectHelperexpectEqual, expectDeepEqualExcluding, expectJsonSchema
A lightweight, dependency-free assertion in a scenarioequals, includes, empty, truth from codeceptjs/assertions
Per-element state in a loopgrabWebElements + WebElement API
A matcher the above do not covergrab* + chai / jest / node:assert
A reusable, project-specific checkCustom helper with see* method using codeceptjs/assertions
Many independent checks in one runhopeThat from codeceptjs/effects
Hiding values from logssecret()