New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: elHandle:screenshot captures full element #1787

Merged
merged 12 commits into from Feb 23, 2018
Copy path View file
@@ -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();

This comment has been minimized.

@brigand

brigand Jan 12, 2018

Contributor

Extracted this out as it's used in several places.

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<?{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
* @returns {!Promise<Object>}
*/
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) {

This comment has been minimized.

@aslushnikov

aslushnikov Feb 12, 2018

Contributor

can we move this functionality into page.screenshot so that it works with clips larger then viewport? Seems reasonable to me.

This comment has been minimized.

@JoelEinbinder

JoelEinbinder Feb 12, 2018

Collaborator

that might be a bit tricky, because resizing the viewport will likely move the bounding box to an unknown location.

This comment has been minimized.

@brigand

brigand Feb 13, 2018

Contributor

I think solving that in page.screenshot should be a separate PR, but resizing the viewport in page.screenshot seems like it would do bad things more often than elementHandle.screenshot does, since at the page level, many things are based on the viewport size. If possible, that should be solved in chromium/protocol.

I'd be good with scrolling in page.screenshot if the clip area requires it, but I'd like to get this PR merged first.

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;
}
/**
Binary file not shown.
Binary file not shown.
Copy path View file
@@ -2035,20 +2035,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 <br> elements', async({page, server}) => {
await page.setContent('hello<br>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');
});
});
@@ -2087,6 +2087,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
<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);
}

This comment has been minimized.

@JoelEinbinder

JoelEinbinder Jan 22, 2018

Collaborator

This will show scrollbars in headful mode. To hide them, add:

::-webkit-scrollbar {
  display: none;
}

This comment has been minimized.

@brigand

brigand Jan 22, 2018

Contributor

Fixed

::-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}) => {
await page.setViewport({width: 500, height: 500});
await page.setContent(`
@@ -2128,7 +2229,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');
});
});
ProTip! Use n and p to navigate between commits in a pull request.