Set up Playwright for browser testing

- Playwright config for local browsers (Firefox by default) and Docker Chrome
- docker-compose.chrome.yml for browserless/chrome container (arm64)
- 4 connectivity smoke tests (home, contact, blog pages)
- 3 auth tests (redirect, login, logout) with auth helpers
- Custom fixtures with remote CDP connect support
- Shell scripts for start/stop/full Docker Chrome workflow
- Makefile targets: playwright, playwright-docker, playwright-full
This commit is contained in:
Firehose Bot
2026-05-18 23:12:42 +01:00
parent 5d20a21499
commit 6b201e7e63
13 changed files with 445 additions and 1 deletions
+41
View File
@@ -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");
}
+33
View File
@@ -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/);
});
});
+28
View File
@@ -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<Fixtures>({
browser: async ({}, use) => {
const browser = await chromium.connectOverCDP(cdpUrl);
await use(browser);
await browser.close();
},
})
: base;
export { expect } from "@playwright/test";
+29
View File
@@ -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();
});
});