docs/element-based-testing.md
CodeceptJS offers multiple ways to write tests. While the traditional I.* actions provide a clean, readable syntax, element-based testing gives you more control and flexibility when working with complex DOM structures.
Element-based testing is useful when:
CodeceptJS uniquely combines both styles. You can freely mix I.* actions with element-based operations in the same test:
// Import element functions
import { element, eachElement, expectElement } from 'codeceptjs/els'
Scenario('checkout flow', async ({ I }) => {
// Use I.* for navigation and high-level actions
I.amOnPage('/products')
I.click('Add to Cart')
// Use element-based for detailed validation
await element('.cart-summary', async cart => {
const total = await cart.getAttribute('data-total')
console.log('Cart total:', total)
})
// Continue with I.* actions
I.click('Checkout')
})
This hybrid approach gives you the best of both worlds - readable high-level actions mixed with low-level control when needed.
Scenario('form validation', async ({ I }) => {
I.amOnPage('/register')
I.fillField('Email', '[email protected]')
I.fillField('Password', 'secret123')
I.click('Register')
I.see('Welcome')
})
import { element, expectElement } from 'codeceptjs/els'
Scenario('form validation', async ({ I }) => {
I.amOnPage('/register')
// Direct form manipulation
await element('#email', async input => {
await input.type('[email protected]')
})
await element('#password', async input => {
await input.type('secret123')
})
await element('button[type="submit"]', async btn => {
await btn.click()
})
// Custom assertion
await expectElement('.welcome-message', async msg => {
const text = await msg.getText()
return text.includes('Welcome')
})
})
Use I.* actions when... | Use element-based when... |
|---|---|
| Simple navigation and clicks | Complex DOM traversal |
| Standard form interactions | Custom validation logic |
| Built-in assertions suffice | Need specific element properties |
| Readability is priority | Working with element collections |
| Single-step operations | Chaining multiple operations on same element |
Element-based testing allows you to chain queries to find child elements, reducing redundant lookups:
import { element } from 'codeceptjs/els'
Scenario('product list', async ({ I }) => {
I.amOnPage('/products')
// Chain into child elements
await element('.product-list', async list => {
const firstProduct = await list.$('.product-item')
const title = await firstProduct.$('.title')
const price = await firstProduct.$('.price')
const titleText = await title.getText()
const priceValue = await price.getText()
console.log(`${titleText}: ${priceValue}`)
})
})
Validate complex form requirements that built-in methods don't cover:
import { element, eachElement } from 'codeceptjs/els'
import { expect } from 'chai'
Scenario('validate form fields', async ({ I }) => {
I.amOnPage('/register')
// Check all required fields are properly marked
await eachElement('[required]', async field => {
const ariaRequired = await field.getAttribute('aria-required')
const required = await field.getAttribute('required')
if (!ariaRequired && !required) {
throw new Error('Required field missing indicators')
}
})
// Fill form with custom validation
await element('#email', async input => {
await input.type('[email protected]')
const value = await input.getValue()
expect(value).to.include('@')
})
I.click('Submit')
})
Work with tabular data using iteration and child element queries:
import { eachElement, element } from 'codeceptjs/els'
Scenario('verify table data', async ({ I }) => {
I.amOnPage('/dashboard')
// Get table row count
await element('table tbody', async tbody => {
const rows = await tbody.$$('tr')
console.log(`Table has ${rows.length} rows`)
})
// Verify each row has expected structure
await eachElement('table tbody tr', async (row, index) => {
const cells = await row.$$('td')
if (cells.length < 3) {
throw new Error(`Row ${index} should have at least 3 columns`)
}
})
})
Wait for and validate dynamic content with custom conditions:
import { element, expectElement } from 'codeceptjs/els'
Scenario('wait for dynamic content', async ({ I }) => {
I.amOnPage('/search')
I.fillField('query', 'test')
I.click('Search')
// Wait for results with custom validation
await expectElement('.search-results', async results => {
const items = await results.$$('.result-item')
return items.length > 0
})
})
Calculate and verify cart totals by iterating through items:
import { element, eachElement } from 'codeceptjs/els'
import { expect } from 'chai'
Scenario('calculate cart total', async ({ I }) => {
I.amOnPage('/cart')
let total = 0
// Sum up all item prices
await eachElement('.cart-item .price', async priceEl => {
const priceText = await priceEl.getText()
const price = parseFloat(priceText.replace('$', ''))
total += price
})
// Verify displayed total matches calculated sum
await element('.cart-total', async totalEl => {
const displayedTotal = await totalEl.getText()
const displayedValue = parseFloat(displayedTotal.replace('$', ''))
expect(displayedValue).to.equal(total)
})
})
Validate filtered results meet specific criteria:
import { element, eachElement, expectAnyElement } from 'codeceptjs/els'
import { expect } from 'chai'
Scenario('filter products by price', async ({ I }) => {
I.amOnPage('/products')
I.click('Under $100')
// Verify all displayed products are under $100
await eachElement('.product-item', async product => {
const priceEl = await product.$('.price')
const priceText = await priceEl.getText()
const price = parseFloat(priceText.replace('$', ''))
expect(price).to.be.below(100)
})
// Check at least one product exists
await expectAnyElement('.product-item', async () => true)
})
Mix styles appropriately - Use I.* for navigation and high-level actions, element-based for complex validation
Use descriptive purposes - Add purpose strings for better debugging logs:
await element(
'verify discount applied',
'.price',
async el => { /* ... */ }
)
Reuse element references - Chain $(locator) to avoid redundant lookups
Handle empty results - Always check if elements exist before accessing properties
Prefer standard assertions - Use I.see(), I.dontSee() when possible for readability
Consider page objects - Combine with Page Objects for reusable element logic
element(), eachElement(), expectElement(), expectAnyElement(), expectAllElements() functionsgetText(), getAttribute(), click(), $$(), etc.)Elements are wrapped in a WebElement class that provides a consistent API across all helpers (Playwright, WebDriver, Puppeteer). Your element-based tests will work the same way regardless of which helper you're using:
// This test works identically with Playwright, WebDriver, or Puppeteer
import { element } from 'codeceptjs/els'
Scenario('portable test', async ({ I }) => {
I.amOnPage('/')
await element('.main-title', async title => {
const text = await title.getText() // Works on all helpers
const className = await title.getAttribute('class')
const visible = await title.isVisible()
const enabled = await title.isEnabled()
})
})