Skip to content

Commit 4821afa

Browse files
committed
Merge branch '4.x' into 4.x-docs-update
2 parents 69115cf + 50a6850 commit 4821afa

77 files changed

Lines changed: 2056 additions & 611 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bin/mcp-server.js

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import path from 'path'
1212
import crypto from 'crypto'
1313
import { spawn } from 'child_process'
1414
import { createRequire } from 'module'
15-
import { existsSync, readdirSync } from 'fs'
15+
import { existsSync, readdirSync, writeFileSync } from 'fs'
16+
import { mkdirp } from 'mkdirp'
1617

1718
const require = createRequire(import.meta.url)
1819

@@ -439,12 +440,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
439440
const helper = Object.values(helpers)[0]
440441
if (helper) {
441442
try {
442-
if (helper.grabAriaSnapshot) result.artifacts.aria = await helper.grabAriaSnapshot()
443-
if (helper.grabCurrentUrl) result.artifacts.url = await helper.grabCurrentUrl()
444-
if (helper.grabBrowserLogs) result.artifacts.consoleLogs = (await helper.grabBrowserLogs()) || []
443+
const traceDir = getTraceDir('mcp', 'run_code')
444+
mkdirp.sync(traceDir)
445+
446+
if (helper.grabAriaSnapshot) {
447+
const aria = await helper.grabAriaSnapshot()
448+
const ariaFile = path.join(traceDir, 'aria.txt')
449+
writeFileSync(ariaFile, aria)
450+
result.artifacts.aria = `file://${ariaFile}`
451+
}
452+
453+
if (helper.grabCurrentUrl) {
454+
result.artifacts.url = await helper.grabCurrentUrl()
455+
}
456+
457+
if (helper.grabBrowserLogs) {
458+
const logs = (await helper.grabBrowserLogs()) || []
459+
const logsFile = path.join(traceDir, 'console.json')
460+
writeFileSync(logsFile, JSON.stringify(logs, null, 2))
461+
result.artifacts.consoleLogs = `file://${logsFile}`
462+
}
463+
445464
if (helper.grabSource) {
446465
const html = await helper.grabSource()
447-
result.artifacts.html = html.substring(0, 10000) + '...'
466+
const htmlFile = path.join(traceDir, 'page.html')
467+
writeFileSync(htmlFile, html)
468+
result.artifacts.html = `file://${htmlFile}`
469+
}
470+
471+
if (helper.saveScreenshot) {
472+
const screenshotFile = path.join(traceDir, 'screenshot.png')
473+
await helper.saveScreenshot(screenshotFile)
474+
result.artifacts.screenshot = `file://${screenshotFile}`
448475
}
449476
} catch (e) {
450477
result.output += ` (Warning: ${e.message})`

docs/data.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ The most efficient way would be to allow test to control its data, i.e. the 3rd
1818
However, accessing database directly is not a good idea as database vendor, schema and data are used by application internally and are out of scope of acceptance test.
1919

2020
Today all modern web applications have REST or GraphQL API . So it is a good idea to use it to create data for a test and delete it after.
21-
API is supposed to be a stable interface and it can be used by acceptance tests. CodeceptJS provides 4 helpers for Data Management via REST and GraphQL API.
21+
API is supposed to be a stable interface and it can be used by acceptance tests. CodeceptJS provides helpers for Data Management via REST and GraphQL API, as well as **[Data Objects](/pageobjects#data-objects)** — class-based page objects with automatic cleanup via lifecycle hooks.
22+
23+
## Data Objects
24+
25+
For a lightweight, class-based approach to managing test data, see **[Data Objects](/pageobjects#data-objects)** in the Page Objects documentation. Data Objects let you create page object classes that manage API data with automatic cleanup via the `_after()` hook — no factory configuration needed.
2226

2327
## REST
2428

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+
```

0 commit comments

Comments
 (0)