From de9693ee81d6f7c878c6fc901c4b283c974723f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 04:45:30 +0000 Subject: [PATCH 1/4] Initial plan From e1a9cc65749911e58ea2faf5b4943c0341365e1d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 05:03:59 +0000 Subject: [PATCH 2/4] Changes before error encountered Co-authored-by: kobenguyent <7845001+kobenguyent@users.noreply.github.com> --- lib/helper/Puppeteer.js | 165 +++++++++++++++++++++++++++++++++------- 1 file changed, 138 insertions(+), 27 deletions(-) diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index 0b417d768..899ee57e6 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -634,9 +634,11 @@ class Puppeteer extends Helper { return } - const els = await this._locate(locator) - assertElementExists(els, locator) - this.context = els[0] + const el = await this._locateElement(locator) + if (!el) { + throw new ElementNotFound(locator, 'Element for within context') + } + this.context = el this.withinLocator = new Locator(locator) } @@ -730,11 +732,13 @@ class Puppeteer extends Helper { * {{ react }} */ async moveCursorTo(locator, offsetX = 0, offsetY = 0) { - const els = await this._locate(locator) - assertElementExists(els, locator) + const el = await this._locateElement(locator) + if (!el) { + throw new ElementNotFound(locator, 'Element to move cursor to') + } // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates - const { x, y } = await getClickablePoint(els[0]) + const { x, y } = await getClickablePoint(el) await this.page.mouse.move(x + offsetX, y + offsetY) return this._waitForAction() } @@ -744,9 +748,10 @@ class Puppeteer extends Helper { * */ async focus(locator) { - const els = await this._locate(locator) - assertElementExists(els, locator, 'Element to focus') - const el = els[0] + const el = await this._locateElement(locator) + if (!el) { + throw new ElementNotFound(locator, 'Element to focus') + } await el.click() await el.focus() @@ -758,10 +763,12 @@ class Puppeteer extends Helper { * */ async blur(locator) { - const els = await this._locate(locator) - assertElementExists(els, locator, 'Element to blur') + const el = await this._locateElement(locator) + if (!el) { + throw new ElementNotFound(locator, 'Element to blur') + } - await blurElement(els[0], this.page) + await blurElement(el, this.page) return this._waitForAction() } @@ -810,11 +817,12 @@ class Puppeteer extends Helper { } if (locator) { - const els = await this._locate(locator) - assertElementExists(els, locator, 'Element') - const el = els[0] + const el = await this._locateElement(locator) + if (!el) { + throw new ElementNotFound(locator, 'Element to scroll into view') + } await el.evaluate(el => el.scrollIntoView()) - const elementCoordinates = await getClickablePoint(els[0]) + const elementCoordinates = await getClickablePoint(el) await this.executeScript((x, y) => window.scrollBy(x, y), elementCoordinates.x + offsetX, elementCoordinates.y + offsetY) } else { await this.executeScript((x, y) => window.scrollTo(x, y), offsetX, offsetY) @@ -882,6 +890,21 @@ class Puppeteer extends Helper { return findElements.call(this, context, locator) } + /** + * Get single element by different locator types, including strict locator + * Should be used in custom helpers: + * + * ```js + * const element = await this.helpers['Puppeteer']._locateElement({name: 'password'}); + * ``` + * + * {{ react }} + */ + async _locateElement(locator) { + const context = await this.context + return findElement.call(this, context, locator) + } + /** * Find a checkbox by providing human-readable text: * NOTE: Assumes the checkable element exists @@ -893,7 +916,9 @@ class Puppeteer extends Helper { async _locateCheckable(locator, providedContext = null) { const context = providedContext || (await this._getContext()) const els = await findCheckable.call(this, locator, context) - assertElementExists(els[0], locator, 'Checkbox or radio') + if (!els || els.length === 0) { + throw new ElementNotFound(locator, 'Checkbox or radio') + } return els[0] } @@ -2124,10 +2149,12 @@ class Puppeteer extends Helper { * {{> waitForClickable }} */ async waitForClickable(locator, waitTimeout) { - const els = await this._locate(locator) - assertElementExists(els, locator) + const el = await this._locateElement(locator) + if (!el) { + throw new ElementNotFound(locator, 'Element to wait for clickable') + } - return this.waitForFunction(isElementClickable, [els[0]], waitTimeout).catch(async e => { + return this.waitForFunction(isElementClickable, [el], waitTimeout).catch(async e => { if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) { throw new Error(`element ${new Locator(locator).toString()} still not clickable after ${waitTimeout || this.options.waitForTimeout / 1000} sec`) } else { @@ -2701,9 +2728,52 @@ class Puppeteer extends Helper { module.exports = Puppeteer +/** + * Build locator string for Puppeteer's Locator API + * Similar to Playwright's buildLocatorString function for consistency + * @param {Locator} locator - CodeceptJS Locator object + * @returns {string} Locator string compatible with Puppeteer's locator() method + */ +function buildLocatorString(locator) { + if (locator.isCustom()) { + return `${locator.type}=${locator.value}` + } + if (locator.isXPath()) { + return `xpath=${locator.value}` + } + return locator.simplify() +} + +/** + * Find elements using Puppeteer's modern Locator API with ElementHandle fallback + * This provides better reliability and waiting behavior while maintaining backward compatibility + * @param {Page|Frame|ElementHandle} matcher - Puppeteer context to search within + * @param {Object|string} locator - Locator specification + * @returns {Promise} Array of ElementHandle objects for compatibility + */ async function findElements(matcher, locator) { if (locator.react) return findReactElements.call(this, locator) locator = new Locator(locator, 'css') + + // Use Locator API for better reliability, then convert to ElementHandles for compatibility + if (matcher.locator) { + const locatorElement = matcher.locator(buildLocatorString(locator)) + const handles = await locatorElement.all() + // Convert Locator elements to ElementHandles for backward compatibility + return Promise.all( + handles.map(async handle => { + // For Puppeteer Locators, we can get ElementHandle using waitHandle() + try { + return await handle.waitHandle() + } catch (e) { + // Fallback for edge cases + return handle + } + }), + ) + } + + // Fallback to legacy approach if Locator not available if (!locator.isXPath()) return matcher.$$(locator.simplify()) // puppeteer version < 19.4.0 is no longer supported. This one is backward support. if (puppeteer.default?.defaultBrowserRevision) { @@ -2712,6 +2782,43 @@ async function findElements(matcher, locator) { return matcher.$x(locator.value) } +/** + * Find a single element using Puppeteer's modern Locator API with ElementHandle fallback + * @param {Page|Frame|ElementHandle} matcher - Puppeteer context to search within + * @param {Object|string} locator - Locator specification + * @returns {Promise} Single ElementHandle object for compatibility + */ +async function findElement(matcher, locator) { + if (locator.react) return findReactElements.call(this, locator) + locator = new Locator(locator, 'css') + + // Use Locator API for better reliability, then get first ElementHandle for compatibility + if (matcher.locator) { + const locatorElement = matcher.locator(buildLocatorString(locator)) + const handle = await locatorElement.first() + // Convert to ElementHandle for backward compatibility + try { + return await handle.waitHandle() + } catch (e) { + // Fallback for edge cases + return handle + } + } + + // Fallback to legacy approach if Locator not available + if (!locator.isXPath()) { + const elements = await matcher.$$(locator.simplify()) + return elements[0] + } + // puppeteer version < 19.4.0 is no longer supported. This one is backward support. + if (puppeteer.default?.defaultBrowserRevision) { + const elements = await matcher.$$(`xpath/${locator.value}`) + return elements[0] + } + const elements = await matcher.$x(locator.value) + return elements[0] +} + async function proceedClick(locator, context = null, options = {}) { let matcher = await this.context if (context) { @@ -2857,15 +2964,19 @@ async function findFields(locator) { } async function proceedDragAndDrop(sourceLocator, destinationLocator) { - const src = await this._locate(sourceLocator) - assertElementExists(src, sourceLocator, 'Source Element') + const src = await this._locateElement(sourceLocator) + if (!src) { + throw new ElementNotFound(sourceLocator, 'Source Element') + } - const dst = await this._locate(destinationLocator) - assertElementExists(dst, destinationLocator, 'Destination Element') + const dst = await this._locateElement(destinationLocator) + if (!dst) { + throw new ElementNotFound(destinationLocator, 'Destination Element') + } - // Note: Using public api .getClickablePoint becaues the .BoundingBox does not take into account iframe offsets - const dragSource = await getClickablePoint(src[0]) - const dragDestination = await getClickablePoint(dst[0]) + // Note: Using public api .getClickablePoint because the .BoundingBox does not take into account iframe offsets + const dragSource = await getClickablePoint(src) + const dragDestination = await getClickablePoint(dst) // Drag start point await this.page.mouse.move(dragSource.x, dragSource.y, { steps: 5 }) From 23ffd503823cca780ef2f0f921b034cbae16e038 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 05:31:06 +0000 Subject: [PATCH 3/4] Changes before error encountered Co-authored-by: kobenguyent <7845001+kobenguyent@users.noreply.github.com> --- lib/helper/Puppeteer.js | 62 ++++++----------------------------------- 1 file changed, 8 insertions(+), 54 deletions(-) diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index 899ee57e6..6965daac1 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -2729,51 +2729,17 @@ class Puppeteer extends Helper { module.exports = Puppeteer /** - * Build locator string for Puppeteer's Locator API - * Similar to Playwright's buildLocatorString function for consistency - * @param {Locator} locator - CodeceptJS Locator object - * @returns {string} Locator string compatible with Puppeteer's locator() method - */ -function buildLocatorString(locator) { - if (locator.isCustom()) { - return `${locator.type}=${locator.value}` - } - if (locator.isXPath()) { - return `xpath=${locator.value}` - } - return locator.simplify() -} - -/** - * Find elements using Puppeteer's modern Locator API with ElementHandle fallback - * This provides better reliability and waiting behavior while maintaining backward compatibility + * Find elements using Puppeteer's native element discovery methods + * Note: Unlike Playwright, Puppeteer's Locator API doesn't have .all() method for multiple elements * @param {Page|Frame|ElementHandle} matcher - Puppeteer context to search within * @param {Object|string} locator - Locator specification - * @returns {Promise} Array of ElementHandle objects for compatibility + * @returns {Promise} Array of ElementHandle objects */ async function findElements(matcher, locator) { if (locator.react) return findReactElements.call(this, locator) locator = new Locator(locator, 'css') - // Use Locator API for better reliability, then convert to ElementHandles for compatibility - if (matcher.locator) { - const locatorElement = matcher.locator(buildLocatorString(locator)) - const handles = await locatorElement.all() - // Convert Locator elements to ElementHandles for backward compatibility - return Promise.all( - handles.map(async handle => { - // For Puppeteer Locators, we can get ElementHandle using waitHandle() - try { - return await handle.waitHandle() - } catch (e) { - // Fallback for edge cases - return handle - } - }), - ) - } - - // Fallback to legacy approach if Locator not available + // Use proven legacy approach - Puppeteer Locator API doesn't have .all() method if (!locator.isXPath()) return matcher.$$(locator.simplify()) // puppeteer version < 19.4.0 is no longer supported. This one is backward support. if (puppeteer.default?.defaultBrowserRevision) { @@ -2783,29 +2749,17 @@ async function findElements(matcher, locator) { } /** - * Find a single element using Puppeteer's modern Locator API with ElementHandle fallback + * Find a single element using Puppeteer's native element discovery methods + * Note: Puppeteer Locator API doesn't have .first() method like Playwright * @param {Page|Frame|ElementHandle} matcher - Puppeteer context to search within * @param {Object|string} locator - Locator specification - * @returns {Promise} Single ElementHandle object for compatibility + * @returns {Promise} Single ElementHandle object */ async function findElement(matcher, locator) { if (locator.react) return findReactElements.call(this, locator) locator = new Locator(locator, 'css') - // Use Locator API for better reliability, then get first ElementHandle for compatibility - if (matcher.locator) { - const locatorElement = matcher.locator(buildLocatorString(locator)) - const handle = await locatorElement.first() - // Convert to ElementHandle for backward compatibility - try { - return await handle.waitHandle() - } catch (e) { - // Fallback for edge cases - return handle - } - } - - // Fallback to legacy approach if Locator not available + // Use proven legacy approach - Puppeteer Locator API doesn't have .first() method if (!locator.isXPath()) { const elements = await matcher.$$(locator.simplify()) return elements[0] From 5e4733adc1b587a235c90089ba8e332ff8319d8b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 06:16:08 +0000 Subject: [PATCH 4/4] Fix TypeScript type definitions for new Puppeteer helper methods Co-authored-by: kobenguyent <7845001+kobenguyent@users.noreply.github.com> --- lib/helper/Puppeteer.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index 6965daac1..35115ab00 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -2731,9 +2731,9 @@ module.exports = Puppeteer /** * Find elements using Puppeteer's native element discovery methods * Note: Unlike Playwright, Puppeteer's Locator API doesn't have .all() method for multiple elements - * @param {Page|Frame|ElementHandle} matcher - Puppeteer context to search within + * @param {Object} matcher - Puppeteer context to search within * @param {Object|string} locator - Locator specification - * @returns {Promise} Array of ElementHandle objects + * @returns {Promise} Array of ElementHandle objects */ async function findElements(matcher, locator) { if (locator.react) return findReactElements.call(this, locator) @@ -2751,9 +2751,9 @@ async function findElements(matcher, locator) { /** * Find a single element using Puppeteer's native element discovery methods * Note: Puppeteer Locator API doesn't have .first() method like Playwright - * @param {Page|Frame|ElementHandle} matcher - Puppeteer context to search within + * @param {Object} matcher - Puppeteer context to search within * @param {Object|string} locator - Locator specification - * @returns {Promise} Single ElementHandle object + * @returns {Promise} Single ElementHandle object */ async function findElement(matcher, locator) { if (locator.react) return findReactElements.call(this, locator)