Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
50 changes: 50 additions & 0 deletions packages/playwright/e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { argosScreenshot as typedArgosScreenshot } from "./src";
// @ts-ignore
import { argosScreenshot as argosScreenshotCjs } from "./dist/index.cjs";
import { argosAriaSnapshot } from "./dist/index.mjs";
import { readMetadata } from "@argos-ci/util";

test.describe.configure({ mode: "serial" });
const screenshotFolder = "screenshots";
Expand Down Expand Up @@ -276,6 +277,55 @@ test.describe("#argosScreenshot", () => {
});
});

test.describe("without a viewport", () => {
// Like an Electron window: the page is sized by its window. The device
// preset of the project sets a `deviceScaleFactor`, which requires a
// viewport, so the context is created by hand.
let context: BrowserContext;
let page: Page;

test.beforeEach(async ({ browser }) => {
context = await browser.newContext({
viewport: null,
deviceScaleFactor: undefined,
});
page = await context.newPage();
await page.goto(fixture("basic.html"));
});

test.afterEach(async () => {
await context.close();
});

test("reads the viewport from the window", async () => {
const attachments = await argosScreenshot(page, "without-viewport", {
fullPage: false,
});
await expectSnapshotToExists("without-viewport");

const screenshot = attachments.find(
(attachment) => attachment.contentType === "image/png",
);
const metadata = await readMetadata(screenshot!.path);
expect(metadata?.viewport).toEqual(
await page.evaluate(() => ({
width: window.innerWidth,
height: window.innerHeight,
})),
);
});

test("does not support the viewports option", async () => {
await expect(
argosScreenshot(page, "without-viewport-viewports", {
viewports: ["iphone-4"],
}),
).rejects.toThrow(
"viewports option is not supported on a page without a viewport, such as an Electron window",
);
});
});

