syntaxai/tdd.md · main · e2e / git-content-browse.spec.ts
// E2E: every blog post in ALL_POSTS is reachable via /GIT/.
//
// Crawls the registry's slugs (lifted into a literal array here so
// the test file doesn't import server-side modules) and asserts:
// 1. /GIT/syntaxai/tdd.md/tree/main/content/blog lists each post
// 2. /GIT/syntaxai/tdd.md/blob/main/content/blog/<slug>.md renders
// the post (markdown rendered via marked into the chrome)
// 3. /GIT/syntaxai/tdd.md/raw/main/content/blog/<slug>.md serves
// the raw markdown
// Plus the tree home (/GIT/syntaxai/tdd.md/tree/main) shows the
// top-level directories (content/, src/, public/, scripts/, etc.).
import { test, expect } from "@playwright/test";
import * as fs from "fs";
import * as path from "path";
// Mirror of c31_blog.ts ALL_POSTS slugs. If a post is added there,
// add the slug here too. Kept inline to avoid pulling server code
// into the test process.
const BLOG_SLUGS = [
"sama-meets-git-cms",
"from-rules-to-checks",
"agentic-coding-corpus-three-patterns",
"claude-code-harness-postmortem",
"three-constraints-agentic-coding",
"tweag-handbook-tdd",
"aider-tdd",
"cursor-tdd",
"claude-code-tdd",
];
const SCREENSHOT_DIR = "test-results/git-content-browse";
test.beforeAll(() => {
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
});
test.describe("/GIT browses the local bare repo", () => {
test("repo root tree lists the top-level directories", async ({ page }) => {
const res = await page.goto("/GIT/syntaxai/tdd.md/tree/main");
expect(res?.status()).toBe(200);
// Top-level dirs we expect after the dev tree was pushed.
for (const dir of ["content", "src", "public", "scripts", "e2e"]) {
await expect(
page.locator(`a[href="/GIT/syntaxai/tdd.md/tree/main/${dir}"]`),
).toBeVisible();
}
// Top-level files
await expect(
page.locator('a[href="/GIT/syntaxai/tdd.md/blob/main/package.json"]'),
).toBeVisible();
await page.screenshot({
path: path.join(SCREENSHOT_DIR, "1-repo-root-tree.png"),
fullPage: true,
});
});
test("content/blog tree lists every post in ALL_POSTS", async ({ page }) => {
const res = await page.goto("/GIT/syntaxai/tdd.md/tree/main/content/blog");
expect(res?.status()).toBe(200);
for (const slug of BLOG_SLUGS) {
const link = page.locator(
`a[href="/GIT/syntaxai/tdd.md/blob/main/content/blog/${slug}.md"]`,
);
await expect(link, `link to ${slug}.md must be present`).toBeVisible();
}
await page.screenshot({
path: path.join(SCREENSHOT_DIR, "2-content-blog-tree.png"),
fullPage: true,
});
});
for (const slug of BLOG_SLUGS) {
test(`blob view renders ${slug}.md as markdown via /GIT`, async ({ page }) => {
const res = await page.goto(
`/GIT/syntaxai/tdd.md/blob/main/content/blog/${slug}.md`,
);
expect(res?.status()).toBe(200);
// The repo-blob-rendered container is what marked.parse output
// lands in. It must exist + be non-empty.
const rendered = page.locator(".repo-blob-rendered");
await expect(rendered).toBeVisible();
const text = (await rendered.textContent()) ?? "";
expect(text.length).toBeGreaterThan(200);
// The breadcrumb must show the file path so users can climb.
await expect(page.locator(".commit-breadcrumb")).toContainText(`${slug}.md`);
});
test(`raw endpoint serves ${slug}.md as text/plain via /GIT`, async ({ request }) => {
const res = await request.get(
`/GIT/syntaxai/tdd.md/raw/main/content/blog/${slug}.md`,
);
expect(res.status()).toBe(200);
expect(res.headers()["content-type"]).toMatch(/text\/plain/);
const body = await res.text();
// Frontmatter or first heading — every blog post has one.
expect(body.length).toBeGreaterThan(200);
});
}
test("path traversal is rejected", async ({ request }) => {
for (const evil of [
"/GIT/syntaxai/tdd.md/blob/main/../etc/passwd",
"/GIT/syntaxai/tdd.md/blob/main/content/../../etc/passwd",
"/GIT/syntaxai/tdd.md/tree/main//content",
]) {
const res = await request.get(evil);
expect(res.status(), `${evil} must 404`).toBe(404);
}
});
test("non-allowed (owner, repo) 404s — only syntaxai/tdd.md is served", async ({
request,
}) => {
const res = await request.get("/GIT/someone/random-repo/tree/main");
expect(res.status()).toBe(404);
});
});