|
| 1 | +# Element-Based Testing |
| 2 | + |
| 3 | +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. |
| 4 | + |
| 5 | +## Why Element-Based Testing? |
| 6 | + |
| 7 | +Element-based testing is useful when: |
| 8 | + |
| 9 | +- **You need direct access to DOM properties** - Inspect attributes, computed styles, or form values |
| 10 | +- **Working with lists and collections** - Iterate over multiple elements with custom logic |
| 11 | +- **Complex assertions** - Validate conditions that built-in methods don't cover |
| 12 | +- **Performance optimization** - Reduce redundant lookups by reusing element references |
| 13 | +- **Custom interactions** - Perform actions not available in standard helper methods |
| 14 | + |
| 15 | +## The CodeceptJS Hybrid Approach |
| 16 | + |
| 17 | +CodeceptJS uniquely combines both styles. You can freely mix `I.*` actions with element-based operations in the same test: |
| 18 | + |
| 19 | +```js |
| 20 | +// Import element functions |
| 21 | +import { element, eachElement, expectElement } from 'codeceptjs/els' |
| 22 | + |
| 23 | +Scenario('checkout flow', async ({ I }) => { |
| 24 | + // Use I.* for navigation and high-level actions |
| 25 | + I.amOnPage('/products') |
| 26 | + I.click('Add to Cart') |
| 27 | + |
| 28 | + // Use element-based for detailed validation |
| 29 | + await element('.cart-summary', async cart => { |
| 30 | + const total = await cart.getAttribute('data-total') |
| 31 | + console.log('Cart total:', total) |
| 32 | + }) |
| 33 | + |
| 34 | + // Continue with I.* actions |
| 35 | + I.click('Checkout') |
| 36 | +}) |
| 37 | +``` |
| 38 | + |
| 39 | +This hybrid approach gives you the best of both worlds - readable high-level actions mixed with low-level control when needed. |
| 40 | + |
| 41 | +## Quick Comparison |
| 42 | + |
| 43 | +### Traditional I.* Approach |
| 44 | + |
| 45 | +```js |
| 46 | +Scenario('form validation', async ({ I }) => { |
| 47 | + I.amOnPage('/register') |
| 48 | + I.fillField('Email', 'test@example.com') |
| 49 | + I.fillField('Password', 'secret123') |
| 50 | + I.click('Register') |
| 51 | + I.see('Welcome') |
| 52 | +}) |
| 53 | +``` |
| 54 | + |
| 55 | +### Element-Based Approach |
| 56 | + |
| 57 | +```js |
| 58 | +import { element, expectElement } from 'codeceptjs/els' |
| 59 | + |
| 60 | +Scenario('form validation', async ({ I }) => { |
| 61 | + I.amOnPage('/register') |
| 62 | + |
| 63 | + // Direct form manipulation |
| 64 | + await element('#email', async input => { |
| 65 | + await input.type('test@example.com') |
| 66 | + }) |
| 67 | + |
| 68 | + await element('#password', async input => { |
| 69 | + await input.type('secret123') |
| 70 | + }) |
| 71 | + |
| 72 | + await element('button[type="submit"]', async btn => { |
| 73 | + await btn.click() |
| 74 | + }) |
| 75 | + |
| 76 | + // Custom assertion |
| 77 | + await expectElement('.welcome-message', async msg => { |
| 78 | + const text = await msg.getText() |
| 79 | + return text.includes('Welcome') |
| 80 | + }) |
| 81 | +}) |
| 82 | +``` |
| 83 | + |
| 84 | +### When to Use Each |
| 85 | + |
| 86 | +| Use `I.*` actions when... | Use element-based when... | |
| 87 | +|---------------------------|---------------------------| |
| 88 | +| Simple navigation and clicks | Complex DOM traversal | |
| 89 | +| Standard form interactions | Custom validation logic | |
| 90 | +| Built-in assertions suffice | Need specific element properties | |
| 91 | +| Readability is priority | Working with element collections | |
| 92 | +| Single-step operations | Chaining multiple operations on same element | |
| 93 | + |
| 94 | +## Element Chaining |
| 95 | + |
| 96 | +Element-based testing allows you to chain queries to find child elements, reducing redundant lookups: |
| 97 | + |
| 98 | +```js |
| 99 | +import { element } from 'codeceptjs/els' |
| 100 | + |
| 101 | +Scenario('product list', async ({ I }) => { |
| 102 | + I.amOnPage('/products') |
| 103 | + |
| 104 | + // Chain into child elements |
| 105 | + await element('.product-list', async list => { |
| 106 | + const firstProduct = await list.$('.product-item') |
| 107 | + const title = await firstProduct.$('.title') |
| 108 | + const price = await firstProduct.$('.price') |
| 109 | + |
| 110 | + const titleText = await title.getText() |
| 111 | + const priceValue = await price.getText() |
| 112 | + |
| 113 | + console.log(`${titleText}: ${priceValue}`) |
| 114 | + }) |
| 115 | +}) |
| 116 | +``` |
| 117 | + |
| 118 | +## Real-World Examples |
| 119 | + |
| 120 | +### Example 1: Form Validation |
| 121 | + |
| 122 | +Validate complex form requirements that built-in methods don't cover: |
| 123 | + |
| 124 | +```js |
| 125 | +import { element, eachElement } from 'codeceptjs/els' |
| 126 | +import { expect } from 'chai' |
| 127 | + |
| 128 | +Scenario('validate form fields', async ({ I }) => { |
| 129 | + I.amOnPage('/register') |
| 130 | + |
| 131 | + // Check all required fields are properly marked |
| 132 | + await eachElement('[required]', async field => { |
| 133 | + const ariaRequired = await field.getAttribute('aria-required') |
| 134 | + const required = await field.getAttribute('required') |
| 135 | + if (!ariaRequired && !required) { |
| 136 | + throw new Error('Required field missing indicators') |
| 137 | + } |
| 138 | + }) |
| 139 | + |
| 140 | + // Fill form with custom validation |
| 141 | + await element('#email', async input => { |
| 142 | + await input.type('test@example.com') |
| 143 | + const value = await input.getValue() |
| 144 | + expect(value).to.include('@') |
| 145 | + }) |
| 146 | + |
| 147 | + I.click('Submit') |
| 148 | +}) |
| 149 | +``` |
| 150 | + |
| 151 | +### Example 2: Data Table Processing |
| 152 | + |
| 153 | +Work with tabular data using iteration and child element queries: |
| 154 | + |
| 155 | +```js |
| 156 | +import { eachElement, element } from 'codeceptjs/els' |
| 157 | + |
| 158 | +Scenario('verify table data', async ({ I }) => { |
| 159 | + I.amOnPage('/dashboard') |
| 160 | + |
| 161 | + // Get table row count |
| 162 | + await element('table tbody', async tbody => { |
| 163 | + const rows = await tbody.$$('tr') |
| 164 | + console.log(`Table has ${rows.length} rows`) |
| 165 | + }) |
| 166 | + |
| 167 | + // Verify each row has expected structure |
| 168 | + await eachElement('table tbody tr', async (row, index) => { |
| 169 | + const cells = await row.$$('td') |
| 170 | + if (cells.length < 3) { |
| 171 | + throw new Error(`Row ${index} should have at least 3 columns`) |
| 172 | + } |
| 173 | + }) |
| 174 | +}) |
| 175 | +``` |
| 176 | + |
| 177 | +### Example 3: Dynamic Content Waiting |
| 178 | + |
| 179 | +Wait for and validate dynamic content with custom conditions: |
| 180 | + |
| 181 | +```js |
| 182 | +import { element, expectElement } from 'codeceptjs/els' |
| 183 | + |
| 184 | +Scenario('wait for dynamic content', async ({ I }) => { |
| 185 | + I.amOnPage('/search') |
| 186 | + I.fillField('query', 'test') |
| 187 | + I.click('Search') |
| 188 | + |
| 189 | + // Wait for results with custom validation |
| 190 | + await expectElement('.search-results', async results => { |
| 191 | + const items = await results.$$('.result-item') |
| 192 | + return items.length > 0 |
| 193 | + }) |
| 194 | +}) |
| 195 | +``` |
| 196 | + |
| 197 | +### Example 4: Shopping Cart Operations |
| 198 | + |
| 199 | +Calculate and verify cart totals by iterating through items: |
| 200 | + |
| 201 | +```js |
| 202 | +import { element, eachElement } from 'codeceptjs/els' |
| 203 | +import { expect } from 'chai' |
| 204 | + |
| 205 | +Scenario('calculate cart total', async ({ I }) => { |
| 206 | + I.amOnPage('/cart') |
| 207 | + |
| 208 | + let total = 0 |
| 209 | + |
| 210 | + // Sum up all item prices |
| 211 | + await eachElement('.cart-item .price', async priceEl => { |
| 212 | + const priceText = await priceEl.getText() |
| 213 | + const price = parseFloat(priceText.replace('$', '')) |
| 214 | + total += price |
| 215 | + }) |
| 216 | + |
| 217 | + // Verify displayed total matches calculated sum |
| 218 | + await element('.cart-total', async totalEl => { |
| 219 | + const displayedTotal = await totalEl.getText() |
| 220 | + const displayedValue = parseFloat(displayedTotal.replace('$', '')) |
| 221 | + expect(displayedValue).to.equal(total) |
| 222 | + }) |
| 223 | +}) |
| 224 | +``` |
| 225 | + |
| 226 | +### Example 5: List Filtering and Validation |
| 227 | + |
| 228 | +Validate filtered results meet specific criteria: |
| 229 | + |
| 230 | +```js |
| 231 | +import { element, eachElement, expectAnyElement } from 'codeceptjs/els' |
| 232 | +import { expect } from 'chai' |
| 233 | + |
| 234 | +Scenario('filter products by price', async ({ I }) => { |
| 235 | + I.amOnPage('/products') |
| 236 | + I.click('Under $100') |
| 237 | + |
| 238 | + // Verify all displayed products are under $100 |
| 239 | + await eachElement('.product-item', async product => { |
| 240 | + const priceEl = await product.$('.price') |
| 241 | + const priceText = await priceEl.getText() |
| 242 | + const price = parseFloat(priceText.replace('$', '')) |
| 243 | + expect(price).to.be.below(100) |
| 244 | + }) |
| 245 | + |
| 246 | + // Check at least one product exists |
| 247 | + await expectAnyElement('.product-item', async () => true) |
| 248 | +}) |
| 249 | +``` |
| 250 | + |
| 251 | +## Best Practices |
| 252 | + |
| 253 | +1. **Mix styles appropriately** - Use `I.*` for navigation and high-level actions, element-based for complex validation |
| 254 | + |
| 255 | +2. **Use descriptive purposes** - Add purpose strings for better debugging logs: |
| 256 | + ```js |
| 257 | + await element( |
| 258 | + 'verify discount applied', |
| 259 | + '.price', |
| 260 | + async el => { /* ... */ } |
| 261 | + ) |
| 262 | + ``` |
| 263 | + |
| 264 | +3. **Reuse element references** - Chain `$(locator)` to avoid redundant lookups |
| 265 | + |
| 266 | +4. **Handle empty results** - Always check if elements exist before accessing properties |
| 267 | + |
| 268 | +5. **Prefer standard assertions** - Use `I.see()`, `I.dontSee()` when possible for readability |
| 269 | + |
| 270 | +6. **Consider page objects** - Combine with Page Objects for reusable element logic |
| 271 | + |
| 272 | +## API Reference |
| 273 | + |
| 274 | +- **[Element Access](els.md)** - Complete reference for `element()`, `eachElement()`, `expectElement()`, `expectAnyElement()`, `expectAllElements()` functions |
| 275 | +- **[WebElement API](WebElement.md)** - Complete reference for WebElement class methods (`getText()`, `getAttribute()`, `click()`, `$$()`, etc.) |
| 276 | + |
| 277 | +## Portability |
| 278 | + |
| 279 | +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: |
| 280 | + |
| 281 | +```js |
| 282 | +// This test works identically with Playwright, WebDriver, or Puppeteer |
| 283 | +import { element } from 'codeceptjs/els' |
| 284 | + |
| 285 | +Scenario('portable test', async ({ I }) => { |
| 286 | + I.amOnPage('/') |
| 287 | + |
| 288 | + await element('.main-title', async title => { |
| 289 | + const text = await title.getText() // Works on all helpers |
| 290 | + const className = await title.getAttribute('class') |
| 291 | + const visible = await title.isVisible() |
| 292 | + const enabled = await title.isEnabled() |
| 293 | + }) |
| 294 | +}) |
| 295 | +``` |
0 commit comments