Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
295 changes: 295 additions & 0 deletions docs/element-based-testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
# Element-Based Testing

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.

## Why Element-Based Testing?

Element-based testing is useful when:

- **You need direct access to DOM properties** - Inspect attributes, computed styles, or form values
- **Working with lists and collections** - Iterate over multiple elements with custom logic
- **Complex assertions** - Validate conditions that built-in methods don't cover
- **Performance optimization** - Reduce redundant lookups by reusing element references
- **Custom interactions** - Perform actions not available in standard helper methods

## The CodeceptJS Hybrid Approach

CodeceptJS uniquely combines both styles. You can freely mix `I.*` actions with element-based operations in the same test:

```js
// 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.

## Quick Comparison

### Traditional I.* Approach

```js
Scenario('form validation', async ({ I }) => {
I.amOnPage('/register')
I.fillField('Email', 'test@example.com')
I.fillField('Password', 'secret123')
I.click('Register')
I.see('Welcome')
})
```

### Element-Based Approach

```js
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('test@example.com')
})

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')
})
})
```

### When to Use Each

| 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 Chaining

Element-based testing allows you to chain queries to find child elements, reducing redundant lookups:

```js
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}`)
})
})
```

## Real-World Examples

### Example 1: Form Validation

Validate complex form requirements that built-in methods don't cover:

```js
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('test@example.com')
const value = await input.getValue()
expect(value).to.include('@')
})

I.click('Submit')
})
```

### Example 2: Data Table Processing

Work with tabular data using iteration and child element queries:

```js
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`)
}
})
})
```

### Example 3: Dynamic Content Waiting

Wait for and validate dynamic content with custom conditions:

```js
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
})
})
```

### Example 4: Shopping Cart Operations

Calculate and verify cart totals by iterating through items:

```js
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)
})
})
```

### Example 5: List Filtering and Validation

Validate filtered results meet specific criteria:

```js
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)
})
```

## Best Practices

1. **Mix styles appropriately** - Use `I.*` for navigation and high-level actions, element-based for complex validation

2. **Use descriptive purposes** - Add purpose strings for better debugging logs:
```js
await element(
'verify discount applied',
'.price',
async el => { /* ... */ }
)
```

3. **Reuse element references** - Chain `$(locator)` to avoid redundant lookups

4. **Handle empty results** - Always check if elements exist before accessing properties

5. **Prefer standard assertions** - Use `I.see()`, `I.dontSee()` when possible for readability

6. **Consider page objects** - Combine with Page Objects for reusable element logic

## API Reference

- **[Element Access](els.md)** - Complete reference for `element()`, `eachElement()`, `expectElement()`, `expectAnyElement()`, `expectAllElements()` functions
- **[WebElement API](WebElement.md)** - Complete reference for WebElement class methods (`getText()`, `getAttribute()`, `click()`, `$$()`, etc.)

## Portability

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:

```js
// 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()
})
})
```
51 changes: 45 additions & 6 deletions docs/els.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
## Element Access

The `els` module provides low-level element manipulation functions for CodeceptJS tests, allowing for more granular control over element interactions and assertions. However, because element representation differs between frameworks, tests using element functions are not portable between helpers. So if you set to use Playwright you won't be able to witch to WebDriver with one config change in CodeceptJS.
The `els` module provides low-level element manipulation functions for CodeceptJS tests, allowing for more granular control over element interactions and assertions. Elements are wrapped in a unified `WebElement` class that provides a consistent API across all helpers (Playwright, WebDriver, Puppeteer).

> **Note:** For a comprehensive guide on element-based testing patterns and best practices, see [Element-Based Testing](element-based-testing.md).

### Usage

Import the els functions in your test file:

```js
const { element, eachElement, expectElement, expectAnyElement, expectAllElements } = require('codeceptjs/els');
import { element, eachElement, expectElement, expectAnyElement, expectAllElements } from 'codeceptjs/els'
```

## element
Expand All @@ -26,7 +28,7 @@ element(locator, fn);

- `purpose` (optional) - A string describing the operation being performed. If omitted, a default purpose will be generated from the function.
- `locator` - A locator string/object to find the element(s).
- `fn` - An async function that receives the element as its argument and performs the desired operation. `el` argument represents an element of an underlying engine used: Playwright, WebDriver, or Puppeteer.
- `fn` - An async function that receives the element as its argument and performs the desired operation. `el` argument is a `WebElement` wrapper providing a consistent API across all helpers.

### Returns

Expand Down Expand Up @@ -104,8 +106,8 @@ Scenario('my test', async ({ I }) => {

// Or simply check if all checkboxes are checked
await eachElement('input[type="checkbox"]', async el => {
const isChecked = await el.isSelected();
if (!isChecked) {
const checked = await el.getProperty('checked');
if (!checked) {
throw new Error('Found unchecked checkbox');
}
});
Expand Down Expand Up @@ -263,7 +265,8 @@ Scenario('validate all elements meet criteria', async ({ I }) => {

// Check if all checkboxes in a form are checked
await expectAllElements('input[type="checkbox"]', async el => {
return await el.isSelected();
const checked = await el.getProperty('checked');
return checked === true;
});

// Verify all items in a list have non-empty text
Expand All @@ -287,3 +290,39 @@ Scenario('validate all elements meet criteria', async ({ I }) => {
- The provided callback must be an async function that returns a boolean
- The assertion message will include which element number failed (e.g., "element #2 of...")
- Throws an error if no helper with `_locate` method is enabled

## WebElement API

Elements passed to your callbacks are wrapped in a `WebElement` class that provides a consistent API across all helpers. For complete documentation of the WebElement API, see [WebElement](WebElement.md).

Quick reference of available methods:

```js
await element('.my-element', async el => {
// Get element information
const text = await el.getText()
const attr = await el.getAttribute('data-value')
const prop = await el.getProperty('value')
const html = await el.getInnerHTML()

// Check state
const visible = await el.isVisible()
const enabled = await el.isEnabled()
const exists = await el.exists()

// Interactions
await el.click()
await el.type('text')

// Child elements
const child = await el.$('.child')
const children = await el.$$('.child')

// Position
const box = await el.getBoundingBox()

// Native access
const helper = el.getHelper()
const native = el.getNativeElement()
})
```
Loading