diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js
index 9b032d56a..0d96b7240 100644
--- a/lib/helper/Playwright.js
+++ b/lib/helper/Playwright.js
@@ -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
diff --git a/test/acceptance/within_test.js b/test/acceptance/within_test.js
index b0b75a3bd..1d98321d6 100644
--- a/test/acceptance/within_test.js
+++ b/test/acceptance/within_test.js
@@ -1,5 +1,12 @@
Feature('within', { retries: 3 })
+Scenario('within with locate().at().find() should scope XPath @Playwright', async ({ I }) => {
+ I.amOnPage('/form/bug5473')
+ await within('#list2', async () => {
+ await I.see('Second', locate('.item').at(1).find('.label'))
+ })
+})
+
Scenario('within on form @WebDriverIO @Puppeteer @Playwright', async ({ I }) => {
I.amOnPage('/form/bug1467')
I.see('TEST TEST')
diff --git a/test/data/app/view/form/bug5473.php b/test/data/app/view/form/bug5473.php
new file mode 100644
index 000000000..a0fa4ad15
--- /dev/null
+++ b/test/data/app/view/form/bug5473.php
@@ -0,0 +1,14 @@
+
+
+
within + locate().at() bug
+
+
+
+
diff --git a/test/unit/helper/Playwright_buildLocatorString_test.js b/test/unit/helper/Playwright_buildLocatorString_test.js
new file mode 100644
index 000000000..f9957a5f7
--- /dev/null
+++ b/test/unit/helper/Playwright_buildLocatorString_test.js
@@ -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')
+ })
+})