TheBird

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![the hero](files/hero.png)\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});