syntaxai/tdd.md · main · e2e / editor-flow.spec.ts
// E2E: the self-hosted editor flow that replaces "edit on GitHub".
//
// Covers the unauthenticated paths only — full GitHub OAuth is not
// scripted here because it requires a real user account and adds
// flakiness against the live site. Authenticated paths (admin direct-
// write, proposal submission) are smoke-tested via the login-wall
// behavior; the actual write paths are unit-tested in src/*.test.ts.
import { test, expect } from "@playwright/test";
test.describe("editor flow (unauthenticated)", () => {
test("docs page exposes propose-an-edit + view-source links", async ({ page }) => {
const res = await page.goto("/sama/sorted");
expect(res?.status()).toBe(200);
const editLink = page.locator('a[href="/edit/sama/sorted"]', { hasText: /propose an edit/i });
await expect(editLink).toBeVisible();
const sourceLink = page.locator('a[href="/content/sama/sorted.md"]', { hasText: /view source/i });
await expect(sourceLink).toBeVisible();
// The link must NOT point at the git.tdd.md subdomain anymore.
const gitLinks = await page.locator('a[href*="git.tdd.md"]').count();
expect(gitLinks, "no docs link should still target git.tdd.md").toBe(0);
});
test("clicking 'view source' returns the raw markdown from tdd.md", async ({ request }) => {
const res = await request.get("/content/sama/sorted.md");
expect(res.status()).toBe(200);
expect(res.headers()["content-type"]).toMatch(/text\/plain/);
const body = await res.text();
// The .md file is the actual source — should contain the SAMA
// Sorted page's frontmatter / heading, never raw HTML chrome.
expect(body).toMatch(/sorted/i);
expect(body).not.toContain("<html");
expect(body).not.toContain("<aside class=\"docs-sidebar\"");
});
test("raw source 404s for unknown slug", async ({ request }) => {
const res = await request.get("/content/sama/does-not-exist.md");
expect(res.status()).toBe(404);
});
test("raw source 404s for non-editable section", async ({ request }) => {
const res = await request.get("/content/etc/passwd.md");
expect(res.status()).toBe(404);
});
test("raw source 404s without .md suffix", async ({ request }) => {
const res = await request.get("/content/sama/sorted");
expect(res.status()).toBe(404);
});
test("/edit/:section/:slug shows the GitHub login wall when unauthenticated", async ({ page }) => {
const res = await page.goto("/edit/sama/sorted");
expect(res?.status()).toBe(401);
await expect(page.getByRole("heading", { name: /edit · S — Sorted/i })).toBeVisible();
const signIn = page.getByRole("link", { name: /sign in with github/i });
await expect(signIn).toBeVisible();
const href = await signIn.getAttribute("href");
expect(href).toContain("/auth/github/start");
expect(href).toContain("to=%2Fedit%2Fsama%2Fsorted");
});
test("/edit returns 404 for unknown slug", async ({ page }) => {
const res = await page.goto("/edit/sama/nonexistent-slug-9999");
expect(res?.status()).toBe(404);
});
test("/edit returns 404 for non-editable section", async ({ page }) => {
const res = await page.goto("/edit/notreal/whatever");
expect(res?.status()).toBe(404);
});
});
test.describe("admin routes are gone (proposal queue retired)", () => {
// The /admin/proposals queue was removed when admin edits started
// committing directly to Forgejo via c14_forgejo.commitFile. The
// routes should now 404 (no fallthrough handler claims them).
test("/admin/proposals returns 404", async ({ request }) => {
const res = await request.get("/admin/proposals", { maxRedirects: 0 });
expect(res.status()).toBe(404);
});
test("/admin/proposals/1/approve returns 404", async ({ request }) => {
const res = await request.post("/admin/proposals/1/approve", { maxRedirects: 0 });
expect(res.status()).toBe(404);
});
});
test.describe("OAuth start preserves return URL", () => {
// Use raw fetch with redirect:"manual" — Playwright's request fixture
// either follows redirects or throws on maxRedirects:0 unreliably,
// which makes the 302 cookie inspection awkward.
const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? "https://tdd.md";
test("?to= is honored as a cookie + GitHub redirect", async () => {
const res = await fetch(`${baseURL}/auth/github/start?to=/edit/sama/sorted`, {
redirect: "manual",
});
expect(res.status).toBe(302);
expect(res.headers.get("location")).toContain("github.com/login/oauth/authorize");
const setCookies = res.headers.getSetCookie?.() ?? [res.headers.get("set-cookie") ?? ""];
const joined = setCookies.join(" | ");
expect(joined).toContain("tdd_oauth_state=");
expect(joined).toContain("tdd_oauth_return=%2Fedit%2Fsama%2Fsorted");
});
test("?to= rejects unsafe return URLs (no scheme jumps)", async () => {
const unsafe = encodeURIComponent("https://evil.example.com/steal");
const res = await fetch(`${baseURL}/auth/github/start?to=${unsafe}`, {
redirect: "manual",
});
expect(res.status).toBe(302);
const setCookies = res.headers.getSetCookie?.() ?? [res.headers.get("set-cookie") ?? ""];
const joined = setCookies.join(" | ");
// State cookie is always set; the return cookie must NOT be set
// for an unsafe URL — that's the open-redirect guard.
expect(joined).toContain("tdd_oauth_state=");
expect(joined).not.toContain("tdd_oauth_return=");
expect(joined).not.toContain("evil.example.com");
});
});
test.describe("docs site smoke", () => {
test("homepage loads", async ({ page }) => {
const res = await page.goto("/");
expect(res?.status()).toBe(200);
await expect(page).toHaveTitle(/sama/i);
});
test("sama landing loads with docs chrome", async ({ page }) => {
const res = await page.goto("/sama");
expect(res?.status()).toBe(200);
await expect(page.locator(".docs-content")).toBeVisible();
});
test("blog index lists at least one post", async ({ page }) => {
const res = await page.goto("/blog");
expect(res?.status()).toBe(200);
});
});