diff --git a/.gitignore b/.gitignore index 2fe4f44..10678a2 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,9 @@ Thumbs.db # Vite vite.config.js.timestamp-* vite.config.ts.timestamp-* + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/package-lock.json b/package-lock.json index 84463c0..9e6c8a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,11 @@ "name": "svader-monorepo", "workspaces": [ "packages/*" - ] + ], + "devDependencies": { + "@playwright/test": "^1.48.1", + "@types/node": "^22.7.9" + } }, "node_modules/@ampproject/remapping": { "version": "2.3.0", @@ -467,6 +471,22 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@playwright/test": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.1.tgz", + "integrity": "sha512-s9RtWoxkOLmRJdw3oFvhFbs9OJS0BzrLUc8Hf6l2UdCNd1rqeEyD4BhCJkvzeEoD1FsK4mirsWwGerhVmYKtZg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.48.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.28", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", @@ -822,6 +842,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "22.7.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.9.tgz", + "integrity": "sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, "node_modules/@types/pug": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", @@ -1578,6 +1608,53 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.1.tgz", + "integrity": "sha512-j8CiHW/V6HxmbntOfyB4+T/uk08tBy6ph0MpBXwuoofkSnLmlfdYNNkFTYD6ofzzlSqLA1fwH4vwvVFvJgLN0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.48.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.1.tgz", + "integrity": "sha512-Yw/t4VAFX/bBr1OzwCuOMZkY1Cnb4z/doAFSwf4huqAGWmf9eMNjmK7NiOljCdLmxeRYcGPPmcDgU0zOlzP0YA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.4.47", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", @@ -2077,6 +2154,13 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, "node_modules/vite": { "version": "5.4.10", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz", diff --git a/package.json b/package.json index 4c72a03..7076600 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,11 @@ "check:svader": "cd packages/svader && npm run check", "check:tests-svelte4": "cd packages/tests-svelte4 && npm run check", "build": "npm run package && npm run build:tests-svelte4", - "build:tests-svelte4": "cd packages/tests-svelte4 && npm run build" + "build:tests-svelte4": "cd packages/tests-svelte4 && npm run build", + "test": "playwright test" + }, + "devDependencies": { + "@playwright/test": "^1.48.1", + "@types/node": "^22.7.9" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..1714057 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,89 @@ +import { defineConfig, devices } from '@playwright/test'; + +// Apparently, Chromium browsers need to be explicitly told to use GPU acceleration in headless mode, +// and software rendering creates slightly different results for WebGL in noise and such, +// while WebGPU has no software fallback at all. +const chromiumHardware = { + args: [ + "--enable-gpu", + ], +}; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + snapshotPathTemplate: '{testDir}/.generated-screenshots/{testFilePath}/{arg}{ext}', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + + contextOptions: { + // Disable animations so screenshots are comparable + reducedMotion: 'reduce', + }, + }, + + projects: [ + { + name: 'Firefox', + use: { + ...devices['Desktop Firefox'], + }, + }, + { + name: 'Microsoft Edge', + use: { + ...devices['Desktop Edge'], + channel: 'msedge', + launchOptions: chromiumHardware, + }, + }, + { + name: 'Google Chrome', + use: { + ...devices['Desktop Chrome'], + channel: 'chrome', + launchOptions: chromiumHardware, + }, + }, + /* TODO: Test these as well */ + // { + // name: 'Chromium', + // use: { + // ...devices['Desktop Chrome'], + // launchOptions: chromiumHardware, + // }, + // }, + // { + // name: 'WebKit', + // use: { ...devices['Desktop Safari'] }, + // }, + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + ], + + webServer: { + command: 'npm run preview:v4', + port: 4173, + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/tests/.generated-screenshots/svelte4.spec.ts/hello-world-webgl.png b/tests/.generated-screenshots/svelte4.spec.ts/hello-world-webgl.png new file mode 100644 index 0000000..6db5e52 Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/hello-world-webgl.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/hello-world-webgpu-unsupported.png b/tests/.generated-screenshots/svelte4.spec.ts/hello-world-webgpu-unsupported.png new file mode 100644 index 0000000..4505311 Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/hello-world-webgpu-unsupported.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/hello-world-webgpu.png b/tests/.generated-screenshots/svelte4.spec.ts/hello-world-webgpu.png new file mode 100644 index 0000000..29d944e Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/hello-world-webgpu.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/landing-page-bubbles-webgl.png b/tests/.generated-screenshots/svelte4.spec.ts/landing-page-bubbles-webgl.png new file mode 100644 index 0000000..79e75b4 Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/landing-page-bubbles-webgl.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/landing-page-bubbles-webgpu-unsupported.png b/tests/.generated-screenshots/svelte4.spec.ts/landing-page-bubbles-webgpu-unsupported.png new file mode 100644 index 0000000..d9e73b9 Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/landing-page-bubbles-webgpu-unsupported.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/landing-page-bubbles-webgpu.png b/tests/.generated-screenshots/svelte4.spec.ts/landing-page-bubbles-webgpu.png new file mode 100644 index 0000000..c9ed826 Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/landing-page-bubbles-webgpu.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/landing-page-halo-webgl.png b/tests/.generated-screenshots/svelte4.spec.ts/landing-page-halo-webgl.png new file mode 100644 index 0000000..d31d39e Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/landing-page-halo-webgl.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/landing-page-halo-webgpu-unsupported.png b/tests/.generated-screenshots/svelte4.spec.ts/landing-page-halo-webgpu-unsupported.png new file mode 100644 index 0000000..ff0eeff Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/landing-page-halo-webgpu-unsupported.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/landing-page-halo-webgpu.png b/tests/.generated-screenshots/svelte4.spec.ts/landing-page-halo-webgpu.png new file mode 100644 index 0000000..0a82217 Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/landing-page-halo-webgpu.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/logo-webgl.png b/tests/.generated-screenshots/svelte4.spec.ts/logo-webgl.png new file mode 100644 index 0000000..91a6f9d Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/logo-webgl.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/logo-webgpu-unsupported.png b/tests/.generated-screenshots/svelte4.spec.ts/logo-webgpu-unsupported.png new file mode 100644 index 0000000..f246ef2 Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/logo-webgpu-unsupported.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/logo-webgpu.png b/tests/.generated-screenshots/svelte4.spec.ts/logo-webgpu.png new file mode 100644 index 0000000..43a5e31 Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/logo-webgpu.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/oversized-canvas-webgl-1.png b/tests/.generated-screenshots/svelte4.spec.ts/oversized-canvas-webgl-1.png new file mode 100644 index 0000000..cb01237 Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/oversized-canvas-webgl-1.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/oversized-canvas-webgl-2.png b/tests/.generated-screenshots/svelte4.spec.ts/oversized-canvas-webgl-2.png new file mode 100644 index 0000000..e50b890 Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/oversized-canvas-webgl-2.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/oversized-canvas-webgpu-1.png b/tests/.generated-screenshots/svelte4.spec.ts/oversized-canvas-webgpu-1.png new file mode 100644 index 0000000..b37c974 Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/oversized-canvas-webgpu-1.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/oversized-canvas-webgpu-2.png b/tests/.generated-screenshots/svelte4.spec.ts/oversized-canvas-webgpu-2.png new file mode 100644 index 0000000..165c726 Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/oversized-canvas-webgpu-2.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/oversized-canvas-webgpu-unsupported-1.png b/tests/.generated-screenshots/svelte4.spec.ts/oversized-canvas-webgpu-unsupported-1.png new file mode 100644 index 0000000..4505311 Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/oversized-canvas-webgpu-unsupported-1.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/oversized-canvas-webgpu-unsupported-2.png b/tests/.generated-screenshots/svelte4.spec.ts/oversized-canvas-webgpu-unsupported-2.png new file mode 100644 index 0000000..4505311 Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/oversized-canvas-webgpu-unsupported-2.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/remount-webgl-1.png b/tests/.generated-screenshots/svelte4.spec.ts/remount-webgl-1.png new file mode 100644 index 0000000..e33d0b0 Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/remount-webgl-1.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/remount-webgl-2.png b/tests/.generated-screenshots/svelte4.spec.ts/remount-webgl-2.png new file mode 100644 index 0000000..03549b3 Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/remount-webgl-2.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/remount-webgpu-1.png b/tests/.generated-screenshots/svelte4.spec.ts/remount-webgpu-1.png new file mode 100644 index 0000000..bac82d9 Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/remount-webgpu-1.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/remount-webgpu-2.png b/tests/.generated-screenshots/svelte4.spec.ts/remount-webgpu-2.png new file mode 100644 index 0000000..62f5d2b Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/remount-webgpu-2.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/remount-webgpu-unsupported-1.png b/tests/.generated-screenshots/svelte4.spec.ts/remount-webgpu-unsupported-1.png new file mode 100644 index 0000000..e33d0b0 Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/remount-webgpu-unsupported-1.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/remount-webgpu-unsupported-2.png b/tests/.generated-screenshots/svelte4.spec.ts/remount-webgpu-unsupported-2.png new file mode 100644 index 0000000..b52a238 Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/remount-webgpu-unsupported-2.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/slider-webgl-1.png b/tests/.generated-screenshots/svelte4.spec.ts/slider-webgl-1.png new file mode 100644 index 0000000..d6be9c5 Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/slider-webgl-1.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/slider-webgl-2.png b/tests/.generated-screenshots/svelte4.spec.ts/slider-webgl-2.png new file mode 100644 index 0000000..394405c Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/slider-webgl-2.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/slider-webgl-3.png b/tests/.generated-screenshots/svelte4.spec.ts/slider-webgl-3.png new file mode 100644 index 0000000..8944917 Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/slider-webgl-3.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/slider-webgpu-1.png b/tests/.generated-screenshots/svelte4.spec.ts/slider-webgpu-1.png new file mode 100644 index 0000000..2e53c3a Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/slider-webgpu-1.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/slider-webgpu-2.png b/tests/.generated-screenshots/svelte4.spec.ts/slider-webgpu-2.png new file mode 100644 index 0000000..089340f Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/slider-webgpu-2.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/slider-webgpu-3.png b/tests/.generated-screenshots/svelte4.spec.ts/slider-webgpu-3.png new file mode 100644 index 0000000..9f364ac Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/slider-webgpu-3.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/slider-webgpu-unsupported-1.png b/tests/.generated-screenshots/svelte4.spec.ts/slider-webgpu-unsupported-1.png new file mode 100644 index 0000000..5017cf7 Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/slider-webgpu-unsupported-1.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/slider-webgpu-unsupported-2.png b/tests/.generated-screenshots/svelte4.spec.ts/slider-webgpu-unsupported-2.png new file mode 100644 index 0000000..5017cf7 Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/slider-webgpu-unsupported-2.png differ diff --git a/tests/.generated-screenshots/svelte4.spec.ts/slider-webgpu-unsupported-3.png b/tests/.generated-screenshots/svelte4.spec.ts/slider-webgpu-unsupported-3.png new file mode 100644 index 0000000..5017cf7 Binary files /dev/null and b/tests/.generated-screenshots/svelte4.spec.ts/slider-webgpu-unsupported-3.png differ diff --git a/tests/svelte4.spec.ts b/tests/svelte4.spec.ts new file mode 100644 index 0000000..3f7694e --- /dev/null +++ b/tests/svelte4.spec.ts @@ -0,0 +1,113 @@ +import { test, expect, TestInfo, Page } from "@playwright/test"; + +// No output on the console is desired +test.beforeEach(async ({ page }) => { + page.on("console", (msg) => { + throw new Error(`Output printed to console:\n${msg.text()}`); + }); +}); + +/** Project names (as defined in playwright.config.ts) that are not expected to support WebGPU. */ +const webGpuUnsupported = [ + "Firefox", +] + +/** + * Takes a screenshot of the current page and compares it to a reference, as indexed by the given arguments. + * If a reference screenshot does not exist, it will be created and used for future assertions. + * + * @param page + * @param info + * @param name A name that identifies this test. + * @param api The API used in the test, either `"webgl"` or `"webgpu"`. + * @param number A number identifying the specific screeenshot within the test, if multiple are included. + */ +async function assertScreenshot(page: Page, info: TestInfo, name: string, api: "webgl" | "webgpu", number?: number) { + const projectName = info.project.name; + const isMobile = info.project.use.isMobile ?? false; + + const mobileString = isMobile ? "-mobile" : ""; + const numberString = number ? `-${number}` : ""; + + const isWebGpu = api === "webgpu"; + const isWebGpuUnsupported = webGpuUnsupported.includes(projectName); + const unsupportedString = (isWebGpu && isWebGpuUnsupported) ? "-unsupported" : ""; + + const fileName = `${name}-${api}${unsupportedString}${mobileString}${numberString}.png`; + + await expect.soft(page).toHaveScreenshot(fileName, { + // Allow for slightly different rendering on different platforms, + // since Chrome and Firefox have slightly different text-rendering and inputs etc. + maxDiffPixelRatio: 0.01, + }); +} + +const apis = ["webgl", "webgpu"] as const; +apis.forEach(api => { + + test(`Hello world [${api}]`, async ({ page }, info) => { + const pageName = "hello-world"; + + await page.goto(`/${pageName}/${api}`); + await assertScreenshot(page, info, pageName, api); + }); + + test(`Remounting canvas [${api}]`, async ({ page }, info) => { + const pageName = "remount"; + + await page.goto(`/${pageName}/${api}`); + let show = page.getByLabel("Show") + await show.uncheck(); + await assertScreenshot(page, info, pageName, api, 1); + for (let i = 0; i < 10; i++) { + await show.check(); + await show.uncheck(); + } + await show.check(); + await assertScreenshot(page, info, pageName, api, 2); + }); + + test(`Oversized canvas [${api}]`, async ({ page }, info) => { + const pageName = "oversized-canvas"; + + await page.goto(`/${pageName}/${api}`); + await assertScreenshot(page, info, pageName, api, 1); + // Scroll to bottom-right corner + await page.evaluate(() => window.scrollBy(document.body.scrollWidth, document.body.scrollHeight)); + await assertScreenshot(page, info, pageName, api, 2); + }); + + test(`Logo [${api}]`, async ({ page }, info) => { + const pageName = "logo"; + + await page.goto(`/${pageName}/${api}`); + await assertScreenshot(page, info, pageName, api); + }); + + test(`Landing page with bubbles [${api}]`, async ({ page }, info) => { + const pageName = "landing-page-bubbles"; + + await page.goto(`/${pageName}/${api}`); + await assertScreenshot(page, info, pageName, api); + }); + + test(`Landing page with a halo [${api}]`, async ({ page }, info) => { + const pageName = "landing-page-halo"; + + await page.goto(`/${pageName}/${api}`); + await assertScreenshot(page, info, pageName, api); + }); + + test(`Slider component [${api}]`, async ({ page }, info) => { + const pageName = "slider"; + + await page.goto(`/${pageName}/${api}`); + const slider = page.getByRole("slider"); + await assertScreenshot(page, info, pageName, api, 1); + slider.fill("1"); + await assertScreenshot(page, info, pageName, api, 2); + slider.fill("0"); + await assertScreenshot(page, info, pageName, api, 3); + }); + +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e398378 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "types": ["node"], + "strict": true, + } +} \ No newline at end of file