import { test, describe, beforeAll, expect, spyOn } from "bun:test";
import { writeFileSync, mkdirSync, readFileSync } from "fs";
import { join } from "path";
import { h, type VNode } from "preact";
import { renderCard } from "../lib/render";
import { RepositoryCard } from "../components/cards/repository";
import { IssueCard } from "../components/cards/issue";
import { PullRequestCard } from "../components/cards/pull-request";
import { ProfileCard } from "../components/cards/profile";
import { StringCard } from "../components/cards/string";
import { CommitCard } from "../components/cards/commit";
import { FileCard } from "../components/cards/file";
import { TagCard } from "../components/cards/tag";
import { PipelineCard } from "../components/cards/pipeline";
import { Avatar } from "../components/shared/avatar";
import { COLORS } from "../components/shared/constants";
import {
repositoryCardSchema,
issueCardSchema,
pullRequestCardSchema,
profileCardSchema,
stringCardSchema,
commitCardSchema,
fileCardSchema,
tagCardSchema,
pipelineCardSchema,
} from "../validation";
import {
createRepoData,
createIssueData,
createPullRequestData,
createLongTitleIssueData,
createLongTitlePullRequestData,
createProfileData,
createStringData,
createCommitData,
createFileData,
createTagData,
createPipelineData,
} from "./fixtures";
const outputDir = join(process.cwd(), "output");
let avatarDataUri: string;
const loadAvatar = (): string => {
const avatarPath = join(
process.cwd(),
"src",
"__tests__",
"assets",
"avatar.jpg",
);
const avatarBase64 = readFileSync(avatarPath).toString("base64");
return `data:image/jpeg;base64,${avatarBase64}`;
};
beforeAll(() => {
mkdirSync(outputDir, { recursive: true });
avatarDataUri = loadAvatar();
});
const savePng = (filename: string, buffer: Uint8Array) => {
writeFileSync(join(outputDir, filename), buffer);
};
const saveSvg = (filename: string, svg: string) => {
writeFileSync(join(outputDir, filename), svg);
};
const renderAndSave = async <P>(component: VNode<P>, filename: string) => {
const { svg, png } = await renderCard(component as VNode);
saveSvg(filename.replace(".png", ".svg"), svg);
savePng(filename, png);
return svg;
};
describe("repository card", () => {
test("renders repository card", async () => {
const data = createRepoData(avatarDataUri);
const validated = repositoryCardSchema.parse(data);
await renderAndSave(h(RepositoryCard, validated), "repository-card.png");
});
});
describe("issue cards", () => {
test("renders open issue", async () => {
const data = createIssueData(avatarDataUri);
const validated = issueCardSchema.parse(data);
await renderAndSave(h(IssueCard, validated), "issue-card.png");
});
test("renders closed issue", async () => {
const data = createIssueData(avatarDataUri, {
issueNumber: 5,
status: "closed",
labels: [{ name: "wontfix", color: "#6a737d" }],
reactionCount: 2,
});
const validated = issueCardSchema.parse(data);
await renderAndSave(h(IssueCard, validated), "issue-card-closed.png");
});
test("renders issue with long title", async () => {
const data = createLongTitleIssueData(avatarDataUri, {
issueNumber: 42,
});
const validated = issueCardSchema.parse(data);
await renderAndSave(h(IssueCard, validated), "issue-card-long-title.png");
});
test("renders issue with long author handle", async () => {
const data = createIssueData(avatarDataUri, {
authorHandle: "very.very.long.tangled.org",
});
const validated = issueCardSchema.parse(data);
await renderAndSave(
h(IssueCard, validated),
"issue-card-long-handle.png",
);
});
});
describe("pull request cards", () => {
test("renders open pull request", async () => {
const data = createPullRequestData(avatarDataUri);
const validated = pullRequestCardSchema.parse(data);
await renderAndSave(h(PullRequestCard, validated), "pull-request-card.png");
});
test("renders merged pull request", async () => {
const data = createPullRequestData(avatarDataUri, {
pullRequestNumber: 2,
status: "merged",
title: "Implement OAuth2 authentication flow",
filesChanged: 5,
additions: 342,
deletions: 28,
});
const validated = pullRequestCardSchema.parse(data);
await renderAndSave(
h(PullRequestCard, validated),
"pull-request-card-merged.png",
);
});
test("renders closed pull request", async () => {
const data = createPullRequestData(avatarDataUri, {
pullRequestNumber: 3,
status: "closed",
title: "WIP: Experimental feature",
});
const validated = pullRequestCardSchema.parse(data);
await renderAndSave(
h(PullRequestCard, validated),
"pull-request-card-closed.png",
);
});
test("renders pull request without diff stats", async () => {
const { filesChanged, additions, deletions, ...data } =
createPullRequestData(avatarDataUri, { rounds: 1 });
const validated = pullRequestCardSchema.parse(data);
expect(validated.filesChanged).toBeUndefined();
const svg = await renderAndSave(
h(PullRequestCard, validated),
"pull-request-card-no-diff.png",
);
expect(svg).not.toContain(COLORS.diff.additions.text);
});
test("renders pull request with diff stats", async () => {
const validated = pullRequestCardSchema.parse(
createPullRequestData(avatarDataUri),
);
const svg = await renderAndSave(
h(PullRequestCard, validated),
"pull-request-card-diff.png",
);
expect(svg).toContain(COLORS.diff.additions.text);
});
test("renders pull request with long title", async () => {
const data = createLongTitlePullRequestData(avatarDataUri, {
pullRequestNumber: 42,
});
const validated = pullRequestCardSchema.parse(data);
await renderAndSave(
h(PullRequestCard, validated),
"pull-request-card-long-title.png",
);
});
test("renders pull request with long author handle", async () => {
const data = createPullRequestData(avatarDataUri, {
authorHandle: "very.very.long.tangled.org",
});
const validated = pullRequestCardSchema.parse(data);
await renderAndSave(
h(PullRequestCard, validated),
"pull-request-card-long-handle.png",
);
});
});
describe("avatar", () => {
test("shows the did's gradient when the image fails to load", async () => {
const fetch = spyOn(globalThis, "fetch").mockResolvedValue(
new Response("blob not found", { status: 400 }),
);
const svg = await renderAndSave(
h(Avatar, {
src: "https://avatar.tangled.sh/sig/did:plc:3rwz3xfw2crswgifqgc3g7zh?size=256",
size: 120,
}),
"avatar-failed.png",
).finally(() => fetch.mockRestore());
expect(svg).toContain("<linearGradient");
expect(svg).toContain("<radialGradient");
expect(svg).not.toMatch(/stop-color="rgb\(\d+ /);
});
test("draws no gradient without a did", async () => {
const svg = await renderAndSave(h(Avatar, { size: 120 }), "avatar-none.png");
expect(svg).not.toContain("Gradient");
});
});
describe("profile cards", () => {
test("renders profile", async () => {
const validated = profileCardSchema.parse(createProfileData(avatarDataUri));
await renderAndSave(h(ProfileCard, validated), "profile-card.png");
});
test("renders profile without description or activity", async () => {
const validated = profileCardSchema.parse(
createProfileData(avatarDataUri, {
handle: "very.very.long.handle.bsky.social",
description: undefined,
activity: [],
}),
);
await renderAndSave(h(ProfileCard, validated), "profile-card-empty.png");
});
});
describe("string cards", () => {
test("renders string", async () => {
const validated = stringCardSchema.parse(createStringData(avatarDataUri));
await renderAndSave(h(StringCard, validated), "string-card.png");
});
test("renders string with long filename", async () => {
const validated = stringCardSchema.parse(
createStringData(avatarDataUri, {
filename: "reproduce-websocket-memory-leak-under-load.test.ts",
description: undefined,
}),
);
await renderAndSave(h(StringCard, validated), "string-card-long-filename.png");
});
});
describe("commit cards", () => {
test("renders commit", async () => {
const validated = commitCardSchema.parse(createCommitData(avatarDataUri));
await renderAndSave(h(CommitCard, validated), "commit-card.png");
});
test("renders commit without diff stats", async () => {
const validated = commitCardSchema.parse(
createCommitData(avatarDataUri, {
subject:
"fix critical memory leak in WebSocket connection handler that causes server crashes under high load conditions in production environments",
filesChanged: 0,
additions: 0,
deletions: 0,
}),
);
await renderAndSave(h(CommitCard, validated), "commit-card-no-diff.png");
});
});
describe("file cards", () => {
test("renders file", async () => {
const validated = fileCardSchema.parse(createFileData(avatarDataUri));
await renderAndSave(h(FileCard, validated), "file-card.png");
});
test("renders file with deep path", async () => {
const validated = fileCardSchema.parse(
createFileData(avatarDataUri, {
path: "web/src/routes/[handle]/[repo]/pulls/[rkey]/[version]/[[range]]/PullViewPage.svelte",
ref: "fix/websocket-memory-leak-under-load",
sizeLabel: undefined,
}),
);
await renderAndSave(h(FileCard, validated), "file-card-deep-path.png");
});
});
describe("tag cards", () => {
test("renders annotated tag", async () => {
const validated = tagCardSchema.parse(createTagData(avatarDataUri));
await renderAndSave(h(TagCard, validated), "tag-card.png");
});
test("renders lightweight tag", async () => {
const validated = tagCardSchema.parse(
createTagData(avatarDataUri, {
name: "release-candidate-2026.10.21-rc.12",
message: undefined,
taggerName: undefined,
createdAt: "",
}),
);
await renderAndSave(h(TagCard, validated), "tag-card-lightweight.png");
});
});
describe("pipeline cards", () => {
test("renders successful push", async () => {
const validated = pipelineCardSchema.parse(createPipelineData(avatarDataUri));
await renderAndSave(h(PipelineCard, validated), "pipeline-card.png");
});
test("renders failed pull request", async () => {
const validated = pipelineCardSchema.parse(
createPipelineData(avatarDataUri, {
workflow: "test",
status: "failed",
durationMs: 41000,
trigger: { kind: "pull_request", ref: "master", source: "stack-sidebar" },
}),
);
await renderAndSave(h(PipelineCard, validated), "pipeline-card-failed.png");
});
test("renders running manual run", async () => {
const validated = pipelineCardSchema.parse(
createPipelineData(avatarDataUri, {
workflow: "integration-tests-against-every-supported-database",
status: "running",
durationMs: 0,
trigger: { kind: "manual" },
}),
);
await renderAndSave(h(PipelineCard, validated), "pipeline-card-running.png");
});
});