From 78691668416c7d42bf1640c21a0ce1e419d31e6e Mon Sep 17 00:00:00 2001 From: Firehose Bot Date: Thu, 8 Oct 2026 09:49:07 +0100 Subject: [PATCH] Add Playwright e2e test setup and configs --- app/assets/test/auth-helpers.ts | 41 +++++++++ app/assets/test/auth.spec.ts | 33 ++++++++ app/assets/test/fixtures.ts | 28 +++++++ app/assets/test/smoke.spec.ts | 29 +++++++ app/playwright.json | 139 +++++++++++++++++++++++++++++++ app/playwright.nono-profile.json | 18 ++++ 6 files changed, 288 insertions(+) create mode 100644 app/assets/test/auth-helpers.ts create mode 100644 app/assets/test/auth.spec.ts create mode 100644 app/assets/test/fixtures.ts create mode 100644 app/assets/test/smoke.spec.ts create mode 100644 app/playwright.json create mode 100644 app/playwright.nono-profile.json diff --git a/app/assets/test/auth-helpers.ts b/app/assets/test/auth-helpers.ts new file mode 100644 index 0000000..9f21b62 --- /dev/null +++ b/app/assets/test/auth-helpers.ts @@ -0,0 +1,41 @@ +/** + * Auth helpers for Playwright e2e tests. + * + * Provides utilities for logging in as a test user. + * The app uses email-only registration (magic-link flow for initial confirmation) + * and email+password for subsequent logins. + */ + +import { Page } from "@playwright/test"; + +export const TEST_USER = { + email: process.env.PLAYWRIGHT_EMAIL || "test@firehose.test", + password: process.env.PLAYWRIGHT_PASSWORD || "testpassword123!", +}; + +/** + * Navigate to the login page, fill in credentials, and submit. + * Returns the page after successful login (redirected to home). + */ +export async function loginAsTestUser(page: Page) { + await page.goto("/users/log-in"); + // Use the password-based login form + await page.getByLabel("Email").fill(TEST_USER.email); + await page.getByLabel("Password").fill(TEST_USER.password); + // Click "Log in and stay logged in" + await page.getByRole("button", { name: /log in and stay logged in/i }).click(); + await page.waitForURL("/"); +} + +/** + * Navigate to the registration page, fill in email, and submit. + * Note: In dev mode, registration is invite-only by default. Set + * `PLAYWRIGHT_ALLOW_REGISTRATION=true` to run tests against a server + * with open registration enabled. + */ +export async function registerTestUser(page: Page) { + await page.goto("/users/register"); + await page.getByLabel("Email").fill(TEST_USER.email); + await page.getByRole("button", { name: /create an account/i }).click(); + await page.waitForURL("/users/log-in"); +} \ No newline at end of file diff --git a/app/assets/test/auth.spec.ts b/app/assets/test/auth.spec.ts new file mode 100644 index 0000000..605903c --- /dev/null +++ b/app/assets/test/auth.spec.ts @@ -0,0 +1,33 @@ +import { test, expect } from "./fixtures"; +import { loginAsTestUser } from "./auth-helpers"; + +test.describe("Authenticated pages", () => { + test("redirects unauthenticated users to login page", async ({ page }) => { + const response = await page.goto("/editor/dashboard"); + // Should redirect to login + expect(response?.url()).toContain("/users/log-in"); + }); + + test("can log in and access the editor dashboard", async ({ page }) => { + await loginAsTestUser(page); + + // After login, we should be on the home page + await expect(page).toHaveURL("/"); + await expect(page.locator("body")).not.toBeEmpty(); + }); + + test("can log out", async ({ page }) => { + await loginAsTestUser(page); + + // Now log out + await page.goto("/users/log-out"); + + // Should redirect to home + await expect(page).toHaveURL("/"); + + // Try accessing an authenticated page + await page.goto("/editor/dashboard"); + // Should redirect to login + await expect(page).toHaveURL(/\/users\/log-in/); + }); +}); \ No newline at end of file diff --git a/app/assets/test/fixtures.ts b/app/assets/test/fixtures.ts new file mode 100644 index 0000000..7c411c9 --- /dev/null +++ b/app/assets/test/fixtures.ts @@ -0,0 +1,28 @@ +/** + * Custom Playwright test fixtures. + * + * Provides a `test` function that connects to a remote Chrome via CDP + * when PLAYWRIGHT_CDP_URL is set (Docker Chrome mode). + * Falls back to the default local browser otherwise. + */ + +import { test as base, chromium } from "@playwright/test"; + +type Fixtures = { + // Browser is provided by Playwright Test by default. + // We only override it when connecting to remote Docker Chrome. +}; + +const cdpUrl = process.env.PLAYWRIGHT_CDP_URL; + +export const test = cdpUrl + ? base.extend({ + browser: async ({}, use) => { + const browser = await chromium.connectOverCDP(cdpUrl); + await use(browser); + await browser.close(); + }, + }) + : base; + +export { expect } from "@playwright/test"; \ No newline at end of file diff --git a/app/assets/test/smoke.spec.ts b/app/assets/test/smoke.spec.ts new file mode 100644 index 0000000..241528e --- /dev/null +++ b/app/assets/test/smoke.spec.ts @@ -0,0 +1,29 @@ +import { test, expect } from "./fixtures"; + +test.describe("Connectivity smoke test", () => { + test("visits the homepage and verifies it loads", async ({ page }) => { + const response = await page.goto("/"); + expect(response?.ok()).toBeTruthy(); + + // Verify the page has content + await expect(page).toHaveTitle(/Firehose/); + }); + + test("visits the contact page", async ({ page }) => { + const response = await page.goto("/contact"); + expect(response?.ok()).toBeTruthy(); + await expect(page.locator("body")).not.toBeEmpty(); + }); + + test("visits the blog engineering index", async ({ page }) => { + const response = await page.goto("/blog/engineering"); + expect(response?.ok()).toBeTruthy(); + await expect(page.locator("body")).not.toBeEmpty(); + }); + + test("visits the blog releases index", async ({ page }) => { + const response = await page.goto("/blog/releases"); + expect(response?.ok()).toBeTruthy(); + await expect(page.locator("body")).not.toBeEmpty(); + }); +}); \ No newline at end of file diff --git a/app/playwright.json b/app/playwright.json new file mode 100644 index 0000000..72f113f --- /dev/null +++ b/app/playwright.json @@ -0,0 +1,139 @@ +{ + "extends": [ + "default" + ], + "meta": { + "name": "playwright", + "version": "", + "description": "Playwright e2e testing — browser binaries on host, connects to localhost Phoenix server", + "author": null + }, + "security": { + "signal_mode": null, + "process_info_mode": null, + "ipc_mode": null, + "capability_elevation": null, + "wsl2_proxy_policy": null + }, + "groups": { + "include": [ + "deny_credentials" + ], + "exclude": [] + }, + "commands": { + "allow": [], + "deny": [] + }, + "filesystem": { + "allow": [ + "$HOME/Library/Caches/ms-playwright", + "$HOME/.local/share/mise/", + "~/Library/Caches/mise/clang/22.1.7", + "~/Library/Caches/mise/dotnet/8.0.300", + "~/Library/Caches/mise/elixir/1.14.3", + "~/Library/Caches/mise/elixir/1.15.8", + "~/Library/Caches/mise/elixir/1.16.0-otp-26", + "~/Library/Caches/mise/elixir/1.16.1-otp-26", + "~/Library/Caches/mise/elixir/1.17-otp-26", + "~/Library/Caches/mise/elixir/1.17.3-otp-27", + "~/Library/Caches/mise/elixir/1.18-otp-27", + "~/Library/Caches/mise/elixir/1.18.3", + "~/Library/Caches/mise/elixir/1.18.3-otp-27", + "~/Library/Caches/mise/elixir/1.19.5-otp-28", + "~/Library/Caches/mise/erlang/25.2.1", + "~/Library/Caches/mise/erlang/26.2.1", + "~/Library/Caches/mise/php" + ], + "read": [ + "~/.cargo/bin", + "~/.config", + "~/Library/Caches/mise/elixir" + ], + "write": [], + "allow_file": [ + "~/Library/Caches/mise/clang/22.1.7/list_bin_paths-67c49.msgpack.z", + "~/Library/Caches/mise/dotnet/8.0.300/list_bin_paths-67c49.msgpack.z", + "~/Library/Caches/mise/elixir/1.14.3/list_bin_paths-67c49.msgpack.z", + "~/Library/Caches/mise/elixir/1.15.8/list_bin_paths-67c49.msgpack.z", + "~/Library/Caches/mise/elixir/1.16.0-otp-26/list_bin_paths-67c49.msgpack.z", + "~/Library/Caches/mise/elixir/1.16.1-otp-26/list_bin_paths-67c49.msgpack.z", + "~/Library/Caches/mise/elixir/1.17-otp-26/list_bin_paths-67c49.msgpack.z", + "~/Library/Caches/mise/elixir/1.17.3-otp-27/list_bin_paths-67c49.msgpack.z", + "~/Library/Caches/mise/elixir/1.18-otp-27/list_bin_paths-67c49.msgpack.z", + "~/Library/Caches/mise/elixir/1.18.3-otp-27/list_bin_paths-67c49.msgpack.z", + "~/Library/Caches/mise/elixir/1.18.3/list_bin_paths-67c49.msgpack.z", + "~/Library/Caches/mise/elixir/1.19.5-otp-28/list_bin_paths-67c49.msgpack.z", + "~/Library/Caches/mise/elixir/idiomatic_filenames-67c49.msgpack.z", + "~/Library/Caches/mise/erlang/25.2.1/list_bin_paths-67c49.msgpack.z", + "~/Library/Caches/mise/erlang/26.2.1/list_bin_paths-67c49.msgpack.z", + "~/Library/Caches/mise/erlang/26.2.2/list_bin_paths-67c49.msgpack.z", + "~/Library/Caches/mise/erlang/27.3.3/list_bin_paths-67c49.msgpack.z", + "~/Library/Caches/mise/erlang/27.3.4/list_bin_paths-67c49.msgpack.z", + "~/Library/Caches/mise/erlang/28.4.2/list_bin_paths-67c49.msgpack.z", + "~/Library/Caches/mise/erlang/28.5/list_bin_paths-67c49.msgpack.z", + "~/Library/Caches/mise/ghc/8.10.7/list_bin_paths-67c49.msgpack.z", + "~/Library/Caches/mise/php/8.4.0RC1/list_bin_paths-67c49.msgpack.z", + "~/Library/Caches/mise/php/remote_versions-67c49.msgpack.z" + ], + "read_file": [ + "$HOME/.gitconfig", + "~/.config/mise/config.toml", + "~/dev/elixir/firehose/mise.toml" + ], + "write_file": [], + "unix_socket": [], + "unix_socket_bind": [], + "unix_socket_dir": [], + "unix_socket_dir_bind": [], + "unix_socket_subtree": [], + "unix_socket_subtree_bind": [], + "deny": [], + "bypass_protection": [], + "suppress_save_prompt": [] + }, + "network": { + "block": false, + "allow_http2": false, + "allow_domain": [], + "open_port": [], + "listen_port": [], + "connect_port": [], + "custom_credentials": {}, + "tls_intercept": null, + "upstream_proxy": null, + "upstream_bypass": [] + }, + "diagnostics": { + "suppress_system_services": [] + }, + "linux": { + "af_unix_mediation": null, + "sandbox_policy": null + }, + "env_credentials": {}, + "environment": null, + "command_policies": null, + "credential_capture": {}, + "credential_providers": {}, + "credential_routes": [], + "workdir": { + "access": "readwrite" + }, + "hooks": {}, + "session_hooks": {}, + "rollback": { + "exclude_patterns": [], + "exclude_globs": [] + }, + "open_urls": null, + "allow_launch_services": null, + "allow_gpu": null, + "allow_parent_of_protected": null, + "interactive": false, + "skipdirs": [], + "packs": [], + "binary": null, + "command_args": [], + "unsafe_macos_seatbelt_rules": [] +} diff --git a/app/playwright.nono-profile.json b/app/playwright.nono-profile.json new file mode 100644 index 0000000..9177b96 --- /dev/null +++ b/app/playwright.nono-profile.json @@ -0,0 +1,18 @@ +{ + "meta": { + "name": "playwright", + "description": "Playwright e2e testing — browser binaries on host, connects to localhost Phoenix server" + }, + "extends": "default", + "groups": { + "include": ["deny_credentials"] + }, + "workdir": { + "access": "readwrite" + }, + "filesystem": { + "allow": [ + "$HOME/Library/Caches/ms-playwright" + ] + } +}