diff --git a/docs/element-based-testing.md b/docs/element-based-testing.md new file mode 100644 index 000000000..c24cfd6e5 --- /dev/null +++ b/docs/element-based-testing.md @@ -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() + }) +}) +``` diff --git a/docs/els.md b/docs/els.md index 91acc10f3..3d6468365 100644 --- a/docs/els.md +++ b/docs/els.md @@ -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 @@ -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 @@ -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'); } }); @@ -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 @@ -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() +}) +``` diff --git a/lib/element/WebElement.js b/lib/element/WebElement.js index 4c30028b2..b0e4adc09 100644 --- a/lib/element/WebElement.js +++ b/lib/element/WebElement.js @@ -82,6 +82,10 @@ class WebElement { async getProperty(name) { switch (this.helperType) { case 'playwright': + // For Locator objects, use inputValue() for the 'value' property + if (name === 'value' && this.element.inputValue) { + return this.element.inputValue() + } return this.element.evaluate((el, propName) => el[propName], name) case 'webdriver': return this.element.getProperty(name) @@ -237,10 +241,15 @@ class WebElement { async type(text, options = {}) { switch (this.helperType) { case 'playwright': + // Playwright Locator objects use fill() instead of type() + if (this.element.fill) { + return this.element.fill(text, options) + } return this.element.type(text, options) case 'webdriver': return this.element.setValue(text) case 'puppeteer': + await this.element.evaluate(el => { el.value = '' }) return this.element.type(text, options) default: throw new Error(`Unsupported helper type: ${this.helperType}`) @@ -257,7 +266,18 @@ class WebElement { switch (this.helperType) { case 'playwright': - childElement = await this.element.$(this._normalizeLocator(locator)) + // Playwright Locator objects use locator() method + if (this.element.locator) { + const childLocator = this.element.locator(this._normalizeLocator(locator)) + // Get the element handle from the locator + try { + childElement = await childLocator.elementHandle() + } catch (e) { + return null + } + } else { + childElement = await this.element.$(this._normalizeLocator(locator)) + } break case 'webdriver': try { @@ -286,7 +306,14 @@ class WebElement { switch (this.helperType) { case 'playwright': - childElements = await this.element.$$(this._normalizeLocator(locator)) + // Playwright Locator objects use locator() method + if (this.element.locator) { + const childLocator = this.element.locator(this._normalizeLocator(locator)) + // Get all element handles from the locator + childElements = await childLocator.elementHandles() + } else { + childElements = await this.element.$$(this._normalizeLocator(locator)) + } break case 'webdriver': childElements = await this.element.$$(this._normalizeLocator(locator)) diff --git a/lib/els.js b/lib/els.js index 107cc303b..56b4a1095 100644 --- a/lib/els.js +++ b/lib/els.js @@ -6,6 +6,7 @@ import recordStep from './step/record.js' import FuncStep from './step/func.js' import { truth } from './assert/truth.js' import { isAsyncFunction, humanizeFunction } from './utils.js' +import WebElement from './element/WebElement.js' function element(purpose, locator, fn) { let stepConfig @@ -28,7 +29,8 @@ function element(purpose, locator, fn) { const els = await step.helper._locate(locator) output.debug(`Found ${els.length} elements, using first element`) - return fn(els[0]) + const wrapped = new WebElement(els[0], step.helper) + return fn(wrapped) }, stepConfig, ) @@ -52,7 +54,8 @@ function eachElement(purpose, locator, fn) { let i = 0 for (const el of els) { try { - await fn(el, i) + const wrapped = new WebElement(el, step.helper) + await fn(wrapped, i) } catch (err) { output.error(`eachElement: failed operation on element #${i} ${el}`) errs.push(err) @@ -74,7 +77,8 @@ function expectElement(locator, fn) { const els = await step.helper._locate(locator) output.debug(`Found ${els.length} elements, first will be used for assertion`) - const result = await fn(els[0]) + const wrapped = new WebElement(els[0], step.helper) + const result = await fn(wrapped) const assertion = truth(`element (${locator})`, fn.toString()) assertion.assert(result) }) @@ -92,7 +96,8 @@ function expectAnyElement(locator, fn) { let found = false for (const el of els) { - const result = await fn(el) + const wrapped = new WebElement(el, step.helper) + const result = await fn(wrapped) if (result) { found = true break @@ -113,7 +118,8 @@ function expectAllElements(locator, fn) { let i = 1 for (const el of els) { output.debug(`checking element #${i}: ${el}`) - const result = await fn(el) + const wrapped = new WebElement(el, step.helper) + const result = await fn(wrapped) const assertion = truth(`element #${i} of (${locator})`, humanizeFunction(fn)) assertion.assert(result) i++ diff --git a/test/acceptance/codecept.els.Playwright.js b/test/acceptance/codecept.els.Playwright.js new file mode 100644 index 000000000..3e45755b0 --- /dev/null +++ b/test/acceptance/codecept.els.Playwright.js @@ -0,0 +1,31 @@ +import TestHelper from '../support/TestHelper.js' + +export const config = { + tests: './els_test.js', + timeout: 10, + output: './output', + grep: '@Playwright', + helpers: { + Playwright: { + url: TestHelper.siteUrl(), + show: false, + restart: process.env.BROWSER_RESTART || false, + browser: process.env.BROWSER || 'chromium', + ignoreHTTPSErrors: true, + waitForTimeout: 5000, + waitForAction: 500, + chromium: { + args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'] + }, + }, + }, + include: {}, + bootstrap: false, + mocha: {}, + plugins: { + screenshotOnFail: { + enabled: true, + }, + }, + name: 'acceptance', +} diff --git a/test/acceptance/els_test.js b/test/acceptance/els_test.js new file mode 100644 index 000000000..46f784408 --- /dev/null +++ b/test/acceptance/els_test.js @@ -0,0 +1,332 @@ +import { element, eachElement, expectElement, expectAnyElement, expectAllElements } from '../../lib/els.js' + +Feature('element functions', { retries: 3 }) + +Scenario('element() should work with first matching element @Playwright', async ({ I }) => { + I.amOnPage('/form/field') + const attr = await element('input[name=name]', async el => { + return await el.getAttribute('id') + }) + if (attr !== 'name') { + throw new Error('getAttribute() should return id') + } +}) + +Scenario('element() should get text content @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/') + const text = await element('h1', async el => { + return await el.getText() + }) + // Verify we got some text + if (text === null || text === undefined) { + throw new Error('getText() should return text content') + } +}) + +Scenario('element() should get attribute @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/form/field') + const nameAttr = await element('input[name=name]', async el => { + return await el.getAttribute('name') + }) + // The input should have a name attribute + if (nameAttr !== 'name') { + throw new Error('getAttribute() should return attribute value') + } +}) + +Scenario('element() should check element visibility @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/') + const visible = await element('h1', async el => { + return await el.isVisible() + }) + // h1 should be visible + if (visible !== true) { + throw new Error('isVisible() should return true for visible element') + } +}) + +Scenario('element() should check if element is enabled @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/form/field') + const enabled = await element('input[name=name]', async el => { + return await el.isEnabled() + }) + // input should be enabled + if (enabled !== true) { + throw new Error('isEnabled() should return true for enabled input') + } +}) + +Scenario('element() should check hidden element is not visible @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/form/field') + const visible = await element('input[name=email]', async el => { + return await el.isVisible() + }) + // This input is hidden via style="display:none" + if (visible !== false) { + throw new Error('isVisible() should return false for hidden element') + } +}) + +Scenario('element() should click element @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/form/checkbox') + const checkbox = await element('input[name=terms]', async el => { + const enabled = await el.isEnabled() + if (enabled) { + await el.click() + } + return enabled + }) + // Click should have worked + I.seeCheckboxIsChecked('input[name=terms]') +}) + +Scenario('element() should type into input @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/form/field') + await element('input[name=name]', async el => { + await el.type('test value') + }) + I.seeInField('input[name=name]', 'test value') +}) + +Scenario('element() should find child elements with $() @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/form/field') + let foundChild = false + await element('form', async form => { + const child = await form.$('input[name=name]') + if (child) { + foundChild = true + // Verify the child is a WebElement + const helper = child.getHelper() + if (!helper) { + throw new Error('Child should have helper') + } + } + }) + if (!foundChild) { + throw new Error('$(locator) should find child element') + } +}) + +Scenario('element() should find multiple child elements with $$() @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/form/field') + let childCount = 0 + await element('form', async form => { + const children = await form.$$('input') + childCount = children.length + }) + // Should find at least some inputs + if (childCount === 0) { + throw new Error('$$(locator) should find child elements') + } +}) + +Scenario('eachElement() should iterate over all matching elements @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/') + let count = 0 + await eachElement('a', async (el, index) => { + count++ + }) + // Should have iterated over at least some links + if (count === 0) { + throw new Error('eachElement should iterate over elements') + } +}) + +Scenario('eachElement() should provide index @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/') + const indices = [] + await eachElement('a', async (el, index) => { + indices.push(index) + }) + // Should have received indices in order + if (indices.length === 0) { + throw new Error('eachElement should provide index') + } + // Verify indices are sequential + for (let i = 0; i < indices.length; i++) { + if (indices[i] !== i) { + throw new Error(`Index should be ${i} but got ${indices[i]}`) + } + } +}) + +Scenario('eachElement() should get text from each element @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/') + const texts = [] + await eachElement('h1, h2, h3', async el => { + const text = await el.getText() + texts.push(text) + }) + // Should have collected text from elements + if (texts.length === 0) { + throw new Error('eachElement should process elements') + } +}) + +Scenario('expectElement() should assert condition on first element @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/') + // Should pass - h1 should be visible + await expectElement('h1', async el => { + return await el.isVisible() + }) +}) + +Scenario('expectElement() should throw when condition is false @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/form/field') + // expectElement should throw when condition returns false + // We verify the element has the expected id first + await expectElement('input[name=name]', async el => { + const id = await el.getAttribute('id') + return id === 'name' + }) +}) + +Scenario('expectElement() should check text content @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/') + await expectElement('h1', async el => { + const text = await el.getText() + return text && text.length > 0 + }) +}) + +Scenario('expectElement() should check attribute value @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/form/field') + await expectElement('input[name=name]', async el => { + const id = await el.getAttribute('id') + return id === 'name' + }) +}) + +Scenario('expectAnyElement() should pass when at least one element matches @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/form/field') + // At least one input should be enabled (all are) + await expectAnyElement('input', async el => { + return await el.isEnabled() + }) +}) + +Scenario('expectAnyElement() should check attribute exists on any element @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/form/field') + // At least one input should have id attribute + await expectAnyElement('input', async el => { + const id = await el.getAttribute('id') + return id !== null + }) +}) + +Scenario('expectAllElements() should pass when all elements match @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/form/field') + // All inputs with type="text" should have name attribute + await expectAllElements('input[type="text"]', async el => { + const name = await el.getAttribute('name') + return name !== null + }) +}) + +Scenario('expectAllElements() should check all elements are in DOM @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/') + // All elements should exist (be in DOM) + await expectAllElements('h1, h2, h3', async el => { + return await el.exists() + }) +}) + +Scenario('element() should get bounding box @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/') + const box = await element('h1', async el => { + return await el.getBoundingBox() + }) + // Should return a bounding box object + if (!box || typeof box.x !== 'number') { + throw new Error('getBoundingBox() should return box with x coordinate') + } +}) + +Scenario('element() should get inner HTML @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/') + const html = await element('h1', async el => { + return await el.getInnerHTML() + }) + // Should return HTML content + if (html === null || html === undefined) { + throw new Error('getInnerHTML() should return HTML content') + } +}) + +Scenario('element() should get outer HTML @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/') + const html = await element('h1', async el => { + return await el.toOuterHTML() + }) + // Should return HTML content + if (html === null || html === undefined || !html.includes(' { + I.amOnPage('/') + const html = await element('h1', async el => { + return await el.toSimplifiedHTML(50) + }) + // Should return simplified HTML content + if (html === null || html === undefined) { + throw new Error('toSimplifiedHTML() should return simplified HTML') + } +}) + +Scenario('element() should get property value @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/form/field') + await element('input[name=name]', async el => { + await el.type('test') + const value = await el.getProperty('value') + if (value !== 'test') { + throw new Error('getProperty(value) should return input value') + } + }) +}) + +Scenario('element() should get input value @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/form/field') + await element('input[name=name]', async el => { + await el.type('test value') + const value = await el.getValue() + if (value !== 'test value') { + throw new Error('getValue() should return input value') + } + }) +}) + +Scenario('element() should work with purpose parameter @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/form/field') + await element('check input value', 'input[name=name]', async el => { + const value = await el.getValue() + if (value !== 'OLD_VALUE') { + throw new Error('input should have initial value') + } + }) +}) + +Scenario('element() should handle chaining with child elements @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/form/field') + await element('form', async form => { + const input = await form.$('input[name=name]') + if (!input) { + throw new Error('child element should be found') + } + const value = await input.getValue() + if (value !== 'OLD_VALUE') { + throw new Error('input should have initial value') + } + }) +}) + +Scenario('element() should work with purpose parameter @Playwright @Puppeteer', async ({ I }) => { + I.amOnPage('/form/field') + await element('check input value', 'input[name=name]', async el => { + const value = await el.getValue() + if (value !== 'OLD_VALUE') { + throw new Error('input should have initial value') + } + }) +}) diff --git a/test/acceptance/session_test.js b/test/acceptance/session_test.js index 6bafb0418..b8508e07d 100644 --- a/test/acceptance/session_test.js +++ b/test/acceptance/session_test.js @@ -1,7 +1,7 @@ import assert from 'assert' import { devices } from 'playwright' -import { within } from 'codeceptjs/effects' -import event from 'codeceptjs' +import { within } from '../../lib/effects.js' +import event from '../../lib/index.js' const output_dir = global.output_dir || './output' diff --git a/test/unit/WebElement_test.js b/test/unit/WebElement_test.js index d96b48e4a..3da5a9570 100644 --- a/test/unit/WebElement_test.js +++ b/test/unit/WebElement_test.js @@ -307,6 +307,7 @@ describe('WebElement', () => { it('should work with Puppeteer helper', async () => { let typedText = '' const mockElement = { + evaluate: (fn) => Promise.resolve(fn({ value: '' })), type: (text, options) => { typedText = text return Promise.resolve() diff --git a/test/unit/els_test.js b/test/unit/els_test.js index 9ce801f3e..6fc51f081 100644 --- a/test/unit/els_test.js +++ b/test/unit/els_test.js @@ -17,6 +17,10 @@ class TestHelper extends Helper { async _locate(locator) { return this.elements } + + _detectHelperType() { + return 'test' + } } describe('els', function () { @@ -36,28 +40,28 @@ describe('els', function () { describe('#element', () => { it('should execute function on first found element', async () => { - helper.elements = ['el1', 'el2', 'el3'] + helper.elements = [{ id: 'el1' }, { id: 'el2' }, { id: 'el3' }] let elementUsed await els.element('my test', '.selector', async el => { - elementUsed = await el + elementUsed = el }) if (elementUsed) { - assert.equal(elementUsed, 'el1') + assert.equal(elementUsed.getNativeElement().id, 'el1') } }) it('should work without purpose parameter', async () => { - helper.elements = ['el1', 'el2'] + helper.elements = [{ id: 'el1' }, { id: 'el2' }] let elementUsed await els.element('.selector', async el => { - elementUsed = await el + elementUsed = el }) if (elementUsed) { - assert.equal(elementUsed, 'el1') + assert.equal(elementUsed.getNativeElement().id, 'el1') } }) @@ -72,7 +76,7 @@ describe('els', function () { }) it('should fail on timeout if timeout is set', async () => { - helper.elements = ['el1', 'el2'] + helper.elements = [{ id: 'el1' }, { id: 'el2' }] try { await els.element( '.selector', @@ -89,40 +93,40 @@ describe('els', function () { }) it('should retry until timeout when retries are set', async () => { - helper.elements = ['el1', 'el2'] + helper.elements = [{ id: 'el1' }, { id: 'el2' }] let attempts = 0 await els.element( '.selector', - async els => { + async el => { attempts++ if (attempts < 2) { throw new Error('keep retrying') } - return els.slice(0, attempts) + return el.getNativeElement().id }, new StepConfig().retry(2), ) await recorder.promise() expect(attempts).to.be.at.least(2) - expect(helper.elements).to.deep.equal(['el1', 'el2']) + expect(helper.elements[0].id).to.equal('el1') }) }) describe('#eachElement', () => { it('should execute function on each element', async () => { - helper.elements = ['el1', 'el2', 'el3'] + helper.elements = [{ id: 'el1' }, { id: 'el2' }, { id: 'el3' }] const usedElements = [] await els.eachElement('.selector', async el => { - usedElements.push(el) + usedElements.push(el.getNativeElement().id) }) assert.deepEqual(usedElements, ['el1', 'el2', 'el3']) }) it('should provide index as second parameter', async () => { - helper.elements = ['el1', 'el2'] + helper.elements = [{ id: 'el1' }, { id: 'el2' }] const indices = [] await els.eachElement('.selector', async (el, i) => { @@ -133,22 +137,22 @@ describe('els', function () { }) it('should work without purpose parameter', async () => { - helper.elements = ['el1', 'el2'] + helper.elements = [{ id: 'el1' }, { id: 'el2' }] const usedElements = [] await els.eachElement('.selector', async el => { - usedElements.push(el) + usedElements.push(el.getNativeElement().id) }) assert.deepEqual(usedElements, ['el1', 'el2']) }) it('should throw first error if operation fails', async () => { - helper.elements = ['el1', 'el2'] + helper.elements = [{ id: 'el1' }, { id: 'el2' }] try { await els.eachElement('.selector', async el => { - throw new Error(`failed on ${el}`) + throw new Error(`failed on ${el.getNativeElement().id}`) }) throw new Error('should have thrown error') } catch (e) { @@ -159,13 +163,13 @@ describe('els', function () { describe('#expectElement', () => { it('should pass when condition is true', async () => { - helper.elements = ['el1'] + helper.elements = [{ id: 'el1' }] await els.expectElement('.selector', async () => true) }) it('should fail when condition is false', async () => { - helper.elements = ['el1'] + helper.elements = [{ id: 'el1' }] try { await els.expectElement('.selector', async () => false) @@ -178,13 +182,13 @@ describe('els', function () { describe('#expectAnyElement', () => { it('should pass when any element matches condition', async () => { - helper.elements = ['el1', 'el2', 'el3'] + helper.elements = [{ id: 'el1' }, { id: 'el2' }, { id: 'el3' }] - await els.expectAnyElement('.selector', async el => el === 'el2') + await els.expectAnyElement('.selector', async el => el.getNativeElement().id === 'el2') }) it('should fail when no element matches condition', async () => { - helper.elements = ['el1', 'el2'] + helper.elements = [{ id: 'el1' }, { id: 'el2' }] try { await els.expectAnyElement('.selector', async () => false) @@ -197,16 +201,16 @@ describe('els', function () { describe('#expectAllElements', () => { it('should pass when all elements match condition', async () => { - helper.elements = ['el1', 'el2'] + helper.elements = [{ id: 'el1' }, { id: 'el2' }] await els.expectAllElements('.selector', async () => true) }) it('should fail when any element does not match condition', async () => { - helper.elements = ['el1', 'el2', 'el3'] + helper.elements = [{ id: 'el1' }, { id: 'el2' }, { id: 'el3' }] try { - await els.expectAllElements('.selector', async el => el !== 'el2') + await els.expectAllElements('.selector', async el => el.getNativeElement().id !== 'el2') throw new Error('should have thrown error') } catch (e) { expect(e.cliMessage()).to.include('element #2 of (.selector)')