fix(screenshot): wait for stable position before taking element screenshot (#3216)
Same goes for scrollIntoViewIfNeeded.
This commit is contained in:
parent
c6180edbfe
commit
9132d23b2b
|
|
@ -11,7 +11,8 @@ Some actions like `page.click()` support `{force: true}` option that disable non
|
||||||
| `check()`<br>`click()`<br>`dblclick()`<br>`hover()`<br>`uncheck()` | [Visible]<br>[Stable]<br>[Enabled]<br>[Receiving Events]<br>[Attached] |
|
| `check()`<br>`click()`<br>`dblclick()`<br>`hover()`<br>`uncheck()` | [Visible]<br>[Stable]<br>[Enabled]<br>[Receiving Events]<br>[Attached] |
|
||||||
| `fill()` | [Visible]<br>[Enabled]<br>[Editable]<br>[Attached] |
|
| `fill()` | [Visible]<br>[Enabled]<br>[Editable]<br>[Attached] |
|
||||||
| `dispatchEvent()`<br>`focus()`<br>`press()`<br>`setInputFiles()`<br>`selectOption()`<br>`type()` | [Attached] |
|
| `dispatchEvent()`<br>`focus()`<br>`press()`<br>`setInputFiles()`<br>`selectOption()`<br>`type()` | [Attached] |
|
||||||
| `selectText()`<br>`scrollIntoViewIfNeeded()`<br>`screenshot()` | [Visible]<br>[Attached] |
|
| `scrollIntoViewIfNeeded()`<br>`screenshot()` | [Visible]<br>[Stable]<br>[Attached] |
|
||||||
|
| `selectText()` | [Visible]<br>[Attached] |
|
||||||
| `getAttribute()`<br>`innerText()`<br>`innerHTML()`<br>`textContent()` | [Attached] |
|
| `getAttribute()`<br>`innerText()`<br>`innerHTML()`<br>`textContent()` | [Attached] |
|
||||||
|
|
||||||
### Visible
|
### Visible
|
||||||
|
|
|
||||||
22
src/dom.ts
22
src/dom.ts
|
|
@ -209,7 +209,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
|
|
||||||
async _waitAndScrollIntoViewIfNeeded(progress: Progress): Promise<void> {
|
async _waitAndScrollIntoViewIfNeeded(progress: Progress): Promise<void> {
|
||||||
while (progress.isRunning()) {
|
while (progress.isRunning()) {
|
||||||
assertDone(throwRetargetableDOMError(await this._waitForVisible(progress)));
|
assertDone(throwRetargetableDOMError(await this._waitForDisplayedAtStablePosition(progress, false /* waitForEnabled */)));
|
||||||
|
|
||||||
progress.throwIfAborted(); // Avoid action that has side-effects.
|
progress.throwIfAborted(); // Avoid action that has side-effects.
|
||||||
const result = throwRetargetableDOMError(await this._scrollRectIntoViewIfNeeded());
|
const result = throwRetargetableDOMError(await this._scrollRectIntoViewIfNeeded());
|
||||||
|
|
@ -321,7 +321,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
if ((options as any).__testHookBeforeStable)
|
if ((options as any).__testHookBeforeStable)
|
||||||
await (options as any).__testHookBeforeStable();
|
await (options as any).__testHookBeforeStable();
|
||||||
if (!force) {
|
if (!force) {
|
||||||
const result = await this._waitForDisplayedAtStablePositionAndEnabled(progress);
|
const result = await this._waitForDisplayedAtStablePosition(progress, true /* waitForEnabled */);
|
||||||
if (result !== 'done')
|
if (result !== 'done')
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
@ -636,15 +636,21 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _waitForDisplayedAtStablePositionAndEnabled(progress: Progress): Promise<'error:notconnected' | 'done'> {
|
async _waitForDisplayedAtStablePosition(progress: Progress, waitForEnabled: boolean): Promise<'error:notconnected' | 'done'> {
|
||||||
progress.logger.info(' waiting for element to be visible, enabled and not moving');
|
if (waitForEnabled)
|
||||||
|
progress.logger.info(` waiting for element to be visible, enabled and not moving`);
|
||||||
|
else
|
||||||
|
progress.logger.info(` waiting for element to be visible and not moving`);
|
||||||
const rafCount = this._page._delegate.rafCountForStablePosition();
|
const rafCount = this._page._delegate.rafCountForStablePosition();
|
||||||
const poll = this._evaluateHandleInUtility(([injected, node, rafCount]) => {
|
const poll = this._evaluateHandleInUtility(([injected, node, { rafCount, waitForEnabled }]) => {
|
||||||
return injected.waitForDisplayedAtStablePositionAndEnabled(node, rafCount);
|
return injected.waitForDisplayedAtStablePosition(node, rafCount, waitForEnabled);
|
||||||
}, rafCount);
|
}, { rafCount, waitForEnabled });
|
||||||
const pollHandler = new InjectedScriptPollHandler(progress, await poll);
|
const pollHandler = new InjectedScriptPollHandler(progress, await poll);
|
||||||
const result = await pollHandler.finish();
|
const result = await pollHandler.finish();
|
||||||
progress.logger.info(' element is visible, enabled and does not move');
|
if (waitForEnabled)
|
||||||
|
progress.logger.info(' element is visible, enabled and does not move');
|
||||||
|
else
|
||||||
|
progress.logger.info(' element is visible and does not move');
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -422,7 +422,7 @@ export default class InjectedScript {
|
||||||
input.dispatchEvent(new Event('change', { 'bubbles': true }));
|
input.dispatchEvent(new Event('change', { 'bubbles': true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForDisplayedAtStablePositionAndEnabled(node: Node, rafCount: number): types.InjectedScriptPoll<'error:notconnected' | 'done'> {
|
waitForDisplayedAtStablePosition(node: Node, rafCount: number, waitForEnabled: boolean): types.InjectedScriptPoll<'error:notconnected' | 'done'> {
|
||||||
let lastRect: types.Rect | undefined;
|
let lastRect: types.Rect | undefined;
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
let samePositionCounter = 0;
|
let samePositionCounter = 0;
|
||||||
|
|
@ -464,7 +464,7 @@ export default class InjectedScript {
|
||||||
const isVisible = !!style && style.visibility !== 'hidden';
|
const isVisible = !!style && style.visibility !== 'hidden';
|
||||||
|
|
||||||
const elementOrButton = element.closest('button, [role=button]') || element;
|
const elementOrButton = element.closest('button, [role=button]') || element;
|
||||||
const isDisabled = ['BUTTON', 'INPUT', 'SELECT'].includes(elementOrButton.nodeName) && elementOrButton.hasAttribute('disabled');
|
const isDisabled = waitForEnabled && ['BUTTON', 'INPUT', 'SELECT'].includes(elementOrButton.nodeName) && elementOrButton.hasAttribute('disabled');
|
||||||
|
|
||||||
if (isDisplayed && isStable && isVisible && !isDisabled)
|
if (isDisplayed && isStable && isVisible && !isDisabled)
|
||||||
return 'done';
|
return 'done';
|
||||||
|
|
|
||||||
|
|
@ -49,4 +49,14 @@ body {
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes move {
|
||||||
|
from { left: -500px; background-color: cyan; }
|
||||||
|
to { left: 0; background-color: rgb(255, 210, 204); }
|
||||||
|
}
|
||||||
|
.box.animation {
|
||||||
|
position: relative;
|
||||||
|
animation: 2s linear 0s move forwards;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -361,6 +361,21 @@ describe('ElementHandle.scrollIntoViewIfNeeded', function() {
|
||||||
await page.setContent('<span style="display:none"><div>Hello</div></span>');
|
await page.setContent('<span style="display:none"><div>Hello</div></span>');
|
||||||
await testWaiting(page, div => div.parentElement.style.display = 'block');
|
await testWaiting(page, div => div.parentElement.style.display = 'block');
|
||||||
});
|
});
|
||||||
|
it('should wait for element to stop moving', async({page, server}) => {
|
||||||
|
await page.setContent(`
|
||||||
|
<style>
|
||||||
|
@keyframes move {
|
||||||
|
from { margin-left: 0; }
|
||||||
|
to { margin-left: 200px; }
|
||||||
|
}
|
||||||
|
div.animated {
|
||||||
|
animation: 2s linear 0s infinite alternate move;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class=animated>moving</div>
|
||||||
|
`);
|
||||||
|
await testWaiting(page, div => div.classList.remove('animated'));
|
||||||
|
});
|
||||||
|
|
||||||
it('should timeout waiting for visible', async({page, server}) => {
|
it('should timeout waiting for visible', async({page, server}) => {
|
||||||
await page.setContent('<div style="display:none">Hello</div>');
|
await page.setContent('<div style="display:none">Hello</div>');
|
||||||
|
|
|
||||||
|
|
@ -537,4 +537,19 @@ describe.skip(ffheadful)('ElementHandle.screenshot', function() {
|
||||||
await utils.verifyViewport(page, 350, 360);
|
await utils.verifyViewport(page, 350, 360);
|
||||||
await context.close();
|
await context.close();
|
||||||
});
|
});
|
||||||
|
it('should wait for element to stop moving', async({page, server}) => {
|
||||||
|
await page.setViewportSize({ width: 500, height: 500 });
|
||||||
|
await page.goto(server.PREFIX + '/grid.html');
|
||||||
|
const elementHandle = await page.$('.box:nth-of-type(3)');
|
||||||
|
await elementHandle.evaluate(e => e.classList.add('animation'));
|
||||||
|
const screenshot = await elementHandle.screenshot();
|
||||||
|
expect(screenshot).toBeGolden('screenshot-element-bounding-box.png');
|
||||||
|
});
|
||||||
|
it('should take screenshot of disabled button', async({page}) => {
|
||||||
|
await page.setViewportSize({ width: 500, height: 500 });
|
||||||
|
await page.setContent(`<button disabled>Click me</button>`);
|
||||||
|
const button = await page.$('button');
|
||||||
|
const screenshot = await button.screenshot();
|
||||||
|
expect(screenshot).toBeInstanceOf(Buffer);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue