Skip to content

Commit b6523ae

Browse files
committed
fix: handle XPath queries within ElementHandles
- Added support for XPath locators when using 'within' on elements - ElementHandles don't have $x method, so we use evaluateHandle to run XPath within the element context - Added page null safety checks in saveScreenshot and _withinEnd - Improved from 27 to 30 passing tests
1 parent 2ff912e commit b6523ae

File tree

1 file changed

+51
-4
lines changed

1 file changed

+51
-4
lines changed

lib/helper/Puppeteer.js

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -691,7 +691,11 @@ class Puppeteer extends Helper {
691691

692692
async _withinEnd() {
693693
this.withinLocator = null
694-
this.context = await this.page.mainFrame().$('body')
694+
if (this.page && !this.page.isClosed?.()) {
695+
this.context = await this.page.mainFrame().$('body')
696+
} else {
697+
this.context = null
698+
}
695699
}
696700

697701
_extractDataFromPerformanceTiming(timing, ...dataNames) {
@@ -2120,6 +2124,12 @@ class Puppeteer extends Helper {
21202124

21212125
this.debug(`Screenshot is saving to ${outputFile}`)
21222126

2127+
// Safety check: ensure page exists and is not closed
2128+
if (!this.page || this.page.isClosed?.()) {
2129+
this.debugSection('Screenshot', 'Page is not available, skipping screenshot')
2130+
return
2131+
}
2132+
21232133
await this.page.screenshot({
21242134
path: outputFile,
21252135
fullPage: fullPageOption,
@@ -2133,7 +2143,7 @@ class Puppeteer extends Helper {
21332143

21342144
this.debug(`${sessionName} - Screenshot is saving to ${outputFile}`)
21352145

2136-
if (activeSessionPage) {
2146+
if (activeSessionPage && !activeSessionPage.isClosed?.()) {
21372147
await activeSessionPage.screenshot({
21382148
path: outputFile,
21392149
fullPage: fullPageOption,
@@ -2867,12 +2877,49 @@ async function findElements(matcher, locator) {
28672877

28682878
// Use proven legacy approach - Puppeteer Locator API doesn't have .all() method
28692879
if (!locator.isXPath()) return matcher.$$(locator.simplify())
2880+
28702881
// puppeteer version < 19.4.0 is no longer supported. This one is backward support.
28712882
if (puppeteer.default?.defaultBrowserRevision) {
28722883
return matcher.$$(`xpath/${locator.value}`)
28732884
}
2874-
// For newer Puppeteer versions, use $x for XPath
2875-
return matcher.$x(locator.value)
2885+
2886+
// For newer Puppeteer versions, use $x for XPath (only available on Page/Frame)
2887+
if (matcher.$x) {
2888+
return matcher.$x(locator.value)
2889+
}
2890+
2891+
// ElementHandles don't support XPath directly
2892+
// Get the frame/page containing this element and use that for XPath search
2893+
try {
2894+
const frame = matcher.contentFrame ? await matcher.contentFrame() : null
2895+
const executionContext = frame || (await matcher.executionContext())
2896+
const frameOrPage = executionContext?._world?._frameManager?._page || await this._getContext()
2897+
2898+
if (frameOrPage.$x) {
2899+
// Search within the element by getting its descendants
2900+
// We need to make XPath relative to the element
2901+
const relativeXPath = locator.value.startsWith('.//') ? locator.value : `.//${locator.value.replace(/^\/\//, '')}`
2902+
2903+
// Use the element as context by evaluating XPath from it
2904+
const elements = await matcher.evaluateHandle((element, xpath) => {
2905+
const iterator = document.evaluate(xpath, element, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
2906+
const results = []
2907+
for (let i = 0; i < iterator.snapshotLength; i++) {
2908+
results.push(iterator.snapshotItem(i))
2909+
}
2910+
return results
2911+
}, relativeXPath)
2912+
2913+
// Convert JSHandle to array of ElementHandles
2914+
const properties = await elements.getProperties()
2915+
return Array.from(properties.values())
2916+
}
2917+
} catch (e) {
2918+
this.debug(`XPath within element failed: ${e.message}`)
2919+
}
2920+
2921+
// Fallback: return empty array
2922+
return []
28762923
}
28772924

28782925
/**

0 commit comments

Comments
 (0)