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') + }) +})