diff --git a/lib/ElementHandle.js b/lib/ElementHandle.js index 2257daf4fc0..e600236dff3 100644 --- a/lib/ElementHandle.js +++ b/lib/ElementHandle.js @@ -58,9 +58,7 @@ class ElementHandle extends JSHandle { */ async _visibleCenter() { await this._scrollIntoViewIfNeeded(); - const box = await this.boundingBox(); - if (!box) - throw new Error('Node is not visible'); + const box = await this._assertBoundingBox(); return { x: box.x + box.width / 2, y: box.y + box.height / 2 @@ -137,24 +135,59 @@ class ElementHandle extends JSHandle { return {x, y, width, height}; } + /** + * @return {!Promise} + */ + async _assertBoundingBox() { + const boundingBox = await this.boundingBox(); + if (boundingBox) + return boundingBox; + + throw new Error('Node is either not visible or not an HTMLElement'); + } + /** * * @param {!Object=} options * @returns {!Promise} */ async screenshot(options = {}) { - await this._scrollIntoViewIfNeeded(); + let needsViewportReset = false; + + let boundingBox = await this._assertBoundingBox(); + + const viewport = this._page.viewport(); + + if (boundingBox.width > viewport.width || boundingBox.height > viewport.height) { + const newViewport = { + width: Math.max(viewport.width, Math.ceil(boundingBox.width)), + height: Math.max(viewport.height, Math.ceil(boundingBox.height)), + }; + await this._page.setViewport(Object.assign({}, viewport, newViewport)); + + needsViewportReset = true; + } + + await this.executionContext().evaluate(function(element) { + element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'}); + }, this); + + boundingBox = await this._assertBoundingBox(); + const { layoutViewport: { pageX, pageY } } = await this._client.send('Page.getLayoutMetrics'); - const boundingBox = await this.boundingBox(); - if (!boundingBox) - throw new Error('Node is not visible'); const clip = Object.assign({}, boundingBox); clip.x += pageX; clip.y += pageY; - return await this._page.screenshot(Object.assign({}, { + + const imageData = await this._page.screenshot(Object.assign({}, { clip }, options)); + + if (needsViewportReset) + await this._page.setViewport(viewport); + + return imageData; } /** diff --git a/test/golden/screenshot-element-larger-than-viewport.png b/test/golden/screenshot-element-larger-than-viewport.png new file mode 100644 index 00000000000..4842968bd9c Binary files /dev/null and b/test/golden/screenshot-element-larger-than-viewport.png differ diff --git a/test/golden/screenshot-element-with-scroll-container.png b/test/golden/screenshot-element-with-scroll-container.png new file mode 100644 index 00000000000..c7860b32a28 Binary files /dev/null and b/test/golden/screenshot-element-with-scroll-container.png differ diff --git a/test/test.js b/test/test.js index 00d23cf6ab3..09f424801f7 100644 --- a/test/test.js +++ b/test/test.js @@ -2072,20 +2072,20 @@ describe('Page', function() { const button = await page.$('button'); await page.evaluate(button => button.style.display = 'none', button); const error = await button.click().catch(err => err); - expect(error.message).toBe('Node is not visible'); + expect(error.message).toBe('Node is either not visible or not an HTMLElement'); }); it('should throw for recursively hidden nodes', async({page, server}) => { await page.goto(server.PREFIX + '/input/button.html'); const button = await page.$('button'); await page.evaluate(button => button.parentElement.style.display = 'none', button); const error = await button.click().catch(err => err); - expect(error.message).toBe('Node is not visible'); + expect(error.message).toBe('Node is either not visible or not an HTMLElement'); }); it('should throw for
elements', async({page, server}) => { await page.setContent('hello
goodbye'); const br = await page.$('br'); const error = await br.click().catch(err => err); - expect(error.message).toBe('Node is not visible'); + expect(error.message).toBe('Node is either not visible or not an HTMLElement'); }); }); @@ -2124,6 +2124,107 @@ describe('Page', function() { const screenshot = await elementHandle.screenshot(); expect(screenshot).toBeGolden('screenshot-element-padding-border.png'); }); + it('should capture full element when larger than viewport', async({page, server}) => { + // compare with .to-screenshot size + await page.setViewport({width: 500, height: 500}); + + await page.setContent(` + something above + +
+
+
+ `); + + await page.evaluate(function() { + window.scrollTo(11, 12); + }); + + const elementHandle = await page.$('div.to-screenshot'); + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden('screenshot-element-larger-than-viewport.png'); + + expect(await page.evaluate(function() { + return { w: window.innerWidth, h: window.innerHeight }; + })).toEqual({ w: 500, h: 500 }); + }); + it('should screenshot element with scroll container', async({page, server}) => { + // compare with .to-screenshot size + await page.setViewport({width: 500, height: 500}); + + await page.setContent(` + something above + +
+
+
+
+
+
+
+ `); + + await page.evaluate(function() { + window.scrollTo(11, 12); + }); + + await page.$eval('div.container1', function(element) { + element.scrollTo(100, 0); + }); + + await page.$eval('div.container2', function(element) { + element.scrollTo(10, 30); + }); + + const elementHandle = await page.$('div.to-screenshot'); + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden('screenshot-element-with-scroll-container.png'); + }); it('should scroll element into view', async({page, server}) => { await page.setViewport({width: 500, height: 500}); await page.setContent(` @@ -2165,7 +2266,7 @@ describe('Page', function() { const elementHandle = await page.$('h1'); await page.evaluate(element => element.remove(), elementHandle); const screenshotError = await elementHandle.screenshot().catch(error => error); - expect(screenshotError.message).toBe('Node is detached from document'); + expect(screenshotError.message).toBe('Node is either not visible or not an HTMLElement'); }); });