Skip to content
Open
Show file tree
Hide file tree
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
10 changes: 8 additions & 2 deletions lib/helper/Playwright.js
Original file line number Diff line number Diff line change
Expand Up @@ -4134,9 +4134,15 @@ class Playwright extends Helper {

export default Playwright

function buildLocatorString(locator) {
export function buildLocatorString(locator) {
if (locator.isXPath()) {
return `xpath=${locator.value}`
// Make XPath relative so it works correctly within scoped contexts (e.g. within()).
// Playwright's XPath engine auto-converts "//..." to ".//..." when the root is not a Document,
// but only when the selector starts with "/". Locator methods like at() wrap XPath in
// parentheses (e.g. "(//...)[position()=1]"), bypassing that auto-conversion.
// We fix this by prepending "." before the first "//" that follows any leading parentheses.
const value = locator.value.replace(/^(\(*)\/\//, '$1.//')
return `xpath=${value}`
}
if (locator.isShadow()) {
// Convert shadow locator to CSS with >> chaining operator
Expand Down
49 changes: 49 additions & 0 deletions test/unit/helper/Playwright_buildLocatorString_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { expect } from 'chai'
import Locator from '../../../lib/locator.js'
import { buildLocatorString } from '../../../lib/helper/Playwright.js'

describe('buildLocatorString', () => {
it('should make plain XPath relative', () => {
const locator = new Locator({ xpath: '//div' })
expect(buildLocatorString(locator)).to.equal('xpath=.//div')
})

it('should make XPath with parentheses (from at()) relative', () => {
const locator = new Locator('.item').at(1)
const result = buildLocatorString(locator)
expect(result).to.match(/^xpath=\(\.\/\//)
})

it('should make XPath from at().find() relative', () => {
const locator = new Locator('.item').at(1).find('.label')
const result = buildLocatorString(locator)
expect(result).to.match(/^xpath=\(\.\/\//)
})

it('should make XPath from first() relative', () => {
const locator = new Locator('.item').first()
const result = buildLocatorString(locator)
expect(result).to.match(/^xpath=\(\.\/\//)
})

it('should make XPath from last() relative', () => {
const locator = new Locator('.item').last()
const result = buildLocatorString(locator)
expect(result).to.match(/^xpath=\(\.\/\//)
})

it('should not double-prefix already relative XPath', () => {
const locator = new Locator({ xpath: './/div' })
expect(buildLocatorString(locator)).to.equal('xpath=.//div')
})

it('should handle XPath that was already relative inside parentheses', () => {
const locator = new Locator({ xpath: '(.//div)[1]' })
expect(buildLocatorString(locator)).to.equal('xpath=(.//div)[1]')
})

it('should return CSS locators unchanged', () => {
const locator = new Locator('.my-class')
expect(buildLocatorString(locator)).to.equal('.my-class')
})
})
Loading