Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 92 additions & 27 deletions lib/helper/Puppeteer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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()
}
Expand All @@ -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()
Expand All @@ -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()
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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]
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -2701,9 +2728,18 @@ class Puppeteer extends Helper {

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 {Object} matcher - Puppeteer context to search within
* @param {Object|string} locator - Locator specification
* @returns {Promise<Array>} Array of ElementHandle objects
*/
async function findElements(matcher, locator) {
if (locator.react) return findReactElements.call(this, locator)
locator = new Locator(locator, 'css')

// 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) {
Expand All @@ -2712,6 +2748,31 @@ async function findElements(matcher, locator) {
return matcher.$x(locator.value)
}

/**
* Find a single element using Puppeteer's native element discovery methods
* Note: Puppeteer Locator API doesn't have .first() method like Playwright
* @param {Object} matcher - Puppeteer context to search within
* @param {Object|string} locator - Locator specification
* @returns {Promise<Object>} Single ElementHandle object
*/
async function findElement(matcher, locator) {
if (locator.react) return findReactElements.call(this, locator)
locator = new Locator(locator, 'css')

// Use proven legacy approach - Puppeteer Locator API doesn't have .first() method
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) {
Expand Down Expand Up @@ -2857,15 +2918,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 })
Expand Down