mirror of
https://github.com/microsoft/playwright.git
synced 2024-11-24 06:49:04 +03:00
feat: introduce disableAnimations
option for screenshots (#11870)
This option stops all kinds of CSS animations while doing screenshot: - CSS animations - CSS transitions - Web Animations Animations get different treatment depending on animation duration: - finite animations are fast-forwarded to its end, issuing the `transitionend` event. - Infinite animations are resetted to its beginning, and then resumed after the screenshot. References #9938, fixes #11912
This commit is contained in:
parent
48cc41f3e7
commit
6f87955243
@ -642,6 +642,14 @@ The quality of the image, between 0-100. Not applicable to `png` images.
|
||||
Hides default white background and allows capturing screenshots with transparency. Not applicable to `jpeg` images.
|
||||
Defaults to `false`.
|
||||
|
||||
### option: ElementHandle.screenshot.disableAnimations
|
||||
- `disableAnimations` <[boolean]>
|
||||
|
||||
When true, stops CSS animations, CSS transitions and Web Animations. Animations get different treatment depending on their duration:
|
||||
- finite animations are fast-forwarded to completion, so they'll fire `transitionend` event.
|
||||
- infinite animations are canceled to initial state, and then played over after the screenshot.
|
||||
|
||||
|
||||
### option: ElementHandle.screenshot.timeout = %%-input-timeout-%%
|
||||
|
||||
## async method: ElementHandle.scrollIntoViewIfNeeded
|
||||
|
@ -620,6 +620,13 @@ The quality of the image, between 0-100. Not applicable to `png` images.
|
||||
Hides default white background and allows capturing screenshots with transparency. Not applicable to `jpeg` images.
|
||||
Defaults to `false`.
|
||||
|
||||
### option: Locator.screenshot.disableAnimations
|
||||
- `disableAnimations` <[boolean]>
|
||||
|
||||
When true, stops CSS animations, CSS transitions and Web Animations. Animations get different treatment depending on their duration:
|
||||
- finite animations are fast-forwarded to completion, so they'll fire `transitionend` event.
|
||||
- infinite animations are canceled to initial state, and then played over after the screenshot.
|
||||
|
||||
### option: Locator.screenshot.timeout = %%-input-timeout-%%
|
||||
|
||||
## async method: Locator.scrollIntoViewIfNeeded
|
||||
|
@ -2668,6 +2668,13 @@ The quality of the image, between 0-100. Not applicable to `png` images.
|
||||
When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Defaults to
|
||||
`false`.
|
||||
|
||||
### option: Page.screenshot.disableAnimations
|
||||
- `disableAnimations` <[boolean]>
|
||||
|
||||
When true, stops CSS animations, CSS transitions and Web Animations. Animations get different treatment depending on their duration:
|
||||
- finite animations are fast-forwarded to completion, so they'll fire `transitionend` event.
|
||||
- infinite animations are canceled to initial state, and then played over after the screenshot.
|
||||
|
||||
### option: Page.screenshot.clip
|
||||
- `clip` <[Object]>
|
||||
- `x` <[float]> x-coordinate of top-left corner of clip area
|
||||
|
@ -1475,6 +1475,7 @@ export type PageScreenshotParams = {
|
||||
quality?: number,
|
||||
omitBackground?: boolean,
|
||||
fullPage?: boolean,
|
||||
disableAnimations?: boolean,
|
||||
clip?: Rect,
|
||||
};
|
||||
export type PageScreenshotOptions = {
|
||||
@ -1483,6 +1484,7 @@ export type PageScreenshotOptions = {
|
||||
quality?: number,
|
||||
omitBackground?: boolean,
|
||||
fullPage?: boolean,
|
||||
disableAnimations?: boolean,
|
||||
clip?: Rect,
|
||||
};
|
||||
export type PageScreenshotResult = {
|
||||
@ -2778,12 +2780,14 @@ export type ElementHandleScreenshotParams = {
|
||||
type?: 'png' | 'jpeg',
|
||||
quality?: number,
|
||||
omitBackground?: boolean,
|
||||
disableAnimations?: boolean,
|
||||
};
|
||||
export type ElementHandleScreenshotOptions = {
|
||||
timeout?: number,
|
||||
type?: 'png' | 'jpeg',
|
||||
quality?: number,
|
||||
omitBackground?: boolean,
|
||||
disableAnimations?: boolean,
|
||||
};
|
||||
export type ElementHandleScreenshotResult = {
|
||||
binary: Binary,
|
||||
|
@ -996,6 +996,7 @@ Page:
|
||||
quality: number?
|
||||
omitBackground: boolean?
|
||||
fullPage: boolean?
|
||||
disableAnimations: boolean?
|
||||
clip: Rect?
|
||||
returns:
|
||||
binary: binary
|
||||
@ -2148,6 +2149,7 @@ ElementHandle:
|
||||
- jpeg
|
||||
quality: number?
|
||||
omitBackground: boolean?
|
||||
disableAnimations: boolean?
|
||||
returns:
|
||||
binary: binary
|
||||
|
||||
|
@ -545,6 +545,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||
quality: tOptional(tNumber),
|
||||
omitBackground: tOptional(tBoolean),
|
||||
fullPage: tOptional(tBoolean),
|
||||
disableAnimations: tOptional(tBoolean),
|
||||
clip: tOptional(tType('Rect')),
|
||||
});
|
||||
scheme.PageSetExtraHTTPHeadersParams = tObject({
|
||||
@ -1035,6 +1036,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||
type: tOptional(tEnum(['png', 'jpeg'])),
|
||||
quality: tOptional(tNumber),
|
||||
omitBackground: tOptional(tBoolean),
|
||||
disableAnimations: tOptional(tBoolean),
|
||||
});
|
||||
scheme.ElementHandleScrollIntoViewIfNeededParams = tObject({
|
||||
timeout: tOptional(tNumber),
|
||||
|
@ -69,6 +69,8 @@ export class Screenshotter {
|
||||
const format = validateScreenshotOptions(options);
|
||||
return this._queue.postTask(async () => {
|
||||
const { viewportSize } = await this._originalViewportSize(progress);
|
||||
await this._preparePageForScreenshot(progress, options.disableAnimations || false);
|
||||
progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.
|
||||
|
||||
if (options.fullPage) {
|
||||
const fullPageSize = await this._fullPageSize(progress);
|
||||
@ -78,11 +80,15 @@ export class Screenshotter {
|
||||
documentRect = trimClipToSize(options.clip, documentRect);
|
||||
const buffer = await this._screenshot(progress, format, documentRect, undefined, fitsViewport, options);
|
||||
progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.
|
||||
await this._restorePageAfterScreenshot();
|
||||
return buffer;
|
||||
}
|
||||
|
||||
const viewportRect = options.clip ? trimClipToSize(options.clip, viewportSize) : { x: 0, y: 0, ...viewportSize };
|
||||
return await this._screenshot(progress, format, undefined, viewportRect, true, options);
|
||||
const buffer = await this._screenshot(progress, format, undefined, viewportRect, true, options);
|
||||
progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.
|
||||
await this._restorePageAfterScreenshot();
|
||||
return buffer;
|
||||
});
|
||||
}
|
||||
|
||||
@ -91,6 +97,9 @@ export class Screenshotter {
|
||||
return this._queue.postTask(async () => {
|
||||
const { viewportSize } = await this._originalViewportSize(progress);
|
||||
|
||||
await this._preparePageForScreenshot(progress, options.disableAnimations || false);
|
||||
progress.throwIfAborted(); // Do not do extra work.
|
||||
|
||||
await handle._waitAndScrollIntoViewIfNeeded(progress);
|
||||
|
||||
progress.throwIfAborted(); // Do not do extra work.
|
||||
@ -107,10 +116,100 @@ export class Screenshotter {
|
||||
documentRect.y += scrollOffset.y;
|
||||
const buffer = await this._screenshot(progress, format, helper.enclosingIntRect(documentRect), undefined, fitsViewport, options);
|
||||
progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.
|
||||
await this._restorePageAfterScreenshot();
|
||||
return buffer;
|
||||
});
|
||||
}
|
||||
|
||||
async _preparePageForScreenshot(progress: Progress, disableAnimations: boolean) {
|
||||
await Promise.all(this._page.frames().map(async frame => {
|
||||
await frame.nonStallingEvaluateInExistingContext('(' + (function(disableAnimations: boolean) {
|
||||
const styleTag = document.createElement('style');
|
||||
styleTag.textContent = `
|
||||
*,
|
||||
* > *,
|
||||
* > * > *,
|
||||
* > * > * > *,
|
||||
* > * > * > * > * { caret-color: transparent !important; }
|
||||
`;
|
||||
document.documentElement.append(styleTag);
|
||||
const infiniteAnimationsToResume: Set<Animation> = new Set();
|
||||
const cleanupCallbacks: (() => void)[] = [];
|
||||
|
||||
if (disableAnimations) {
|
||||
const collectRoots = (root: Document | ShadowRoot, roots: (Document|ShadowRoot)[] = []): (Document|ShadowRoot)[] => {
|
||||
roots.push(root);
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
do {
|
||||
const node = walker.currentNode;
|
||||
const shadowRoot = node instanceof Element ? node.shadowRoot : null;
|
||||
if (shadowRoot)
|
||||
collectRoots(shadowRoot, roots);
|
||||
} while (walker.nextNode());
|
||||
return roots;
|
||||
};
|
||||
const handleAnimations = (root: Document|ShadowRoot): void => {
|
||||
for (const animation of root.getAnimations()) {
|
||||
if (!animation.effect || animation.playbackRate === 0 || infiniteAnimationsToResume.has(animation))
|
||||
continue;
|
||||
const endTime = animation.effect.getComputedTiming().endTime;
|
||||
if (Number.isFinite(endTime)) {
|
||||
try {
|
||||
animation.finish();
|
||||
} catch (e) {
|
||||
// animation.finish() should not throw for
|
||||
// finite animations, but we'd like to be on the
|
||||
// safe side.
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
animation.cancel();
|
||||
infiniteAnimationsToResume.add(animation);
|
||||
} catch (e) {
|
||||
// animation.cancel() should not throw for
|
||||
// infinite animations, but we'd like to be on the
|
||||
// safe side.
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const root of collectRoots(document)) {
|
||||
const handleRootAnimations: (() => void) = handleAnimations.bind(null, root);
|
||||
handleRootAnimations();
|
||||
root.addEventListener('transitionrun', handleRootAnimations);
|
||||
root.addEventListener('animationstart', handleRootAnimations);
|
||||
cleanupCallbacks.push(() => {
|
||||
root.removeEventListener('transitionrun', handleRootAnimations);
|
||||
root.removeEventListener('animationstart', handleRootAnimations);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.__cleanupScreenshot = () => {
|
||||
styleTag.remove();
|
||||
for (const animation of infiniteAnimationsToResume) {
|
||||
try {
|
||||
animation.play();
|
||||
} catch (e) {
|
||||
// animation.play() should never throw, but
|
||||
// we'd like to be on the safe side.
|
||||
}
|
||||
}
|
||||
for (const cleanupCallback of cleanupCallbacks)
|
||||
cleanupCallback();
|
||||
delete window.__cleanupScreenshot;
|
||||
};
|
||||
}).toString() + `)(${disableAnimations || false})`, false, 'utility').catch(() => {});
|
||||
}));
|
||||
progress.cleanupWhenAborted(() => this._restorePageAfterScreenshot());
|
||||
}
|
||||
|
||||
async _restorePageAfterScreenshot() {
|
||||
await Promise.all(this._page.frames().map(async frame => {
|
||||
frame.nonStallingEvaluateInExistingContext('window.__cleanupScreenshot && window.__cleanupScreenshot()', false, 'utility').catch(() => {});
|
||||
}));
|
||||
}
|
||||
|
||||
private async _screenshot(progress: Progress, format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, fitsViewport: boolean | undefined, options: types.ElementScreenshotOptions): Promise<Buffer> {
|
||||
if ((options as any).__testHookBeforeScreenshot)
|
||||
await (options as any).__testHookBeforeScreenshot();
|
||||
@ -122,35 +221,8 @@ export class Screenshotter {
|
||||
}
|
||||
progress.throwIfAborted(); // Avoid extra work.
|
||||
|
||||
const restoreBlinkingCaret = async () => {
|
||||
await Promise.all(this._page.frames().map(async frame => {
|
||||
frame.nonStallingEvaluateInExistingContext('window.__cleanupScreenshot && window.__cleanupScreenshot()', false, 'utility').catch(() => {});
|
||||
}));
|
||||
};
|
||||
await Promise.all(this._page.frames().map(async frame => {
|
||||
await frame.nonStallingEvaluateInExistingContext((function() {
|
||||
const styleTag = document.createElement('style');
|
||||
styleTag.textContent = `
|
||||
* { caret-color: transparent !important; }
|
||||
* > * { caret-color: transparent !important; }
|
||||
* > * > * { caret-color: transparent !important; }
|
||||
* > * > * > * { caret-color: transparent !important; }
|
||||
* > * > * > * > * { caret-color: transparent !important; }
|
||||
`;
|
||||
document.documentElement.append(styleTag);
|
||||
window.__cleanupScreenshot = () => {
|
||||
styleTag.remove();
|
||||
delete window.__cleanupScreenshot;
|
||||
};
|
||||
}).toString(), true, 'utility').catch(() => {});
|
||||
}));
|
||||
progress.cleanupWhenAborted(() => restoreBlinkingCaret());
|
||||
progress.throwIfAborted(); // Avoid extra work.
|
||||
|
||||
const buffer = await this._page._delegate.takeScreenshot(progress, format, documentRect, viewportRect, options.quality, fitsViewport);
|
||||
progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.
|
||||
await restoreBlinkingCaret();
|
||||
progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.
|
||||
if (shouldSetDefaultBackground)
|
||||
await this._page._delegate.setBackgroundColor();
|
||||
progress.throwIfAborted(); // Avoid side effects.
|
||||
|
@ -51,6 +51,7 @@ export type ElementScreenshotOptions = TimeoutOptions & {
|
||||
type?: 'png' | 'jpeg',
|
||||
quality?: number,
|
||||
omitBackground?: boolean,
|
||||
disableAnimations?: boolean,
|
||||
};
|
||||
|
||||
export type ScreenshotOptions = ElementScreenshotOptions & {
|
||||
|
18
packages/playwright-core/types/types.d.ts
vendored
18
packages/playwright-core/types/types.d.ts
vendored
@ -8064,6 +8064,12 @@ export interface ElementHandle<T=Node> extends JSHandle<T> {
|
||||
* @param options
|
||||
*/
|
||||
screenshot(options?: {
|
||||
/**
|
||||
* When true, stops CSS animations, CSS transitions and Web Animations. Animations get different treatment depending on
|
||||
* their duration:
|
||||
*/
|
||||
disableAnimations?: boolean;
|
||||
|
||||
/**
|
||||
* Hides default white background and allows capturing screenshots with transparency. Not applicable to `jpeg` images.
|
||||
* Defaults to `false`.
|
||||
@ -9361,6 +9367,12 @@ export interface Locator {
|
||||
* @param options
|
||||
*/
|
||||
screenshot(options?: {
|
||||
/**
|
||||
* When true, stops CSS animations, CSS transitions and Web Animations. Animations get different treatment depending on
|
||||
* their duration:
|
||||
*/
|
||||
disableAnimations?: boolean;
|
||||
|
||||
/**
|
||||
* Hides default white background and allows capturing screenshots with transparency. Not applicable to `jpeg` images.
|
||||
* Defaults to `false`.
|
||||
@ -15698,6 +15710,12 @@ export interface PageScreenshotOptions {
|
||||
height: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* When true, stops CSS animations, CSS transitions and Web Animations. Animations get different treatment depending on
|
||||
* their duration:
|
||||
*/
|
||||
disableAnimations?: boolean;
|
||||
|
||||
/**
|
||||
* When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Defaults to
|
||||
* `false`.
|
||||
|
21
tests/assets/css-transition.html
Normal file
21
tests/assets/css-transition.html
Normal file
@ -0,0 +1,21 @@
|
||||
<!DOCTYPE HTML>
|
||||
<style>
|
||||
div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 200px;
|
||||
height: 100px;
|
||||
background-color: red;
|
||||
transition: all 10s;
|
||||
}
|
||||
.transition {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
</style>
|
||||
<div></div>
|
||||
<script>
|
||||
window.addEventListener('load', () => {
|
||||
document.querySelector('div').classList.add('transition');
|
||||
}, false);
|
||||
</script>
|
32
tests/assets/rotate-pseudo.html
Normal file
32
tests/assets/rotate-pseudo.html
Normal file
@ -0,0 +1,32 @@
|
||||
<html>
|
||||
<head>
|
||||
<style type="text/css">
|
||||
div::after {
|
||||
position: absolute;
|
||||
content: " ";
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 200px;
|
||||
height: 100px;
|
||||
background-color: red;
|
||||
animation-name: z-spin;
|
||||
animation-duration: 5s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
div {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
@keyframes z-spin {
|
||||
0% { transform: rotateZ(0deg); }
|
||||
100% { transform: rotateZ(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body >
|
||||
<div class="square"></div>
|
||||
</body>
|
||||
</html>
|
31
tests/assets/rotate-z-shadow-dom.html
Normal file
31
tests/assets/rotate-z-shadow-dom.html
Normal file
@ -0,0 +1,31 @@
|
||||
<html>
|
||||
<head></head>
|
||||
<body></body>
|
||||
<script>
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const shadow = document.body.attachShadow({mode: 'open'});
|
||||
shadow.append(document.createElement('div'));
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 200px;
|
||||
height: 100px;
|
||||
background-color: red;
|
||||
animation-name: z-spin;
|
||||
animation-duration: 5s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
|
||||
@keyframes z-spin {
|
||||
0% { transform: rotateZ(0deg); }
|
||||
100% { transform: rotateZ(360deg); }
|
||||
}
|
||||
`;
|
||||
shadow.append(style);
|
||||
}, false);
|
||||
</script>
|
||||
</html>
|
@ -13,7 +13,7 @@
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
|
||||
|
||||
@keyframes z-spin {
|
||||
0% { transform: rotateZ(0deg); }
|
||||
100% { transform: rotateZ(360deg); }
|
||||
|
23
tests/assets/web-animation.html
Normal file
23
tests/assets/web-animation.html
Normal file
@ -0,0 +1,23 @@
|
||||
<!DOCTYPE HTML>
|
||||
<style>
|
||||
div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 200px;
|
||||
height: 100px;
|
||||
background-color: red;
|
||||
}
|
||||
</style>
|
||||
<div></div>
|
||||
<script>
|
||||
document.querySelector('div').animate(
|
||||
[
|
||||
{ transform: 'rotate(0deg)' },
|
||||
{ transform: 'rotate(360deg)' }
|
||||
], {
|
||||
duration: 3000,
|
||||
iterations: Infinity
|
||||
}
|
||||
);
|
||||
</script>
|
@ -339,3 +339,279 @@ it.describe('page screenshot', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
async function rafraf(page) {
|
||||
// Do a double raf since single raf does not
|
||||
// actually guarantee a new animation frame.
|
||||
await page.evaluate(() => new Promise(x => {
|
||||
requestAnimationFrame(() => requestAnimationFrame(x));
|
||||
}));
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
animation?: Animation;
|
||||
_EVENTS?: string[];
|
||||
}
|
||||
}
|
||||
|
||||
it.describe('page screenshot animations', () => {
|
||||
it('should not capture infinite css animation', async ({ page, server }) => {
|
||||
await page.goto(server.PREFIX + '/rotate-z.html');
|
||||
const div = page.locator('div');
|
||||
const screenshot = await div.screenshot({
|
||||
disableAnimations: true,
|
||||
});
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
await rafraf(page);
|
||||
const newScreenshot = await div.screenshot({
|
||||
disableAnimations: true,
|
||||
});
|
||||
expect(newScreenshot.equals(screenshot)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not capture pseudo element css animation', async ({ page, server }) => {
|
||||
await page.goto(server.PREFIX + '/rotate-pseudo.html');
|
||||
const div = page.locator('div');
|
||||
const screenshot = await div.screenshot({
|
||||
disableAnimations: true,
|
||||
});
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
await rafraf(page);
|
||||
const newScreenshot = await div.screenshot({
|
||||
disableAnimations: true,
|
||||
});
|
||||
expect(newScreenshot.equals(screenshot)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not capture css animations in shadow DOM', async ({ page, server }) => {
|
||||
await page.goto(server.PREFIX + '/rotate-z-shadow-dom.html');
|
||||
const screenshot = await page.screenshot({
|
||||
disableAnimations: true,
|
||||
});
|
||||
for (let i = 0; i < 4; ++i) {
|
||||
await rafraf(page);
|
||||
const newScreenshot = await page.screenshot({
|
||||
disableAnimations: true,
|
||||
});
|
||||
expect(newScreenshot.equals(screenshot)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should stop animations that happen right before screenshot', async ({ page, server, mode }) => {
|
||||
it.skip(mode !== 'default');
|
||||
await page.goto(server.PREFIX + '/rotate-z.html');
|
||||
// Stop rotating bar.
|
||||
await page.$eval('div', el => el.style.setProperty('animation', 'none'));
|
||||
const buffer1 = await page.screenshot({
|
||||
disableAnimations: true,
|
||||
// Start rotating bar right before screenshot.
|
||||
__testHookBeforeScreenshot: async () => {
|
||||
await page.$eval('div', el => el.style.removeProperty('animation'));
|
||||
},
|
||||
} as any);
|
||||
await rafraf(page);
|
||||
const buffer2 = await page.screenshot({
|
||||
disableAnimations: true,
|
||||
});
|
||||
expect(buffer1.equals(buffer2)).toBe(true);
|
||||
});
|
||||
|
||||
it('should resume infinite animations', async ({ page, server }) => {
|
||||
await page.goto(server.PREFIX + '/rotate-z.html');
|
||||
await page.screenshot({
|
||||
disableAnimations: true,
|
||||
});
|
||||
const buffer1 = await page.screenshot();
|
||||
await rafraf(page);
|
||||
const buffer2 = await page.screenshot();
|
||||
expect(buffer1.equals(buffer2)).toBe(false);
|
||||
});
|
||||
|
||||
it('should not capture infinite web animations', async ({ page, server }) => {
|
||||
await page.goto(server.PREFIX + '/web-animation.html');
|
||||
const div = page.locator('div');
|
||||
const screenshot = await div.screenshot({
|
||||
disableAnimations: true,
|
||||
});
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
await rafraf(page);
|
||||
const newScreenshot = await div.screenshot({
|
||||
disableAnimations: true,
|
||||
});
|
||||
expect(newScreenshot.equals(screenshot)).toBe(true);
|
||||
}
|
||||
// Should resume infinite web animation.
|
||||
const buffer1 = await page.screenshot();
|
||||
await rafraf(page);
|
||||
const buffer2 = await page.screenshot();
|
||||
expect(buffer1.equals(buffer2)).toBe(false);
|
||||
});
|
||||
|
||||
it('should fire transitionend for finite transitions', async ({ page, server }) => {
|
||||
await page.goto(server.PREFIX + '/css-transition.html');
|
||||
const div = page.locator('div');
|
||||
await div.evaluate(el => {
|
||||
el.addEventListener('transitionend', () => window['__TRANSITION_END'] = true, false);
|
||||
});
|
||||
|
||||
await it.step('make sure transition is actually running', async () => {
|
||||
const screenshot1 = await page.screenshot();
|
||||
await rafraf(page);
|
||||
const screenshot2 = await page.screenshot();
|
||||
expect(screenshot1.equals(screenshot2)).toBe(false);
|
||||
});
|
||||
|
||||
// Make a screenshot that finishes all finite animations.
|
||||
const screenshot1 = await div.screenshot({
|
||||
disableAnimations: true,
|
||||
});
|
||||
await rafraf(page);
|
||||
// Make sure finite transition is not restarted.
|
||||
const screenshot2 = await div.screenshot();
|
||||
expect(screenshot1.equals(screenshot2)).toBe(true);
|
||||
|
||||
expect(await page.evaluate(() => window['__TRANSITION_END'])).toBe(true);
|
||||
});
|
||||
|
||||
it('should capture screenshots after layoutchanges in transitionend event', async ({ page, server }) => {
|
||||
await page.goto(server.PREFIX + '/css-transition.html');
|
||||
const div = page.locator('div');
|
||||
await div.evaluate(el => {
|
||||
el.addEventListener('transitionend', () => {
|
||||
const time = Date.now();
|
||||
// Block main thread for 200ms, emulating heavy layout.
|
||||
while (Date.now() - time < 200) ;
|
||||
const h1 = document.createElement('h1');
|
||||
h1.textContent = 'woof-woof';
|
||||
document.body.append(h1);
|
||||
}, false);
|
||||
});
|
||||
|
||||
await it.step('make sure transition is actually running', async () => {
|
||||
const screenshot1 = await page.screenshot();
|
||||
await rafraf(page);
|
||||
const screenshot2 = await page.screenshot();
|
||||
expect(screenshot1.equals(screenshot2)).toBe(false);
|
||||
});
|
||||
|
||||
// 1. Make a screenshot that finishes all finite animations
|
||||
// and triggers layout.
|
||||
const screenshot1 = await page.screenshot({
|
||||
disableAnimations: true,
|
||||
});
|
||||
|
||||
// 2. Make a second screenshot after h1 is on screen.
|
||||
await expect(page.locator('h1')).toBeVisible();
|
||||
await expect(page.locator('h1')).toHaveText('woof-woof');
|
||||
const screenshot2 = await page.screenshot();
|
||||
|
||||
// 3. Make sure both screenshots are equal, meaning that
|
||||
// first screenshot actually was taken after transitionend
|
||||
// changed layout.
|
||||
expect(screenshot1.equals(screenshot2)).toBe(true);
|
||||
});
|
||||
|
||||
it('should not change animation with playbackRate equal to 0', async ({ page, server }) => {
|
||||
await page.goto(server.PREFIX + '/rotate-z.html');
|
||||
await page.evaluate(async () => {
|
||||
window.animation = document.getAnimations()[0];
|
||||
window.animation.updatePlaybackRate(0);
|
||||
await window.animation.ready;
|
||||
window.animation.currentTime = 500;
|
||||
});
|
||||
const screenshot1 = await page.screenshot({
|
||||
disableAnimations: true,
|
||||
});
|
||||
await rafraf(page);
|
||||
const screenshot2 = await page.screenshot({
|
||||
disableAnimations: true,
|
||||
});
|
||||
expect(screenshot1.equals(screenshot2)).toBe(true);
|
||||
expect(await page.evaluate(() => ({
|
||||
playbackRate: window.animation.playbackRate,
|
||||
currentTime: window.animation.currentTime,
|
||||
}))).toEqual({
|
||||
playbackRate: 0,
|
||||
currentTime: 500,
|
||||
});
|
||||
});
|
||||
|
||||
it('should trigger particular events for css transitions', async ({ page, server }) => {
|
||||
await page.goto(server.PREFIX + '/css-transition.html');
|
||||
const div = page.locator('div');
|
||||
await div.evaluate(async el => {
|
||||
window._EVENTS = [];
|
||||
el.addEventListener('transitionend', () => {
|
||||
window._EVENTS.push('transitionend');
|
||||
console.log('transitionend');
|
||||
}, false);
|
||||
const animation = el.getAnimations()[0];
|
||||
animation.oncancel = () => window._EVENTS.push('oncancel');
|
||||
animation.onfinish = () => window._EVENTS.push('onfinish');
|
||||
animation.onremove = () => window._EVENTS.push('onremove');
|
||||
await animation.ready;
|
||||
});
|
||||
await Promise.all([
|
||||
page.screenshot({ disableAnimations: true }),
|
||||
page.waitForEvent('console', msg => msg.text() === 'transitionend'),
|
||||
]);
|
||||
expect(await page.evaluate(() => window._EVENTS)).toEqual([
|
||||
'onfinish', 'transitionend'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should trigger particular events for INfinite css animation', async ({ page, server }) => {
|
||||
await page.goto(server.PREFIX + '/rotate-z.html');
|
||||
const div = page.locator('div');
|
||||
await div.evaluate(async el => {
|
||||
window._EVENTS = [];
|
||||
el.addEventListener('animationcancel', () => {
|
||||
window._EVENTS.push('animationcancel');
|
||||
console.log('animationcancel');
|
||||
}, false);
|
||||
const animation = el.getAnimations()[0];
|
||||
animation.oncancel = () => window._EVENTS.push('oncancel');
|
||||
animation.onfinish = () => window._EVENTS.push('onfinish');
|
||||
animation.onremove = () => window._EVENTS.push('onremove');
|
||||
await animation.ready;
|
||||
});
|
||||
await Promise.all([
|
||||
page.screenshot({ disableAnimations: true }),
|
||||
page.waitForEvent('console', msg => msg.text() === 'animationcancel'),
|
||||
]);
|
||||
expect(await page.evaluate(() => window._EVENTS)).toEqual([
|
||||
'oncancel', 'animationcancel'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should trigger particular events for finite css animation', async ({ page, server }) => {
|
||||
await page.goto(server.PREFIX + '/rotate-z.html');
|
||||
const div = page.locator('div');
|
||||
await div.evaluate(async el => {
|
||||
window._EVENTS = [];
|
||||
// Make CSS animation to be finite.
|
||||
el.style.setProperty('animation-iteration-count', '1000');
|
||||
el.addEventListener('animationend', () => {
|
||||
window._EVENTS.push('animationend');
|
||||
console.log('animationend');
|
||||
}, false);
|
||||
const animation = el.getAnimations()[0];
|
||||
animation.oncancel = () => window._EVENTS.push('oncancel');
|
||||
animation.onfinish = () => window._EVENTS.push('onfinish');
|
||||
animation.onremove = () => window._EVENTS.push('onremove');
|
||||
await animation.ready;
|
||||
});
|
||||
// Ensure CSS animation is finite.
|
||||
expect(await div.evaluate(async el => Number.isFinite(el.getAnimations()[0].effect.getComputedTiming().endTime))).toBe(true);
|
||||
await Promise.all([
|
||||
page.screenshot({ disableAnimations: true }),
|
||||
page.waitForEvent('console', msg => msg.text() === 'animationend'),
|
||||
]);
|
||||
expect(await page.evaluate(() => window._EVENTS)).toEqual([
|
||||
'onfinish', 'animationend'
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user