From 41cd6e35a254a25629e9899e97b31f16ee411d40 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+copilot@users.noreply.github.com> Date: 2025年8月21日 15:25:19 +0000 Subject: [PATCH 1/3] Initial plan From 6f1a4ef3a49530e5f5b645781488cb83954f628a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+copilot@users.noreply.github.com> Date: 2025年8月22日 04:23:49 +0000 Subject: [PATCH 2/3] Changes before error encountered Co-authored-by: kobenguyent <7845001+kobenguyent@users.noreply.github.com> --- docs/WebElement.md | 251 ++++++++++ lib/element/WebElement.js | 327 +++++++++++++ lib/helper/Playwright.js | 7 +- lib/helper/Puppeteer.js | 16 +- lib/helper/WebDriver.js | 16 +- test/unit/WebElement_integration_test.js | 151 ++++++ test/unit/WebElement_test.js | 567 +++++++++++++++++++++++ 7 files changed, 1331 insertions(+), 4 deletions(-) create mode 100644 docs/WebElement.md create mode 100644 lib/element/WebElement.js create mode 100644 test/unit/WebElement_integration_test.js create mode 100644 test/unit/WebElement_test.js diff --git a/docs/WebElement.md b/docs/WebElement.md new file mode 100644 index 000000000..43f0b5597 --- /dev/null +++ b/docs/WebElement.md @@ -0,0 +1,251 @@ +# WebElement API + +The WebElement class provides a unified interface for interacting with elements across different CodeceptJS helpers (Playwright, WebDriver, Puppeteer). It wraps native element instances and provides consistent methods regardless of the underlying helper. + +## Basic Usage + +```javascript +// Get WebElement instances from any helper +const element = await I.grabWebElement('#button') +const elements = await I.grabWebElements('.items') + +// Use consistent API across all helpers +const text = await element.getText() +const isVisible = await element.isVisible() +await element.click() +await element.type('Hello World') + +// Find child elements +const childElement = await element.$('.child-selector') +const childElements = await element.$$('.child-items') +``` + +## API Methods + +### Element Properties + +#### `getText()` + +Get the text content of the element. + +```javascript +const text = await element.getText() +console.log(text) // "Button Text" +``` + +#### `getAttribute(name)` + +Get the value of a specific attribute. + +```javascript +const id = await element.getAttribute('id') +const className = await element.getAttribute('class') +``` + +#### `getProperty(name)` + +Get the value of a JavaScript property. + +```javascript +const value = await element.getProperty('value') +const checked = await element.getProperty('checked') +``` + +#### `getInnerHTML()` + +Get the inner HTML content of the element. + +```javascript +const html = await element.getInnerHTML() +console.log(html) // "Content" +``` + +#### `getValue()` + +Get the value of input elements. + +```javascript +const inputValue = await element.getValue() +``` + +### Element State + +#### `isVisible()` + +Check if the element is visible. + +```javascript +const visible = await element.isVisible() +if (visible) { + console.log('Element is visible') +} +``` + +#### `isEnabled()` + +Check if the element is enabled (not disabled). + +```javascript +const enabled = await element.isEnabled() +if (enabled) { + await element.click() +} +``` + +#### `exists()` + +Check if the element exists in the DOM. + +```javascript +const exists = await element.exists() +if (exists) { + console.log('Element exists') +} +``` + +#### `getBoundingBox()` + +Get the element's bounding box (position and size). + +```javascript +const box = await element.getBoundingBox() +console.log(box) // { x: 100, y: 200, width: 150, height: 50 } +``` + +### Element Interactions + +#### `click(options)` + +Click the element. + +```javascript +await element.click() +// With options (Playwright/Puppeteer) +await element.click({ button: 'right' }) +``` + +#### `type(text, options)` + +Type text into the element. + +```javascript +await element.type('Hello World') +// With options (Playwright/Puppeteer) +await element.type('Hello', { delay: 100 }) +``` + +### Child Element Search + +#### `$(locator)` + +Find the first child element matching the locator. + +```javascript +const childElement = await element.$('.child-class') +if (childElement) { + await childElement.click() +} +``` + +#### `$$(locator)` + +Find all child elements matching the locator. + +```javascript +const childElements = await element.$$('.child-items') +for (const child of childElements) { + const text = await child.getText() + console.log(text) +} +``` + +### Native Access + +#### `getNativeElement()` + +Get the original native element instance. + +```javascript +const nativeElement = element.getNativeElement() +// For Playwright: ElementHandle +// For WebDriver: WebElement +// For Puppeteer: ElementHandle +``` + +#### `getHelper()` + +Get the helper instance that created this WebElement. + +```javascript +const helper = element.getHelper() +console.log(helper.constructor.name) // "Playwright", "WebDriver", or "Puppeteer" +``` + +## Locator Support + +The `$()` and `$$()` methods support various locator formats: + +```javascript +// CSS selectors +await element.$('.class-name') +await element.$('#element-id') + +// CodeceptJS locator objects +await element.$({ css: '.my-class' }) +await element.$({ xpath: '//div[@class="test"]' }) +await element.$({ id: 'element-id' }) +await element.$({ name: 'field-name' }) +await element.$({ className: 'my-class' }) +``` + +## Cross-Helper Compatibility + +The same WebElement code works across all supported helpers: + +```javascript +// This code works identically with Playwright, WebDriver, and Puppeteer +const loginForm = await I.grabWebElement('#login-form') +const usernameField = await loginForm.$('[name="username"]') +const passwordField = await loginForm.$('[name="password"]') +const submitButton = await loginForm.$('button[type="submit"]') + +await usernameField.type('user@example.com') +await passwordField.type('password123') +await submitButton.click() +``` + +## Migration from Native Elements + +If you were previously using native elements, you can gradually migrate: + +```javascript +// Old way - helper-specific +const nativeElements = await I.grabWebElements('.items') +// Different API for each helper + +// New way - unified +const webElements = await I.grabWebElements('.items') +// Same API across all helpers + +// Backward compatibility +const nativeElement = webElements[0].getNativeElement() +// Use native methods if needed +``` + +## Error Handling + +WebElement methods will throw appropriate errors when operations fail: + +```javascript +try { + const element = await I.grabWebElement('#nonexistent') +} catch (error) { + console.log('Element not found') +} + +try { + await element.click() +} catch (error) { + console.log('Click failed:', error.message) +} +``` diff --git a/lib/element/WebElement.js b/lib/element/WebElement.js new file mode 100644 index 000000000..41edfd86d --- /dev/null +++ b/lib/element/WebElement.js @@ -0,0 +1,327 @@ +const assert = require('assert') + +/** + * Unified WebElement class that wraps native element instances from different helpers + * and provides a consistent API across all supported helpers (Playwright, WebDriver, Puppeteer). + */ +class WebElement { + constructor(element, helper) { + this.element = element + this.helper = helper + this.helperType = this._detectHelperType(helper) + } + + _detectHelperType(helper) { + if (!helper) return 'unknown' + + const className = helper.constructor.name + if (className === 'Playwright') return 'playwright' + if (className === 'WebDriver') return 'webdriver' + if (className === 'Puppeteer') return 'puppeteer' + + return 'unknown' + } + + /** + * Get the native element instance + * @returns {ElementHandle|WebElement|ElementHandle} Native element + */ + getNativeElement() { + return this.element + } + + /** + * Get the helper instance + * @returns {Helper} Helper instance + */ + getHelper() { + return this.helper + } + + /** + * Get text content of the element + * @returns {Promise} Element text content + */ + async getText() { + switch (this.helperType) { + case 'playwright': + return this.element.textContent() + case 'webdriver': + return this.element.getText() + case 'puppeteer': + return this.element.evaluate(el => el.textContent) + default: + throw new Error(`Unsupported helper type: ${this.helperType}`) + } + } + + /** + * Get attribute value of the element + * @param {string} name Attribute name + * @returns {Promise} Attribute value + */ + async getAttribute(name) { + switch (this.helperType) { + case 'playwright': + return this.element.getAttribute(name) + case 'webdriver': + return this.element.getAttribute(name) + case 'puppeteer': + return this.element.evaluate((el, attrName) => el.getAttribute(attrName), name) + default: + throw new Error(`Unsupported helper type: ${this.helperType}`) + } + } + + /** + * Get property value of the element + * @param {string} name Property name + * @returns {Promise} Property value + */ + async getProperty(name) { + switch (this.helperType) { + case 'playwright': + return this.element.evaluate((el, propName) => el[propName], name) + case 'webdriver': + return this.element.getProperty(name) + case 'puppeteer': + return this.element.evaluate((el, propName) => el[propName], name) + default: + throw new Error(`Unsupported helper type: ${this.helperType}`) + } + } + + /** + * Get innerHTML of the element + * @returns {Promise} Element innerHTML + */ + async getInnerHTML() { + switch (this.helperType) { + case 'playwright': + return this.element.innerHTML() + case 'webdriver': + return this.element.getProperty('innerHTML') + case 'puppeteer': + return this.element.evaluate(el => el.innerHTML) + default: + throw new Error(`Unsupported helper type: ${this.helperType}`) + } + } + + /** + * Get value of the element (for input elements) + * @returns {Promise} Element value + */ + async getValue() { + switch (this.helperType) { + case 'playwright': + return this.element.inputValue() + case 'webdriver': + return this.element.getValue() + case 'puppeteer': + return this.element.evaluate(el => el.value) + default: + throw new Error(`Unsupported helper type: ${this.helperType}`) + } + } + + /** + * Check if element is visible + * @returns {Promise} True if element is visible + */ + async isVisible() { + switch (this.helperType) { + case 'playwright': + return this.element.isVisible() + case 'webdriver': + return this.element.isDisplayed() + case 'puppeteer': + return this.element.evaluate(el => { + const style = window.getComputedStyle(el) + return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' + }) + default: + throw new Error(`Unsupported helper type: ${this.helperType}`) + } + } + + /** + * Check if element is enabled + * @returns {Promise} True if element is enabled + */ + async isEnabled() { + switch (this.helperType) { + case 'playwright': + return this.element.isEnabled() + case 'webdriver': + return this.element.isEnabled() + case 'puppeteer': + return this.element.evaluate(el => !el.disabled) + default: + throw new Error(`Unsupported helper type: ${this.helperType}`) + } + } + + /** + * Check if element exists in DOM + * @returns {Promise} True if element exists + */ + async exists() { + try { + switch (this.helperType) { + case 'playwright': + // For Playwright, if we have the element, it exists + return await this.element.evaluate(el => !!el) + case 'webdriver': + // For WebDriver, if we have the element, it exists + return true + case 'puppeteer': + // For Puppeteer, if we have the element, it exists + return await this.element.evaluate(el => !!el) + default: + throw new Error(`Unsupported helper type: ${this.helperType}`) + } + } catch (e) { + return false + } + } + + /** + * Get bounding box of the element + * @returns {Promise} Bounding box with x, y, width, height properties + */ + async getBoundingBox() { + switch (this.helperType) { + case 'playwright': + return this.element.boundingBox() + case 'webdriver': + const rect = await this.element.getRect() + return { + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, + } + case 'puppeteer': + return this.element.boundingBox() + default: + throw new Error(`Unsupported helper type: ${this.helperType}`) + } + } + + /** + * Click the element + * @param {Object} options Click options + * @returns {Promise} + */ + async click(options = {}) { + switch (this.helperType) { + case 'playwright': + return this.element.click(options) + case 'webdriver': + return this.element.click() + case 'puppeteer': + return this.element.click(options) + default: + throw new Error(`Unsupported helper type: ${this.helperType}`) + } + } + + /** + * Type text into the element + * @param {string} text Text to type + * @param {Object} options Type options + * @returns {Promise} + */ + async type(text, options = {}) { + switch (this.helperType) { + case 'playwright': + return this.element.type(text, options) + case 'webdriver': + return this.element.setValue(text) + case 'puppeteer': + return this.element.type(text, options) + default: + throw new Error(`Unsupported helper type: ${this.helperType}`) + } + } + + /** + * Find first child element matching the locator + * @param {string|Object} locator Element locator + * @returns {Promise} WebElement instance or null if not found + */ + async $(locator) { + let childElement + + switch (this.helperType) { + case 'playwright': + childElement = await this.element.$(this._normalizeLocator(locator)) + break + case 'webdriver': + try { + childElement = await this.element.$(this._normalizeLocator(locator)) + } catch (e) { + return null + } + break + case 'puppeteer': + childElement = await this.element.$(this._normalizeLocator(locator)) + break + default: + throw new Error(`Unsupported helper type: ${this.helperType}`) + } + + return childElement ? new WebElement(childElement, this.helper) : null + } + + /** + * Find all child elements matching the locator + * @param {string|Object} locator Element locator + * @returns {Promise} Array of WebElement instances + */ + async $$(locator) { + let childElements + + switch (this.helperType) { + case 'playwright': + childElements = await this.element.$$(this._normalizeLocator(locator)) + break + case 'webdriver': + childElements = await this.element.$$(this._normalizeLocator(locator)) + break + case 'puppeteer': + childElements = await this.element.$$(this._normalizeLocator(locator)) + break + default: + throw new Error(`Unsupported helper type: ${this.helperType}`) + } + + return childElements.map(el => new WebElement(el, this.helper)) + } + + /** + * Normalize locator for element search + * @param {string|Object} locator Locator to normalize + * @returns {string} Normalized CSS selector + * @private + */ + _normalizeLocator(locator) { + if (typeof locator === 'string') { + return locator + } + + if (typeof locator === 'object') { + // Handle CodeceptJS locator objects + if (locator.css) return locator.css + if (locator.xpath) return locator.xpath + if (locator.id) return `#${locator.id}` + if (locator.name) return `[name="${locator.name}"]` + if (locator.className) return `.${locator.className}` + } + + return locator.toString() + } +} + +module.exports = WebElement diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index ef91c8d67..bdd1b66ce 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -33,6 +33,7 @@ const RemoteBrowserConnectionRefused = require('./errors/RemoteBrowserConnection const Popup = require('./extras/Popup') const Console = require('./extras/Console') const { findReact, findVue, findByPlaywrightLocator } = require('./extras/PlaywrightReactVueLocator') +const WebElement = require('../element/WebElement') let playwright let perfTiming @@ -1341,7 +1342,8 @@ class Playwright extends Helper { * */ async grabWebElements(locator) { - return this._locate(locator) + const elements = await this._locate(locator) + return elements.map(element => new WebElement(element, this)) } /** @@ -1349,7 +1351,8 @@ class Playwright extends Helper { * */ async grabWebElement(locator) { - return this._locateElement(locator) + const element = await this._locateElement(locator) + return new WebElement(element, this) } /** diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index c023bc84a..0b417d768 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -38,6 +38,7 @@ const { highlightElement } = require('./scripts/highlightElement') const { blurElement } = require('./scripts/blurElement') const { dontSeeElementError, seeElementError, dontSeeElementInDOMError, seeElementInDOMError } = require('./errors/ElementAssertion') const { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } = require('./network/actions') +const WebElement = require('../element/WebElement') let puppeteer let perfTiming @@ -924,7 +925,20 @@ class Puppeteer extends Helper { * */ async grabWebElements(locator) { - return this._locate(locator) + const elements = await this._locate(locator) + return elements.map(element => new WebElement(element, this)) + } + + /** + * {{> grabWebElement }} + * + */ + async grabWebElement(locator) { + const elements = await this._locate(locator) + if (elements.length === 0) { + throw new ElementNotFound(locator, 'Element') + } + return new WebElement(elements[0], this) } /** diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js index 5c23dbc27..69793ab1c 100644 --- a/lib/helper/WebDriver.js +++ b/lib/helper/WebDriver.js @@ -21,6 +21,7 @@ const { focusElement } = require('./scripts/focusElement') const { blurElement } = require('./scripts/blurElement') const { dontSeeElementError, seeElementError, seeElementInDOMError, dontSeeElementInDOMError } = require('./errors/ElementAssertion') const { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } = require('./network/actions') +const WebElement = require('../element/WebElement') const SHADOW = 'shadow' const webRoot = 'body' @@ -936,7 +937,20 @@ class WebDriver extends Helper { * */ async grabWebElements(locator) { - return this._locate(locator) + const elements = await this._locate(locator) + return elements.map(element => new WebElement(element, this)) + } + + /** + * {{> grabWebElement }} + * + */ + async grabWebElement(locator) { + const elements = await this._locate(locator) + if (elements.length === 0) { + throw new ElementNotFound(locator, 'Element') + } + return new WebElement(elements[0], this) } /** diff --git a/test/unit/WebElement_integration_test.js b/test/unit/WebElement_integration_test.js new file mode 100644 index 000000000..c168dcf64 --- /dev/null +++ b/test/unit/WebElement_integration_test.js @@ -0,0 +1,151 @@ +const { expect } = require('chai') +const WebElement = require('../../lib/element/WebElement') + +describe('Helper Integration with WebElement', () => { + describe('WebElement method testing across helpers', () => { + it('should work consistently across all helper types', async () => { + const testCases = [ + { + helperType: 'Playwright', + mockElement: { + textContent: () => Promise.resolve('playwright-text'), + getAttribute: name => Promise.resolve(`playwright-${name}`), + isVisible: () => Promise.resolve(true), + click: () => Promise.resolve(), + $: selector => Promise.resolve({ id: 'child-playwright' }), + }, + }, + { + helperType: 'WebDriver', + mockElement: { + getText: () => Promise.resolve('webdriver-text'), + getAttribute: name => Promise.resolve(`webdriver-${name}`), + isDisplayed: () => Promise.resolve(true), + click: () => Promise.resolve(), + $: selector => Promise.resolve({ id: 'child-webdriver' }), + }, + }, + { + helperType: 'Puppeteer', + mockElement: { + evaluate: (fn, ...args) => { + if (fn.toString().includes('textContent')) return Promise.resolve('puppeteer-text') + if (fn.toString().includes('getAttribute')) return Promise.resolve(`puppeteer-${args[0]}`) + if (fn.toString().includes('getComputedStyle')) return Promise.resolve(true) + return Promise.resolve('default') + }, + click: () => Promise.resolve(), + $: selector => Promise.resolve({ id: 'child-puppeteer' }), + }, + }, + ] + + for (const testCase of testCases) { + const mockHelper = { constructor: { name: testCase.helperType } } + const webElement = new WebElement(testCase.mockElement, mockHelper) + + // Test that all methods exist and are callable + expect(webElement.getText).to.be.a('function') + expect(webElement.getAttribute).to.be.a('function') + expect(webElement.isVisible).to.be.a('function') + expect(webElement.click).to.be.a('function') + expect(webElement.$).to.be.a('function') + expect(webElement.$$).to.be.a('function') + + // Test that methods return expected values + const text = await webElement.getText() + expect(text).to.include(testCase.helperType.toLowerCase()) + + const attr = await webElement.getAttribute('id') + expect(attr).to.include(testCase.helperType.toLowerCase()) + + const visible = await webElement.isVisible() + expect(visible).to.equal(true) + + // Test child element search returns WebElement + const childElement = await webElement.$('.child') + if (childElement) { + expect(childElement).to.be.instanceOf(WebElement) + } + + // Test native element access + expect(webElement.getNativeElement()).to.equal(testCase.mockElement) + expect(webElement.getHelper()).to.equal(mockHelper) + } + }) + }) + + describe('Helper method mocking', () => { + it('should verify grabWebElement returns WebElement for all helpers', () => { + // Mock implementations for each helper's grabWebElement method + const mockPlaywrightGrabWebElement = function (locator) { + const element = { type: 'playwright-element' } + return new WebElement(element, this) + } + + const mockWebDriverGrabWebElement = function (locator) { + const elements = [{ type: 'webdriver-element' }] + if (elements.length === 0) throw new Error('Element not found') + return new WebElement(elements[0], this) + } + + const mockPuppeteerGrabWebElement = function (locator) { + const elements = [{ type: 'puppeteer-element' }] + if (elements.length === 0) throw new Error('Element not found') + return new WebElement(elements[0], this) + } + + // Test each helper's method behavior + const playwrightHelper = { constructor: { name: 'Playwright' } } + const webdriverHelper = { constructor: { name: 'WebDriver' } } + const puppeteerHelper = { constructor: { name: 'Puppeteer' } } + + const playwrightResult = mockPlaywrightGrabWebElement.call(playwrightHelper, '.test') + const webdriverResult = mockWebDriverGrabWebElement.call(webdriverHelper, '.test') + const puppeteerResult = mockPuppeteerGrabWebElement.call(puppeteerHelper, '.test') + + expect(playwrightResult).to.be.instanceOf(WebElement) + expect(webdriverResult).to.be.instanceOf(WebElement) + expect(puppeteerResult).to.be.instanceOf(WebElement) + + expect(playwrightResult.getNativeElement().type).to.equal('playwright-element') + expect(webdriverResult.getNativeElement().type).to.equal('webdriver-element') + expect(puppeteerResult.getNativeElement().type).to.equal('puppeteer-element') + }) + + it('should verify grabWebElements returns WebElement array for all helpers', () => { + // Mock implementations for each helper's grabWebElements method + const mockPlaywrightGrabWebElements = function (locator) { + const elements = [{ type: 'playwright-element1' }, { type: 'playwright-element2' }] + return elements.map(element => new WebElement(element, this)) + } + + const mockWebDriverGrabWebElements = function (locator) { + const elements = [{ type: 'webdriver-element1' }, { type: 'webdriver-element2' }] + return elements.map(element => new WebElement(element, this)) + } + + const mockPuppeteerGrabWebElements = function (locator) { + const elements = [{ type: 'puppeteer-element1' }, { type: 'puppeteer-element2' }] + return elements.map(element => new WebElement(element, this)) + } + + // Test each helper's method behavior + const playwrightHelper = { constructor: { name: 'Playwright' } } + const webdriverHelper = { constructor: { name: 'WebDriver' } } + const puppeteerHelper = { constructor: { name: 'Puppeteer' } } + + const playwrightResults = mockPlaywrightGrabWebElements.call(playwrightHelper, '.test') + const webdriverResults = mockWebDriverGrabWebElements.call(webdriverHelper, '.test') + const puppeteerResults = mockPuppeteerGrabWebElements.call(puppeteerHelper, '.test') + + expect(playwrightResults).to.have.length(2) + expect(webdriverResults).to.have.length(2) + expect(puppeteerResults).to.have.length(2) + + playwrightResults.forEach(result => expect(result).to.be.instanceOf(WebElement)) + webdriverResults.forEach(result => expect(result).to.be.instanceOf(WebElement)) + puppeteerResults.forEach(result => expect(result).to.be.instanceOf(WebElement)) + }) + }) +}) diff --git a/test/unit/WebElement_test.js b/test/unit/WebElement_test.js new file mode 100644 index 000000000..6c80f29c2 --- /dev/null +++ b/test/unit/WebElement_test.js @@ -0,0 +1,567 @@ +const { expect } = require('chai') +const WebElement = require('../../lib/element/WebElement') + +describe('WebElement', () => { + describe('constructor and helper detection', () => { + it('should detect Playwright helper', () => { + const mockElement = {} + const mockHelper = { constructor: { name: 'Playwright' } } + const webElement = new WebElement(mockElement, mockHelper) + + expect(webElement.helperType).to.equal('playwright') + expect(webElement.getNativeElement()).to.equal(mockElement) + expect(webElement.getHelper()).to.equal(mockHelper) + }) + + it('should detect WebDriver helper', () => { + const mockElement = {} + const mockHelper = { constructor: { name: 'WebDriver' } } + const webElement = new WebElement(mockElement, mockHelper) + + expect(webElement.helperType).to.equal('webdriver') + }) + + it('should detect Puppeteer helper', () => { + const mockElement = {} + const mockHelper = { constructor: { name: 'Puppeteer' } } + const webElement = new WebElement(mockElement, mockHelper) + + expect(webElement.helperType).to.equal('puppeteer') + }) + + it('should handle unknown helper', () => { + const mockElement = {} + const mockHelper = { constructor: { name: 'Unknown' } } + const webElement = new WebElement(mockElement, mockHelper) + + expect(webElement.helperType).to.equal('unknown') + }) + }) + + describe('getText()', () => { + it('should work with Playwright helper', async () => { + const mockElement = { + textContent: () => Promise.resolve('test text'), + } + const mockHelper = { constructor: { name: 'Playwright' } } + const webElement = new WebElement(mockElement, mockHelper) + + const text = await webElement.getText() + expect(text).to.equal('test text') + }) + + it('should work with WebDriver helper', async () => { + const mockElement = { + getText: () => Promise.resolve('test text'), + } + const mockHelper = { constructor: { name: 'WebDriver' } } + const webElement = new WebElement(mockElement, mockHelper) + + const text = await webElement.getText() + expect(text).to.equal('test text') + }) + + it('should work with Puppeteer helper', async () => { + const mockElement = { + evaluate: fn => Promise.resolve('test text'), + } + const mockHelper = { constructor: { name: 'Puppeteer' } } + const webElement = new WebElement(mockElement, mockHelper) + + const text = await webElement.getText() + expect(text).to.equal('test text') + }) + + it('should throw error for unknown helper', async () => { + const mockElement = {} + const mockHelper = { constructor: { name: 'Unknown' } } + const webElement = new WebElement(mockElement, mockHelper) + + try { + await webElement.getText() + expect.fail('Should have thrown an error') + } catch (error) { + expect(error.message).to.include('Unsupported helper type: unknown') + } + }) + }) + + describe('getAttribute()', () => { + it('should work with Playwright helper', async () => { + const mockElement = { + getAttribute: name => Promise.resolve(name === 'id' ? 'test-id' : null), + } + const mockHelper = { constructor: { name: 'Playwright' } } + const webElement = new WebElement(mockElement, mockHelper) + + const attr = await webElement.getAttribute('id') + expect(attr).to.equal('test-id') + }) + + it('should work with WebDriver helper', async () => { + const mockElement = { + getAttribute: name => Promise.resolve(name === 'class' ? 'test-class' : null), + } + const mockHelper = { constructor: { name: 'WebDriver' } } + const webElement = new WebElement(mockElement, mockHelper) + + const attr = await webElement.getAttribute('class') + expect(attr).to.equal('test-class') + }) + + it('should work with Puppeteer helper', async () => { + const mockElement = { + evaluate: (fn, attrName) => Promise.resolve(attrName === 'data-test' ? 'test-value' : null), + } + const mockHelper = { constructor: { name: 'Puppeteer' } } + const webElement = new WebElement(mockElement, mockHelper) + + const attr = await webElement.getAttribute('data-test') + expect(attr).to.equal('test-value') + }) + }) + + describe('getProperty()', () => { + it('should work with Playwright helper', async () => { + const mockElement = { + evaluate: (fn, propName) => Promise.resolve(propName === 'value' ? 'test-value' : null), + } + const mockHelper = { constructor: { name: 'Playwright' } } + const webElement = new WebElement(mockElement, mockHelper) + + const prop = await webElement.getProperty('value') + expect(prop).to.equal('test-value') + }) + + it('should work with WebDriver helper', async () => { + const mockElement = { + getProperty: name => Promise.resolve(name === 'checked' ? true : null), + } + const mockHelper = { constructor: { name: 'WebDriver' } } + const webElement = new WebElement(mockElement, mockHelper) + + const prop = await webElement.getProperty('checked') + expect(prop).to.equal(true) + }) + + it('should work with Puppeteer helper', async () => { + const mockElement = { + evaluate: (fn, propName) => Promise.resolve(propName === 'disabled' ? false : null), + } + const mockHelper = { constructor: { name: 'Puppeteer' } } + const webElement = new WebElement(mockElement, mockHelper) + + const prop = await webElement.getProperty('disabled') + expect(prop).to.equal(false) + }) + }) + + describe('isVisible()', () => { + it('should work with Playwright helper', async () => { + const mockElement = { + isVisible: () => Promise.resolve(true), + } + const mockHelper = { constructor: { name: 'Playwright' } } + const webElement = new WebElement(mockElement, mockHelper) + + const visible = await webElement.isVisible() + expect(visible).to.equal(true) + }) + + it('should work with WebDriver helper', async () => { + const mockElement = { + isDisplayed: () => Promise.resolve(false), + } + const mockHelper = { constructor: { name: 'WebDriver' } } + const webElement = new WebElement(mockElement, mockHelper) + + const visible = await webElement.isVisible() + expect(visible).to.equal(false) + }) + + it('should work with Puppeteer helper', async () => { + const mockElement = { + evaluate: fn => Promise.resolve(true), // Simulates visible element + } + const mockHelper = { constructor: { name: 'Puppeteer' } } + const webElement = new WebElement(mockElement, mockHelper) + + const visible = await webElement.isVisible() + expect(visible).to.equal(true) + }) + }) + + describe('isEnabled()', () => { + it('should work with Playwright helper', async () => { + const mockElement = { + isEnabled: () => Promise.resolve(true), + } + const mockHelper = { constructor: { name: 'Playwright' } } + const webElement = new WebElement(mockElement, mockHelper) + + const enabled = await webElement.isEnabled() + expect(enabled).to.equal(true) + }) + + it('should work with WebDriver helper', async () => { + const mockElement = { + isEnabled: () => Promise.resolve(false), + } + const mockHelper = { constructor: { name: 'WebDriver' } } + const webElement = new WebElement(mockElement, mockHelper) + + const enabled = await webElement.isEnabled() + expect(enabled).to.equal(false) + }) + + it('should work with Puppeteer helper', async () => { + const mockElement = { + evaluate: fn => Promise.resolve(true), // Simulates enabled element + } + const mockHelper = { constructor: { name: 'Puppeteer' } } + const webElement = new WebElement(mockElement, mockHelper) + + const enabled = await webElement.isEnabled() + expect(enabled).to.equal(true) + }) + }) + + describe('click()', () => { + it('should work with Playwright helper', async () => { + let clicked = false + const mockElement = { + click: options => { + clicked = true + return Promise.resolve() + }, + } + const mockHelper = { constructor: { name: 'Playwright' } } + const webElement = new WebElement(mockElement, mockHelper) + + await webElement.click() + expect(clicked).to.equal(true) + }) + + it('should work with WebDriver helper', async () => { + let clicked = false + const mockElement = { + click: () => { + clicked = true + return Promise.resolve() + }, + } + const mockHelper = { constructor: { name: 'WebDriver' } } + const webElement = new WebElement(mockElement, mockHelper) + + await webElement.click() + expect(clicked).to.equal(true) + }) + + it('should work with Puppeteer helper', async () => { + let clicked = false + const mockElement = { + click: options => { + clicked = true + return Promise.resolve() + }, + } + const mockHelper = { constructor: { name: 'Puppeteer' } } + const webElement = new WebElement(mockElement, mockHelper) + + await webElement.click() + expect(clicked).to.equal(true) + }) + }) + + describe('type()', () => { + it('should work with Playwright helper', async () => { + let typedText = '' + const mockElement = { + type: (text, options) => { + typedText = text + return Promise.resolve() + }, + } + const mockHelper = { constructor: { name: 'Playwright' } } + const webElement = new WebElement(mockElement, mockHelper) + + await webElement.type('test input') + expect(typedText).to.equal('test input') + }) + + it('should work with WebDriver helper', async () => { + let typedText = '' + const mockElement = { + setValue: text => { + typedText = text + return Promise.resolve() + }, + } + const mockHelper = { constructor: { name: 'WebDriver' } } + const webElement = new WebElement(mockElement, mockHelper) + + await webElement.type('test input') + expect(typedText).to.equal('test input') + }) + + it('should work with Puppeteer helper', async () => { + let typedText = '' + const mockElement = { + type: (text, options) => { + typedText = text + return Promise.resolve() + }, + } + const mockHelper = { constructor: { name: 'Puppeteer' } } + const webElement = new WebElement(mockElement, mockHelper) + + await webElement.type('test input') + expect(typedText).to.equal('test input') + }) + }) + + describe('child element search', () => { + it('should find single child element with $()', async () => { + const mockChildElement = { id: 'child' } + const mockElement = { + $: selector => Promise.resolve(selector === '.child' ? mockChildElement : null), + } + const mockHelper = { constructor: { name: 'Playwright' } } + const webElement = new WebElement(mockElement, mockHelper) + + const childElement = await webElement.$('.child') + expect(childElement).to.be.instanceOf(WebElement) + expect(childElement.getNativeElement()).to.equal(mockChildElement) + }) + + it('should return null when child element not found', async () => { + const mockElement = { + $: selector => Promise.resolve(null), + } + const mockHelper = { constructor: { name: 'Playwright' } } + const webElement = new WebElement(mockElement, mockHelper) + + const childElement = await webElement.$('.nonexistent') + expect(childElement).to.be.null + }) + + it('should find multiple child elements with $$()', async () => { + const mockChildElements = [{ id: 'child1' }, { id: 'child2' }] + const mockElement = { + $$: selector => Promise.resolve(selector === '.children' ? mockChildElements : []), + } + const mockHelper = { constructor: { name: 'Playwright' } } + const webElement = new WebElement(mockElement, mockHelper) + + const childElements = await webElement.$$('.children') + expect(childElements).to.have.length(2) + expect(childElements[0]).to.be.instanceOf(WebElement) + expect(childElements[1]).to.be.instanceOf(WebElement) + expect(childElements[0].getNativeElement()).to.equal(mockChildElements[0]) + expect(childElements[1].getNativeElement()).to.equal(mockChildElements[1]) + }) + + it('should return empty array when no child elements found', async () => { + const mockElement = { + $$: selector => Promise.resolve([]), + } + const mockHelper = { constructor: { name: 'Playwright' } } + const webElement = new WebElement(mockElement, mockHelper) + + const childElements = await webElement.$$('.nonexistent') + expect(childElements).to.have.length(0) + }) + }) + + describe('_normalizeLocator()', () => { + let webElement + + beforeEach(() => { + const mockElement = {} + const mockHelper = { constructor: { name: 'Playwright' } } + webElement = new WebElement(mockElement, mockHelper) + }) + + it('should handle string locators', () => { + expect(webElement._normalizeLocator('.test')).to.equal('.test') + expect(webElement._normalizeLocator('#test')).to.equal('#test') + }) + + it('should handle locator objects with css', () => { + expect(webElement._normalizeLocator({ css: '.test-css' })).to.equal('.test-css') + }) + + it('should handle locator objects with xpath', () => { + expect(webElement._normalizeLocator({ xpath: '//div' })).to.equal('//div') + }) + + it('should handle locator objects with id', () => { + expect(webElement._normalizeLocator({ id: 'test-id' })).to.equal('#test-id') + }) + + it('should handle locator objects with name', () => { + expect(webElement._normalizeLocator({ name: 'test-name' })).to.equal('[name="test-name"]') + }) + + it('should handle locator objects with className', () => { + expect(webElement._normalizeLocator({ className: 'test-class' })).to.equal('.test-class') + }) + + it('should convert unknown objects to string', () => { + const obj = { toString: () => 'custom-locator' } + expect(webElement._normalizeLocator(obj)).to.equal('custom-locator') + }) + }) + + describe('getBoundingBox()', () => { + it('should work with Playwright helper', async () => { + const mockBoundingBox = { x: 10, y: 20, width: 100, height: 50 } + const mockElement = { + boundingBox: () => Promise.resolve(mockBoundingBox), + } + const mockHelper = { constructor: { name: 'Playwright' } } + const webElement = new WebElement(mockElement, mockHelper) + + const box = await webElement.getBoundingBox() + expect(box).to.deep.equal(mockBoundingBox) + }) + + it('should work with WebDriver helper', async () => { + const mockRect = { x: 15, y: 25, width: 120, height: 60 } + const mockElement = { + getRect: () => Promise.resolve(mockRect), + } + const mockHelper = { constructor: { name: 'WebDriver' } } + const webElement = new WebElement(mockElement, mockHelper) + + const box = await webElement.getBoundingBox() + expect(box).to.deep.equal(mockRect) + }) + + it('should work with Puppeteer helper', async () => { + const mockBoundingBox = { x: 5, y: 10, width: 80, height: 40 } + const mockElement = { + boundingBox: () => Promise.resolve(mockBoundingBox), + } + const mockHelper = { constructor: { name: 'Puppeteer' } } + const webElement = new WebElement(mockElement, mockHelper) + + const box = await webElement.getBoundingBox() + expect(box).to.deep.equal(mockBoundingBox) + }) + }) + + describe('getValue()', () => { + it('should work with Playwright helper', async () => { + const mockElement = { + inputValue: () => Promise.resolve('input-value'), + } + const mockHelper = { constructor: { name: 'Playwright' } } + const webElement = new WebElement(mockElement, mockHelper) + + const value = await webElement.getValue() + expect(value).to.equal('input-value') + }) + + it('should work with WebDriver helper', async () => { + const mockElement = { + getValue: () => Promise.resolve('input-value'), + } + const mockHelper = { constructor: { name: 'WebDriver' } } + const webElement = new WebElement(mockElement, mockHelper) + + const value = await webElement.getValue() + expect(value).to.equal('input-value') + }) + + it('should work with Puppeteer helper', async () => { + const mockElement = { + evaluate: fn => Promise.resolve('input-value'), + } + const mockHelper = { constructor: { name: 'Puppeteer' } } + const webElement = new WebElement(mockElement, mockHelper) + + const value = await webElement.getValue() + expect(value).to.equal('input-value') + }) + }) + + describe('getInnerHTML()', () => { + it('should work with Playwright helper', async () => { + const mockElement = { + innerHTML: () => Promise.resolve('inner'), + } + const mockHelper = { constructor: { name: 'Playwright' } } + const webElement = new WebElement(mockElement, mockHelper) + + const html = await webElement.getInnerHTML() + expect(html).to.equal('inner') + }) + + it('should work with WebDriver helper', async () => { + const mockElement = { + getProperty: prop => Promise.resolve(prop === 'innerHTML' ? '
content
' : null), + } + const mockHelper = { constructor: { name: 'WebDriver' } } + const webElement = new WebElement(mockElement, mockHelper) + + const html = await webElement.getInnerHTML() + expect(html).to.equal('
content
') + }) + + it('should work with Puppeteer helper', async () => { + const mockElement = { + evaluate: fn => Promise.resolve('

