From b67c4f211bb785187688ba10b44301265d07cb32 Mon Sep 17 00:00:00 2001 From: mkelvers Date: Thu, 25 Jun 2026 21:29:17 +0200 Subject: [PATCH] chore: remove playwright e2e test infrastructure --- bun.lock | 9 -- justfile | 3 - package.json | 2 - playwright.config.ts | 29 ------ tests/e2e/global-setup.ts | 24 ----- tests/e2e/smoke.e2e.ts | 197 -------------------------------------- tsconfig.json | 2 +- 7 files changed, 1 insertion(+), 265 deletions(-) delete mode 100644 playwright.config.ts delete mode 100644 tests/e2e/global-setup.ts delete mode 100644 tests/e2e/smoke.e2e.ts diff --git a/bun.lock b/bun.lock index ef421e6b..90e56ace 100644 --- a/bun.lock +++ b/bun.lock @@ -9,7 +9,6 @@ "htmx.org": "1.9.12", }, "devDependencies": { - "@playwright/test": "^1.61.1", "@tailwindcss/cli": "^4.3.0", "@types/node": "^24.0.0", "lefthook": "^2.1.6", @@ -148,8 +147,6 @@ "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="], - "@playwright/test": ["@playwright/test@1.61.1", "", { "dependencies": { "playwright": "1.61.1" }, "bin": { "playwright": "cli.js" } }, "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig=="], - "@tailwindcss/cli": ["@tailwindcss/cli@4.3.0", "", { "dependencies": { "@parcel/watcher": "^2.5.1", "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "enhanced-resolve": "^5.21.0", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.3.0" }, "bin": { "tailwindcss": "dist/index.mjs" } }, "sha512-X9kdlqyMopO9fewbgHsEeuy31YzMHbdZ9VsKt004tB+mxSg1CNbyhZYCzvhciN0AM4R4b5lvIprPjtNq7iQxpQ=="], "@tailwindcss/node": ["@tailwindcss/node@4.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.0" } }, "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g=="], @@ -186,8 +183,6 @@ "enhanced-resolve": ["enhanced-resolve@5.23.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-yJN/BOOLxcOW2aQgeif9mSnaUB8KtvmMMp56oA1kx1CRfBKbhZm2pJ+NBY+3eOboHxix8lfjWpHE0Ei5U8RbSA=="], - "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "hls.js": ["hls.js@1.6.16", "", {}, "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA=="], @@ -262,10 +257,6 @@ "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], - "playwright": ["playwright@1.61.1", "", { "dependencies": { "playwright-core": "1.61.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ=="], - - "playwright-core": ["playwright-core@1.61.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg=="], - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], diff --git a/justfile b/justfile index 3ccc472a..2bf8e025 100644 --- a/justfile +++ b/justfile @@ -23,9 +23,6 @@ bench: bench-all: go test -bench=. -benchmem ./... -e2e: - bun run test:e2e - build-go: @go build -o server ./cmd/server diff --git a/package.json b/package.json index a7b6ac95..121ef5f6 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "lint:ts": "bunx oxlint --ignore-path .oxlintignore static --tsconfig ./tsconfig.json --type-aware --max-warnings 0", "lint:ts:fix": "bunx oxlint --ignore-path .oxlintignore static --tsconfig ./tsconfig.json --type-aware --max-warnings 0 --fix", "test": "bun test", - "test:e2e": "playwright test", "test:go": "go test ./...", "test:ts": "bun test", "typecheck": "bunx tsc -p tsconfig.json --noEmit", @@ -24,7 +23,6 @@ "htmx.org": "1.9.12" }, "devDependencies": { - "@playwright/test": "^1.61.1", "@tailwindcss/cli": "^4.3.0", "@types/node": "^24.0.0", "lefthook": "^2.1.6", diff --git a/playwright.config.ts b/playwright.config.ts deleted file mode 100644 index fea61eb3..00000000 --- a/playwright.config.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { defineConfig, devices } from "@playwright/test"; - -const port = process.env.PORT ?? "3100"; -const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? `http://127.0.0.1:${port}`; -const databaseFile = process.env.DATABASE_FILE ?? `/tmp/mal-e2e-${port}.db`; - -export default defineConfig({ - testDir: "./tests/e2e", - testMatch: "**/*.e2e.ts", - globalSetup: "./tests/e2e/global-setup.ts", - timeout: 30_000, - expect: { timeout: 5000 }, - fullyParallel: true, - reporter: process.env.CI === undefined ? "list" : "github", - use: { baseURL, trace: "retain-on-failure" }, - webServer: - process.env.PLAYWRIGHT_BASE_URL === undefined - ? { - command: `PORT=${port} DATABASE_FILE=${databaseFile} GIN_MODE=test go run ./cmd/server`, - url: baseURL, - reuseExistingServer: process.env.CI === undefined, - timeout: 120_000, - } - : undefined, - projects: [ - { name: "chromium", use: { ...devices["Desktop Chrome"] } }, - { name: "mobile", use: { ...devices["Pixel 5"] } }, - ], -}); diff --git a/tests/e2e/global-setup.ts b/tests/e2e/global-setup.ts deleted file mode 100644 index 727cd2e5..00000000 --- a/tests/e2e/global-setup.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { spawnSync } from "node:child_process"; - -const port = process.env.PORT ?? "3100"; - -export default function globalSetup() { - if (process.env.PLAYWRIGHT_BASE_URL !== undefined) { - return; - } - - const username = process.env.E2E_USERNAME ?? "e2e@example.com"; - const password = process.env.E2E_PASSWORD ?? "e2e-password"; - const databaseFile = process.env.DATABASE_FILE ?? `/tmp/mal-e2e-${port}.db`; - - const result = spawnSync("go", ["run", "./cmd/user", username, password], { - env: { ...process.env, DATABASE_FILE: databaseFile, GIN_MODE: "test" }, - input: "y\n", - stdio: ["pipe", "pipe", "pipe"], - encoding: "utf8", - }); - - if (result.status !== 0) { - throw new Error(`Failed to seed e2e user:\n${result.stderr || result.stdout}`); - } -} diff --git a/tests/e2e/smoke.e2e.ts b/tests/e2e/smoke.e2e.ts deleted file mode 100644 index dcf7e256..00000000 --- a/tests/e2e/smoke.e2e.ts +++ /dev/null @@ -1,197 +0,0 @@ -import type { Page } from "@playwright/test"; - -import { expect, test } from "@playwright/test"; - -const username = process.env.E2E_USERNAME ?? "e2e@example.com"; -const password = process.env.E2E_PASSWORD ?? "e2e-password"; - -const signIn = async (page: Page) => { - const response = await page.request.post("/login", { form: { username, password } }); - - expect(response.status()).toBeLessThan(400); -}; - -const signInThroughUI = async (page: Page) => { - await page.goto("/login"); - await page.getByRole("textbox", { name: /email address/iu }).fill(username); - await page.locator('input[name="password"]').fill(password); - await page.getByRole("button", { name: /sign in/iu }).click(); - await expect(page).toHaveURL(/\/$/u); -}; - -test("redirects protected pages to login", async ({ page }) => { - const response = await page.goto("/"); - - expect(response?.status()).toBeLessThan(400); - await expect(page).toHaveURL(/\/login$/u); -}); - -test("renders the login page", async ({ page }) => { - await page.goto("/login"); - - await expect(page).toHaveTitle(/MyAnimeList: Login/u); - await expect(page.getByRole("textbox", { name: /email address/iu })).toBeVisible(); - await expect(page.locator('input[name="password"]')).toBeVisible(); - await expect(page.getByRole("button", { name: /sign in/iu })).toBeVisible(); -}); - -test("shows an error for invalid credentials", async ({ page }) => { - await page.goto("/login"); - - await page.getByRole("textbox", { name: /email address/iu }).fill(username); - await page.locator('input[name="password"]').fill("wrong-password"); - await page.getByRole("button", { name: /sign in/iu }).click(); - - await expect(page).toHaveURL(/\/login$/u); - await expect(page.getByRole("alert")).toContainText(/invalid username or password/iu); -}); - -test("toggles password visibility on the login page", async ({ page }) => { - await page.goto("/login"); - - const passwordInput = page.locator('input[name="password"]'); - await expect(passwordInput).toHaveAttribute("type", "password"); - - await page.getByRole("button", { name: /show password/iu }).click(); - await expect(passwordInput).toHaveAttribute("type", "text"); - await expect(page.getByRole("button", { name: /hide password/iu })).toBeVisible(); - - await page.getByRole("button", { name: /hide password/iu }).click(); - await expect(passwordInput).toHaveAttribute("type", "password"); -}); - -test("signs in and renders authenticated home navigation", async ({ page }) => { - const airingResponse = (async () => { - try { - return await page.waitForResponse( - (response) => response.url().includes("/api/catalog/airing"), - { timeout: 10_000 }, - ); - } catch { - return undefined; - } - })(); - - await signInThroughUI(page); - - const primaryNavigation = page.getByRole("navigation", { name: /primary navigation/iu }); - await expect(page).toHaveTitle(/MyAnimeList: Home/u); - await expect(primaryNavigation).toBeVisible(); - await expect(primaryNavigation.getByRole("link", { name: "Browse", exact: true })).toBeVisible(); - await expect( - primaryNavigation.getByRole("link", { name: "Top Picks", exact: true }), - ).toBeVisible(); - await expect( - primaryNavigation.getByRole("link", { name: "Watchlist", exact: true }), - ).toBeVisible(); - await expect(page.getByRole("heading", { name: /airing & popular/iu })).toBeVisible(); - await airingResponse; -}); - -test("renders login page even with an existing session", async ({ page }) => { - await signIn(page); - - const response = await page.goto("/login"); - - expect(response?.status()).toBe(200); - await expect(page).toHaveURL(/\/login$/u); - await expect(page.getByRole("button", { name: /sign in/iu })).toBeVisible(); -}); - -test("renders the empty watchlist state", async ({ page }) => { - await signIn(page); - - await page.goto("/watchlist"); - - await expect(page).toHaveURL(/\/watchlist$/u); - await expect(page.getByRole("heading", { name: /^watchlist$/iu })).toBeVisible(); - await expect(page.getByText(/your watchlist is empty/iu)).toBeVisible(); - await expect(page.getByRole("button", { name: /^all$/iu })).toBeVisible(); - await expect(page.getByRole("button", { name: /watching/iu })).toBeVisible(); - await expect(page.getByRole("link", { name: /go home/iu })).toBeVisible(); -}); - -test("renders the authenticated search page", async ({ page }) => { - await signIn(page); - - await page.goto("/search"); - - await expect(page).toHaveURL(/\/search$/u); - await expect(page).toHaveTitle(/MyAnimeList: Search/u); - await expect(page.getByRole("searchbox", { name: /search anime/iu })).toBeVisible(); - await expect(page.getByRole("searchbox", { name: /search anime/iu })).toHaveAttribute( - "placeholder", - "Search anime...", - ); -}); - -test("logs out and protects pages again", async ({ page }) => { - await signIn(page); - - await page.goto("/logout"); - await expect(page).toHaveURL(/\/login$/u); - - await page.goto("/watchlist"); - await expect(page).toHaveURL(/\/login$/u); -}); - -test("returns an API token for valid credentials", async ({ request }) => { - const response = await request.post("/api/auth/login", { - data: { username, password, name: "playwright" }, - }); - - expect(response.status()).toBe(200); - const body = (await response.json()) as { token?: string; user?: { username?: string } }; - expect(body.token).toEqual(expect.any(String)); - expect(body.user?.username).toBe(username); -}); - -test("rejects invalid API login payloads and credentials", async ({ request }) => { - const missingPassword = await request.post("/api/auth/login", { data: { username } }); - expect(missingPassword.status()).toBe(400); - - const wrongPassword = await request.post("/api/auth/login", { - data: { username, password: "wrong-password" }, - }); - expect(wrongPassword.status()).toBe(401); -}); - -test("protects API routes without a valid session", async ({ request }) => { - const response = await request.get("/api/search-quick?q="); - - expect(response.status()).toBe(401); - await expect(response.json()).resolves.toMatchObject({ error: "Unauthorized" }); -}); - -test("allows authenticated API requests with a bearer token", async ({ request }) => { - const login = await request.post("/api/auth/login", { - data: { username, password, name: "playwright-api" }, - }); - const body = (await login.json()) as { token: string }; - - const response = await request.get("/api/search-quick?q=", { - headers: { Authorization: `Bearer ${body.token}` }, - }); - - expect(response.status()).toBe(200); - await expect(response.json()).resolves.toEqual([]); -}); - -test("serves static assets without authentication", async ({ request }) => { - const response = await request.get("/dist/tailwind.css"); - - expect(response.status()).toBe(200); - expect(response.headers()["content-type"]).toContain("text/css"); -}); - -test("serves app assets and the manifest without authentication", async ({ request }) => { - const [appScript, manifest] = await Promise.all([ - request.get("/dist/static/app.js"), - request.get("/static/assets/manifest.json"), - ]); - - expect(appScript.status()).toBe(200); - expect(appScript.headers()["content-type"]).toContain("javascript"); - expect(manifest.status()).toBe(200); - expect(manifest.headers()["content-type"]).toContain("application/json"); -}); diff --git a/tsconfig.json b/tsconfig.json index 3686eea2..b284d2d7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,5 +13,5 @@ "removeComments": false, "skipLibCheck": true }, - "include": ["static/**/*.ts", "scripts/**/*.ts", "tests/e2e/**/*.ts", "playwright.config.ts"] + "include": ["static/**/*.ts", "scripts/**/*.ts"] }