diff --git a/fixtures/wuwa/game8-events-2026-08-14.expected.json b/fixtures/wuwa/game8-events-2026-08-14.expected.json index 4a33620..ad4978b 100644 --- a/fixtures/wuwa/game8-events-2026-08-14.expected.json +++ b/fixtures/wuwa/game8-events-2026-08-14.expected.json @@ -4,7 +4,7 @@ "game": "wuwa", "title": "A Glimpse of Xuanfang", "type": "other", - "summary": null, + "summary": "A Glimpse of Xuanfang is an exploration based event that tasks players to complete quests, puzzles or other designated actions to fill up progress percentage.", "startsAt": "2026-07-10T00:00:00.000Z", "startPrecision": "day", "endsAt": "2026-08-19T00:00:00.000Z", @@ -25,7 +25,7 @@ "game": "wuwa", "title": "Gifts of Aftertune", "type": "other", - "summary": null, + "summary": "Gifts of Aftertune is a log-in based event where players can get 5 Lustrous Tides and 5 Radiant Tides after logging in for a total of 7 days .", "startsAt": "2026-07-10T00:00:00.000Z", "startPrecision": "day", "endsAt": "2026-08-19T00:00:00.000Z", @@ -46,7 +46,7 @@ "game": "wuwa", "title": "Gifts of Starpath", "type": "other", - "summary": null, + "summary": "Gifts of Starpath is a convene based reward event where characters can earn additional Radiant Tides by pulling on the Starpath Reverbs banner.", "startsAt": "2026-07-10T00:00:00.000Z", "startPrecision": "day", "endsAt": "2026-08-19T00:00:00.000Z", @@ -62,12 +62,33 @@ "firstSeenAt": "2026-08-14T00:00:00.000Z", "updatedAt": "2026-08-14T00:00:00.000Z" }, + { + "id": "wuwa:mingshen-notices:2026-07-10", + "game": "wuwa", + "title": "Mingshen Notices", + "type": "other", + "summary": null, + "startsAt": "2026-07-10T00:00:00.000Z", + "startPrecision": "day", + "endsAt": null, + "endPrecision": "unknown", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Wuthering-Waves/archives/453473", + "sourceId": "wuwa-game8-events", + "status": "published", + "confidence": 0.75, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-08-14T00:00:00.000Z", + "updatedAt": "2026-08-14T00:00:00.000Z" + }, { "id": "wuwa:shape-of-yesterday:2026-07-10", "game": "wuwa", "title": "Shape of Yesterday", "type": "other", - "summary": null, + "summary": "Shape of Yesterday tasks players to take photos at specific locations around Xuanfang Hold. Some of these locations may require quest progression to unlock.", "startsAt": "2026-07-10T00:00:00.000Z", "startPrecision": "day", "endsAt": null, @@ -88,7 +109,7 @@ "game": "wuwa", "title": "Lament Recon: Tacet Crisis", "type": "other", - "summary": null, + "summary": "In Lament Recon: Tacet Crisis, defeat waves of enemies by upgrading character skills and projectile related enhancements as you navigate the battlefield with your character.", "startsAt": "2026-07-11T00:00:00.000Z", "startPrecision": "day", "endsAt": "2026-08-19T00:00:00.000Z", @@ -104,6 +125,27 @@ "firstSeenAt": "2026-08-14T00:00:00.000Z", "updatedAt": "2026-08-14T00:00:00.000Z" }, + { + "id": "wuwa:recaptured-action-highlights:2026-07-16", + "game": "wuwa", + "title": "Recaptured: Action Highlights", + "type": "other", + "summary": "Recaptured: Action Highlights is a combat-photography event where players have to capture specific character actions through their idles or specific combos. Players can also add filters or effects for an additional layer of creativity.", + "startsAt": "2026-07-16T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-08-06T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://game8.co/games/Wuthering-Waves/archives/453473", + "sourceId": "wuwa-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": "wuwa:ascendant-aces:2026-07-30", "game": "wuwa", @@ -130,7 +172,7 @@ "game": "wuwa", "title": "In Search of Lost Jade", "type": "other", - "summary": null, + "summary": "In Search of Lost Jade is a web browser event where players accompany a Jadeplume Eagle out of a cave and back to its nest.", "startsAt": "2026-07-30T00:00:00.000Z", "startPrecision": "day", "endsAt": "2026-08-13T00:00:00.000Z", @@ -151,7 +193,7 @@ "game": "wuwa", "title": "Virtual Crisis: Quadrant Trials", "type": "other", - "summary": null, + "summary": "Virtual Crisis: Quadrant Trials is a boss focused combat event where players get to increase the difficulty between each stage by selecting boss upgrades or help their own characters by selecting self-buffs.", "startsAt": "2026-07-30T00:00:00.000Z", "startPrecision": "day", "endsAt": "2026-08-19T00:00:00.000Z", @@ -172,7 +214,7 @@ "game": "wuwa", "title": "Lollo Campaign: New Journey", "type": "other", - "summary": null, + "summary": "Lollo Campaign: New Journey is the newest iteration of the Lollo Campaign event where players get to earn stamps by logging in and spending Waveplates for a chance to spin a reward wheel.", "startsAt": "2026-08-06T00:00:00.000Z", "startPrecision": "day", "endsAt": "2026-08-19T00:00:00.000Z", diff --git a/src/ingest/parsers/game8.ts b/src/ingest/parsers/game8.ts index f874579..cf99f0f 100644 --- a/src/ingest/parsers/game8.ts +++ b/src/ingest/parsers/game8.ts @@ -52,6 +52,14 @@ const INCLUDED_SECTIONS = [ // nothing. /list of (all )?banners/i, /current banners/i, + // Wuthering Waves follows its listing tables with a block that repeats every + // live event as its own sub-section — the shape Persona 5 uses, and the only + // place on the page the blurbs live. The heading names a version + // ("3.6 Events Summary"), so it is matched on the two words it always ends + // with. It has to be *recognised* rather than merely fallen through: an + // unrecognised heading is read as an event name, which would put the section + // title where the first event's should be. + /events summary/i, ]; /** @@ -82,7 +90,8 @@ const EXCLUDED_SECTIONS = [ const START_LABEL = /^(event|test run|banner)?\s*start(\s+date)?$/i; const END_LABEL = /^(event|test run|banner)?\s*end(\s+date)?$/i; /** Label/value rows carrying a whole range in one cell. */ -const RANGE_LABEL = /^(availability period|event period|duration|period|dates)$/i; +const RANGE_LABEL = + /^(availability period|event period|(event )?duration|period|dates)$/i; /** Column-table header matchers. */ const COL_TITLE = /^(.*\b)?(events?|banners?)$/i; @@ -133,12 +142,16 @@ export function parseGame8EventsPage( } else if (INCLUDED_SECTIONS.some((re) => re.test(heading))) { sectionIncluded = true; currentTitle = null; - } else if (node.kind !== "h4") { - // An unrecognised h2/h3 names an event. An unrecognised h4 does not — - // Genshin uses them for sub-headings *within* one event ("Availability - // Period", "Characters & Rewards for this Test Run"), so letting one - // claim the title would rename "Character Test Runs" to the label above - // its own date table. + } else if (node.kind !== "h4" || currentTitle === null) { + // An unrecognised h2/h3 names an event. An unrecognised h4 may only + // *fill* an empty title, never take one: Genshin uses h4 for + // sub-headings *within* one event ("Availability Period", "Characters & + // Rewards for this Test Run"), and there the event's own h3 has already + // claimed the slot, so letting the label overwrite it would rename + // "Character Test Runs" to whatever sits above its date table. Where + // the slot is empty the h4 is all there is — Wuthering Waves names each + // sub-section of its events-summary block with one, and a reader that + // refuses it publishes the section heading once and drops the rest. currentTitle = heading; } continue; diff --git a/test/adapters/game8.test.ts b/test/adapters/game8.test.ts index a5d1bf4..dc1f8d5 100644 --- a/test/adapters/game8.test.ts +++ b/test/adapters/game8.test.ts @@ -217,16 +217,6 @@ describe("new source shapes", () => { 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"); - }); }); describe("endfield", () => { @@ -341,6 +331,55 @@ describe("p5x", () => { }); }); +describe("wuwa", () => { + const fixture = "fixtures/wuwa/game8-events-2026-08-14"; + + test("recovers each event's blurb from its own h4 section", async () => { + // The listing table's second column holds unlock conditions, not prose. The + // description lives further down, in a per-event section whose title is an + // h4 and whose dates are labelled `Event Duration` — so both the section + // reader and the range vocabulary have to admit it before the blurb the + // page has always carried can reach a reader. + const events = await runAdapter(adapter("wuwa-game8-events"), fixture); + const shape = events.find((e) => e.title === "Shape of Yesterday"); + expect(shape?.summary).toContain("photos at specific locations"); + const jade = events.find((e) => e.title === "In Search of Lost Jade"); + expect(jade?.summary).toContain("web browser event"); + }); + + test("the enclosing summary heading never becomes an event", async () => { + // `3.5 Events Summary` heads the block; the h4s inside it name the events. + // A reader that lets the h3 keep the title publishes the section as an + // event and swallows every h4 below it. + const events = await runAdapter(adapter("wuwa-game8-events"), fixture); + expect(events.map((e) => e.title)).not.toContain("3.5 Events Summary"); + }); + + test("publishes the events that only the summary block dates", async () => { + // Neither of these appears in the Ongoing or Upcoming tables, so before the + // summary block was readable they were silently absent from the calendar. + const events = await runAdapter(adapter("wuwa-game8-events"), fixture); + const titles = events.map((e) => e.title); + expect(titles).toContain("Recaptured: Action Highlights"); + expect(titles).toContain("Mingshen Notices"); + }); + + test("an unlock condition is still not a description", async () => { + // "Unlocked by default" sits where the blurb would go and the page offers + // no prose for this event anywhere, so the slot stays empty. + const events = await runAdapter(adapter("wuwa-game8-events"), fixture); + const aces = events.find((e) => e.title === "Ascendant Aces"); + expect(aces?.summary).toBeNull(); + }); + + test("parses ranges carrying a year on both sides", async () => { + const events = await runAdapter(adapter("wuwa-game8-events"), fixture); + 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"); + }); +}); + describe("arknights wiki", () => { const fixture = "fixtures/arknights/akwiki-events-2026-08-17"; const akwiki = adapter("arknights-akwiki-events");