chore: add Playwright E2E testing infrastructure (#673)

* feat: add Playwright testing infrastructure

- Add @playwright/test dependency
- Add playwright.config.ts with Chromium setup
- Add test scripts (test, test:ui, test:headed)
- Add hello world test to verify setup works

This is the foundation for adding comprehensive E2E tests.
Tests run against the Vite dev server on port 1420.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* ci: add E2E test workflow

Runs Playwright tests on pull requests using Chromium.
Uploads test artifacts on failure.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: rename e2e to playwright, fix CI test

- Rename workflow from "E2E Tests" to "Playwright"
- Rename scripts from test:e2e to test:playwright
- Fix test to not require Tauri APIs (just check server responds)
- Run prettier

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: CJ Pais <cj@cjpais.com>
This commit is contained in:
Josh Ribakoff 2026-02-06 01:15:19 -08:00 committed by GitHub
parent 9ea1555816
commit 9ef71046ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 91 additions and 1 deletions

29
.github/workflows/playwright.yml vendored Normal file
View file

@ -0,0 +1,29 @@
name: "Playwright"
on: [pull_request]
jobs:
playwright:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Install Playwright browsers
run: bunx playwright install chromium
- name: Run Playwright tests
run: bun run test:playwright
- name: Upload test results
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 7

5
.gitignore vendored
View file

@ -29,3 +29,8 @@ dist-ssr
/target/
recording_*
.crush/
# Playwright
playwright-report/
test-results/
blob-report/

View file

@ -31,6 +31,7 @@
"zustand": "^5.0.8",
},
"devDependencies": {
"@playwright/test": "^1.58.0",
"@tauri-apps/cli": "^2.9.1",
"@types/node": "^24.9.1",
"@types/react": "^18.3.26",
@ -204,6 +205,8 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@playwright/test": ["@playwright/test@1.58.0", "", { "dependencies": { "playwright": "1.58.0" }, "bin": { "playwright": "cli.js" } }, "sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.5", "", { "os": "android", "cpu": "arm" }, "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ=="],
@ -608,6 +611,10 @@
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"playwright": ["playwright@1.58.0", "", { "dependencies": { "playwright-core": "1.58.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ=="],
"playwright-core": ["playwright-core@1.58.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
@ -730,6 +737,8 @@
"eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
}
}

View file

@ -13,7 +13,9 @@
"format": "prettier --write . && cd src-tauri && cargo fmt",
"format:check": "prettier --check . && cd src-tauri && cargo fmt -- --check",
"format:frontend": "prettier --write .",
"format:backend": "cd src-tauri && cargo fmt"
"format:backend": "cd src-tauri && cargo fmt",
"test:playwright": "playwright test",
"test:playwright:ui": "playwright test --ui"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.16",
@ -42,6 +44,7 @@
"zustand": "^5.0.8"
},
"devDependencies": {
"@playwright/test": "^1.58.0",
"@tauri-apps/cli": "^2.9.1",
"@types/node": "^24.9.1",
"@types/react": "^18.3.26",

26
playwright.config.ts Normal file
View file

@ -0,0 +1,26 @@
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./tests",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: "html",
use: {
baseURL: "http://localhost:1420",
trace: "on-first-retry",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
webServer: {
command: "bunx vite dev",
url: "http://localhost:1420",
reuseExistingServer: !process.env.CI,
timeout: 30000,
},
});

18
tests/app.spec.ts Normal file
View file

@ -0,0 +1,18 @@
import { test, expect } from "@playwright/test";
test.describe("Handy App", () => {
test("dev server responds", async ({ page }) => {
// Just verify the dev server is running and responds
const response = await page.goto("/");
expect(response?.status()).toBe(200);
});
test("page has html structure", async ({ page }) => {
await page.goto("/");
// Verify basic HTML structure exists
const html = await page.content();
expect(html).toContain("<html");
expect(html).toContain("<body");
});
});