mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-07-14 18:15:56 -04:00
- Replace bun:test with Vitest across all 22 test files - Create @sofa/test package with shared setup and DB test helpers - setup.ts: vi.mock for @sofa/db/client, Bun.randomUUIDv7 polyfill - db.ts: in-memory SQLite via better-sqlite3, seed helpers - Add per-project vitest configs (packages/core, apps/web) - Add root vitest.config.ts with projects and v8 coverage - Set up @vitest/browser + Playwright for web component tests - Move validateBackupDatabase from core/backup.ts to @sofa/db/client - Derive required backup tables from schema instead of hard-coded list - Update CI workflow with Playwright install and Codecov upload - Update CLAUDE.md documentation for Vitest Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
155 lines
4.3 KiB
TypeScript
155 lines
4.3 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
|
|
import { personFilmography, persons } from "@sofa/db/schema";
|
|
import { clearAllTables, eq, testDb } from "@sofa/test/db";
|
|
|
|
const TINY_PNG = Buffer.from(
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIHWP4//8/AwAI/AL+X2NDNwAAAABJRU5ErkJggg==",
|
|
"base64",
|
|
);
|
|
|
|
const defaultPersonDetails = {
|
|
id: 100,
|
|
name: "Updated Person",
|
|
biography: "Bio",
|
|
birthday: null,
|
|
deathday: null,
|
|
place_of_birth: null,
|
|
profile_path: "/new-profile.png",
|
|
known_for_department: "Acting",
|
|
popularity: 10,
|
|
imdb_id: null,
|
|
};
|
|
|
|
const defaultCombinedCredits = {
|
|
cast: [
|
|
{
|
|
id: 501,
|
|
media_type: "movie",
|
|
title: "Cached Movie",
|
|
name: undefined,
|
|
overview: "Overview",
|
|
release_date: "2024-01-01",
|
|
first_air_date: undefined,
|
|
poster_path: "/poster.png",
|
|
backdrop_path: "/backdrop.png",
|
|
popularity: 5,
|
|
vote_average: 7.5,
|
|
vote_count: 42,
|
|
character: "Lead",
|
|
},
|
|
],
|
|
crew: [],
|
|
};
|
|
|
|
const { mockGetPersonDetails, mockGetPersonCombinedCredits } = vi.hoisted(() => ({
|
|
mockGetPersonDetails: vi.fn(),
|
|
mockGetPersonCombinedCredits: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@sofa/tmdb/client", () => ({
|
|
getPersonDetails: mockGetPersonDetails,
|
|
getPersonCombinedCredits: mockGetPersonCombinedCredits,
|
|
}));
|
|
|
|
import { fetchFullFilmography, getOrFetchPerson } from "../src/person";
|
|
|
|
let combinedCreditsCalls = 0;
|
|
|
|
beforeEach(() => {
|
|
clearAllTables();
|
|
process.env.IMAGE_CACHE_ENABLED = "false";
|
|
combinedCreditsCalls = 0;
|
|
mockGetPersonDetails.mockImplementation(async () => ({ ...defaultPersonDetails }));
|
|
mockGetPersonCombinedCredits.mockImplementation(async () => {
|
|
combinedCreditsCalls++;
|
|
return { cast: [...defaultCombinedCredits.cast], crew: [] };
|
|
});
|
|
vi.spyOn(globalThis, "fetch").mockImplementation((async (
|
|
_input: string | URL | Request,
|
|
_init?: RequestInit,
|
|
) => {
|
|
return new Response(TINY_PNG, {
|
|
status: 200,
|
|
headers: { "content-type": "image/png" },
|
|
});
|
|
}) as typeof fetch);
|
|
});
|
|
|
|
afterEach(() => {
|
|
delete process.env.IMAGE_CACHE_ENABLED;
|
|
});
|
|
|
|
describe("getOrFetchPerson", () => {
|
|
test("backfills a missing profile thumbhash for an already-fetched person", async () => {
|
|
testDb
|
|
.insert(persons)
|
|
.values({
|
|
id: "p1",
|
|
tmdbId: 100,
|
|
name: "Existing Person",
|
|
profilePath: "/profile.png",
|
|
profileThumbHash: null,
|
|
lastFetchedAt: new Date(),
|
|
})
|
|
.run();
|
|
|
|
const person = await getOrFetchPerson("p1");
|
|
const stored = testDb.select().from(persons).where(eq(persons.id, "p1")).get();
|
|
|
|
expect(person?.profileThumbHash).toBeTypeOf("string");
|
|
expect(stored?.profileThumbHash).toBe(person?.profileThumbHash);
|
|
});
|
|
|
|
test("replaces a stale profile thumbhash when shell hydration changes the image path", async () => {
|
|
testDb
|
|
.insert(persons)
|
|
.values({
|
|
id: "p1",
|
|
tmdbId: 100,
|
|
name: "Shell Person",
|
|
profilePath: "/old-profile.png",
|
|
profileThumbHash: "stale-hash",
|
|
lastFetchedAt: null,
|
|
})
|
|
.run();
|
|
|
|
const person = await getOrFetchPerson("p1");
|
|
const stored = testDb.select().from(persons).where(eq(persons.id, "p1")).get();
|
|
|
|
expect(stored?.profilePath).toBe("/new-profile.png");
|
|
expect(person?.profileThumbHash).toBeTypeOf("string");
|
|
expect(person?.profileThumbHash).not.toBe("stale-hash");
|
|
expect(stored?.profileThumbHash).toBe(person?.profileThumbHash);
|
|
});
|
|
});
|
|
|
|
describe("fetchFullFilmography", () => {
|
|
test("stores and reuses synced filmography rows from the database", async () => {
|
|
testDb
|
|
.insert(persons)
|
|
.values({
|
|
id: "p1",
|
|
tmdbId: 100,
|
|
name: "Cached Person",
|
|
lastFetchedAt: new Date(),
|
|
})
|
|
.run();
|
|
|
|
const first = await fetchFullFilmography("p1");
|
|
const second = await fetchFullFilmography("p1");
|
|
const storedPerson = testDb.select().from(persons).where(eq(persons.id, "p1")).get();
|
|
const storedCredits = testDb
|
|
.select()
|
|
.from(personFilmography)
|
|
.where(eq(personFilmography.personId, "p1"))
|
|
.all();
|
|
|
|
expect(first).toHaveLength(1);
|
|
expect(second).toEqual(first);
|
|
expect(combinedCreditsCalls).toBe(1);
|
|
expect(storedPerson?.filmographyLastFetchedAt).toBeInstanceOf(Date);
|
|
expect(storedCredits).toHaveLength(1);
|
|
});
|
|
});
|