diff --git a/docs/api.md b/docs/api.md index 41bbc9e399f..6e4c0240e4f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -44,6 +44,7 @@ * [page.$$(selector)](#pageselector) * [page.$$eval(selector, pageFunction[, ...args])](#pageevalselector-pagefunction-args) * [page.$eval(selector, pageFunction[, ...args])](#pageevalselector-pagefunction-args) + * [page.$x(expression)](#pagexexpression) * [page.addScriptTag(options)](#pageaddscripttagoptions) * [page.addStyleTag(options)](#pageaddstyletagoptions) * [page.authenticate(credentials)](#pageauthenticatecredentials) @@ -94,7 +95,6 @@ * [page.waitForFunction(pageFunction[, options[, ...args]])](#pagewaitforfunctionpagefunction-options-args) * [page.waitForNavigation(options)](#pagewaitfornavigationoptions) * [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options) - * [page.xpath(expression)](#pagexpathexpression) - [class: Keyboard](#class-keyboard) * [keyboard.down(key[, options])](#keyboarddownkey-options) * [keyboard.press(key[, options])](#keyboardpresskey-options) @@ -126,6 +126,7 @@ * [frame.$$(selector)](#frameselector) * [frame.$$eval(selector, pageFunction[, ...args])](#frameevalselector-pagefunction-args) * [frame.$eval(selector, pageFunction[, ...args])](#frameevalselector-pagefunction-args) + * [frame.$x(expression)](#framexexpression) * [frame.addScriptTag(options)](#frameaddscripttagoptions) * [frame.addStyleTag(options)](#frameaddstyletagoptions) * [frame.childFrames()](#framechildframes) @@ -142,7 +143,6 @@ * [frame.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#framewaitforselectororfunctionortimeout-options-args) * [frame.waitForFunction(pageFunction[, options[, ...args]])](#framewaitforfunctionpagefunction-options-args) * [frame.waitForSelector(selector[, options])](#framewaitforselectorselector-options) - * [frame.xpath(expression)](#framexpathexpression) - [class: ExecutionContext](#class-executioncontext) * [executionContext.evaluate(pageFunction, ...args)](#executioncontextevaluatepagefunction-args) * [executionContext.evaluateHandle(pageFunction, ...args)](#executioncontextevaluatehandlepagefunction-args) @@ -157,6 +157,7 @@ - [class: ElementHandle](#class-elementhandle) * [elementHandle.$(selector)](#elementhandleselector) * [elementHandle.$$(selector)](#elementhandleselector) + * [elementHandle.$x(expression)](#elementhandlexexpression) * [elementHandle.asElement()](#elementhandleaselement) * [elementHandle.boundingBox()](#elementhandleboundingbox) * [elementHandle.click([options])](#elementhandleclickoptions) @@ -173,7 +174,6 @@ * [elementHandle.toString()](#elementhandletostring) * [elementHandle.type(text[, options])](#elementhandletypetext-options) * [elementHandle.uploadFile(...filePaths)](#elementhandleuploadfilefilepaths) - * [elementHandle.xpath(expression)](#elementhandlexpathexpression) - [class: Request](#class-request) * [request.abort([errorCode])](#requestaborterrorcode) * [request.continue([overrides])](#requestcontinueoverrides) @@ -531,6 +531,14 @@ const html = await page.$eval('.main-container', e => e.outerHTML); Shortcut for [page.mainFrame().$eval(selector, pageFunction)](#frameevalselector-pagefunction-args). +#### page.$x(expression) +- `expression` <[string]> Expression to [evaluate](https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate). +- returns: <[Promise]<[Array]<[ElementHandle]>>> + +The method evluates the XPath expression. + +Shortcut for [page.mainFrame().$x(expression)](#frameexpression) + #### page.addScriptTag(options) - `options` <[Object]> - `url` <[string]> Url of a script to be added. @@ -1226,13 +1234,6 @@ puppeteer.launch().then(async browser => { ``` Shortcut for [page.mainFrame().waitForSelector(selector[, options])](#framewaitforselectorselector-options). -#### page.xpath(expression) -- `expression` <[string]> Expression to [evaluate](https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate). -- returns: <[Promise]> Promise which resolves to ElementHandle pointing to the page element. - -The method evluates the XPath expression. If there's no such element within the page, the method will resolve to `null`. - -Shortcut for [page.mainFrame().xpath(expression)](#framexpathexpression) ### class: Keyboard @@ -1518,6 +1519,12 @@ const preloadHref = await frame.$eval('link[rel=preload]', el => el.href); const html = await frame.$eval('.main-container', e => e.outerHTML); ``` +#### frame.$x(expression) +- `expression` <[string]> Expression to [evaluate](https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate). +- returns: <[Promise]<[Array]<[ElementHandle]>>> + +The method evluates the XPath expression. + #### frame.addScriptTag(options) - `options` <[Object]> - `url` <[string]> Url of a script to be added. @@ -1682,12 +1689,6 @@ puppeteer.launch().then(async browser => { }); ``` -#### frame.xpath(expression) -- `expression` <[string]> Expression to [evaluate](https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate). -- returns: <[Promise]> Promise which resolves to ElementHandle pointing to the frame element. - -The method evluates the XPath expression. If there's no such element within the frame, the method will resolve to `null`. - ### class: ExecutionContext The class represents a context for JavaScript execution. Examples of JavaScript contexts are: @@ -1860,6 +1861,12 @@ The method runs `element.querySelector` within the page. If no element matches t The method runs `element.querySelectorAll` within the page. If no elements match the selector, the return value resolve to `[]`. +#### elementHandle.$x(expression) +- `expression` <[string]> Expression to [evaluate](https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate). +- returns: <[Promise]> Promise which resolves to ElementHandle pointing to the frame element. + +The method evluates the XPath expression relative to the elementHandle. If there's no such element, the method will resolve to `null`. + #### elementHandle.asElement() - returns: <[elementhandle]> @@ -1988,12 +1995,6 @@ await elementHandle.press('Enter'); This method expects `elementHandle` to point to an [input element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). -#### elementHandle.xpath(expression) -- `expression` <[string]> Expression to [evaluate](https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate). -- returns: <[Promise]> Promise which resolves to ElementHandle pointing to the frame element. - -The method evluates the XPath expression relative to the elementHandle. If there's no such element, the method will resolve to `null`. - ### class: Request Whenever the page sends a request, the following events are emitted by puppeteer's page: diff --git a/lib/ElementHandle.js b/lib/ElementHandle.js index 517d57012b7..40cc8ab4e78 100644 --- a/lib/ElementHandle.js +++ b/lib/ElementHandle.js @@ -195,21 +195,30 @@ class ElementHandle extends JSHandle { /** * @param {string} expression - * @return {!Promise} + * @return {!Promise>} */ - async xpath(expression) { - const handle = await this.executionContext().evaluateHandle( + async $x(expression) { + const arrayHandle = await this.executionContext().evaluateHandle( (element, expression) => { const document = element.ownerDocument || element; - return document.evaluate(expression, element, null, XPathResult.FIRST_ORDERED_NODE_TYPE).singleNodeValue; + const iterator = document.evaluate(expression, element, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE); + const array = []; + let item; + while ((item = iterator.iterateNext())) + array.push(item); + return array; }, this, expression ); - const element = handle.asElement(); - if (element) - return element; - await handle.dispose(); - return null; + const properties = await arrayHandle.getProperties(); + await arrayHandle.dispose(); + const result = []; + for (const property of properties.values()) { + const elementHandle = property.asElement(); + if (elementHandle) + result.push(elementHandle); + } + return result; } } diff --git a/lib/FrameManager.js b/lib/FrameManager.js index 1508e4af22b..1a3822b840f 100644 --- a/lib/FrameManager.js +++ b/lib/FrameManager.js @@ -307,11 +307,11 @@ class Frame { /** * @param {string} expression - * @return {!Promise} + * @return {!Promise>} */ - async xpath(expression) { + async $x(expression) { const document = await this._document(); - const value = await document.xpath(expression); + const value = await document.$x(expression); return value; } diff --git a/lib/Page.js b/lib/Page.js index 82df3cf7300..46019550e25 100644 --- a/lib/Page.js +++ b/lib/Page.js @@ -239,10 +239,10 @@ class Page extends EventEmitter { /** * @param {string} expression - * @return {!Promise} + * @return {!Promise>} */ - async xpath(expression) { - return this.mainFrame().xpath(expression); + async $x(expression) { + return this.mainFrame().$x(expression); } /** diff --git a/test/test.js b/test/test.js index 40a068c6ac6..4c58fdb61a4 100644 --- a/test/test.js +++ b/test/test.js @@ -1727,15 +1727,21 @@ describe('Page', function() { }); }); - describe('Path.xpath', function() { + describe('Path.$x', function() { it('should query existing element', async({page, server}) => { await page.setContent('
test
'); - const element = await page.xpath('/html/body/section'); - expect(element).toBeTruthy(); + const elements = await page.$x('/html/body/section'); + expect(elements[0]).toBeTruthy(); + expect(elements.length).toBe(1); }); - it('should return null for non-existing element', async({page, server}) => { - const element = await page.xpath('/html/body/non-existing-element'); - expect(element).toBe(null); + it('should return empty array for non-existing element', async({page, server}) => { + const element = await page.$x('/html/body/non-existing-element'); + expect(element).toEqual([]); + }); + it('should return multiple elements', async({page, sever}) => { + await page.setContent('
'); + const elements = await page.$x('/html/body/div'); + expect(elements.length).toBe(2); }); }); @@ -1930,22 +1936,22 @@ describe('Page', function() { }); - describe('ElementHandle.xpath', function() { + describe('ElementHandle.$x', function() { it('should query existing element', async({page, server}) => { await page.goto(server.PREFIX + '/playground.html'); await page.setContent('
A
'); const html = await page.$('html'); - const second = await html.xpath(`./body/div[contains(@class, 'second')]`); - const inner = await second.xpath(`./div[contains(@class, 'inner')]`); - const content = await page.evaluate(e => e.textContent, inner); + const second = await html.$x(`./body/div[contains(@class, 'second')]`); + const inner = await second[0].$x(`./div[contains(@class, 'inner')]`); + const content = await page.evaluate(e => e.textContent, inner[0]); expect(content).toBe('A'); }); it('should return null for non-existing element', async({page, server}) => { await page.setContent('
B
'); const html = await page.$('html'); - const second = await html.xpath(`/div[contains(@class, 'third')]`); - expect(second).toBe(null); + const second = await html.$x(`/div[contains(@class, 'third')]`); + expect(second).toEqual([]); }); });