/* Copyright 2024-2025 New Vector Ltd. Copyright 2023 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE files in the repository root for full details. */ import { test, expect as baseExpect, type ElementHandle, type ExpectMatcherState, type Locator, type Page, type PageAssertionsToHaveScreenshotOptions, type MatcherReturnType, } from "@playwright/test"; import { sanitizeForFilePath } from "playwright-core/lib/utils"; import { extname } from "node:path"; import { ANNOTATION } from "../stale-screenshot-reporter.js"; // Based on https://github.com/microsoft/playwright/blob/2b77ed4d7aafa85a600caa0b0d101b72c8437eeb/packages/playwright/src/util.ts#L206C8-L210C2 function sanitizeFilePathBeforeExtension(filePath: string): string { const ext = extname(filePath); const base = filePath.substring(0, filePath.length - ext.length); return sanitizeForFilePath(base) + ext; } export interface ToMatchScreenshotOptions extends PageAssertionsToHaveScreenshotOptions { css?: string; } export type Expectations = { toMatchScreenshot: ( this: ExpectMatcherState, receiver: Page | Locator, name: `${string}.png`, options?: ToMatchScreenshotOptions, ) => Promise; }; /** * Provides an upgrade to the `toHaveScreenshot` expectation. * Unfortunately, we can't just extend the existing `toHaveScreenshot` expectation */ export const expect = baseExpect.extend({ async toMatchScreenshot(receiver, name, options) { const testInfo = test.info(); if (!testInfo) throw new Error(`toMatchScreenshot() must be called during the test`); if (!testInfo.tags.includes("@screenshot")) { throw new Error("toMatchScreenshot() must be used in a test tagged with @screenshot"); } const page = "page" in receiver ? receiver.page() : receiver; let style: ElementHandle | undefined; if (options?.css) { // We add a custom style tag before taking screenshots style = (await page.addStyleTag({ content: options.css, })) as ElementHandle; } const screenshotName = sanitizeFilePathBeforeExtension(name); await baseExpect(receiver).toHaveScreenshot(screenshotName, options); await style?.evaluate((tag) => tag.remove()); testInfo.annotations.push({ type: ANNOTATION, description: testInfo.snapshotPath(screenshotName), }); return { pass: true, message: (): string => "", name: "toMatchScreenshot" }; }, });