Files
edgeever/scripts/prepare-poster-thumbnails.test.mjs

125 lines
6.0 KiB
JavaScript

import { afterEach, expect, test } from "bun:test";
import { mkdtemp, mkdir, writeFile, readFile, readdir, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { sources, sourceState, ensurePosterThumbnails, thumbnailsCurrent } from "./prepare-poster-thumbnails.mjs";
const roots = [];
afterEach(async () => { await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true }))); });
async function fixture() {
const root = await mkdtemp(path.join(tmpdir(), "poster-thumbnail-test-"));
roots.push(root);
for (const source of sources) {
const file = path.join(root, source === "apps/web/src/lib/poster-design" ? `${source}/templates.ts` : source);
await mkdir(path.dirname(file), { recursive: true });
await writeFile(file, source.endsWith("package.json") ? '{"dependencies":{"fabric":"1"}}' : source);
}
const output = path.join(root, "apps/web/src/assets/poster-template-previews");
const catalog = [{ id: "one", recommended: true }, { id: "two", recommended: false }];
const documents = new Map();
const rendered = [];
let opened = 0, failed = false;
const options = { root, output, log() {}, renderer: async () => {
opened++;
return {
list: async () => structuredClone(catalog),
prepare: async (id, language) => JSON.stringify({ id, language, body: documents.get(id) ?? "initial" }),
render: async (id, language) => {
rendered.push(`${id}.${language}`);
if (failed) throw new Error("font download failed");
return Buffer.from(`RIFF0000WEBP${id}.${language}.${documents.get(id) ?? "initial"}`).toString("base64");
},
close: async () => {},
};
} };
const change = async () => writeFile(path.join(root, "apps/web/src/lib/poster-design/templates.ts"), String(Date.now()) + Math.random());
return { root, output, options, catalog, documents, rendered, change, opened: () => opened, fail: () => { failed = true; } };
}
test("fresh builds skip the browser; editing one template renders only its two languages", async () => {
const f = await fixture();
expect((await ensurePosterThumbnails(f.options)).generated).toBe(4);
f.rendered.length = 0;
expect(await ensurePosterThumbnails(f.options)).toMatchObject({ generated: 0, skipped: true });
expect(f.opened()).toBe(1);
const before = await readFile(path.join(f.output, "two.zh.webp"));
f.documents.set("one", "new layout");
await f.change();
expect((await ensurePosterThumbnails(f.options)).generated).toBe(2);
expect(f.rendered).toEqual(["one.zh", "one.en"]);
expect(await readFile(path.join(f.output, "two.zh.webp"))).toEqual(before);
});
test("new templates get images and removed templates lose orphaned thumbnails", async () => {
const f = await fixture();
await ensurePosterThumbnails(f.options);
f.rendered.length = 0;
f.catalog.push({ id: "three", recommended: true });
await f.change();
expect((await ensurePosterThumbnails(f.options)).generated).toBe(2);
expect(f.rendered).toEqual(["three.zh", "three.en"]);
f.catalog.splice(0, 1);
await f.change();
expect((await ensurePosterThumbnails(f.options)).generated).toBe(0);
expect(await readdir(f.output)).not.toContain("one.zh.webp");
});
test("missing or corrupt images repair only themselves; painter changes refresh all", async () => {
const f = await fixture();
await ensurePosterThumbnails(f.options);
await rm(path.join(f.output, "one.en.webp"));
await writeFile(path.join(f.output, "two.zh.webp"), "corrupt");
f.rendered.length = 0;
expect((await ensurePosterThumbnails(f.options)).generated).toBe(2);
expect(f.rendered).toEqual(["one.en", "two.zh"]);
await writeFile(path.join(f.root, "apps/web/src/lib/poster-canvas.ts"), "new painter");
expect((await ensurePosterThumbnails(f.options)).generated).toBe(4);
});
test("failed generation preserves previous assets and manifest, and releases the lock", async () => {
const f = await fixture();
await ensurePosterThumbnails(f.options);
const before = await readFile(path.join(f.output, "manifest.json"));
const image = await readFile(path.join(f.output, "one.zh.webp"));
f.documents.set("one", "changed");
f.fail();
await f.change();
await expect(ensurePosterThumbnails(f.options)).rejects.toThrow("font download failed");
expect(await readFile(path.join(f.output, "manifest.json"))).toEqual(before);
expect(await readFile(path.join(f.output, "one.zh.webp"))).toEqual(image);
expect(await readdir(path.join(f.root, "node_modules/.cache/edgeever"))).toEqual([]);
});
test("simultaneous dev/build requests serialize and render once", async () => {
const f = await fixture();
const results = await Promise.all([ensurePosterThumbnails(f.options), ensurePosterThumbnails(f.options)]);
expect(results.map(result => result.generated).sort()).toEqual([0, 4]);
expect(f.opened()).toBe(1);
});
test("Windows CRLF checkouts remain current without regeneration", async () => {
const f = await fixture();
const file = path.join(f.root, "apps/web/src/lib/poster-design/templates.ts");
await writeFile(file, "line one\nline two\n");
await ensurePosterThumbnails(f.options);
await writeFile(file, "line one\r\nline two\r\n");
expect(await thumbnailsCurrent(f.output, await sourceState(f.root))).toBe(true);
expect((await ensurePosterThumbnails(f.options)).skipped).toBe(true);
});
test("a save during generation cannot publish a manifest for stale sources", async () => {
const f = await fixture();
await ensurePosterThumbnails(f.options);
const before = await readFile(path.join(f.output, "manifest.json"));
await f.change();
const original = f.options.renderer;
f.options.renderer = async () => {
const runtime = await original();
const prepare = runtime.prepare;
runtime.prepare = async (...args) => { await f.change(); return prepare(...args); };
return runtime;
};
await expect(ensurePosterThumbnails(f.options)).rejects.toThrow("sources changed during generation");
expect(await readFile(path.join(f.output, "manifest.json"))).toEqual(before);
});