Skip to content

Commit e1a9cc6

Browse files
Copilotkobenguyent
andcommitted
Changes before error encountered
Co-authored-by: kobenguyent <7845001+kobenguyent@users.noreply.github.com>
1 parent de9693e commit e1a9cc6

File tree

1 file changed

+138
-27
lines changed

1 file changed

+138
-27
lines changed

lib/helper/Puppeteer.js

Lines changed: 138 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -634,9 +634,11 @@ class Puppeteer extends Helper {
634634
return
635635
}
636636

637-
const els = await this._locate(locator)
638-
assertElementExists(els, locator)
639-
this.context = els[0]
637+
const el = await this._locateElement(locator)
638+
if (!el) {
639+
throw new ElementNotFound(locator, 'Element for within context')
640+
}
641+
this.context = el
640642

641643
this.withinLocator = new Locator(locator)
642644
}
@@ -730,11 +732,13 @@ class Puppeteer extends Helper {
730732
* {{ react }}
731733
*/
732734
async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
733-
const els = await this._locate(locator)
734-
assertElementExists(els, locator)
735+
const el = await this._locateElement(locator)
736+
if (!el) {
737+
throw new ElementNotFound(locator, 'Element to move cursor to')
738+
}
735739

736740
// Use manual mouse.move instead of .hover() so the offset can be added to the coordinates
737-
const { x, y } = await getClickablePoint(els[0])
741+
const { x, y } = await getClickablePoint(el)
738742
await this.page.mouse.move(x + offsetX, y + offsetY)
739743
return this._waitForAction()
740744
}
@@ -744,9 +748,10 @@ class Puppeteer extends Helper {
744748
*
745749
*/
746750
async focus(locator) {
747-
const els = await this._locate(locator)
748-
assertElementExists(els, locator, 'Element to focus')
749-
const el = els[0]
751+
const el = await this._locateElement(locator)
752+
if (!el) {
753+
throw new ElementNotFound(locator, 'Element to focus')
754+
}
750755

751756
await el.click()
752757
await el.focus()
@@ -758,10 +763,12 @@ class Puppeteer extends Helper {
758763
*
759764
*/
760765
async blur(locator) {
761-
const els = await this._locate(locator)
762-
assertElementExists(els, locator, 'Element to blur')
766+
const el = await this._locateElement(locator)
767+
if (!el) {
768+
throw new ElementNotFound(locator, 'Element to blur')
769+
}
763770

764-
await blurElement(els[0], this.page)
771+
await blurElement(el, this.page)
765772
return this._waitForAction()
766773
}
767774

@@ -810,11 +817,12 @@ class Puppeteer extends Helper {
810817
}
811818

812819
if (locator) {
813-
const els = await this._locate(locator)
814-
assertElementExists(els, locator, 'Element')
815-
const el = els[0]
820+
const el = await this._locateElement(locator)
821+
if (!el) {
822+
throw new ElementNotFound(locator, 'Element to scroll into view')
823+
}
816824
await el.evaluate(el => el.scrollIntoView())
817-
const elementCoordinates = await getClickablePoint(els[0])
825+
const elementCoordinates = await getClickablePoint(el)
818826
await this.executeScript((x, y) => window.scrollBy(x, y), elementCoordinates.x + offsetX, elementCoordinates.y + offsetY)
819827
} else {
820828
await this.executeScript((x, y) => window.scrollTo(x, y), offsetX, offsetY)
@@ -882,6 +890,21 @@ class Puppeteer extends Helper {
882890
return findElements.call(this, context, locator)
883891
}
884892

893+
/**
894+
* Get single element by different locator types, including strict locator
895+
* Should be used in custom helpers:
896+
*
897+
* ```js
898+
* const element = await this.helpers['Puppeteer']._locateElement({name: 'password'});
899+
* ```
900+
*
901+
* {{ react }}
902+
*/
903+
async _locateElement(locator) {
904+
const context = await this.context
905+
return findElement.call(this, context, locator)
906+
}
907+
885908
/**
886909
* Find a checkbox by providing human-readable text:
887910
* NOTE: Assumes the checkable element exists
@@ -893,7 +916,9 @@ class Puppeteer extends Helper {
893916
async _locateCheckable(locator, providedContext = null) {
894917
const context = providedContext || (await this._getContext())
895918
const els = await findCheckable.call(this, locator, context)
896-
assertElementExists(els[0], locator, 'Checkbox or radio')
919+
if (!els || els.length === 0) {
920+
throw new ElementNotFound(locator, 'Checkbox or radio')
921+
}
897922
return els[0]
898923
}
899924

@@ -2124,10 +2149,12 @@ class Puppeteer extends Helper {
21242149
* {{> waitForClickable }}
21252150
*/
21262151
async waitForClickable(locator, waitTimeout) {
2127-
const els = await this._locate(locator)
2128-
assertElementExists(els, locator)
2152+
const el = await this._locateElement(locator)
2153+
if (!el) {
2154+
throw new ElementNotFound(locator, 'Element to wait for clickable')
2155+
}
21292156

2130-
return this.waitForFunction(isElementClickable, [els[0]], waitTimeout).catch(async e => {
2157+
return this.waitForFunction(isElementClickable, [el], waitTimeout).catch(async e => {
21312158
if (/Waiting failed/i.test(e.message) || /failed: timeout/i.test(e.message)) {
21322159
throw new Error(`element ${new Locator(locator).toString()} still not clickable after ${waitTimeout || this.options.waitForTimeout / 1000} sec`)
21332160
} else {
@@ -2701,9 +2728,52 @@ class Puppeteer extends Helper {
27012728

27022729
module.exports = Puppeteer
27032730

2731+
/**
2732+
* Build locator string for Puppeteer's Locator API
2733+
* Similar to Playwright's buildLocatorString function for consistency
2734+
* @param {Locator} locator - CodeceptJS Locator object
2735+
* @returns {string} Locator string compatible with Puppeteer's locator() method
2736+
*/
2737+
function buildLocatorString(locator) {
2738+
if (locator.isCustom()) {
2739+
return `${locator.type}=${locator.value}`
2740+
}
2741+
if (locator.isXPath()) {
2742+
return `xpath=${locator.value}`
2743+
}
2744+
return locator.simplify()
2745+
}
2746+
2747+
/**
2748+
* Find elements using Puppeteer's modern Locator API with ElementHandle fallback
2749+
* This provides better reliability and waiting behavior while maintaining backward compatibility
2750+
* @param {Page|Frame|ElementHandle} matcher - Puppeteer context to search within
2751+
* @param {Object|string} locator - Locator specification
2752+
* @returns {Promise<ElementHandle[]>} Array of ElementHandle objects for compatibility
2753+
*/
27042754
async function findElements(matcher, locator) {
27052755
if (locator.react) return findReactElements.call(this, locator)
27062756
locator = new Locator(locator, 'css')
2757+
2758+
// Use Locator API for better reliability, then convert to ElementHandles for compatibility
2759+
if (matcher.locator) {
2760+
const locatorElement = matcher.locator(buildLocatorString(locator))
2761+
const handles = await locatorElement.all()
2762+
// Convert Locator elements to ElementHandles for backward compatibility
2763+
return Promise.all(
2764+
handles.map(async handle => {
2765+
// For Puppeteer Locators, we can get ElementHandle using waitHandle()
2766+
try {
2767+
return await handle.waitHandle()
2768+
} catch (e) {
2769+
// Fallback for edge cases
2770+
return handle
2771+
}
2772+
}),
2773+
)
2774+
}
2775+
2776+
// Fallback to legacy approach if Locator not available
27072777
if (!locator.isXPath()) return matcher.$$(locator.simplify())
27082778
// puppeteer version < 19.4.0 is no longer supported. This one is backward support.
27092779
if (puppeteer.default?.defaultBrowserRevision) {
@@ -2712,6 +2782,43 @@ async function findElements(matcher, locator) {
27122782
return matcher.$x(locator.value)
27132783
}
27142784

2785+
/**
2786+
* Find a single element using Puppeteer's modern Locator API with ElementHandle fallback
2787+
* @param {Page|Frame|ElementHandle} matcher - Puppeteer context to search within
2788+
* @param {Object|string} locator - Locator specification
2789+
* @returns {Promise<ElementHandle>} Single ElementHandle object for compatibility
2790+
*/
2791+
async function findElement(matcher, locator) {
2792+
if (locator.react) return findReactElements.call(this, locator)
2793+
locator = new Locator(locator, 'css')
2794+
2795+
// Use Locator API for better reliability, then get first ElementHandle for compatibility
2796+
if (matcher.locator) {
2797+
const locatorElement = matcher.locator(buildLocatorString(locator))
2798+
const handle = await locatorElement.first()
2799+
// Convert to ElementHandle for backward compatibility
2800+
try {
2801+
return await handle.waitHandle()
2802+
} catch (e) {
2803+
// Fallback for edge cases
2804+
return handle
2805+
}
2806+
}
2807+
2808+
// Fallback to legacy approach if Locator not available
2809+
if (!locator.isXPath()) {
2810+
const elements = await matcher.$$(locator.simplify())
2811+
return elements[0]
2812+
}
2813+
// puppeteer version < 19.4.0 is no longer supported. This one is backward support.
2814+
if (puppeteer.default?.defaultBrowserRevision) {
2815+
const elements = await matcher.$$(`xpath/${locator.value}`)
2816+
return elements[0]
2817+
}
2818+
const elements = await matcher.$x(locator.value)
2819+
return elements[0]
2820+
}
2821+
27152822
async function proceedClick(locator, context = null, options = {}) {
27162823
let matcher = await this.context
27172824
if (context) {
@@ -2857,15 +2964,19 @@ async function findFields(locator) {
28572964
}
28582965

28592966
async function proceedDragAndDrop(sourceLocator, destinationLocator) {
2860-
const src = await this._locate(sourceLocator)
2861-
assertElementExists(src, sourceLocator, 'Source Element')
2967+
const src = await this._locateElement(sourceLocator)
2968+
if (!src) {
2969+
throw new ElementNotFound(sourceLocator, 'Source Element')
2970+
}
28622971

2863-
const dst = await this._locate(destinationLocator)
2864-
assertElementExists(dst, destinationLocator, 'Destination Element')
2972+
const dst = await this._locateElement(destinationLocator)
2973+
if (!dst) {
2974+
throw new ElementNotFound(destinationLocator, 'Destination Element')
2975+
}
28652976

2866-
// Note: Using public api .getClickablePoint becaues the .BoundingBox does not take into account iframe offsets
2867-
const dragSource = await getClickablePoint(src[0])
2868-
const dragDestination = await getClickablePoint(dst[0])
2977+
// Note: Using public api .getClickablePoint because the .BoundingBox does not take into account iframe offsets
2978+
const dragSource = await getClickablePoint(src)
2979+
const dragDestination = await getClickablePoint(dst)
28692980

28702981
// Drag start point
28712982
await this.page.mouse.move(dragSource.x, dragSource.y, { steps: 5 })

0 commit comments

Comments
 (0)