feat: add Chaos Zero Nightmare, which the game8 parser already reads
No parser work at all — a GameId, a games entry, a SOURCES line, a fixture and a test. The page lists six current events and the parser publishes four; the two it skips print "Start Date: -", and no start means no event ID, so that is the skip-never-guess rule working rather than a silent drop. The cost is worth stating plainly: this is the ninth game8 source, and game8's edge answers the Actions runner with a 202 and a bot-management body, so the lane is built from a checked-in fixture in CI from day one and only a manual `bun run refresh` moves it. freshness() discloses that in the footer, which is what it is for. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ecbf9c0422
commit
6c7369db10
@@ -37,8 +37,8 @@ A web app that aggregates live and upcoming events across popular gacha games, p
|
|||||||
calendar, sorts them by end date or by what the reader is partway through, tracks day-by-day
|
calendar, sorts them by end date or by what the reader is partway through, tracks day-by-day
|
||||||
progress on events that repeat daily, and lets a user mark events completed.
|
progress on events that repeat daily, and lets a user mark events completed.
|
||||||
|
|
||||||
**Status: working app, refreshing itself on a schedule.** Schema, eight parsers, sixteen sources across
|
**Status: working app, refreshing itself on a schedule.** Schema, eight parsers, seventeen sources across
|
||||||
fifteen games, the full interface, offline support, a static server, a Docker image and CI all exist and
|
sixteen games, the full interface, offline support, a static server, a Docker image and CI all exist and
|
||||||
are tested. The refresh runner (`bun run refresh`) fetches, caches raw snapshots and rebuilds the
|
are tested. The refresh runner (`bun run refresh`) fetches, caches raw snapshots and rebuilds the
|
||||||
feed; `.github/workflows/refresh.yml` runs it twice a day and commits only when a page actually
|
feed; `.github/workflows/refresh.yml` runs it twice a day and commits only when a page actually
|
||||||
changed. The SQLite layer and the review queue are still specified in `docs/` but not built, so the
|
changed. The SQLite layer and the review queue are still specified in `docs/` but not built, so the
|
||||||
@@ -119,7 +119,7 @@ src/client/ React app, service worker, manifest
|
|||||||
theme.ts — dark or light, and what a game hue reads as on each
|
theme.ts — dark or light, and what a game hue reads as on each
|
||||||
scripts/ build-feed.ts, build-static.ts, parse-fixture.ts (offline), refresh-sources.ts (fetches)
|
scripts/ build-feed.ts, build-static.ts, parse-fixture.ts (offline), refresh-sources.ts (fetches)
|
||||||
serve.ts static server + /api/health
|
serve.ts static server + /api/health
|
||||||
test/ 660 tests
|
test/ 670 tests
|
||||||
fixtures/<game>/ raw HTML + .expected.json per source — pinned, kept forever
|
fixtures/<game>/ raw HTML + .expected.json per source — pinned, kept forever
|
||||||
snapshots/ current page per source, rewritten by refresh — see its README
|
snapshots/ current page per source, rewritten by refresh — see its README
|
||||||
```
|
```
|
||||||
@@ -164,7 +164,7 @@ These come from how gacha games actually schedule things, and they cause most bu
|
|||||||
- **Skip, never guess.** Every function in `dates.ts` returns `null` rather than inferring a missing
|
- **Skip, never guess.** Every function in `dates.ts` returns `null` rather than inferring a missing
|
||||||
year, month, or end. `readColumnTable` drops a row it cannot date. An omitted event is a
|
year, month, or end. `readColumnTable` drops a row it cannot date. An omitted event is a
|
||||||
recoverable disappointment; a confidently wrong date is the failure this product exists to prevent.
|
recoverable disappointment; a confidently wrong date is the failure this product exists to prevent.
|
||||||
- **Parsers are keyed by site, not game.** One `game8` parser serves eight sources and `fandom` two;
|
- **Parsers are keyed by site, not game.** One `game8` parser serves nine sources and `fandom` two;
|
||||||
`wikigg`, `akwiki`, `bawiki`, `holodoriwiki`, `iopwiki` and `stellasorawiki` serve one each — the first two share a host family
|
`wikigg`, `akwiki`, `bawiki`, `holodoriwiki`, `iopwiki` and `stellasorawiki` serve one each — the first two share a host family
|
||||||
and have entirely different templates, and the last two are both Miraheze wikis whose page
|
and have entirely different templates, and the last two are both Miraheze wikis whose page
|
||||||
templates have nothing in common. Adding a source for a known site is one `SOURCES` entry; a new
|
templates have nothing in common. Adding a source for a known site is one `SOURCES` entry; a new
|
||||||
@@ -242,9 +242,9 @@ Sources are community wikis. Treat them as a guest would:
|
|||||||
|
|
||||||
- Honor `robots.txt`; set a descriptive `User-Agent` with a contact URL.
|
- Honor `robots.txt`; set a descriptive `User-Agent` with a contact URL.
|
||||||
- One request per source per refresh cycle, minimum 6 hours apart.
|
- One request per source per refresh cycle, minimum 6 hours apart.
|
||||||
- **Space requests to one host**, honouring its `Crawl-delay` and defaulting to 2s. Eight of the
|
- **Space requests to one host**, honouring its `Crawl-delay` and defaulting to 2s. Nine of the
|
||||||
sixteen sources are game8.co pages, so the per-source floor alone still permits one cycle to arrive
|
seventeen sources are game8.co pages, so the per-source floor alone still permits one cycle to arrive
|
||||||
as eight back-to-back requests to a single site — which is the shape an edge network throttles, and
|
as nine back-to-back requests to a single site — which is the shape an edge network throttles, and
|
||||||
what a burst looks like from the far end regardless of our intent.
|
what a burst looks like from the far end regardless of our intent.
|
||||||
- Send `If-None-Match` / `If-Modified-Since`; treat `304` as "skip, unchanged".
|
- Send `If-None-Match` / `If-Modified-Since`; treat `304` as "skip, unchanged".
|
||||||
- Cache raw snapshots so re-parsing never re-fetches. **Iterate against fixtures, not the network.**
|
- Cache raw snapshots so re-parsing never re-fetches. **Iterate against fixtures, not the network.**
|
||||||
@@ -256,9 +256,9 @@ training, and no `User-agent: *` rule applies to our paths. Keep it that way: do
|
|||||||
rate, and do not add an LLM that consumes page content.
|
rate, and do not add an LLM that consumes page content.
|
||||||
|
|
||||||
**game8.co does not answer a GitHub Actions runner** (confirmed 2026-08-17). Its edge returns
|
**game8.co does not answer a GitHub Actions runner** (confirmed 2026-08-17). Its edge returns
|
||||||
`202 Accepted` with a bot-management body to every one of the eight game8 sources, from the first
|
`202 Accepted` with a bot-management body to every one of the nine game8 sources, from the first
|
||||||
scheduled cycle onward — `last confirmed: never` — while the same URLs return `200` and parse
|
scheduled cycle onward — `last confirmed: never` — while the same URLs return `200` and parse
|
||||||
cleanly from a normal address. So `robots.txt` permits us and the network does not, and those eight
|
cleanly from a normal address. So `robots.txt` permits us and the network does not, and those nine
|
||||||
games have only ever been built from checked-in fixtures in CI.
|
games have only ever been built from checked-in fixtures in CI.
|
||||||
|
|
||||||
The per-host spacing above does not fix this and was not meant to: a 202 on the very first request
|
The per-host spacing above does not fix this and was not meant to: a 202 on the very first request
|
||||||
@@ -285,6 +285,7 @@ re-litigated each pass:
|
|||||||
| `prydwen.gg`, `gametora.com` | **Cleared, unbuilt.** `User-agent: *` allows the paths we would want. prydwen sets `Crawl-delay: 10`, far below our one-per-6h |
|
| `prydwen.gg`, `gametora.com` | **Cleared, unbuilt.** `User-agent: *` allows the paths we would want. prydwen sets `Crawl-delay: 10`, far below our one-per-6h |
|
||||||
| `iopwiki.com` | **Built** (2026-08-19), Girls' Frontline 2 — see § IOP Wiki below. `robots.txt` is two lines, `User-agent: *` and `Crawl-Delay: 20`, no `Disallow` anywhere |
|
| `iopwiki.com` | **Built** (2026-08-19), Girls' Frontline 2 — see § IOP Wiki below. `robots.txt` is two lines, `User-agent: *` and `Crawl-Delay: 20`, no `Disallow` anywhere |
|
||||||
| `stellasora.miraheze.org` | **Built** (2026-08-19), from the front page's `Current Banners` module and **not** `/wiki/Banner_List` — see § Stella Sora below |
|
| `stellasora.miraheze.org` | **Built** (2026-08-19), from the front page's `Current Banners` module and **not** `/wiki/Banner_List` — see § Stella Sora below |
|
||||||
|
| `game8.co/games/Chaos-Zero-Nightmare` | **Built** (2026-08-19). Zero parser work — the existing `game8` parser reads it. The ninth game8 source, so fixture-backed in CI from day one |
|
||||||
|
|
||||||
`.github/ISSUE_TEMPLATE/feature_request.yml` points readers at that table by heading, so a source
|
`.github/ISSUE_TEMPLATE/feature_request.yml` points readers at that table by heading, so a source
|
||||||
request can be checked against it before anyone writes it up — the loudest feedback on the first
|
request can be checked against it before anyone writes it up — the loudest feedback on the first
|
||||||
@@ -610,6 +611,6 @@ to an open page). Four things hold it up:
|
|||||||
(PRD F7). `freshness()` in `src/shared/feed.ts` is the one definition: it takes the newest
|
(PRD F7). `freshness()` in `src/shared/feed.ts` is the one definition: it takes the newest
|
||||||
`lastSuccessAt` and **never `generatedAt`**, which is a build stamp that would call a
|
`lastSuccessAt` and **never `generatedAt`**, which is a build stamp that would call a
|
||||||
fixture-backed calendar minutes old, and it treats a game as only as fresh as its *oldest* source,
|
fixture-backed calendar minutes old, and it treats a game as only as fresh as its *oldest* source,
|
||||||
so one live wiki cannot vouch for a stalled sibling. Given that eight sources cannot be fetched
|
so one live wiki cannot vouch for a stalled sibling. Given that nine sources cannot be fetched
|
||||||
from CI at all (§ Scraping conduct), this disclosure is the only thing standing between a reader and
|
from CI at all (§ Scraping conduct), this disclosure is the only thing standing between a reader and
|
||||||
a confidently stale calendar — do not let a future change source it from the build clock.
|
a confidently stale calendar — do not let a future change source it from the build clock.
|
||||||
|
|||||||
+2
-2
@@ -10,7 +10,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
export const GameId = z.enum([
|
export const GameId = z.enum([
|
||||||
"genshin", "hsr", "zzz", "wuwa", "arknights", "endfield", "nte", "nikki", "p5x", "r1999",
|
"genshin", "hsr", "zzz", "wuwa", "arknights", "endfield", "nte", "nikki", "p5x", "r1999",
|
||||||
"ba", "fgo", "holodori", "gfl2", "stellasora",
|
"ba", "fgo", "holodori", "gfl2", "stellasora", "czn",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const EventType = z.enum([
|
export const EventType = z.enum([
|
||||||
@@ -308,7 +308,7 @@ nothing to add, which is the point worth carrying to the next game: an override
|
|||||||
the game moves nobody, while the same override added a year later re-labels every tick already
|
the game moves nobody, while the same override added a year later re-labels every tick already
|
||||||
logged under the old clock. Get it right at introduction or accept a migration.
|
logged under the old clock. Get it right at introduction or accept a migration.
|
||||||
|
|
||||||
Infinity Nikki, P5X and Blue Archive carry **no `resetOffsets`
|
Infinity Nikki, P5X, Blue Archive and Chaos Zero Nightmare carry **no `resetOffsets`
|
||||||
entry**, so they take the regional default. That is an assumption, not a verified server map — none
|
entry**, so they take the regional default. That is an assumption, not a verified server map — none
|
||||||
of those sources states one. Two of the 2026-08-19 additions are worth separating out, because they
|
of those sources states one. Two of the 2026-08-19 additions are worth separating out, because they
|
||||||
are silent for different reasons and neither is the usual one:
|
are silent for different reasons and neither is the usual one:
|
||||||
|
|||||||
+1
-1
@@ -48,7 +48,7 @@ Consequences worth internalising:
|
|||||||
|
|
||||||
| Parser | Site | Sources using it |
|
| Parser | Site | Sources using it |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `game8` | game8.co article calendars | Genshin, Star Rail, Wuthering Waves, ZZZ, Endfield, NTE, Infinity Nikki, Persona 5: The Phantom X |
|
| `game8` | game8.co article calendars | Genshin, Star Rail, Wuthering Waves, ZZZ, Endfield, NTE, Infinity Nikki, Persona 5: The Phantom X, Chaos Zero Nightmare |
|
||||||
| `wikigg` | wiki.gg MediaWiki `mp-event` templates | Endfield |
|
| `wikigg` | wiki.gg MediaWiki `mp-event` templates | Endfield |
|
||||||
| `akwiki` | arknights.wiki.gg's `mrfz-wtable` "Ongoing/upcoming" table | Arknights |
|
| `akwiki` | arknights.wiki.gg's `mrfz-wtable` "Ongoing/upcoming" table | Arknights |
|
||||||
| `fandom` | Fandom wikis via the MediaWiki `action=parse` API — two page templates: `Event \| Time Period \| Version` wikitables, and FGO's picture-fenced `ONGOING EVENTS` blocks | Reverse: 1999, Fate/Grand Order |
|
| `fandom` | Fandom wikis via the MediaWiki `action=parse` API — two page templates: `Event \| Time Period \| Version` wikitables, and FGO's picture-fenced `ONGOING EVENTS` blocks | Reverse: 1999, Fate/Grand Order |
|
||||||
|
|||||||
+2
-2
@@ -59,7 +59,7 @@ have a source — this table is the launch scope, not a roster to keep in sync.
|
|||||||
|
|
||||||
Adding a game must require no change to `GachaEvent` — only a `GameId` entry, its `games.ts`
|
Adding a game must require no change to `GachaEvent` — only a `GameId` entry, its `games.ts`
|
||||||
metadata, and a source registration. That is the test of whether the data model is right, and it has
|
metadata, and a source registration. That is the test of whether the data model is right, and it has
|
||||||
held: the fifteen games here have cost the event schema nothing. Per-game *metadata* does occasionally
|
held: the sixteen games here have cost the event schema nothing. Per-game *metadata* does occasionally
|
||||||
grow (Reverse: 1999 needed `resetHourLocal` for a 05:00 reset), which is a different file and moves
|
grow (Reverse: 1999 needed `resetHourLocal` for a 05:00 reset), which is a different file and moves
|
||||||
no stored key. A game may have several sources; see `docs/INGESTION.md` § Three layers.
|
no stored key. A game may have several sources; see `docs/INGESTION.md` § Three layers.
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ default `"game"` — the board every existing reader already has). Merged, the o
|
|||||||
`endingSoonestFirst`, the same comparator behind the list's "Ending soonest" (F2), so the two views
|
`endingSoonestFirst`, the same comparator behind the list's "Ending soonest" (F2), so the two views
|
||||||
cannot mean different things by the same words and an unannounced end still sorts behind every dated
|
cannot mean different things by the same words and an unannounced end still sorts behind every dated
|
||||||
one rather than claiming a place in the queue. Merged there is also no lane heading to say whose
|
one rather than claiming a place in the queue. Merged there is also no lane heading to say whose
|
||||||
event a bar is, and hue alone cannot answer that once fifteen games share a stack — so each bar
|
event a bar is, and hue alone cannot answer that once sixteen games share a stack — so each bar
|
||||||
carries its game's short name, with the full name in its tooltip. A bar too narrow for a tag and a
|
carries its game's short name, with the full name in its tooltip. A bar too narrow for a tag and a
|
||||||
title both keeps the title: a chopped game name reads as a broken word, and the hue and the tooltip
|
title both keeps the title: a chopped game name reads as a broken word, and the hue and the tooltip
|
||||||
still answer it.
|
still answer it.
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "czn:following-the-foxs-footsteps:2026-05-27",
|
||||||
|
"game": "czn",
|
||||||
|
"title": "Following the Fox's Footsteps",
|
||||||
|
"type": "other",
|
||||||
|
"summary": "Check out Hilde's story by Following the Fox's Footsteps!",
|
||||||
|
"startsAt": "2026-05-27T00:00:00.000Z",
|
||||||
|
"startPrecision": "day",
|
||||||
|
"endsAt": "2026-06-17T00:00:00.000Z",
|
||||||
|
"endPrecision": "day",
|
||||||
|
"regionScoped": false,
|
||||||
|
"regionEnds": null,
|
||||||
|
"sourceUrl": "https://game8.co/games/Chaos-Zero-Nightmare/archives/559899",
|
||||||
|
"sourceId": "czn-game8-events",
|
||||||
|
"status": "published",
|
||||||
|
"confidence": 0.85,
|
||||||
|
"extractionMethod": "parser",
|
||||||
|
"version": 1,
|
||||||
|
"firstSeenAt": "2026-08-14T00:00:00.000Z",
|
||||||
|
"updatedAt": "2026-08-14T00:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "czn:beach-cafe-festival:2026-07-29",
|
||||||
|
"game": "czn",
|
||||||
|
"title": "Beach Cafe Festival",
|
||||||
|
"type": "other",
|
||||||
|
"summary": "Beach Cafe Festival has you collecting puzzle pieces by fulfilling seasonal missions to unlock pieces of Midsummer Night Memory photos, and earn rewards based on the number of puzzle pieces you've acquired.",
|
||||||
|
"startsAt": "2026-07-29T00:00:00.000Z",
|
||||||
|
"startPrecision": "day",
|
||||||
|
"endsAt": "2026-09-30T00:00:00.000Z",
|
||||||
|
"endPrecision": "day",
|
||||||
|
"regionScoped": false,
|
||||||
|
"regionEnds": null,
|
||||||
|
"sourceUrl": "https://game8.co/games/Chaos-Zero-Nightmare/archives/559899",
|
||||||
|
"sourceId": "czn-game8-events",
|
||||||
|
"status": "published",
|
||||||
|
"confidence": 0.85,
|
||||||
|
"extractionMethod": "parser",
|
||||||
|
"version": 1,
|
||||||
|
"firstSeenAt": "2026-08-14T00:00:00.000Z",
|
||||||
|
"updatedAt": "2026-08-14T00:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "czn:chasing-the-remanants-of-light:2026-07-29",
|
||||||
|
"game": "czn",
|
||||||
|
"title": "Chasing the Remanants of Light",
|
||||||
|
"type": "other",
|
||||||
|
"summary": "Login daily for a total of 14 days and earn rewards!",
|
||||||
|
"startsAt": "2026-07-29T00:00:00.000Z",
|
||||||
|
"startPrecision": "day",
|
||||||
|
"endsAt": "2026-09-08T00:00:00.000Z",
|
||||||
|
"endPrecision": "day",
|
||||||
|
"regionScoped": false,
|
||||||
|
"regionEnds": null,
|
||||||
|
"sourceUrl": "https://game8.co/games/Chaos-Zero-Nightmare/archives/559899",
|
||||||
|
"sourceId": "czn-game8-events",
|
||||||
|
"status": "published",
|
||||||
|
"confidence": 0.85,
|
||||||
|
"extractionMethod": "parser",
|
||||||
|
"version": 1,
|
||||||
|
"firstSeenAt": "2026-08-14T00:00:00.000Z",
|
||||||
|
"updatedAt": "2026-08-14T00:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "czn:virtual-tactical-simulation-hilde:2026-07-29",
|
||||||
|
"game": "czn",
|
||||||
|
"title": "Virtual Tactical Simulation - Hilde",
|
||||||
|
"type": "other",
|
||||||
|
"summary": "Virtual Tactical Simulation for combatant Hilde will be available for use during Hilde's banner duration. Play her trial stage using fixed save data to obtain rewards while also testing the new combatant.",
|
||||||
|
"startsAt": "2026-07-29T00:00:00.000Z",
|
||||||
|
"startPrecision": "day",
|
||||||
|
"endsAt": "2026-08-19T00:00:00.000Z",
|
||||||
|
"endPrecision": "day",
|
||||||
|
"regionScoped": false,
|
||||||
|
"regionEnds": null,
|
||||||
|
"sourceUrl": "https://game8.co/games/Chaos-Zero-Nightmare/archives/559899",
|
||||||
|
"sourceId": "czn-game8-events",
|
||||||
|
"status": "published",
|
||||||
|
"confidence": 0.85,
|
||||||
|
"extractionMethod": "parser",
|
||||||
|
"version": 1,
|
||||||
|
"firstSeenAt": "2026-08-14T00:00:00.000Z",
|
||||||
|
"updatedAt": "2026-08-14T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
File diff suppressed because one or more lines are too long
@@ -143,6 +143,17 @@ const SOURCES: SourceSpec[] = [
|
|||||||
url: "https://stellasora.miraheze.org/wiki/Main_Page",
|
url: "https://stellasora.miraheze.org/wiki/Main_Page",
|
||||||
parserId: "stellasorawiki",
|
parserId: "stellasorawiki",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "czn-game8-events",
|
||||||
|
game: "czn",
|
||||||
|
// The ninth game8 source, and the cost is worth stating plainly: game8's
|
||||||
|
// edge answers the Actions runner with a 202 and a bot-management body
|
||||||
|
// (AGENTS.md § Scraping conduct), so this lane is built from a checked-in
|
||||||
|
// fixture in CI from day one and only a manual `bun run refresh` moves it.
|
||||||
|
// `freshness()` discloses that in the footer, which is what it is for.
|
||||||
|
url: "https://game8.co/games/Chaos-Zero-Nightmare/archives/559899",
|
||||||
|
parserId: "game8",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function toAdapter(spec: SourceSpec): Adapter {
|
function toAdapter(spec: SourceSpec): Adapter {
|
||||||
|
|||||||
@@ -132,6 +132,9 @@ export const GAMES: Record<GameId, GameMeta> = {
|
|||||||
// starts, 12:59 and 19:59 ends), so there is no reset hour to read off it
|
// starts, 12:59 and 19:59 ends), so there is no reset hour to read off it
|
||||||
// either. See the fgo entry below for the same reasoning at more length.
|
// either. See the fgo entry below for the same reasoning at more length.
|
||||||
stellasora: { id: "stellasora", name: "Stella Sora", short: "Stella Sora", hue: "#2E9E9E", studio: "Yostar", dailyTasks: "Daily missions" },
|
stellasora: { id: "stellasora", name: "Stella Sora", short: "Stella Sora", hue: "#2E9E9E", studio: "Yostar", dailyTasks: "Daily missions" },
|
||||||
|
// No `resetOffsets`: the source is Game8, which states day-precision prose
|
||||||
|
// and no clock at all — the same silence p5x has, and the same answer.
|
||||||
|
czn: { id: "czn", name: "Chaos Zero Nightmare", short: "CZN", hue: "#B84A9C", studio: "Smilegate", dailyTasks: "Daily missions" },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GAME_LIST: GameMeta[] = Object.values(GAMES);
|
export const GAME_LIST: GameMeta[] = Object.values(GAMES);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const GameId = z.enum([
|
|||||||
"holodori", // hololive Dreams
|
"holodori", // hololive Dreams
|
||||||
"gfl2", // Girls' Frontline 2: Exilium
|
"gfl2", // Girls' Frontline 2: Exilium
|
||||||
"stellasora", // Stella Sora
|
"stellasora", // Stella Sora
|
||||||
|
"czn", // Chaos Zero Nightmare
|
||||||
]);
|
]);
|
||||||
export type GameId = z.infer<typeof GameId>;
|
export type GameId = z.infer<typeof GameId>;
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const CASES: Array<{ adapter: Adapter; fixture: string }> = [
|
|||||||
{ adapter: adapter("holodori-holodoriwiki-events"), fixture: "fixtures/holodori/holodoriwiki-events-2026-08-18" },
|
{ adapter: adapter("holodori-holodoriwiki-events"), fixture: "fixtures/holodori/holodoriwiki-events-2026-08-18" },
|
||||||
{ adapter: adapter("gfl2-iopwiki-events"), fixture: "fixtures/gfl2/iopwiki-events-2026-08-19" },
|
{ adapter: adapter("gfl2-iopwiki-events"), fixture: "fixtures/gfl2/iopwiki-events-2026-08-19" },
|
||||||
{ adapter: adapter("stellasora-stellasorawiki-events"), fixture: "fixtures/stellasora/stellasorawiki-events-2026-08-19" },
|
{ adapter: adapter("stellasora-stellasorawiki-events"), fixture: "fixtures/stellasora/stellasorawiki-events-2026-08-19" },
|
||||||
|
{ adapter: adapter("czn-game8-events"), fixture: "fixtures/czn/game8-events-2026-08-19" },
|
||||||
];
|
];
|
||||||
|
|
||||||
async function runAdapter(adapter: Adapter, fixture: string) {
|
async function runAdapter(adapter: Adapter, fixture: string) {
|
||||||
@@ -1314,3 +1315,39 @@ describe("Stella Sora wiki", () => {
|
|||||||
).toThrow(/redesigned/);
|
).toThrow(/redesigned/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Chaos Zero Nightmare (game8)", () => {
|
||||||
|
const fixture = "fixtures/czn/game8-events-2026-08-19";
|
||||||
|
const czn = adapter("czn-game8-events");
|
||||||
|
|
||||||
|
test("reuses the game8 parser with no new parsing code", () => {
|
||||||
|
// The whole point of this source: a ninth game8 page is a registry entry,
|
||||||
|
// a fixture and a test. If this ever stops being true the page has moved
|
||||||
|
// to a template the parser does not know.
|
||||||
|
expect(czn.parserId).toBe("game8");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("publishes the four datable events and skips the two without a start", async () => {
|
||||||
|
// The page carries six current events. Two print `Start Date: -`, and no
|
||||||
|
// start means no event ID — so skipping them is the rule working rather
|
||||||
|
// than a silent drop. Counted independently off the fixture: six
|
||||||
|
// `Start Date` rows, two of them `-`.
|
||||||
|
const events = await runAdapter(czn, fixture);
|
||||||
|
expect(events).toHaveLength(4);
|
||||||
|
|
||||||
|
const titles = events.map((e) => e.title);
|
||||||
|
expect(titles).toContain("Beach Cafe Festival");
|
||||||
|
expect(titles).toContain("Chasing the Remanants of Light");
|
||||||
|
// Both appear on the page and neither states a start.
|
||||||
|
expect(titles).not.toContain("Full-Scale Offensive Season 3");
|
||||||
|
expect(titles).not.toContain("Virtual Tactical Simulation - Yuki");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keeps Game8's day precision rather than inventing a clock", async () => {
|
||||||
|
// Game8 prints "July 29, 2026" and no time of day anywhere on this page.
|
||||||
|
for (const e of await runAdapter(czn, fixture)) {
|
||||||
|
expect(e.startPrecision).toBe("day");
|
||||||
|
expect(e.startsAt.endsWith("T00:00:00.000Z")).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user