blog.test.ts
4.2 kB · typescript · 121 lines
1import { afterAll, expect, test } from "bun:test";2import { mkdirSync, rmSync, writeFileSync } from "node:fs";3import { join } from "node:path";4import { tmpdir } from "node:os";5import { collect, posts, render } from "./blog.ts";6import { forget, walk, type Output, type Site, type Spec } from "./build.ts";78/* TREE */910const home = join(tmpdir(), `mrlyblog-${process.pid}`);1112const post = (slug: string, text: string) => {13 mkdirSync(join(home, slug), { recursive: true });14 writeFileSync(join(home, slug, "index.md"), text);15};1617const beside = (slug: string, path: string, body: string | Uint8Array) => {18 mkdirSync(join(home, slug, path.slice(0, path.lastIndexOf("/") + 1) || "."), { recursive: true });19 writeFileSync(join(home, slug, path), body);20};2122post("older-news", "---\ntitle: Older\ndate: 2026-01-02\nlead: The first one.\n---\n\nWords.\n");23post("newer-news", "---\ntitle: Newer\ndate: 2026-05-06\nlead: The second one.\n---\n\n\n\nWords.\n");24beside("newer-news", "files/hero.png", new Uint8Array([137, 80, 78, 71, 0, 1]));25beside("newer-news", "NOTES.md", "# notes\n");2627afterAll(() => rmSync(home, { recursive: true, force: true }));2829/* SITE */3031const site = (path = home) =>32 ({33 root: home,34 config: { inputs: { blog: { path, deep: true } } },35 input: () => ({ name: "blog", path, files: walk(path, true), missing: !walk(path, true).length }),36 ships: new Map<string, string>(),37 }) as unknown as Site;3839const spec = (out: string[] = []) =>40 ({41 blog: {42 page: (_: Site, leaf: { kind: string; slug: string; image: string; body: string; posts: { slug: string }[] }) => {43 out.push(`${leaf.kind}:${leaf.slug}:${leaf.image}:${leaf.posts.map((one) => one.slug).join(",")}`);44 return leaf.body;45 },46 md: (_: Site, text: string) => text,47 },48 }) as unknown as Spec;4950const routes = (one = site()) => collect(one).routes;5152const find = (list: Output[], path: string) => list.find((item) => item.path === path);5354/* ORDER */5556test("the listing lists the posts newest first", () => {57 forget();58 const one = site();59 expect(posts(one).map((p) => p.slug)).toEqual(["newer-news", "older-news"]);60 const listing = routes(one).find((r) => r.route === "/blog/")!;61 expect(listing.data).toEqual(["newer-news", "older-news"]);62 expect(listing.at).toBe("2026-05-06");63});6465/* NOTHING */6667test("a site with no blog folder gets no routes", () => {68 forget();69 const bare = { root: home, config: {}, input: () => ({ name: "blog", path: "", files: [], missing: true }) } as unknown as Site;70 expect(collect(bare).routes).toEqual([]);71 expect(posts(bare)).toEqual([]);72});7374/* SIBLINGS */7576test("every file beside index.md ships under the post with its own type", () => {77 forget();78 const one = site();79 const files = routes(one).find((r) => r.kind === "blogfiles")!;80 const out = render(one, files, spec());81 expect(find(out, "blog/newer-news/files/hero.png")?.type).toBe("image/png");82 expect(find(out, "blog/newer-news/NOTES.md")?.type).toBe("text/plain; charset=utf-8");83 expect(files.urls?.map((u) => u.route)).toEqual(["/blog/newer-news/files/hero.png"]);84});8586/* SHIPS */8788test("every sibling is named in site.ships at collect time", () => {89 forget();90 const one = site();91 routes(one);92 expect(one.ships.get(join(home, "newer-news", "files", "hero.png"))).toBe("/blog/newer-news/files/hero.png");93 expect(one.ships.get(join(home, "newer-news", "NOTES.md"))).toBe("/blog/newer-news/NOTES.md");94});9596/* FIGURE */9798test("the first figure in the body is the og:image", () => {99 forget();100 const one = site();101 expect(posts(one)[0]!.image).toBe("/blog/newer-news/files/hero.png");102 expect(posts(one)[1]!.image).toBe("");103});104105/* FRONT MATTER */106107test("a post with no lead throws by name", () => {108 forget();109 post("broken-post", "---\ntitle: Broken\ndate: 2026-02-02\n---\n\nWords.\n");110 expect(() => posts(site())).toThrow(/broken-post names no lead/);111 rmSync(join(home, "broken-post"), { recursive: true, force: true });112});113114/* SLUG */115116test("a folder that is not a slug throws by name", () => {117 forget();118 post("Bad_Slug", "---\ntitle: Bad\ndate: 2026-02-02\nlead: No.\n---\n\nWords.\n");119 expect(() => posts(site())).toThrow(/Bad_Slug is not a slug/);120 rmSync(join(home, "Bad_Slug"), { recursive: true, force: true });121});