From 7e28dbafb56298346b66e55031c458afdea651b3 Mon Sep 17 00:00:00 2001 From: Eli Sherer Date: Tue, 10 Oct 2017 09:14:09 +0300 Subject: [PATCH] feat(ElementHandle): add EH.boundingBox and EH.screenshot This patch: - adds `ElementHandle.boundingBox()` method to get bounding box of element relative to the page - adds `ElementHandle.screenshot()` method to capture a screenshot of an element --- docs/api.md | 18 ++++++ lib/ElementHandle.js | 52 +++++++++++++++--- lib/ExecutionContext.js | 8 +-- .../screenshot-element-bounding-box.png | Bin 0 -> 461 bytes test/test.js | 29 ++++++++++ 5 files changed, 94 insertions(+), 13 deletions(-) create mode 100644 test/golden/screenshot-element-bounding-box.png diff --git a/docs/api.md b/docs/api.md index f4d43cc6..1dbcd0b9 100644 --- a/docs/api.md +++ b/docs/api.md @@ -137,6 +137,7 @@ + [jsHandle.toString()](#jshandletostring) * [class: ElementHandle](#class-elementhandle) + [elementHandle.asElement()](#elementhandleaselement) + + [elementHandle.boundingBox()](#elementhandleboundingbox) + [elementHandle.click([options])](#elementhandleclickoptions) + [elementHandle.dispose()](#elementhandledispose) + [elementHandle.executionContext()](#elementhandleexecutioncontext) @@ -146,6 +147,7 @@ + [elementHandle.hover()](#elementhandlehover) + [elementHandle.jsonValue()](#elementhandlejsonvalue) + [elementHandle.press(key[, options])](#elementhandlepresskey-options) + + [elementHandle.screenshot([options])](#elementhandlescreenshotoptions) + [elementHandle.tap()](#elementhandletap) + [elementHandle.toString()](#elementhandletostring) + [elementHandle.type(text[, options])](#elementhandletypetext-options) @@ -1535,6 +1537,15 @@ ElementHandle instances can be used as arguments in [`page.$eval()`](#pageevalse #### elementHandle.asElement() - returns: <[ElementHandle]> +#### elementHandle.boundingBox() +- returns: <[Object]> + - x <[number]> the x coordinate of the element in pixels. + - y <[number]> the y coordinate of the element in pixels. + - width <[number]> the width of the element in pixels. + - height <[number]> the height of the element in pixels. + +This method returns the bounding box of the element (relative to the main frame), or `null` if element is detached from dom. + #### elementHandle.click([options]) - `options` <[Object]> - `button` <[string]> `left`, `right`, or `middle`, defaults to `left`. @@ -1603,6 +1614,13 @@ Returns a JSON representation of the object. The JSON is generated by running [` Focuses the element, and then uses [`keyboard.down`](#keyboarddownkey-options) and [`keyboard.up`](#keyboardupkey). +#### elementHandle.screenshot([options]) +- `options` <[Object]> Same options as in [page.screenshot](#pagescreenshotoptions). +- returns: <[Promise]<[Buffer]>> Promise which resolves to buffer with captured screenshot. + +This method scrolls element into view if needed, and then uses [page.screenshot](#pagescreenshotoptions) to take a screenshot of the element. +If the element is detached from DOM, the method throws an error. + #### elementHandle.tap() - returns: <[Promise]> Promise which resolves when the element is successfully tapped. Promise gets rejected if the element is detached from DOM. diff --git a/lib/ElementHandle.js b/lib/ElementHandle.js index dace1b04..dfd4329d 100644 --- a/lib/ElementHandle.js +++ b/lib/ElementHandle.js @@ -40,25 +40,28 @@ class ElementHandle extends JSHandle { return this; } - /** - * @return {!Promise<{x: number, y: number}>} - */ - async _visibleCenter() { + async _scrollIntoViewIfNeeded() { const error = await this.executionContext().evaluate(element => { if (!element.ownerDocument.contains(element)) return 'Node is detached from document'; if (element.nodeType !== Node.ELEMENT_NODE) return 'Node is not of type HTMLElement'; element.scrollIntoViewIfNeeded(); + return false; }, this); if (error) throw new Error(error); - const {model} = await this._client.send('DOM.getBoxModel', { - objectId: this._remoteObject.objectId - }); + } + + /** + * @return {!Promise<{x: number, y: number}>} + */ + async _visibleCenter() { + await this._scrollIntoViewIfNeeded(); + const box = await this.boundingBox(); return { - x: (model.content[0] + model.content[4]) / 2, - y: (model.content[1] + model.content[5]) / 2 + x: box.x + box.width / 2, + y: box.y + box.height / 2 }; } @@ -111,6 +114,37 @@ class ElementHandle extends JSHandle { await this.focus(); await this._page.keyboard.press(key, options); } + + /** + * @return {!Promise} + */ + async boundingBox() { + const boxModel = await this._client.send('DOM.getBoxModel', { + objectId: this._remoteObject.objectId + }); + if (!boxModel || !boxModel.model) + return null; + return { + x: boxModel.model.margin[0], + y: boxModel.model.margin[1], + width: boxModel.model.width, + height: boxModel.model.height + }; + } + + /** + * + * @param {!Object=} options + * @returns {!Promise} + */ + async screenshot(options = {}) { + await this._scrollIntoViewIfNeeded(); + const boundingBox = await this.boundingBox(); + + return await this._page.screenshot(Object.assign({}, { + clip: boundingBox + }, options)); + } } module.exports = ElementHandle; diff --git a/lib/ExecutionContext.js b/lib/ExecutionContext.js index 7753d153..14c801e7 100644 --- a/lib/ExecutionContext.js +++ b/lib/ExecutionContext.js @@ -29,8 +29,8 @@ class ExecutionContext { } /** - * @param {function()|string} pageFunction - * @param {!Array<*>} args + * @param {function(*)|string} pageFunction + * @param {...*} args * @return {!Promise<(!Object|undefined)>} */ async evaluate(pageFunction, ...args) { @@ -41,8 +41,8 @@ class ExecutionContext { } /** - * @param {function()|string} pageFunction - * @param {!Array<*>} args + * @param {function(*)|string} pageFunction + * @param {...*} args * @return {!Promise} */ async evaluateHandle(pageFunction, ...args) { diff --git a/test/golden/screenshot-element-bounding-box.png b/test/golden/screenshot-element-bounding-box.png new file mode 100644 index 0000000000000000000000000000000000000000..32e05bf05b40cb680aef6812924b00af4302469b GIT binary patch literal 461 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2o;fFi!DwaSX|5e0yWRH&dd_@sHnI zyPH`OuZ1+5rYN{f;@Ik<*LZ<{LQ}^Aj+~30zqpme*$=*))zqe4;!)(8kyW%^lY7g7 z=q53DX(mT5sa@*xpZuQJXT9V6-f7L&hc~U^W1hgRVE6d__u@aRPPemhWyzRTM?}^h_zdhT}pNUJM*KKk2oYPOM)*VeUbl^R* xL1(qH{YFzSg window.scrollBy(50, 100)); + const elementHandle = await page.$('.box:nth-of-type(3)'); + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden('screenshot-element-bounding-box.png'); + })); + }); + describe('input', function() { it('should click the button', SX(async function() { await page.goto(PREFIX + '/input/button.html');