test.describe("with `waitForBackgroundImages`", () => {
// A 1x1 transparent PNG, served by the route below.
const PNG = Buffer.from(
Expand Down
32 changes: 20 additions & 12 deletions packages/playwright/src/screenshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ import {
checkIsFrame,
checkIsPage,
checkIsUsingArgosReporter,
getPage,
getPathAndMetadata,
getSnapshotNames,
getTestInfo,
getViewportSize,
PNG_EXTENSION,
prepare,
screenshotToSnapshotPath,
Expand Down Expand Up @@ -61,6 +61,7 @@ export type ArgosScreenshotOptions = {

/**
* Viewports to take screenshots of.
* Not supported on pages without a viewport, such as Electron windows.
*/
viewports?: ViewportOption[];

Expand Down Expand Up @@ -228,6 +229,19 @@ export async function argosScreenshot(
if (!name) {
throw new Error("The `name` argument is required.");
}
if (viewports) {
if (checkIsFrame(handler)) {
throw new Error(`viewports option is not supported with an iframe`);
}
// A page without a viewport (Electron window, `viewport: null`) is sized
// by its window. Setting one can't be undone: the page would stop
// following its window and lose its device scale factor.
if (!handler.viewportSize()) {
throw new Error(
`viewports option is not supported on a page without a viewport, such as an Electron window`,
);
}
}

const screenshotTarget =
typeof element === "string"
Expand All @@ -245,10 +259,6 @@ export async function argosScreenshot(

await prepare({ handler, useArgosReporter, root });

const originalViewportSize = checkIsFrame(handler)
? null
: getViewportSize(handler);

const fullPage =
options.fullPage !== undefined
? options.fullPage
Expand Down Expand Up @@ -390,13 +400,12 @@ export async function argosScreenshot(

// If no viewports are specified, take a single screenshot
if (viewports) {
if (checkIsFrame(handler)) {
throw new Error(`viewports option is not supported with an iframe`);
}
const page = getPage(handler);
const originalViewportSize = page.viewportSize();
// Take screenshots for each viewport
for (const viewport of viewports) {
const viewportSize = resolveViewport(viewport);
await setViewportSize(handler, viewportSize);
await setViewportSize(page, viewportSize);
// The viewport suffix is part of the name, so the baselines need it too.
const attachments = await stabilizeAndScreenshot(
getScreenshotName(name, { viewportWidth: viewportSize.width }),
Expand All @@ -408,10 +417,9 @@ export async function argosScreenshot(
}

// Restore the original viewport size
if (!originalViewportSize) {
throw new Error(`Invariant: viewport size must be saved`);
if (originalViewportSize) {
await setViewportSize(page, originalViewportSize);
}
await setViewportSize(handler, originalViewportSize);
} else {
const attachments = await stabilizeAndScreenshot(name, baseNames);
allAttachments.push(...attachments);
Expand Down
62 changes: 61 additions & 1 deletion packages/playwright/src/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { describe, it, expect } from "vitest";
import {
checkIsUsingArgosReporter,
getAutomaticScreenshotName,
getBrowserMetadata,
getSnapshotNames,
} from "./util";
import type { TestCase, TestResult } from "@playwright/test/reporter";
import type { TestInfo } from "@playwright/test";
import type { Browser, Page, TestInfo } from "@playwright/test";

describe("getAutomaticScreenshotName", () => {
const createMockTest = (
Expand Down Expand Up @@ -261,3 +262,62 @@ describe("checkIsUsingArgosReporter", () => {
expect(checkIsUsingArgosReporter(testInfo)).toBe(false);
});
});

const ELECTRON_USER_AGENT =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) my-app/1.0.0 Chrome/140.0.7339.41 Electron/38.2.0 Safari/537.36";

const CHROME_USER_AGENT =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.41 Safari/537.36";

describe("getBrowserMetadata", () => {
/**
* Create a page whose `evaluate` resolves to `userAgent`, as the real
* function only runs in a browser.
*/
const createMockPage = (args: {
browser?: Browser;
userAgent?: string;
}): Page =>
({
context: () => ({ browser: () => args.browser ?? null }),
evaluate: async () => args.userAgent,
}) as unknown as Page;

it("returns the browser of the context", async () => {
const browser = {
browserType: () => ({ name: () => "chromium" }),
version: () => "140.0.7339.41",
} as unknown as Browser;
const page = createMockPage({ browser });
expect(await getBrowserMetadata(page)).toEqual({
name: "chromium",
version: "140.0.7339.41",
});
});

it("detects Electron when the context has no browser", async () => {
const page = createMockPage({ userAgent: ELECTRON_USER_AGENT });
expect(await getBrowserMetadata(page)).toEqual({
name: "electron",
version: "38.2.0",
});
});

it("reads Electron pre-release versions", async () => {
const page = createMockPage({
userAgent: ELECTRON_USER_AGENT.replace(
"Electron/38.2.0",
"Electron/39.0.0-beta.3",
),
});
expect(await getBrowserMetadata(page)).toEqual({
name: "electron",
version: "39.0.0-beta.3",
});
});

it("returns undefined when the browser can't be identified", async () => {
const page = createMockPage({ userAgent: CHROME_USER_AGENT });
expect(await getBrowserMetadata(page)).toBeUndefined();
});
});
78 changes: 49 additions & 29 deletions packages/playwright/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,36 @@ export function getPage(handler: Page | Frame): Page {
/**
* Get the viewport size.
*/
export function getViewportSize(page: Page) {
export async function getViewportSize(page: Page): Promise<ViewportSize> {
const viewportSize = page.viewportSize();
if (!viewportSize) {
throw new Error("Snapshots can't be taken without a viewport.");
if (viewportSize) {
return viewportSize;
}
return viewportSize;
// No emulated viewport (Electron windows, `viewport: null`): the page is
// sized by its window.
return page.evaluate(() => ({
width: window.innerWidth,
height: window.innerHeight,
}));
}

/**
* Get the browser running the page, or `undefined` if it can't be identified.
*/
export async function getBrowserMetadata(
page: Page,
): Promise<ScreenshotMetadata["browser"]> {
const browser = page.context().browser();
if (browser) {
return { name: browser.browserType().name(), version: browser.version() };
}
// Electron contexts have no browser, but Electron adds its version to the
// default user agent. An app that overrides it stays unidentified.
const userAgent = await page.evaluate(() => navigator.userAgent);
const electronVersion = userAgent.match(/\bElectron\/(\S+)/)?.[1];
return electronVersion
? { name: "electron", version: electronVersion }
: undefined;
}

type SnapshotNames = {
Expand Down Expand Up @@ -307,28 +331,27 @@ export async function getPathAndMetadata(args: {

const dir = dirname(path);

const [colorScheme, mediaType, libMetadata, testMetadata] = await Promise.all(
[
handler.evaluate(() =>
((window as any).__ARGOS__ as ArgosGlobal).getColorScheme(),
),
handler.evaluate(() =>
((window as any).__ARGOS__ as ArgosGlobal).getMediaType(),
),
getLibraryMetadata(),
getTestMetadata(testInfo),
dir !== root ? mkdir(dir, { recursive: true }) : null,
],
);

const viewportSize = checkIsFrame(handler) ? null : getViewportSize(handler);
const [
colorScheme,
mediaType,
libMetadata,
testMetadata,
viewportSize,
browser,
] = await Promise.all([
handler.evaluate(() =>
((window as any).__ARGOS__ as ArgosGlobal).getColorScheme(),
),
handler.evaluate(() =>
((window as any).__ARGOS__ as ArgosGlobal).getMediaType(),
),
getLibraryMetadata(),
getTestMetadata(testInfo),
checkIsFrame(handler) ? null : getViewportSize(handler),
getBrowserMetadata(getPage(handler)),
dir !== root ? mkdir(dir, { recursive: true }) : null,
]);

const browser = getPage(handler).context().browser();
if (!browser) {
throw new Error("Can't take screenshots without a browser.");
}
const browserName = browser.browserType().name();
const browserVersion = browser.version();
const url = overrides?.url ?? handler.url();

const metadata: ScreenshotMetadata = {
Expand All @@ -337,10 +360,7 @@ export async function getPathAndMetadata(args: {
mediaType,
test: testMetadata,
story: overrides?.story,
browser: {
name: browserName,
version: browserVersion,
},
browser,
...libMetadata,
};

Expand Down
Loading