feat: add Umamusume, from Game8's banner list
uma.moe stays declined — its API sits behind a Cloudflare Turnstile proof header, and an adapter would mean defeating a deliberate access control. This is the surface that is simply open, and it needed the parser change in the commit before this one to be readable at all. The stable URL matters: Game8 also publishes monthly "August 2026 Release Schedule" pages whose id changes every month, which a static registry cannot follow. "List of All Banners" does not move. Four current banners, both columns of the side-by-side table, and none of the Previous Banners below them. The tenth game8 source, so it carries the same CI blindness as the other nine and only a manual refresh moves it. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
220e73ff7a
commit
04c69fd3df
@@ -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, seventeen sources across
|
**Status: working app, refreshing itself on a schedule.** Schema, eight parsers, eighteen sources across
|
||||||
sixteen games, the full interface, offline support, a static server, a Docker image and CI all exist and
|
seventeen 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/ 674 tests
|
test/ 683 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 nine sources and `fandom` two;
|
- **Parsers are keyed by site, not game.** One `game8` parser serves ten 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
|
||||||
@@ -256,9 +256,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. Nine of the
|
- **Space requests to one host**, honouring its `Crawl-delay` and defaulting to 2s. Ten of the
|
||||||
seventeen sources are game8.co pages, so the per-source floor alone still permits one cycle to arrive
|
eighteen sources are game8.co pages, so the per-source floor alone still permits one cycle to arrive
|
||||||
as nine back-to-back requests to a single site — which is the shape an edge network throttles, and
|
as ten 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.**
|
||||||
@@ -270,9 +270,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 nine game8 sources, from the first
|
`202 Accepted` with a bot-management body to every one of the ten 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 nine
|
cleanly from a normal address. So `robots.txt` permits us and the network does not, and those ten
|
||||||
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
|
||||||
@@ -300,6 +300,7 @@ re-litigated each pass:
|
|||||||
| `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 |
|
| `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 |
|
||||||
|
| `game8.co/games/Umamusume-Pretty-Derby` | **Built** (2026-08-19), off the stable `List of All Banners` page, not the monthly release-schedule pages whose URL changes every month. Cost a widening of `game8.ts`'s section and column vocabulary — see § Working on parsers |
|
||||||
|
|
||||||
`.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
|
||||||
@@ -625,6 +626,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 nine sources cannot be fetched
|
so one live wiki cannot vouch for a stalled sibling. Given that ten 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", "czn",
|
"ba", "fgo", "holodori", "gfl2", "stellasora", "czn", "uma",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
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, Blue Archive and Chaos Zero Nightmare carry **no `resetOffsets`
|
Infinity Nikki, P5X, Blue Archive, Chaos Zero Nightmare and Umamusume 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, Chaos Zero Nightmare |
|
| `game8` | game8.co article calendars | Genshin, Star Rail, Wuthering Waves, ZZZ, Endfield, NTE, Infinity Nikki, Persona 5: The Phantom X, Chaos Zero Nightmare, Umamusume |
|
||||||
| `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 sixteen games here have cost the event schema nothing. Per-game *metadata* does occasionally
|
held: the seventeen 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 sixteen games share a stack — so each bar
|
event a bar is, and hue alone cannot answer that once seventeen 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": "uma:3-star-guaranteed-1-5-anniversary-scout-character:2026-07-22",
|
||||||
|
"game": "uma",
|
||||||
|
"title": "3 Star Guaranteed 1.5 Anniversary Scout (Character)",
|
||||||
|
"type": "other",
|
||||||
|
"summary": null,
|
||||||
|
"startsAt": "2026-07-22T00:00:00.000Z",
|
||||||
|
"startPrecision": "day",
|
||||||
|
"endsAt": "2026-08-27T00:00:00.000Z",
|
||||||
|
"endPrecision": "day",
|
||||||
|
"regionScoped": false,
|
||||||
|
"regionEnds": null,
|
||||||
|
"sourceUrl": "https://game8.co/games/Umamusume-Pretty-Derby/archives/536311",
|
||||||
|
"sourceId": "uma-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": "uma:3-star-guaranteed-1-5-anniversary-scout-support:2026-07-22",
|
||||||
|
"game": "uma",
|
||||||
|
"title": "3 Star Guaranteed 1.5 Anniversary Scout (Support)",
|
||||||
|
"type": "other",
|
||||||
|
"summary": null,
|
||||||
|
"startsAt": "2026-07-22T00:00:00.000Z",
|
||||||
|
"startPrecision": "day",
|
||||||
|
"endsAt": "2026-08-27T00:00:00.000Z",
|
||||||
|
"endPrecision": "day",
|
||||||
|
"regionScoped": false,
|
||||||
|
"regionEnds": null,
|
||||||
|
"sourceUrl": "https://game8.co/games/Umamusume-Pretty-Derby/archives/536311",
|
||||||
|
"sourceId": "uma-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": "uma:daiichi-ruby-power-ssr-and-k-s-miracle-guts-sr:2026-08-12",
|
||||||
|
"game": "uma",
|
||||||
|
"title": "Daiichi Ruby Power SSR and K.S. Miracle Guts SR",
|
||||||
|
"type": "other",
|
||||||
|
"summary": null,
|
||||||
|
"startsAt": "2026-08-12T00:00:00.000Z",
|
||||||
|
"startPrecision": "day",
|
||||||
|
"endsAt": "2026-08-21T00:00:00.000Z",
|
||||||
|
"endPrecision": "day",
|
||||||
|
"regionScoped": false,
|
||||||
|
"regionEnds": null,
|
||||||
|
"sourceUrl": "https://game8.co/games/Umamusume-Pretty-Derby/archives/536311",
|
||||||
|
"sourceId": "uma-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": "uma:seeking-the-pearl-rocket-star:2026-08-12",
|
||||||
|
"game": "uma",
|
||||||
|
"title": "Seeking the Pearl (Rocket☆Star)",
|
||||||
|
"type": "other",
|
||||||
|
"summary": null,
|
||||||
|
"startsAt": "2026-08-12T00:00:00.000Z",
|
||||||
|
"startPrecision": "day",
|
||||||
|
"endsAt": "2026-08-21T00:00:00.000Z",
|
||||||
|
"endPrecision": "day",
|
||||||
|
"regionScoped": false,
|
||||||
|
"regionEnds": null,
|
||||||
|
"sourceUrl": "https://game8.co/games/Umamusume-Pretty-Derby/archives/536311",
|
||||||
|
"sourceId": "uma-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
@@ -154,6 +154,21 @@ const SOURCES: SourceSpec[] = [
|
|||||||
url: "https://game8.co/games/Chaos-Zero-Nightmare/archives/559899",
|
url: "https://game8.co/games/Chaos-Zero-Nightmare/archives/559899",
|
||||||
parserId: "game8",
|
parserId: "game8",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "uma-game8-events",
|
||||||
|
game: "uma",
|
||||||
|
// `uma.moe` stays declined — its API sits behind a Cloudflare Turnstile
|
||||||
|
// proof header, and an adapter would mean defeating a deliberate access
|
||||||
|
// control. This is the surface that is simply open.
|
||||||
|
//
|
||||||
|
// The stable URL matters here: Game8 also publishes monthly
|
||||||
|
// `August 2026 Release Schedule` pages whose id changes every month, which
|
||||||
|
// a static registry cannot follow. "List of All Banners" does not move.
|
||||||
|
//
|
||||||
|
// The tenth game8 source, with the same CI blindness as the other nine.
|
||||||
|
url: "https://game8.co/games/Umamusume-Pretty-Derby/archives/536311",
|
||||||
|
parserId: "game8",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function toAdapter(spec: SourceSpec): Adapter {
|
function toAdapter(spec: SourceSpec): Adapter {
|
||||||
|
|||||||
@@ -135,6 +135,11 @@ export const GAMES: Record<GameId, GameMeta> = {
|
|||||||
// No `resetOffsets`: the source is Game8, which states day-precision prose
|
// 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.
|
// 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" },
|
czn: { id: "czn", name: "Chaos Zero Nightmare", short: "CZN", hue: "#B84A9C", studio: "Smilegate", dailyTasks: "Daily missions" },
|
||||||
|
// No `resetOffsets`. The source's own column is headed `Availability (UTC)`,
|
||||||
|
// which states the zone the *dates* are in and says nothing about where the
|
||||||
|
// server's day rolls — and every row on it is a bare date anyway, with no
|
||||||
|
// time of day to read a reset out of. Same silence as p5x and czn.
|
||||||
|
uma: { id: "uma", name: "Umamusume: Pretty Derby", short: "Umamusume", hue: "#6FBF44", studio: "Cygames", dailyTasks: "Daily races, missions" },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GAME_LIST: GameMeta[] = Object.values(GAMES);
|
export const GAME_LIST: GameMeta[] = Object.values(GAMES);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const GameId = z.enum([
|
|||||||
"gfl2", // Girls' Frontline 2: Exilium
|
"gfl2", // Girls' Frontline 2: Exilium
|
||||||
"stellasora", // Stella Sora
|
"stellasora", // Stella Sora
|
||||||
"czn", // Chaos Zero Nightmare
|
"czn", // Chaos Zero Nightmare
|
||||||
|
"uma", // Umamusume: Pretty Derby
|
||||||
]);
|
]);
|
||||||
export type GameId = z.infer<typeof GameId>;
|
export type GameId = z.infer<typeof GameId>;
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ const CASES: Array<{ adapter: Adapter; fixture: string }> = [
|
|||||||
{ 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" },
|
{ adapter: adapter("czn-game8-events"), fixture: "fixtures/czn/game8-events-2026-08-19" },
|
||||||
|
{ adapter: adapter("uma-game8-events"), fixture: "fixtures/uma/game8-events-2026-08-19" },
|
||||||
];
|
];
|
||||||
|
|
||||||
async function runAdapter(adapter: Adapter, fixture: string) {
|
async function runAdapter(adapter: Adapter, fixture: string) {
|
||||||
@@ -1352,6 +1353,39 @@ describe("Chaos Zero Nightmare (game8)", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Umamusume (game8 banner pages)", () => {
|
||||||
|
const fixture = "fixtures/uma/game8-events-2026-08-19";
|
||||||
|
const uma = adapter("uma-game8-events");
|
||||||
|
|
||||||
|
test("reads the four current banners, both columns of them", async () => {
|
||||||
|
// Counted off the page: `All Current Banners` lays Standard and Paid
|
||||||
|
// banners side by side inside one <table>, two rows each.
|
||||||
|
const events = await runAdapter(uma, fixture);
|
||||||
|
expect(events.map((e) => e.title)).toEqual([
|
||||||
|
"3 Star Guaranteed 1.5 Anniversary Scout (Character)",
|
||||||
|
"3 Star Guaranteed 1.5 Anniversary Scout (Support)",
|
||||||
|
"Daiichi Ruby Power SSR and K.S. Miracle Guts SR",
|
||||||
|
"Seeking the Pearl (Rocket☆Star)",
|
||||||
|
]);
|
||||||
|
const pearl = events.find((e) => e.title === "Seeking the Pearl (Rocket☆Star)");
|
||||||
|
// The page prints "8/12/2026 - 8/21/2026" and no time of day.
|
||||||
|
expect(pearl?.startsAt).toBe("2026-08-12T00:00:00.000Z");
|
||||||
|
expect(pearl?.endsAt).toBe("2026-08-21T00:00:00.000Z");
|
||||||
|
expect(pearl?.startPrecision).toBe("day");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("never publishes the Previous Banners table", async () => {
|
||||||
|
// Dated exactly like the live rows and sitting directly below them, so the
|
||||||
|
// only thing separating them is the heading. `previous events` does not
|
||||||
|
// match `Previous Banners` — which is why that pattern was added.
|
||||||
|
const titles = (await runAdapter(uma, fixture)).map((e) => e.title);
|
||||||
|
expect(titles).not.toContain("Yukino Bijin (Darl'n Snowflake)");
|
||||||
|
expect(titles).not.toContain(
|
||||||
|
"Smart Falcon Power SSR and Silence Suzuka Speed SSR",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("game8 column vocabulary", () => {
|
describe("game8 column vocabulary", () => {
|
||||||
// The parser directly rather than through an adapter: `canParse` guards the
|
// The parser directly rather than through an adapter: `canParse` guards the
|
||||||
// seam against a redesigned *page*, and these are hand-built table snippets.
|
// seam against a redesigned *page*, and these are hand-built table snippets.
|
||||||
|
|||||||
Reference in New Issue
Block a user