Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 62 additions & 11 deletions visual-js/visual-playwright/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,20 @@ import {
} from '@saucelabs/visual';
import { backOff } from 'exponential-backoff';
import { SauceVisualParams } from './types';
import { buildSnapshotMetadata, getOpts, parseOpts, setOpts } from './utils';
import {
buildSnapshotMetadata,
getOpts,
parseOpts,
setOpts,
constrainClipToViewport,
} from './utils';

const clientVersion = 'PKG_VERSION';

export class VisualPlaywright {
constructor(public client: string = `visual-playwright/${clientVersion}`) {}
uploadedDiffIds: Record<string, string[]> = {};
originalViewportSize: { width: number; height: number } | null = null;

public get api() {
let api = globalThis.visualApi;
Expand Down Expand Up @@ -131,6 +138,45 @@ ${e instanceof Error ? e.message : JSON.stringify(e)}
}
}

/**
* Resize the viewport to fit the document height and store the original dimensions.
* @param page
* @private
*/
async fitViewport(page: Page) {
const viewportSize = page.viewportSize();
const pageScrollHeight = await page.evaluate(() => {
return Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight,
);
});

if (
pageScrollHeight &&
viewportSize &&
pageScrollHeight > viewportSize.height
) {
this.originalViewportSize = viewportSize;
await page.setViewportSize({
...viewportSize,
height: pageScrollHeight,
});
}
}

/**
* Cleanup any mutations made to the viewport / elements.
* @param page
* @private
*/
async resetViewport(page: Page) {
if (this.originalViewportSize) {
await page.setViewportSize(this.originalViewportSize);
this.originalViewportSize = null;
}
}

public async takePlaywrightScreenshot(
page: Page,
info: {
Expand Down Expand Up @@ -169,22 +215,21 @@ ${e instanceof Error ? e.message : JSON.stringify(e)}
fullPage = true,
style,
timeout,
autoSizeViewport = false,
} = screenshotOptions;
let ignoreRegions: RegionIn[] = [];

const promises: Promise<unknown>[] = [
// Wait for all fonts to be loaded & ready
page.evaluate(() => document.fonts.ready),
];
await page.evaluate(() => document.fonts.ready);

if (autoSizeViewport) {
await this.fitViewport(page);
}

if (delay) {
// If a delay has been configured by the user, append it to our promises
promises.push(new Promise((resolve) => setTimeout(resolve, delay)));
await new Promise((resolve) => setTimeout(resolve, delay));
}

// Await all queued / concurrent promises before resuming
await Promise.all(promises);

const clip = clipSelector
? await page.evaluate(
({ clipSelector }) => {
Expand Down Expand Up @@ -288,15 +333,21 @@ ${e instanceof Error ? e.message : JSON.stringify(e)}

const devicePixelRatio = await page.evaluate(() => window.devicePixelRatio);

const constrainedClip = constrainClipToViewport(clip, page.viewportSize());

const screenshotBuffer = await page.screenshot({
fullPage,
fullPage: autoSizeViewport ? false : fullPage,
style,
timeout,
animations,
caret,
clip,
clip: autoSizeViewport ? constrainedClip : clip,
});

if (autoSizeViewport) {
await this.resetViewport(page);
}

// Inject scripts to get dom snapshot
let dom: string | undefined;
const script = await getDomScript();
Expand Down
2 changes: 1 addition & 1 deletion visual-js/visual-playwright/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface SauceVisualParams {
screenshotOptions?: Pick<
PageScreenshotOptions,
'animations' | 'caret' | 'fullPage' | 'style' | 'timeout'
>;
> & { autoSizeViewport?: boolean };
/**
* Whether we should capture a dom snapshot.
*/
Expand Down
76 changes: 76 additions & 0 deletions visual-js/visual-playwright/src/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { describe, expect, it } from '@jest/globals';
import { constrainClipToViewport } from './utils';

describe('constrainClipToViewport', () => {
it('should constrain an element to the viewport when extends out of bounds', () => {
const result = constrainClipToViewport(
{
x: 50,
y: 50,
width: 100,
height: 100,
},
{ width: 100, height: 100 },
);

expect(result).toEqual({
x: 50,
y: 50,
width: 50,
height: 50,
});
});

it('should constrain an element to the viewport when extends out of bounds negative', () => {
const result = constrainClipToViewport(
{
x: -50,
y: -50,
width: 100,
height: 100,
},
{ width: 100, height: 100 },
);

expect(result).toEqual({
x: 0,
y: 0,
width: 50,
height: 50,
});
});

it('should not mutate an element that is already in bounds', () => {
const result = constrainClipToViewport(
{
x: 10,
y: 10,
width: 50,
height: 50,
},
{ width: 100, height: 100 },
);

expect(result).toEqual({
x: 10,
y: 10,
width: 50,
height: 50,
});
});

it('should not change an element in bounds', () => {
const initial = {
x: 50,
y: 50,
width: 50,
height: 50,
};
const result = constrainClipToViewport(initial, {
width: 100,
height: 100,
});

expect(result).toEqual(initial);
});
});
41 changes: 41 additions & 0 deletions visual-js/visual-playwright/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,44 @@ export const buildSnapshotMetadata = ({
testName: null,
};
};

interface Clip {
width: number;
height: number;
x: number;
y: number;
}

export const constrainClipToViewport = (
clip: Clip | undefined,
viewport: { width: number; height: number } | null,
): Clip | undefined => {
if (!clip || !viewport) {
return;
}

const x = Math.min(Math.max(Math.round(clip.x), 0), viewport.width);
const y = Math.min(Math.max(Math.round(clip.y), 0), viewport.height);
const width = Math.min(
viewport.width - Math.abs(clip.x),
Math.round(clip.width),
);
const height = Math.min(
viewport.height - Math.abs(clip.y),
Math.round(clip.height),
);

if (width === 0 || height === 0) {
console.warn(
'Sauce Visual: Skipping clipping due to requested element existing outside screenshot bounds.',
);
return;
}

return {
x,
y,
width,
height,
};
};
Loading