feat: add Star Rail, Wuthering Waves and Zenless Zone Zero

Three more Game8 templates. ZZZ spans each event name across two rows with a
rowspan, so a flat cell reader sees [title, "Start", date] then ["End",
date]; losing that pairing would silently halve the calendar.

Section matching also had to widen — these pages head their sections
"Featured Events", "List of Events and Schedule" and "Ongoing Events".

Verified independently of the parser: HSR's year-less summary table lists
six ongoing events and all six are captured via their detail rows, so
nothing is silently dropped.

28 events added across the three games.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-15 00:47:50 +02:00
co-authored by Claude Opus 5
parent 90be312942
commit 817ad41dcd
9 changed files with 10064 additions and 13 deletions
+53 -9
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { genshinGame8, nteGame8 } from "../../src/ingest/adapters/index.ts";
import { adapterById } from "../../src/ingest/adapters/index.ts";
import type { Adapter } from "../../src/ingest/adapters/types.ts";
import { inferType } from "../../src/ingest/parsers/game8.ts";
import { GachaEvent, type EventType } from "../../src/shared/schema.ts";
@@ -10,15 +10,21 @@ import { GachaEvent, type EventType } from "../../src/shared/schema.ts";
*/
const NOW = "2026-08-14T00:00:00.000Z";
function adapter(id: string): Adapter {
const found = adapterById(id);
if (found === undefined) throw new Error(`no adapter '${id}'`);
return found;
}
const genshinGame8 = adapter("genshin-game8-events");
const nteGame8 = adapter("nte-game8-events");
const CASES: Array<{ adapter: Adapter; fixture: string }> = [
{
adapter: genshinGame8,
fixture: "fixtures/genshin/game8-events-2026-08-14",
},
{
adapter: nteGame8,
fixture: "fixtures/nte/game8-events-2026-08-14",
},
{ adapter: genshinGame8, fixture: "fixtures/genshin/game8-events-2026-08-14" },
{ adapter: nteGame8, fixture: "fixtures/nte/game8-events-2026-08-14" },
{ adapter: adapter("hsr-game8-events"), fixture: "fixtures/hsr/game8-events-2026-08-14" },
{ adapter: adapter("wuwa-game8-events"), fixture: "fixtures/wuwa/game8-events-2026-08-14" },
{ adapter: adapter("zzz-game8-events"), fixture: "fixtures/zzz/game8-events-2026-08-14" },
];
async function runAdapter(adapter: Adapter, fixture: string) {
@@ -161,3 +167,41 @@ describe("inferType", () => {
expect(inferType(title)).toBe(expected);
});
});
describe("new source shapes", () => {
test("zzz recovers events from rowspan Start/End rows", async () => {
// The event name spans two rows, so a flat cell reader sees
// [title, "Start", date] then ["End", date]. Losing the pairing would
// silently halve the calendar.
const events = await runAdapter(
adapter("zzz-game8-events"),
"fixtures/zzz/game8-events-2026-08-14",
);
const summer = events.find((e) => e.title === "Summer Waves Rolls In");
expect(summer?.startsAt).toBe("2026-07-29T00:00:00.000Z");
expect(summer?.endsAt).toBe("2026-09-07T00:00:00.000Z");
expect(events.every((e) => e.endsAt !== null)).toBe(true);
});
test("hsr keeps events whose end is not announced", async () => {
// "Jul. 24, 2026 - End of 4.6" has a real start and no knowable end.
// Publishing it with a guessed end would be the worst possible outcome.
const events = await runAdapter(
adapter("hsr-game8-events"),
"fixtures/hsr/game8-events-2026-08-14",
);
const open = events.filter((e) => e.endsAt === null);
expect(open.length).toBeGreaterThan(0);
for (const e of open) expect(e.endPrecision).toBe("unknown");
});
test("wuwa parses ranges carrying a year on both sides", async () => {
const events = await runAdapter(
adapter("wuwa-game8-events"),
"fixtures/wuwa/game8-events-2026-08-14",
);
const jade = events.find((e) => e.title === "In Search of Lost Jade");
expect(jade?.startsAt).toBe("2026-07-30T00:00:00.000Z");
expect(jade?.endsAt).toBe("2026-08-13T00:00:00.000Z");
});
});