Skip to content

Commit b6aed85

Browse files
DavertMikDavertMikclaude
authored
feat: wrap elements in WebElement for unified API across helpers (#5497)
* feat: wrap elements in WebElement for unified API across helpers - Import and use WebElement wrapper in lib/els.js for all element functions - Provides consistent API (getText, getAttribute, click, etc.) across Playwright, WebDriver, Puppeteer - Update unit tests to work with WebElement instances - Add comprehensive element-based testing guide (docs/element-based-testing.md) - Update docs/els.md to remove portability warning and link to new guide Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use proper chai assertion syntax in examples - Replace invalid assert greaterThan/lessThan with expect().to.be.above/below - Add proper chai imports in examples that use assertions - Use expect() style consistently throughout Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: restructure element-based testing documentation - Move API reference from element-based-testing.md to els.md - Keep element-based-testing.md as a user guide with examples - Update els.md import syntax to use ES6 imports - Add complete WebElement API reference to els.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: remove duplicate WebElement API from els.md - els.md now references WebElement.md for full API - element-based-testing.md updated to reference both docs - No content duplication, clearer documentation structure Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add acceptance tests for els module - Add comprehensive acceptance tests for element(), eachElement(), expectElement(), expectAnyElement(), expectAllElements() - Update WebElement class to handle Playwright Locator objects: - type() uses fill() for Locator objects - $() and $$() use locator() and elementHandle() methods for Locator objects - Fix session_test.js import paths for ESM - 30 acceptance tests + 15 unit tests passing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use inputValue() for getProperty('value') in WebElement For Playwright Locator objects, getProperty('value') should use inputValue() method instead of evaluate() to get the current value of input elements after fill() is called. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fixed puppeteer tests * fixed test --------- Co-authored-by: DavertMik <davert@testomat.io> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2f8bc5c commit b6aed85

File tree

9 files changed

+776
-41
lines changed

9 files changed

+776
-41
lines changed

docs/element-based-testing.md

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
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+
```

docs/els.md

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
## Element Access
22

3-
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.
3+
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).
4+
5+
> **Note:** For a comprehensive guide on element-based testing patterns and best practices, see [Element-Based Testing](element-based-testing.md).
46
57
### Usage
68

79
Import the els functions in your test file:
810

911
```js
10-
const { element, eachElement, expectElement, expectAnyElement, expectAllElements } = require('codeceptjs/els');
12+
import { element, eachElement, expectElement, expectAnyElement, expectAllElements } from 'codeceptjs/els'
1113
```
1214

1315
## element
@@ -26,7 +28,7 @@ element(locator, fn);
2628

2729
- `purpose` (optional) - A string describing the operation being performed. If omitted, a default purpose will be generated from the function.
2830
- `locator` - A locator string/object to find the element(s).
29-
- `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.
31+
- `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.
3032

3133
### Returns
3234

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

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

264266
// Check if all checkboxes in a form are checked
265267
await expectAllElements('input[type="checkbox"]', async el => {
266-
return await el.isSelected();
268+
const checked = await el.getProperty('checked');
269+
return checked === true;
267270
});
268271

269272
// Verify all items in a list have non-empty text
@@ -287,3 +290,39 @@ Scenario('validate all elements meet criteria', async ({ I }) => {
287290
- The provided callback must be an async function that returns a boolean
288291
- The assertion message will include which element number failed (e.g., "element #2 of...")
289292
- Throws an error if no helper with `_locate` method is enabled
293+
294+
## WebElement API
295+
296+
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).
297+
298+
Quick reference of available methods:
299+
300+
```js
301+
await element('.my-element', async el => {
302+
// Get element information
303+
const text = await el.getText()
304+
const attr = await el.getAttribute('data-value')
305+
const prop = await el.getProperty('value')
306+
const html = await el.getInnerHTML()
307+
308+
// Check state
309+
const visible = await el.isVisible()
310+
const enabled = await el.isEnabled()
311+
const exists = await el.exists()
312+
313+
// Interactions
314+
await el.click()
315+
await el.type('text')
316+
317+
// Child elements
318+
const child = await el.$('.child')
319+
const children = await el.$$('.child')
320+
321+
// Position
322+
const box = await el.getBoundingBox()
323+
324+
// Native access
325+
const helper = el.getHelper()
326+
const native = el.getNativeElement()
327+
})
328+
```

0 commit comments

Comments
 (0)