Skip to content

Commit de21279

Browse files
authored
[WIP] [FEATURE REQUEST](puppeteer) migrate locators from ElementHandle to Locator (#5096)
1 parent c95f78d commit de21279

File tree

1 file changed

+92
-27
lines changed

1 file changed

+92
-27
lines changed

lib/helper/Puppeteer.js

Lines changed: 92 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,18 @@ class Puppeteer extends Helper {
27012728

27022729
module.exports = Puppeteer
27032730

2731+
/**
2732+
* Find elements using Puppeteer's native element discovery methods
2733+
* Note: Unlike Playwright, Puppeteer's Locator API doesn't have .all() method for multiple elements
2734+
* @param {Object} matcher - Puppeteer context to search within
2735+
* @param {Object|string} locator - Locator specification
2736+
* @returns {Promise<Array>} Array of ElementHandle objects
2737+
*/
27042738
async function findElements(matcher, locator) {
27052739
if (locator.react) return findReactElements.call(this, locator)
27062740
locator = new Locator(locator, 'css')
2741+
2742+
// Use proven legacy approach - Puppeteer Locator API doesn't have .all() method
27072743
if (!locator.isXPath()) return matcher.$$(locator.simplify())
27082744
// puppeteer version < 19.4.0 is no longer supported. This one is backward support.
27092745
if (puppeteer.default?.defaultBrowserRevision) {
@@ -2712,6 +2748,31 @@ async function findElements(matcher, locator) {
27122748
return matcher.$x(locator.value)
27132749
}
27142750

2751+
/**
2752+
* Find a single element using Puppeteer's native element discovery methods
2753+
* Note: Puppeteer Locator API doesn't have .first() method like Playwright
2754+
* @param {Object} matcher - Puppeteer context to search within
2755+
* @param {Object|string} locator - Locator specification
2756+
* @returns {Promise<Object>} Single ElementHandle object
2757+
*/
2758+
async function findElement(matcher, locator) {
2759+
if (locator.react) return findReactElements.call(this, locator)
2760+
locator = new Locator(locator, 'css')
2761+
2762+
// Use proven legacy approach - Puppeteer Locator API doesn't have .first() method
2763+
if (!locator.isXPath()) {
2764+
const elements = await matcher.$$(locator.simplify())
2765+
return elements[0]
2766+
}
2767+
// puppeteer version < 19.4.0 is no longer supported. This one is backward support.
2768+
if (puppeteer.default?.defaultBrowserRevision) {
2769+
const elements = await matcher.$$(`xpath/${locator.value}`)
2770+
return elements[0]
2771+
}
2772+
const elements = await matcher.$x(locator.value)
2773+
return elements[0]
2774+
}
2775+
27152776
async function proceedClick(locator, context = null, options = {}) {
27162777
let matcher = await this.context
27172778
if (context) {
@@ -2857,15 +2918,19 @@ async function findFields(locator) {
28572918
}
28582919

28592920
async function proceedDragAndDrop(sourceLocator, destinationLocator) {
2860-
const src = await this._locate(sourceLocator)
2861-
assertElementExists(src, sourceLocator, 'Source Element')
2921+
const src = await this._locateElement(sourceLocator)
2922+
if (!src) {
2923+
throw new ElementNotFound(sourceLocator, 'Source Element')
2924+
}
28622925

2863-
const dst = await this._locate(destinationLocator)
2864-
assertElementExists(dst, destinationLocator, 'Destination Element')
2926+
const dst = await this._locateElement(destinationLocator)
2927+
if (!dst) {
2928+
throw new ElementNotFound(destinationLocator, 'Destination Element')
2929+
}
28652930

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])
2931+
// Note: Using public api .getClickablePoint because the .BoundingBox does not take into account iframe offsets
2932+
const dragSource = await getClickablePoint(src)
2933+
const dragDestination = await getClickablePoint(dst)
28692934

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

0 commit comments

Comments
 (0)