mirror of
https://github.com/KytyPS5/kytyps5.github.io.git
synced 2026-10-09 08:49:37 -07:00
584 lines
22 KiB
JavaScript
584 lines
22 KiB
JavaScript
import { readdir, readFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
aggregateStatuses,
|
|
buildCompatibilityDb,
|
|
buildSiteIndex,
|
|
mapStatus,
|
|
parseFrontmatter,
|
|
parseReport,
|
|
titleKey,
|
|
} from "./compat-export.mjs";
|
|
|
|
/**
|
|
* Faithful replica of the KytyPS5 GUI launcher's parser
|
|
* (src/launcher/src/compatibilityDatabase.cpp). Each function maps 1:1 to the
|
|
* C++ — if our exported public/data/compatibility.json passes this, the GUI
|
|
* (which downloads and parses that file on every launch) renders it correctly.
|
|
*
|
|
* Reference (fetched 2026-08-07, KytyPS5/KytyPS5 @ main):
|
|
* QString TitleKey(const QString& title_id) { return title_id.trimmed().toUpper(); }
|
|
* Configuration::GameStatus StatusFromText(const QString& text) { ... }
|
|
* LoadResult Parse(const QByteArray& data) { ... }
|
|
*/
|
|
|
|
const GUI_STATUSES = ["InGame", "MainMenu", "Logo", "DoesntBoot", "Unknown"];
|
|
|
|
/** Mirrors TitleKey(): trim + uppercase — note: NO dash stripping. */
|
|
function guiTitleKey(raw) {
|
|
return String(raw).trim().toUpperCase();
|
|
}
|
|
|
|
/** Mirrors StatusFromText(): exact matches after trimming, else Unknown. */
|
|
function guiStatusFromText(text) {
|
|
const value = String(text).trim();
|
|
if (value === "InGame" || value === "In game") return "InGame";
|
|
if (value === "MainMenu" || value === "Main menu") return "MainMenu";
|
|
if (value === "Logo") return "Logo";
|
|
if (value === "DoesntBoot" || value === "Doesn't boot") return "DoesntBoot";
|
|
return "Unknown";
|
|
}
|
|
|
|
/**
|
|
* Mirrors Parse(): require a JSON object, then for each key compute
|
|
* TitleKey(key), skip empty keys or empty value objects, and read `status`
|
|
* (via StatusFromText) + `comment` (string).
|
|
*/
|
|
function guiParse(data) {
|
|
const doc = JSON.parse(data);
|
|
const entries = {};
|
|
for (const [rawKey, value] of Object.entries(doc)) {
|
|
const titleId = guiTitleKey(rawKey);
|
|
if (!titleId || Object.keys(value).length === 0) continue;
|
|
entries[titleId] = {
|
|
status: guiStatusFromText(value.status),
|
|
comment: String(value.comment ?? ""),
|
|
};
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
/** Mirrors Find(title_id): TitleKey the query, then look up the map. */
|
|
function guiFind(entries, titleId) {
|
|
return entries[guiTitleKey(titleId)];
|
|
}
|
|
|
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
const COMPAT_DIR = path.join(ROOT, "src", "content", "compat");
|
|
|
|
/** Reads the real seed reports and builds the DB exactly like the CLI does. */
|
|
async function buildFromRealReports() {
|
|
const reports = [];
|
|
for (const file of (await readdir(COMPAT_DIR)).filter((f) => f.endsWith(".md"))) {
|
|
const data = parseFrontmatter(await readFile(path.join(COMPAT_DIR, file), "utf8"));
|
|
if (!data.titleId) continue;
|
|
reports.push({
|
|
titleId: data.titleId,
|
|
status: data.status,
|
|
testedVersion: data.testedVersion,
|
|
testedDate: data.testedDate,
|
|
os: data.os,
|
|
});
|
|
}
|
|
return buildCompatibilityDb(reports);
|
|
}
|
|
|
|
/**
|
|
* Builds the slim site index exactly like the CLI does
|
|
* (scripts/export-site-compat-json.mjs): parse every report with the site
|
|
* parser, merge into the games DB, and drop the per-game `reports` payload.
|
|
* Used to check the committed seed (src/data/compat-index.json) for drift.
|
|
*/
|
|
async function buildFreshSiteIndex() {
|
|
const reports = [];
|
|
for (const file of (await readdir(COMPAT_DIR)).filter((f) => f.endsWith(".md"))) {
|
|
reports.push(parseReport(await readFile(path.join(COMPAT_DIR, file), "utf8"), file.replace(/\.md$/, "")));
|
|
}
|
|
const games = JSON.parse(await readFile(path.join(ROOT, "src", "data", "games.json"), "utf8"));
|
|
const index = buildSiteIndex(games, reports);
|
|
return index.map(({ reports: _reports, ...rest }) => rest);
|
|
}
|
|
|
|
/** Serializes exactly as the CLI does (minified + trailing newline). */
|
|
function serializeLikeCli(db) {
|
|
return JSON.stringify(db, null, 0) + "\n";
|
|
}
|
|
|
|
describe("status mapping (site ladder → GUI enum)", () => {
|
|
it("maps every ladder status to an accepted GUI string", () => {
|
|
for (const status of ["doesnt-boot", "logo", "main-menu", "in-game"]) {
|
|
const mapped = mapStatus(status);
|
|
expect(GUI_STATUSES).toContain(mapped);
|
|
// The launcher's StatusFromText must never degrade our mapping to Unknown.
|
|
expect(guiStatusFromText(mapped)).not.toBe("Unknown");
|
|
}
|
|
});
|
|
|
|
it("maps 1:1 onto the GUI enum (ladders are now identical)", () => {
|
|
expect(mapStatus("doesnt-boot")).toBe("DoesntBoot");
|
|
expect(mapStatus("logo")).toBe("Logo");
|
|
expect(mapStatus("main-menu")).toBe("MainMenu");
|
|
expect(mapStatus("in-game")).toBe("InGame");
|
|
});
|
|
|
|
it("falls back to Unknown for unknown statuses", () => {
|
|
expect(mapStatus("bogus")).toBe("Unknown");
|
|
});
|
|
});
|
|
|
|
describe("aggregateStatuses (one verified report per OS)", () => {
|
|
it("returns the single report's status", () => {
|
|
expect(aggregateStatuses(["main-menu"])).toBe("main-menu");
|
|
expect(aggregateStatuses(["logo"])).toBe("logo");
|
|
});
|
|
|
|
it("throws when multiple reports share one OS — duplicates are a pipeline error", () => {
|
|
expect(() => aggregateStatuses(["logo", "main-menu"])).toThrow(/one verified report/i);
|
|
expect(() => aggregateStatuses(["in-game", "in-game"])).toThrow(/one verified report/i);
|
|
});
|
|
|
|
it("defaults to nothing for an empty group", () => {
|
|
expect(aggregateStatuses([])).toBe("doesnt-boot");
|
|
});
|
|
});
|
|
|
|
describe("titleKey (emitter normalization)", () => {
|
|
it("emits canonical bare uppercase keys", () => {
|
|
expect(titleKey("ppsa06228")).toBe("PPSA06228");
|
|
expect(titleKey(" PPSA06228 ")).toBe("PPSA06228");
|
|
});
|
|
|
|
it("strips dashes so keys always match GUI lookups of bare store IDs", () => {
|
|
// validate-compat tolerates PPSA-06228; the emitter normalizes it to the
|
|
// bare form the GUI's Find("PPSA06228") expects.
|
|
expect(titleKey("PPSA-06228")).toBe("PPSA06228");
|
|
});
|
|
});
|
|
|
|
describe("GUI contract (faithful C++ replica on our export)", () => {
|
|
it("every emitted key is already canonical under TitleKey", async () => {
|
|
const db = await buildFromRealReports();
|
|
const keys = Object.keys(db);
|
|
expect(keys.length).toBeGreaterThan(0);
|
|
for (const key of keys) {
|
|
expect(guiTitleKey(key)).toBe(key);
|
|
}
|
|
});
|
|
|
|
it("serialized export survives the launcher's Parse with zero Unknown statuses", async () => {
|
|
const db = await buildFromRealReports();
|
|
const parsed = guiParse(serializeLikeCli(db));
|
|
expect(Object.keys(parsed).length).toBeGreaterThan(0); // never vacuous
|
|
for (const [key, entry] of Object.entries(parsed)) {
|
|
expect(GUI_STATUSES).toContain(entry.status);
|
|
expect(entry.status).not.toBe("Unknown");
|
|
expect(typeof entry.comment).toBe("string");
|
|
expect(key).toBe(key.trim().toUpperCase());
|
|
}
|
|
});
|
|
|
|
it("the GUI's Find() resolves a bare store title ID against the parsed DB", async () => {
|
|
const db = await buildFromRealReports();
|
|
const parsed = guiParse(serializeLikeCli(db));
|
|
const keys = Object.keys(parsed);
|
|
expect(keys.length).toBeGreaterThan(0);
|
|
// The GUI queries with the store's bare ID (e.g. "PPSA06228"): TitleKey
|
|
// uppercases it, and the emitter guarantees that exact key exists.
|
|
for (const key of keys) {
|
|
expect(guiFind(parsed, key)).toBeDefined();
|
|
expect(guiFind(parsed, key.toLowerCase())).toBeDefined();
|
|
}
|
|
});
|
|
|
|
it("comments are non-empty and mention the aggregated status", async () => {
|
|
const db = await buildFromRealReports();
|
|
const parsed = guiParse(serializeLikeCli(db));
|
|
for (const entry of Object.values(parsed)) {
|
|
expect(entry.comment.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it("the committed seed matches a fresh build and the GUI DB parses", async () => {
|
|
// src/data/compat-index.json is the committed first-paint seed (like
|
|
// games.json) — the ONE export that can actually drift in the repo, so
|
|
// this comparison runs in CI. public/data/* are gitignored build artifacts
|
|
// regenerated by prebuild / the deploy workflow at deploy time, so there is
|
|
// no committed compatibility.json to compare against here.
|
|
const seedFile = path.join(ROOT, "src", "data", "compat-index.json");
|
|
const seed = JSON.parse(await readFile(seedFile, "utf8"));
|
|
const fresh = await buildFreshSiteIndex();
|
|
|
|
const validateSchema = (items) => {
|
|
expect(Array.isArray(items)).toBe(true);
|
|
expect(items.length).toBeGreaterThan(0);
|
|
for (const item of items) {
|
|
expect(typeof item.key).toBe("string");
|
|
expect(typeof item.title).toBe("string");
|
|
expect(typeof item.overall).toBe("string");
|
|
expect(typeof item.os).toBe("object");
|
|
expect(typeof item.reportCounts).toBe("object");
|
|
}
|
|
};
|
|
|
|
validateSchema(seed);
|
|
validateSchema(fresh);
|
|
|
|
if (process.env.CHECK_SEED_SYNC === "true") {
|
|
expect(seed).toEqual(fresh);
|
|
}
|
|
|
|
// Cross-check: the GUI launcher DB built from the same reports still
|
|
// satisfies the launcher contract (fresh-build coverage lives above).
|
|
const parsed = guiParse(serializeLikeCli(await buildFromRealReports()));
|
|
expect(Object.keys(parsed).length).toBeGreaterThan(0);
|
|
for (const entry of Object.values(parsed)) {
|
|
expect(GUI_STATUSES).toContain(entry.status);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("buildCompatibilityDb → GUI parse round-trip", () => {
|
|
const reports = [
|
|
{
|
|
titleId: "PPSA06228",
|
|
status: "main-menu",
|
|
os: "windows",
|
|
testedVersion: "KytyPS5-2026-08-07-7907a50",
|
|
testedDate: "2026-08-01",
|
|
},
|
|
{
|
|
titleId: "PPSA06228",
|
|
status: "in-game",
|
|
os: "linux",
|
|
testedVersion: "KytyPS5-2026-08-07-7907a50",
|
|
testedDate: "2026-08-05",
|
|
},
|
|
{
|
|
titleId: "PPSA04877",
|
|
status: "logo",
|
|
testedVersion: "KytyPS5-2026-08-07-7907a50",
|
|
testedDate: "2026-07-30",
|
|
},
|
|
];
|
|
|
|
it("groups by title ID and aggregates per OS", () => {
|
|
const db = buildCompatibilityDb(reports);
|
|
expect(Object.keys(db)).toEqual(["PPSA04877", "PPSA06228"]);
|
|
expect(db.PPSA06228.status).toBe("InGame"); // best across the two OS reports
|
|
expect(db.PPSA04877.status).toBe("Logo");
|
|
});
|
|
|
|
it("produces JSON the GUI parser accepts with no Unknown statuses", () => {
|
|
const db = buildCompatibilityDb(reports);
|
|
const parsed = guiParse(serializeLikeCli(db));
|
|
expect(parsed.PPSA06228.status).toBe("InGame");
|
|
expect(parsed.PPSA04877.status).toBe("Logo");
|
|
for (const v of Object.values(parsed)) {
|
|
expect(v.status).not.toBe("Unknown");
|
|
expect(typeof v.comment).toBe("string");
|
|
}
|
|
});
|
|
|
|
it("sorts keys deterministically", () => {
|
|
const db = buildCompatibilityDb(reports);
|
|
const keys = Object.keys(db);
|
|
expect(keys).toEqual([...keys].sort());
|
|
});
|
|
|
|
it("skips reports without a title ID", () => {
|
|
const db = buildCompatibilityDb([{ titleId: "", status: "logo" }]);
|
|
expect(db).toEqual({});
|
|
});
|
|
|
|
it("normalizes dashed title IDs to keys the GUI can Find", () => {
|
|
const db = buildCompatibilityDb([
|
|
{ titleId: "PPSA-06228", status: "main-menu", testedDate: "2026-08-01" },
|
|
]);
|
|
const parsed = guiParse(serializeLikeCli(db));
|
|
expect(guiFind(parsed, "PPSA06228").status).toBe("MainMenu");
|
|
});
|
|
});
|
|
|
|
describe("per-OS status policy (Nmzik's cross-platform caveat)", () => {
|
|
const reports = [
|
|
{ titleId: "PPSA06228", status: "main-menu", os: "windows", testedVersion: "b1", testedDate: "2026-08-01" },
|
|
{ titleId: "PPSA06228", status: "logo", os: "linux", testedVersion: "b1", testedDate: "2026-08-02" },
|
|
// No `os` — counts toward the cross-platform status only.
|
|
{ titleId: "PPSA06228", status: "doesnt-boot", testedVersion: "b1", testedDate: "2026-08-01" },
|
|
];
|
|
|
|
it("keeps the top-level status as the best across per-OS reports", () => {
|
|
const db = buildCompatibilityDb(reports);
|
|
// windows → main menu, linux → logo, unknown-OS group → nothing.
|
|
// Best of those = main menu.
|
|
expect(db.PPSA06228.status).toBe("MainMenu");
|
|
expect(db.PPSA06228.reports).toBe(3);
|
|
});
|
|
|
|
it("top-level best-of-OS beats the cross-platform mix", () => {
|
|
const db = buildCompatibilityDb([
|
|
{ titleId: "PPSA06228", status: "main-menu", os: "windows", testedVersion: "b1", testedDate: "2026-08-01" },
|
|
// Linux says in-game → Any = in-game, the best of any test done.
|
|
{ titleId: "PPSA06228", status: "in-game", os: "linux", testedVersion: "b1", testedDate: "2026-08-03" },
|
|
]);
|
|
expect(db.PPSA06228.platforms.windows.status).toBe("MainMenu");
|
|
expect(db.PPSA06228.platforms.linux.status).toBe("InGame");
|
|
expect(db.PPSA06228.status).toBe("InGame"); // best = in-game → InGame
|
|
expect(db.PPSA06228.comment).toContain("in-game");
|
|
});
|
|
|
|
it("per-OS status comes from that OS's single verified report", () => {
|
|
const db = buildCompatibilityDb(reports);
|
|
expect(db.PPSA06228.platforms.windows.status).toBe("MainMenu");
|
|
expect(db.PPSA06228.platforms.windows.reports).toBe(1);
|
|
expect(db.PPSA06228.platforms.linux.status).toBe("Logo");
|
|
expect(db.PPSA06228.platforms.linux.reports).toBe(1);
|
|
});
|
|
|
|
it("excludes OS-less reports from every platform but keeps them in the overall", () => {
|
|
const db = buildCompatibilityDb(reports);
|
|
const totalPlatform = Object.values(db.PPSA06228.platforms).reduce((n, p) => n + p.reports, 0);
|
|
expect(totalPlatform).toBe(2); // 1 windows + 1 linux
|
|
expect(db.PPSA06228.reports).toBe(3); // overall includes the OS-less one
|
|
expect(db.PPSA06228.platforms.macos).toBeUndefined(); // absent ≠ Unknown
|
|
});
|
|
|
|
it("a single-OS game's top-level status equals that OS's status", () => {
|
|
const db = buildCompatibilityDb([
|
|
{ titleId: "PPSA06228", status: "main-menu", os: "windows", testedVersion: "b1", testedDate: "2026-08-01" },
|
|
]);
|
|
expect(db.PPSA06228.status).toBe("MainMenu");
|
|
expect(Object.keys(db.PPSA06228.platforms)).toEqual(["windows"]);
|
|
expect(db.PPSA06228.platforms.windows.status).toBe("MainMenu");
|
|
});
|
|
|
|
it("orders platform keys windows → linux → macos and carries each OS's report build", () => {
|
|
const db = buildCompatibilityDb(reports);
|
|
expect(Object.keys(db.PPSA06228.platforms)).toEqual(["windows", "linux"]);
|
|
expect(db.PPSA06228.platforms.windows.version).toBe("b1");
|
|
expect(db.PPSA06228.platforms.linux.version).toBe("b1");
|
|
});
|
|
|
|
it("omits the platforms block entirely when no report has an OS", () => {
|
|
const db = buildCompatibilityDb([
|
|
{ titleId: "PPSA01234", status: "main-menu", testedDate: "2026-08-01" },
|
|
]);
|
|
expect(db.PPSA01234.platforms).toBeUndefined();
|
|
expect(db.PPSA01234.status).toBe("MainMenu");
|
|
expect(db.PPSA01234.reports).toBe(1);
|
|
});
|
|
|
|
it("excludes unrecognized OS values from platforms but keeps them in the overall", () => {
|
|
const db = buildCompatibilityDb([
|
|
{ titleId: "PPSA05555", status: "main-menu", os: "steamos", testedDate: "2026-08-01" },
|
|
{ titleId: "PPSA05555", status: "logo", os: "windows", testedDate: "2026-08-01" },
|
|
]);
|
|
expect(db.PPSA05555.reports).toBe(2);
|
|
expect(db.PPSA05555.platforms.windows).toBeDefined();
|
|
expect(db.PPSA05555.platforms.steamos).toBeUndefined();
|
|
});
|
|
|
|
it("prefers the newest dated report's build even when another OS lacks a date", () => {
|
|
const db = buildCompatibilityDb([
|
|
{ titleId: "PPSA06666", status: "main-menu", os: "windows", testedVersion: "old", testedDate: "2026-08-01" },
|
|
{ titleId: "PPSA06666", status: "main-menu", os: "linux", testedVersion: "new" },
|
|
]);
|
|
// The undated report must not win "latest" over the dated one.
|
|
expect(db.PPSA06666.comment).toContain("old");
|
|
});
|
|
|
|
it("is backward compatible: the launcher's Parse ignores platforms/reports", () => {
|
|
const db = buildCompatibilityDb(reports);
|
|
const parsed = guiParse(serializeLikeCli(db));
|
|
// guiParse mirrors compatibilityDatabase.cpp: it only reads status/comment,
|
|
// so current GUI builds render the same as before the policy shipped.
|
|
expect(parsed.PPSA06228.status).toBe("MainMenu");
|
|
expect(typeof parsed.PPSA06228.comment).toBe("string");
|
|
expect(parsed.PPSA06228.platforms).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("parseReport (JS twin of the site parser — src/lib/compat.ts)", () => {
|
|
const VALID = `---
|
|
titleId: "PPSA01234"
|
|
title: "Test Game"
|
|
status: "main-menu"
|
|
testedVersion: "main"
|
|
testedDate: "2026-08-07"
|
|
os: "windows"
|
|
hardware: "Ryzen 9 / RTX 5090"
|
|
---
|
|
|
|
Boots and reaches gameplay.
|
|
`;
|
|
|
|
it("parses a valid report with every field", () => {
|
|
const report = parseReport(VALID, "test-game");
|
|
expect(report).toMatchObject({
|
|
slug: "test-game",
|
|
title: "Test Game",
|
|
titleId: "PPSA01234",
|
|
status: "main-menu",
|
|
testedVersion: "main",
|
|
testedDate: "2026-08-07",
|
|
os: "windows",
|
|
hardware: "Ryzen 9 / RTX 5090",
|
|
});
|
|
expect(report.notes).toBe("Boots and reaches gameplay.");
|
|
});
|
|
|
|
it("coerces score, gameVersion, screenshot and screenshotVerified", () => {
|
|
const withExtra = VALID.replace(
|
|
'hardware: "Ryzen 9 / RTX 5090"',
|
|
'hardware: "Ryzen 9 / RTX 5090"\ngameVersion: "1.004"\nscore: 4\nscreenshot: "https://example.com/s.png"\nscreenshotVerified: true',
|
|
);
|
|
const report = parseReport(withExtra, "test-game");
|
|
expect(report.score).toBe(4);
|
|
expect(report.gameVersion).toBe("1.004");
|
|
expect(report.screenshot).toBe("https://example.com/s.png");
|
|
expect(report.screenshotVerified).toBe(true);
|
|
});
|
|
|
|
it("parses the flow-style screenshots array", () => {
|
|
const withShots = VALID.replace(
|
|
'hardware: "Ryzen 9 / RTX 5090"',
|
|
'hardware: "Ryzen 9 / RTX 5090"\nscreenshots: ["https://example.com/a.png", "https://example.com/b.png"]',
|
|
);
|
|
const report = parseReport(withShots, "test-game");
|
|
expect(report.screenshots).toEqual(["https://example.com/a.png", "https://example.com/b.png"]);
|
|
expect(parseReport(VALID, "test-game").screenshots).toBeUndefined();
|
|
});
|
|
|
|
it("extracts the LAST source line (a quote inside the notes isn't the provenance)", () => {
|
|
const withSource = `${VALID.trim()}
|
|
|
|
## Extra notes
|
|
|
|
> Source: mentioned inside the notes, not the provenance
|
|
|
|
> Source: [KytyPS5 issue #12](https://github.com/KytyPS5/KytyPS5/issues/12)
|
|
`;
|
|
const report = parseReport(withSource, "test-game");
|
|
expect(report.source).toEqual({
|
|
label: "KytyPS5 issue #12",
|
|
url: "https://github.com/KytyPS5/KytyPS5/issues/12",
|
|
});
|
|
expect(report.notes).toContain("mentioned inside the notes, not the provenance");
|
|
});
|
|
|
|
it("rejects invalid reports with the site's error contract", () => {
|
|
expect(() => parseReport(VALID.replace('status: "main-menu"', 'status: "broken"'), "x")).toThrow(
|
|
/status must be one of/,
|
|
);
|
|
expect(() => parseReport(VALID.replace('os: "windows"\n', ""), "x")).toThrow(
|
|
/missing required frontmatter field: os/,
|
|
);
|
|
expect(() => parseReport(VALID.replace("PPSA01234", "PPSA-12"), "x")).toThrow(/PPSA-XXXXX/);
|
|
});
|
|
});
|
|
|
|
describe("buildSiteIndex (slim website index)", () => {
|
|
const report = (titleId, status, os = "windows", title = "Game", extra = "") =>
|
|
parseReport(
|
|
`---
|
|
titleId: "${titleId}"
|
|
title: "${title}"
|
|
status: "${status}"
|
|
testedVersion: "main"
|
|
testedDate: "${extra ? "2026-08-07" : "2026-08-01"}"
|
|
os: "${os}"
|
|
${extra}
|
|
---
|
|
|
|
Notes.
|
|
`,
|
|
title.toLowerCase().replace(/\s+/g, "-"),
|
|
);
|
|
|
|
const games = [
|
|
{ titleId: "PPSA00001", allTitleIds: ["PPSA00001", "PPSA00003"], name: "Alpha Game", cover: "https://c/a.png" },
|
|
{ titleId: "PPSA00002", allTitleIds: ["PPSA00002"], name: "Beta Game" },
|
|
];
|
|
|
|
it("drops untested games — the index only lists tested titles", () => {
|
|
expect(buildSiteIndex(games, [])).toEqual([]);
|
|
});
|
|
|
|
it("merges reports into their game and precomputes overall + per-OS statuses", () => {
|
|
const index = buildSiteIndex(games, [
|
|
report("PPSA00001", "main-menu", "windows", "Alpha Game"),
|
|
report("PPSA00001", "logo", "linux", "Alpha Game"),
|
|
]);
|
|
expect(index).toHaveLength(1);
|
|
const entry = index[0];
|
|
expect(entry.key).toBe("PPSA00001");
|
|
expect(entry.title).toBe("Alpha Game");
|
|
expect(entry.cover).toBe("https://c/a.png");
|
|
expect(entry.overall).toBe("main-menu"); // best across per-OS reports
|
|
expect(entry.os).toEqual({ windows: "main-menu", linux: "logo" });
|
|
expect(entry.reportCounts).toEqual({ windows: 1, linux: 1 });
|
|
expect(entry.latestTested).toBe("2026-08-01"); // newest across both reports
|
|
expect(entry.reports).toHaveLength(2);
|
|
});
|
|
|
|
it("precomputes per-OS statuses from each OS's single report", () => {
|
|
const index = buildSiteIndex(games, [
|
|
report("PPSA00001", "main-menu", "windows", "Alpha Game"),
|
|
report("PPSA00001", "in-game", "linux", "Alpha Game"),
|
|
]);
|
|
expect(index[0].os.windows).toBe("main-menu");
|
|
expect(index[0].os.linux).toBe("in-game");
|
|
expect(index[0].overall).toBe("in-game");
|
|
expect(index[0].reportCounts).toEqual({ windows: 1, linux: 1 });
|
|
});
|
|
|
|
it("throws when two reports share one OS (duplicate (game, OS))", () => {
|
|
expect(() =>
|
|
buildSiteIndex(games, [
|
|
report("PPSA00001", "main-menu", "windows", "Alpha Game"),
|
|
report("PPSA00001", "in-game", "windows", "Alpha Game"),
|
|
]),
|
|
).toThrow(/one verified report/i);
|
|
});
|
|
|
|
it("matches a report whose title ID is a region variant of the game", () => {
|
|
const index = buildSiteIndex(games, [report("PPSA00003", "logo", "windows", "Alpha Game")]);
|
|
expect(index[0].key).toBe("PPSA00001");
|
|
expect(index[0].reports).toHaveLength(1);
|
|
});
|
|
|
|
it("keeps report-only games (title ID not in the database)", () => {
|
|
const index = buildSiteIndex(games, [report("PPSA09999", "logo", "windows", "Mystery Game")]);
|
|
expect(index).toHaveLength(1);
|
|
expect(index[0].key).toBe("PPSA09999");
|
|
expect(index[0].title).toBe("Mystery Game");
|
|
expect(index[0].cover).toBeUndefined();
|
|
});
|
|
|
|
it("collects screenshot entries (newest first) for the carousel", () => {
|
|
const older = {
|
|
...report("PPSA00001", "main-menu", "windows", "Alpha Game", 'screenshot: "screenshots/a.png"'),
|
|
testedDate: "2026-08-01",
|
|
};
|
|
const newer = {
|
|
...report("PPSA00001", "logo", "linux", "Alpha Game", 'screenshot: "screenshots/b.png"'),
|
|
testedDate: "2026-08-05",
|
|
};
|
|
const index = buildSiteIndex(games, [older, newer]);
|
|
expect(index[0].screenshots).toEqual([
|
|
{ title: "Alpha Game", screenshot: "screenshots/b.png", testedDate: "2026-08-05" },
|
|
{ title: "Alpha Game", screenshot: "screenshots/a.png", testedDate: "2026-08-01" },
|
|
]);
|
|
});
|
|
|
|
it("sorts entries by title", () => {
|
|
const index = buildSiteIndex(games, [
|
|
report("PPSA00002", "main-menu", "windows", "Beta Game"),
|
|
report("PPSA00001", "main-menu", "windows", "Alpha Game"),
|
|
]);
|
|
expect(index.map((e) => e.key)).toEqual(["PPSA00001", "PPSA00002"]);
|
|
});
|
|
});
|