diff --git a/docs/api.md b/docs/api.md index 55c4f1ce8d..975ecf5ae5 100644 --- a/docs/api.md +++ b/docs/api.md @@ -4452,7 +4452,7 @@ Selector describes an element in the page. It can be used to obtain `ElementHand Selector has the following format: `engine=body [>> engine=body]*`. Here `engine` is one of the supported [selector engines](selectors.md) (e.g. `css` or `xpath`), and `body` is a selector body in the format of the particular engine. When multiple `engine=body` clauses are present (separated by `>>`), next one is queried relative to the previous one's result. For convenience, selectors in the wrong format are heuristically converted to the right format: -- selector starting with `//` is assumed to be `xpath=selector`; +- selector starting with `//` or `..` is assumed to be `xpath=selector`; - selector starting and ending with a quote (either `"` or `'`) is assumed to be `text=selector`; - otherwise selector is assumed to be `css=selector`. @@ -4478,6 +4478,9 @@ const handle = await page.$('//html/body/div'); // converted to 'text="foo"' const handle = await page.$('"foo"'); +// queries '../span' xpath selector starting with the result of 'div' css selector +const handle = await page.$('div >> ../span'); + // queries 'span' css selector inside the div handle const handle = await divHandle.$('css=span'); ``` diff --git a/docs/selectors.md b/docs/selectors.md index d802311a8d..99da8b9930 100644 --- a/docs/selectors.md +++ b/docs/selectors.md @@ -31,7 +31,7 @@ document Selector engine name can be prefixed with `*` to capture element that matches the particular clause instead of the last one. For example, `css=article >> text=Hello` captures the element with the text `Hello`, and `*css=article >> text=Hello` (note the `*`) captures the `article` element that contains some element with the text `Hello`. For convenience, selectors in the wrong format are heuristically converted to the right format: -- Selector starting with `//` is assumed to be `xpath=selector`. Example: `page.click('//html')` is converted to `page.click('xpath=//html')`. +- Selector starting with `//` or `..` is assumed to be `xpath=selector`. Example: `page.click('//html')` is converted to `page.click('xpath=//html')`. - Selector starting and ending with a quote (either `"` or `'`) is assumed to be `text=selector`. Example: `page.click('"foo"')` is converted to `page.click('text="foo"')`. - Otherwise, selector is assumed to be `css=selector`. Example: `page.click('div')` is converted to `page.click('css=div')`. @@ -108,7 +108,7 @@ Note that `` is not an html element, but rather a shadow XPath engine is equivalent to [`Document.evaluate`](https://developer.mozilla.org/en/docs/Web/API/Document/evaluate). Example: `xpath=//html/body`. -Malformed selector starting with `//` is assumed to be an xpath selector. For example, Playwright converts `page.$('//html/body')` to `page.$('xpath=//html/body')`. +Malformed selector starting with `//` or `..` is assumed to be an xpath selector. For example, Playwright converts `page.$('//html/body')` to `page.$('xpath=//html/body')`. Note that `xpath` does not pierce shadow roots. diff --git a/src/common/selectorParser.ts b/src/common/selectorParser.ts index 89e0ad3f68..e227b6afca 100644 --- a/src/common/selectorParser.ts +++ b/src/common/selectorParser.ts @@ -43,9 +43,10 @@ export function parseSelector(selector: string): ParsedSelector { } else if (part.length > 1 && part[0] === "'" && part[part.length - 1] === "'") { name = 'text'; body = part; - } else if (/^\(*\/\//.test(part)) { + } else if (/^\(*\/\//.test(part) || part.startsWith('..')) { // If selector starts with '//' or '//' prefixed with multiple opening // parenthesis, consider xpath. @see https://github.com/microsoft/playwright/issues/817 + // If selector starts with '..', consider xpath as well. name = 'xpath'; body = part; } else { diff --git a/test/queryselector.jest.js b/test/queryselector.jest.js index 3ba9a67e5a..e4b17f3744 100644 --- a/test/queryselector.jest.js +++ b/test/queryselector.jest.js @@ -263,6 +263,13 @@ describe('Page.$', function() { const element = await page.$('(//section)[1]'); expect(element).toBeTruthy(); }); + it('should auto-detect xpath selector starting with ..', async({page, server}) => { + await page.setContent('
test
'); + const span = await page.$('"test" >> ../span'); + expect(await span.evaluate(e => e.nodeName)).toBe('SPAN'); + const div = await page.$('"test" >> ..'); + expect(await div.evaluate(e => e.nodeName)).toBe('DIV'); + }); it('should auto-detect text selector', async({page, server}) => { await page.setContent('
test
'); const element = await page.$('"test"');