Skip to content

Commit 0397204

Browse files
miraoclaude
andcommitted
fix: make XPath relative in buildLocatorString for within() scope (#5473)
Playwright's XPath engine auto-converts "//..." to ".//..." when searching within an element, but only when the selector starts with "/". Locator methods like at(), first(), last() wrap XPath in parentheses (e.g. "(//...)[position()=1]"), bypassing that auto-conversion and causing XPath to search from the document root instead of the within() scope. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 27eea12 commit 0397204

File tree

2 files changed

+57
-2
lines changed

2 files changed

+57
-2
lines changed

lib/helper/Playwright.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4134,9 +4134,15 @@ class Playwright extends Helper {
41344134

41354135
export default Playwright
41364136

4137-
function buildLocatorString(locator) {
4137+
export function buildLocatorString(locator) {
41384138
if (locator.isXPath()) {
4139-
return `xpath=${locator.value}`
4139+
// Make XPath relative so it works correctly within scoped contexts (e.g. within()).
4140+
// Playwright's XPath engine auto-converts "//..." to ".//..." when the root is not a Document,
4141+
// but only when the selector starts with "/". Locator methods like at() wrap XPath in
4142+
// parentheses (e.g. "(//...)[position()=1]"), bypassing that auto-conversion.
4143+
// We fix this by prepending "." before the first "//" that follows any leading parentheses.
4144+
const value = locator.value.replace(/^(\(*)\/\//, '$1.//')
4145+
return `xpath=${value}`
41404146
}
41414147
if (locator.isShadow()) {
41424148
// Convert shadow locator to CSS with >> chaining operator
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { expect } from 'chai'
2+
import Locator from '../../../lib/locator.js'
3+
import { buildLocatorString } from '../../../lib/helper/Playwright.js'
4+
5+
describe('buildLocatorString', () => {
6+
it('should make plain XPath relative', () => {
7+
const locator = new Locator({ xpath: '//div' })
8+
expect(buildLocatorString(locator)).to.equal('xpath=.//div')
9+
})
10+
11+
it('should make XPath with parentheses (from at()) relative', () => {
12+
const locator = new Locator('.item').at(1)
13+
const result = buildLocatorString(locator)
14+
expect(result).to.match(/^xpath=\(\.\/\//)
15+
})
16+
17+
it('should make XPath from at().find() relative', () => {
18+
const locator = new Locator('.item').at(1).find('.label')
19+
const result = buildLocatorString(locator)
20+
expect(result).to.match(/^xpath=\(\.\/\//)
21+
})
22+
23+
it('should make XPath from first() relative', () => {
24+
const locator = new Locator('.item').first()
25+
const result = buildLocatorString(locator)
26+
expect(result).to.match(/^xpath=\(\.\/\//)
27+
})
28+
29+
it('should make XPath from last() relative', () => {
30+
const locator = new Locator('.item').last()
31+
const result = buildLocatorString(locator)
32+
expect(result).to.match(/^xpath=\(\.\/\//)
33+
})
34+
35+
it('should not double-prefix already relative XPath', () => {
36+
const locator = new Locator({ xpath: './/div' })
37+
expect(buildLocatorString(locator)).to.equal('xpath=.//div')
38+
})
39+
40+
it('should handle XPath that was already relative inside parentheses', () => {
41+
const locator = new Locator({ xpath: '(.//div)[1]' })
42+
expect(buildLocatorString(locator)).to.equal('xpath=(.//div)[1]')
43+
})
44+
45+
it('should return CSS locators unchanged', () => {
46+
const locator = new Locator('.my-class')
47+
expect(buildLocatorString(locator)).to.equal('.my-class')
48+
})
49+
})

0 commit comments

Comments
 (0)