paragraph

'), + } + const mockHelper = { constructor: { name: 'Puppeteer' } } + const webElement = new WebElement(mockElement, mockHelper) + + const html = await webElement.getInnerHTML() + expect(html).to.equal('

paragraph

') + }) + }) + + describe('exists()', () => { + it('should work with Playwright helper', async () => { + const mockElement = { + evaluate: fn => Promise.resolve(true), + } + const mockHelper = { constructor: { name: 'Playwright' } } + const webElement = new WebElement(mockElement, mockHelper) + + const exists = await webElement.exists() + expect(exists).to.equal(true) + }) + + it('should work with WebDriver helper', async () => { + const mockElement = {} + const mockHelper = { constructor: { name: 'WebDriver' } } + const webElement = new WebElement(mockElement, mockHelper) + + const exists = await webElement.exists() + expect(exists).to.equal(true) + }) + + it('should work with Puppeteer helper', async () => { + const mockElement = { + evaluate: fn => Promise.resolve(true), + } + const mockHelper = { constructor: { name: 'Puppeteer' } } + const webElement = new WebElement(mockElement, mockHelper) + + const exists = await webElement.exists() + expect(exists).to.equal(true) + }) + + it('should handle errors and return false', async () => { + const mockElement = { + evaluate: () => Promise.reject(new Error('Element not found')), + } + const mockHelper = { constructor: { name: 'Playwright' } } + const webElement = new WebElement(mockElement, mockHelper) + + const exists = await webElement.exists() + expect(exists).to.equal(false) + }) + }) +}) From 903e58883064a236cf2788b7d82dd5c0b5daf38f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+copilot@users.noreply.github.com> Date: 2025年8月22日 11:18:45 +0000 Subject: [PATCH 3/3] Fix helper tests to expect WebElement instances instead of native elements Co-authored-by: kobenguyent <7845001+kobenguyent@users.noreply.github.com> --- test/helper/Playwright_test.js | 12 ++++++++---- test/helper/Puppeteer_test.js | 3 ++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/test/helper/Playwright_test.js b/test/helper/Playwright_test.js index f02e4a6ec..0e8432d5b 100644 --- a/test/helper/Playwright_test.js +++ b/test/helper/Playwright_test.js @@ -1707,7 +1707,8 @@ describe('Playwright - HAR', () => { await I.amOnPage('/form/focus_blur_elements') const webElements = await I.grabWebElements('#button') - assert.equal(webElements[0], "locator('#button').first()") + assert.equal(webElements[0].constructor.name, 'WebElement') + assert.equal(webElements[0].getNativeElement(), "locator('#button').first()") assert.isAbove(webElements.length, 0) }) @@ -1715,7 +1716,8 @@ describe('Playwright - HAR', () => { await I.amOnPage('/form/focus_blur_elements') const webElement = await I.grabWebElement('#button') - assert.equal(webElement, "locator('#button').first()") + assert.equal(webElement.constructor.name, 'WebElement') + assert.equal(webElement.getNativeElement(), "locator('#button').first()") }) }) }) @@ -1751,7 +1753,8 @@ describe('using data-testid attribute', () => { await I.amOnPage('/') const webElements = await I.grabWebElements({ pw: '[data-testid="welcome"]' }) - assert.equal(webElements[0]._selector, '[data-testid="welcome"]>> nth=0') + assert.equal(webElements[0].constructor.name, 'WebElement') + assert.equal(webElements[0].getNativeElement()._selector, '[data-testid="welcome"]>> nth=0') assert.equal(webElements.length, 1) }) @@ -1759,7 +1762,8 @@ describe('using data-testid attribute', () => { await I.amOnPage('/') const webElements = await I.grabWebElements('h1[data-testid="welcome"]') - assert.equal(webElements[0]._selector, 'h1[data-testid="welcome"]>> nth=0') + assert.equal(webElements[0].constructor.name, 'WebElement') + assert.equal(webElements[0].getNativeElement()._selector, 'h1[data-testid="welcome"]>> nth=0') assert.equal(webElements.length, 1) }) }) diff --git a/test/helper/Puppeteer_test.js b/test/helper/Puppeteer_test.js index 494b093cc..06ef40e05 100644 --- a/test/helper/Puppeteer_test.js +++ b/test/helper/Puppeteer_test.js @@ -1193,7 +1193,8 @@ describe('Puppeteer - Trace', () => { await I.amOnPage('/form/focus_blur_elements') const webElements = await I.grabWebElements('#button') - assert.include(webElements[0].constructor.name, 'CdpElementHandle') + assert.equal(webElements[0].constructor.name, 'WebElement') + assert.include(webElements[0].getNativeElement().constructor.name, 'CdpElementHandle') assert.isAbove(webElements.length, 0) }) })

AltStyle によって変換されたページ (->オリジナル) /