From f24696be625bd81f2caac1a46eff19e251ae930b Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Mon, 18 May 2020 17:58:23 -0700 Subject: [PATCH] feat: add page convenience methods for textContent and getAttribute (#2235) This patch adds: - `page.innerText()` / `frame.innerText()` - `page.innerHTML()` / `frame.innerHTML()` - `page.textContent()` / `frame.textContent()` - `page.getAttribute()` / `frame.getAttribute()` Fixes #2143 --- docs/api.md | 84 ++++++++++++++++++++++++++++++++++++-- src/dom.ts | 6 ++- src/frames.ts | 20 +++++++++ src/page.ts | 16 ++++++++ test/elementhandle.spec.js | 4 ++ test/page.spec.js | 1 + 6 files changed, 126 insertions(+), 5 deletions(-) diff --git a/docs/api.md b/docs/api.md index bc71f59036..5330ea12d0 100644 --- a/docs/api.md +++ b/docs/api.md @@ -719,10 +719,13 @@ page.removeListener('request', logRequest); - [page.focus(selector[, options])](#pagefocusselector-options) - [page.frame(options)](#pageframeoptions) - [page.frames()](#pageframes) +- [page.getAttribute(selector, name[, options])](#pagegetattributeselector-name-options) - [page.goBack([options])](#pagegobackoptions) - [page.goForward([options])](#pagegoforwardoptions) - [page.goto(url[, options])](#pagegotourl-options) - [page.hover(selector[, options])](#pagehoverselector-options) +- [page.innerHTML(selector[, options])](#pageinnerhtmlselector-options) +- [page.innerText(selector[, options])](#pageinnertextselector-options) - [page.isClosed()](#pageisclosed) - [page.keyboard](#pagekeyboard) - [page.mainFrame()](#pagemainframe) @@ -740,6 +743,7 @@ page.removeListener('request', logRequest); - [page.setExtraHTTPHeaders(headers)](#pagesetextrahttpheadersheaders) - [page.setInputFiles(selector, files[, options])](#pagesetinputfilesselector-files-options) - [page.setViewportSize(viewportSize)](#pagesetviewportsizeviewportsize) +- [page.textContent(selector[, options])](#pagetextcontentselector-options) - [page.title()](#pagetitle) - [page.type(selector, text[, options])](#pagetypeselector-text-options) - [page.uncheck(selector, [options])](#pageuncheckselector-options) @@ -1349,6 +1353,15 @@ Returns frame matching the specified criteria. Either `name` or `url` must be sp #### page.frames() - returns: <[Array]<[Frame]>> An array of all frames attached to the page. +#### page.getAttribute(selector, name[, options]) +- `selector` <[string]> A selector to search for an element. If there are multiple elements satisfying the selector, the first will be picked. See [working with selectors](#working-with-selectors) for more details. +- `name` <[string]> Attribute name to get the value for. +- `options` <[Object]> + - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. +- returns: <[Promise]> + +Returns element attribute value. + #### page.goBack([options]) - `options` <[Object]> Navigation parameters which might have the following properties: - `timeout` <[number]> Maximum navigation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultNavigationTimeout(timeout)](#browsercontextsetdefaultnavigationtimeouttimeout), [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout), [page.setDefaultNavigationTimeout(timeout)](#pagesetdefaultnavigationtimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. @@ -1419,6 +1432,22 @@ If there's no element matching `selector`, the method waits until a matching ele Shortcut for [page.mainFrame().hover(selector[, options])](#framehoverselector-options). +#### page.innerHTML(selector[, options]) +- `selector` <[string]> A selector to search for an element. If there are multiple elements satisfying the selector, the first will be picked. See [working with selectors](#working-with-selectors) for more details. +- `options` <[Object]> + - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. +- returns: <[Promise]<[string]>> + +Resolves to the `element.innerHTML`. + +#### page.innerText(selector[, options]) +- `selector` <[string]> A selector to search for an element. If there are multiple elements satisfying the selector, the first will be picked. See [working with selectors](#working-with-selectors) for more details. +- `options` <[Object]> + - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. +- returns: <[Promise]<[string]>> + +Resolves to the `element.innerText`. + #### page.isClosed() - returns: <[boolean]> @@ -1703,11 +1732,22 @@ await page.setViewportSize({ await page.goto('https://example.com'); ``` +#### page.textContent(selector[, options]) +- `selector` <[string]> A selector to search for an element. If there are multiple elements satisfying the selector, the first will be picked. See [working with selectors](#working-with-selectors) for more details. +- `options` <[Object]> + - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. +- returns: <[Promise]> + +Resolves to the `element.textContent`. + + #### page.title() - returns: <[Promise]<[string]>> The page's title. Shortcut for [page.mainFrame().title()](#frametitle). + + #### page.type(selector, text[, options]) - `selector` <[string]> A selector of an element to type into. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](#working-with-selectors) for more details. - `text` <[string]> A text to type into a focused element. @@ -1991,8 +2031,11 @@ console.log(text); - [frame.fill(selector, value[, options])](#framefillselector-value-options) - [frame.focus(selector[, options])](#framefocusselector-options) - [frame.frameElement()](#frameframeelement) +- [frame.getAttribute(selector, name[, options])](#framegetattributeselector-name-options) - [frame.goto(url[, options])](#framegotourl-options) - [frame.hover(selector[, options])](#framehoverselector-options) +- [frame.innerHTML(selector[, options])](#frameinnerhtmlselector-options) +- [frame.innerText(selector[, options])](#frameinnertextselector-options) - [frame.isDetached()](#frameisdetached) - [frame.name()](#framename) - [frame.parentFrame()](#frameparentframe) @@ -2000,6 +2043,7 @@ console.log(text); - [frame.selectOption(selector, values[, options])](#frameselectoptionselector-values-options) - [frame.setContent(html[, options])](#framesetcontenthtml-options) - [frame.setInputFiles(selector, files[, options])](#framesetinputfilesselector-files-options) +- [frame.textContent(selector[, options])](#frametextcontentselector-options) - [frame.title()](#frametitle) - [frame.type(selector, text[, options])](#frametypeselector-text-options) - [frame.uncheck(selector, [options])](#frameuncheckselector-options) @@ -2269,6 +2313,15 @@ const contentFrame = await frameElement.contentFrame(); console.log(frame === contentFrame); // -> true ``` +#### frame.getAttribute(selector, name[, options]) +- `selector` <[string]> A selector to search for an element. If there are multiple elements satisfying the selector, the first will be picked. See [working with selectors](#working-with-selectors) for more details. +- `name` <[string]> Attribute name to get the value for. +- `options` <[Object]> + - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. +- returns: <[Promise]> + +Returns element attribute value. + #### frame.goto(url[, options]) - `url` <[string]> URL to navigate frame to. The url should include scheme, e.g. `https://`. - `options` <[Object]> Navigation parameters which might have the following properties: @@ -2312,6 +2365,22 @@ console.log(frame === contentFrame); // -> true This method fetches an element with `selector`, scrolls it into view if needed, and then uses [page.mouse](#pagemouse) to hover over the center of the element. If there's no element matching `selector`, the method waits until a matching element appears in the DOM. If the element is detached during the actionability checks, the action is retried. +#### frame.innerHTML(selector[, options]) +- `selector` <[string]> A selector to search for an element. If there are multiple elements satisfying the selector, the first will be picked. See [working with selectors](#working-with-selectors) for more details. +- `options` <[Object]> + - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. +- returns: <[Promise]<[string]>> + +Resolves to the `element.innerHTML`. + +#### frame.innerText(selector[, options]) +- `selector` <[string]> A selector to search for an element. If there are multiple elements satisfying the selector, the first will be picked. See [working with selectors](#working-with-selectors) for more details. +- `options` <[Object]> + - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. +- returns: <[Promise]<[string]>> + +Resolves to the `element.innerText`. + #### frame.isDetached() - returns: <[boolean]> @@ -2400,6 +2469,15 @@ This method expects `selector` to point to an [input element](https://developer. Sets the value of the file input to these file paths or files. If some of the `filePaths` are relative paths, then they are resolved relative to the [current working directory](https://nodejs.org/api/process.html#process_process_cwd). For empty array, clears the selected files. +#### frame.textContent(selector[, options]) +- `selector` <[string]> A selector to search for an element. If there are multiple elements satisfying the selector, the first will be picked. See [working with selectors](#working-with-selectors) for more details. +- `options` <[Object]> + - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. +- returns: <[Promise]> + +Resolves to the `element.textContent`. + + #### frame.title() - returns: <[Promise]<[string]>> The page's title. @@ -2776,7 +2854,7 @@ Calls [focus](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus #### elementHandle.getAttribute(name) - `name` <[string]> Attribute name to get the value for. -- returns: <[Promise]> Resolves to the attribute value. +- returns: <[Promise]> Returns element attribute value. @@ -2798,10 +2876,10 @@ This method scrolls element into view if needed, and then uses [page.mouse](#pag If the element is detached from DOM, the method throws an error. #### elementHandle.innerHTML() -- returns: <[Promise]> Resolves to the `element.innerHTML`. +- returns: <[Promise]<[string]>> Resolves to the `element.innerHTML`. #### elementHandle.innerText() -- returns: <[Promise]> Resolves to the `element.innerText`. +- returns: <[Promise]<[string]>> Resolves to the `element.innerText`. #### elementHandle.ownerFrame() - returns: <[Promise]> Returns the frame containing the given element. diff --git a/src/dom.ts b/src/dom.ts index 745c352131..a1983dea55 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -146,16 +146,18 @@ export class ElementHandle extends js.JSHandle { return this._evaluateInUtility(({node}) => node.textContent, {}); } - async innerText(): Promise { + async innerText(): Promise { return this._evaluateInUtility(({node}) => { if (node.nodeType !== Node.ELEMENT_NODE) throw new Error('Not an element'); + if (node.namespaceURI !== 'http://www.w3.org/1999/xhtml') + throw new Error('Not an HTMLElement'); const element = node as unknown as HTMLElement; return element.innerText; }, {}); } - async innerHTML(): Promise { + async innerHTML(): Promise { return this._evaluateInUtility(({node}) => { if (node.nodeType !== Node.ELEMENT_NODE) throw new Error('Not an element'); diff --git a/src/frames.ts b/src/frames.ts index 6dc08fda0e..01b7479b93 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -738,6 +738,26 @@ export class Frame { (handle, deadline) => handle.focus()); } + async textContent(selector: string, options: types.TimeoutOptions = {}): Promise { + return await this._retryWithSelectorIfNotConnected('textContent', selector, options, + (handle, deadline) => handle.textContent()); + } + + async innerText(selector: string, options: types.TimeoutOptions = {}): Promise { + return await this._retryWithSelectorIfNotConnected('innerText', selector, options, + (handle, deadline) => handle.innerText()); + } + + async innerHTML(selector: string, options: types.TimeoutOptions = {}): Promise { + return await this._retryWithSelectorIfNotConnected('innerHTML', selector, options, + (handle, deadline) => handle.innerHTML()); + } + + async getAttribute(selector: string, name: string, options: types.TimeoutOptions = {}): Promise { + return await this._retryWithSelectorIfNotConnected('getAttribute', selector, options, + (handle, deadline) => handle.getAttribute(name) as Promise); + } + async hover(selector: string, options: dom.PointerActionOptions & types.PointerActionWaitOptions = {}) { await this._retryWithSelectorIfNotConnected('hover', selector, options, (handle, deadline) => handle.hover(helper.optionsWithUpdatedTimeout(options, deadline))); diff --git a/src/page.ts b/src/page.ts index 50cd7e1b98..048856bd45 100644 --- a/src/page.ts +++ b/src/page.ts @@ -456,6 +456,22 @@ export class Page extends ExtendedEventEmitter implements InnerLogger { return this.mainFrame().focus(selector, options); } + async textContent(selector: string, options?: types.TimeoutOptions): Promise { + return this.mainFrame().textContent(selector, options); + } + + async innerText(selector: string, options?: types.TimeoutOptions): Promise { + return this.mainFrame().innerText(selector, options); + } + + async innerHTML(selector: string, options?: types.TimeoutOptions): Promise { + return this.mainFrame().innerHTML(selector, options); + } + + async getAttribute(selector: string, name: string, options?: types.TimeoutOptions): Promise { + return this.mainFrame().getAttribute(selector, name, options); + } + async hover(selector: string, options?: dom.PointerActionOptions & types.PointerActionWaitOptions) { return this.mainFrame().hover(selector, options); } diff --git a/test/elementhandle.spec.js b/test/elementhandle.spec.js index 91033e8103..eb1a2f00ea 100644 --- a/test/elementhandle.spec.js +++ b/test/elementhandle.spec.js @@ -366,20 +366,24 @@ describe('ElementHandle convenience API', function() { await page.goto(`${server.PREFIX}/dom.html`); const handle = await page.$('#outer'); expect(await handle.getAttribute('name')).toBe('value'); + expect(await page.getAttribute('#outer', 'name')).toBe('value'); }); it('innerHTML should work', async({page, server}) => { await page.goto(`${server.PREFIX}/dom.html`); const handle = await page.$('#outer'); expect(await handle.innerHTML()).toBe('
Text,\nmore text
'); + expect(await page.innerHTML('#outer')).toBe('
Text,\nmore text
'); }); it('innerText should work', async({page, server}) => { await page.goto(`${server.PREFIX}/dom.html`); const handle = await page.$('#inner'); expect(await handle.innerText()).toBe('Text, more text'); + expect(await page.innerText('#inner')).toBe('Text, more text'); }); it('textContent should work', async({page, server}) => { await page.goto(`${server.PREFIX}/dom.html`); const handle = await page.$('#inner'); expect(await handle.textContent()).toBe('Text,\nmore text'); + expect(await page.textContent('#inner')).toBe('Text,\nmore text'); }); }); diff --git a/test/page.spec.js b/test/page.spec.js index 63a1068a23..1cc69dc4a1 100644 --- a/test/page.spec.js +++ b/test/page.spec.js @@ -1203,3 +1203,4 @@ describe('Page api coverage', function() { expect(await frame.evaluate(() => document.querySelector('textarea').value)).toBe('a'); }); }); +