feat: elHandle:screenshot captures full element (#1787)

feat: make ElementHandle.screenshot work with large elements

This patch increases the viewport size if the element is bigger than
viewport.

Fixes #1779
This commit is contained in:
Frankie Bagnardi 2018-02-23 15:13:08 -07:00 committed by Andrey Lushnikov
parent b07e705dc9
commit 56a475f86b
4 changed files with 146 additions and 12 deletions

View File

@ -58,9 +58,7 @@ class ElementHandle extends JSHandle {
*/ */
async _visibleCenter() { async _visibleCenter() {
await this._scrollIntoViewIfNeeded(); await this._scrollIntoViewIfNeeded();
const box = await this.boundingBox(); const box = await this._assertBoundingBox();
if (!box)
throw new Error('Node is not visible');
return { return {
x: box.x + box.width / 2, x: box.x + box.width / 2,
y: box.y + box.height / 2 y: box.y + box.height / 2
@ -137,24 +135,59 @@ class ElementHandle extends JSHandle {
return {x, y, width, height}; return {x, y, width, height};
} }
/**
* @return {!Promise<?{x: number, y: number, width: number, height: number}>}
*/
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 * @param {!Object=} options
* @returns {!Promise<Object>} * @returns {!Promise<Object>}
*/ */
async screenshot(options = {}) { 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 { 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); const clip = Object.assign({}, boundingBox);
clip.x += pageX; clip.x += pageX;
clip.y += pageY; clip.y += pageY;
return await this._page.screenshot(Object.assign({}, {
const imageData = await this._page.screenshot(Object.assign({}, {
clip clip
}, options)); }, options));
if (needsViewportReset)
await this._page.setViewport(viewport);
return imageData;
} }
/** /**

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -2072,20 +2072,20 @@ describe('Page', function() {
const button = await page.$('button'); const button = await page.$('button');
await page.evaluate(button => button.style.display = 'none', button); await page.evaluate(button => button.style.display = 'none', button);
const error = await button.click().catch(err => err); 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}) => { it('should throw for recursively hidden nodes', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html'); await page.goto(server.PREFIX + '/input/button.html');
const button = await page.$('button'); const button = await page.$('button');
await page.evaluate(button => button.parentElement.style.display = 'none', button); await page.evaluate(button => button.parentElement.style.display = 'none', button);
const error = await button.click().catch(err => err); 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 <br> elements', async({page, server}) => { it('should throw for <br> elements', async({page, server}) => {
await page.setContent('hello<br>goodbye'); await page.setContent('hello<br>goodbye');
const br = await page.$('br'); const br = await page.$('br');
const error = await br.click().catch(err => err); 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(); const screenshot = await elementHandle.screenshot();
expect(screenshot).toBeGolden('screenshot-element-padding-border.png'); 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
<style>div.spacer {
border: 2px solid red;
background: red;
height: 600px;
width: 52px;
}
div.to-screenshot {
border: 2px solid blue;
background: rgba(0, 0, 0, 0.5);
width: 600px;
height: 200.5px;
margin-left: 50px;
transform: scaleY(1.2);
}
::-webkit-scrollbar {
display: none;
}
</style>
<div class="spacer"></div>
<div class="to-screenshot"></div>
<div class="spacer"></div>
`);
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
<style>div.spacer {
border: 2px solid red;
background: red;
height: 600px;
width: 52px;
}
div.container1 {
width: 600px;
height: 600px;
overflow: auto;
}
div.container2 {
width: 620px;
height: 620px;
overflow: auto;
}
div.to-screenshot {
border: 2px solid blue;
background: rgba(0, 0, 0, 0.5);
width: 580px;
height: 580px;
margin-top: 50px;
margin-left: 200px;
margin-right: 50px;
}
::-webkit-scrollbar {
display: none;
}
</style>
<div class="spacer"></div>
<div class="container1">
<div class="container2">
<div class="to-screenshot"></div>
</div>
</div>
<div class="spacer"></div>
`);
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}) => { it('should scroll element into view', async({page, server}) => {
await page.setViewport({width: 500, height: 500}); await page.setViewport({width: 500, height: 500});
await page.setContent(` await page.setContent(`
@ -2165,7 +2266,7 @@ describe('Page', function() {
const elementHandle = await page.$('h1'); const elementHandle = await page.$('h1');
await page.evaluate(element => element.remove(), elementHandle); await page.evaluate(element => element.remove(), elementHandle);
const screenshotError = await elementHandle.screenshot().catch(error => error); 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');
}); });
}); });