diff --git a/.eslintrc b/.eslintrc index 05fd975..49de453 100644 --- a/.eslintrc +++ b/.eslintrc @@ -94,7 +94,13 @@ } ], "simple-import-sort/imports": "warn", - "@vitest/no-focused-tests": "error" + "@vitest/no-focused-tests": "error", + "@vitest/expect-expect": [ + "error", + { + "assertFunctionNames": ["expect", "compareScreenshots"] + } + ] }, "settings": { "react": { diff --git a/test/compare-screenshots.ts b/test/compare-screenshots.ts new file mode 100644 index 0000000..0c5f405 --- /dev/null +++ b/test/compare-screenshots.ts @@ -0,0 +1,49 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { expect } from "vitest"; + +import { ScreenshotWithOffset } from "@cloudscape-design/browser-test-tools/page-objects"; +import useBrowser from "@cloudscape-design/browser-test-tools/use-browser"; + +import ScenarioPageObject from "./scenario-page-object"; + +// Default window size to ensure 4-columns layout is used. +const windowSize = { width: 1600, height: 800 }; + +export interface ScreenshotTestConfiguration { + width?: number; + height?: number; + skipBrowsers?: Array; + retries?: number; +} + +interface PermutationScreenshot extends ScreenshotWithOffset { + id: string; +} + +export type TestCallback = ( + page: ScenarioPageObject, +) => Promise; + +export default function compareScreenshots(...args: [ScreenshotTestConfiguration, TestCallback] | [TestCallback]) { + let testFn: TestCallback; + let configuration: ScreenshotTestConfiguration; + if (args.length === 1) { + configuration = {}; + testFn = args[0]; + } else { + configuration = args[0]; + testFn = args[1]; + } + + return useBrowser({ ...windowSize, ...configuration }, async (browser) => { + const page = new ScenarioPageObject(browser); + const result = await testFn(page); + + if (result && "image" in result) { + const imageBuffer = result.image; + + expect(imageBuffer).toMatchImageSnapshot(); + } + })(); +} diff --git a/test/scenario-page-object.ts b/test/scenario-page-object.ts new file mode 100644 index 0000000..f3e0b29 --- /dev/null +++ b/test/scenario-page-object.ts @@ -0,0 +1,45 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { ScreenshotPageObject, ScreenshotWithOffset } from "@cloudscape-design/browser-test-tools/page-objects"; + +const screenshotAreaSelector = ".screenshot-area"; + +export default class ScenarioPageObject extends ScreenshotPageObject { + private baseUrl?: URL; + + constructor( + browser: ConstructorParameters[0], + public readonly forceScrollAndMerge: boolean = false, + ) { + super(browser); + } + + captureScreenshotArea(): Promise { + return this.captureBySelector(screenshotAreaSelector); + } + + async openScenario(scenarioName: string, queryParams?: Record) { + await this.openIntegrationTestPage(scenarioName, queryParams); + } + + async openIntegrationTestPage(pageName: string, queryParams: Record = {}) { + if (pageName.includes("?")) { + throw new Error( + `Scenario name "${pageName}" may not contain query parameters. To pass extra params use the second argument.`, + ); + } + const params = new URLSearchParams(queryParams); + params.append("motionDisabled", "true"); + params.append("screenshotMode", "true"); + const pagePath = `/#/${pageName}?${params.toString()}`; + await this.browser.url(pagePath); + await this.waitForVisible("main"); + await this.waitForJsTimers(100); + } + + async openPage(pagePath: string) { + const pageUrl = new URL(pagePath, this.baseUrl).toString(); + await this.browser.url(pageUrl); + } +} diff --git a/test/utils.ts b/test/utils.ts deleted file mode 100644 index 4df03d6..0000000 --- a/test/utils.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { BasePageObject } from "@cloudscape-design/browser-test-tools/page-objects"; -import useBrowser from "@cloudscape-design/browser-test-tools/use-browser"; - -// Default window size to ensure 4-columns layout is used. -const windowSize = { width: 1600, height: 800 }; - -export function setupTest

}>( - url: string, - PageClass: new (browser: WebdriverIO.Browser) => P, - test: (page: P) => Promise, -) { - return useBrowser(windowSize, async (browser) => { - await browser.url(`${url}?screenshotMode=true`); - const page = new PageClass(browser); - await page.waitForVisible("main"); - - // Custom initialization. - if (page.init) { - await page.init(); - } - - await test(page); - }); -} diff --git a/test/visual/avatar.test.ts b/test/visual/avatar.test.ts new file mode 100644 index 0000000..66610db --- /dev/null +++ b/test/visual/avatar.test.ts @@ -0,0 +1,21 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { describe, test } from "vitest"; + +import compareScreenshots from "../compare-screenshots"; + +describe("Avatar", () => { + test("custom style", () => + compareScreenshots(async (page) => { + await page.openScenario("avatar/custom-style"); + return page.captureScreenshotArea(); + })); + + test("permutations", () => + compareScreenshots(async (page) => { + await page.openScenario("avatar/permutations"); + // The page does not actually use our permutations component, + // therefore we capture the entire screenshot area at once. + return page.captureScreenshotArea(); + })); +}); diff --git a/test/visual/index.test.ts b/test/visual/index.test.ts index cef09f7..2f7ea17 100644 --- a/test/visual/index.test.ts +++ b/test/visual/index.test.ts @@ -1,11 +1,10 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import path from "path"; -import { expect, test } from "vitest"; +import { test } from "vitest"; -import { ScreenshotPageObject } from "@cloudscape-design/browser-test-tools/page-objects"; - -import { setupTest } from "../utils"; +import compareScreenshots from "../compare-screenshots"; +import ScenarioPageObject from "../scenario-page-object"; const pagesMap = import.meta.glob("../../pages/**/*.page.tsx", { as: "raw" }); const pages = Object.keys(pagesMap) @@ -13,12 +12,12 @@ const pages = Object.keys(pagesMap) .map((page) => "/#/" + path.relative("../../pages/", page)); test.each(pages)("matches snapshot for %s", (route) => - setupTest(route, ScreenshotPageObject, async (page) => { + compareScreenshots(async (page: ScenarioPageObject) => { + await page.openScenario(route); const hasScreenshotArea = await page.isExisting(".screenshot-area"); if (hasScreenshotArea) { - const pngString = await page.fullPageScreenshot(); - expect(pngString).toMatchImageSnapshot(); + return page.captureScreenshotArea(); } - })(), + }), );