diff --git a/.gitignore b/.gitignore index cf0daba..6de568b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,9 @@ dist # code coverage coverage *.lcov +playwright-report/ +test-results/ +blob-report/ # logs logs diff --git a/bun.lock b/bun.lock index 90e56ac..ef421e6 100644 --- a/bun.lock +++ b/bun.lock @@ -9,6 +9,7 @@ "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", @@ -147,6 +148,8 @@ "@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=="], @@ -183,6 +186,8 @@ "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=="], @@ -257,6 +262,10 @@ "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 2d07761..3ccc472 100644 --- a/justfile +++ b/justfile @@ -15,6 +15,7 @@ lint-go: test: go test ./... + bun test bench: go test -bench=. -benchmem -count=5 ./internal/anime/... ./integrations/jikan/... ./internal/playback/... @@ -22,6 +23,9 @@ 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 9213d98..a7b6ac9 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,10 @@ "lint:go": "golangci-lint run ./...", "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", "watch:css": "bunx --bun @tailwindcss/cli -i ./static/assets/style.css -o ./dist/tailwind.css --watch" }, @@ -20,6 +24,7 @@ "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 new file mode 100644 index 0000000..5982201 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,27 @@ +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}`; + +export default defineConfig({ + testDir: "./tests/e2e", + testMatch: "**/*.e2e.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=/tmp/mal-e2e.db 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/smoke.e2e.ts b/tests/e2e/smoke.e2e.ts new file mode 100644 index 0000000..dac6ec5 --- /dev/null +++ b/tests/e2e/smoke.e2e.ts @@ -0,0 +1,23 @@ +import { expect, test } from "@playwright/test"; + +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.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("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"); +}); diff --git a/tsconfig.json b/tsconfig.json index b284d2d..3686eea 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,5 +13,5 @@ "removeComments": false, "skipLibCheck": true }, - "include": ["static/**/*.ts", "scripts/**/*.ts"] + "include": ["static/**/*.ts", "scripts/**/*.ts", "tests/e2e/**/*.ts", "playwright.config.ts"] }