feat: add Stella Sora, from the wiki's front page rather than its banner list
This wiki publishes the same schedule twice and the fuller surface is the worse one. /wiki/Banner_List has 55 clean rows with full wall clocks and states no timezone anywhere on the page; the front page's Current Banners module emits the same instants as real <time datetime="...-07:00"> elements. The two agree exactly, which is strong evidence the list is UTC and is still only evidence — so we read the surface that says what it means and pay four live banners instead of a full history for it. If an editor ever states the zone on Banner_List, that page becomes the better source immediately. Two ways this could have failed silently. The template writes its BEM underscores as __, so a selector written against the class name a browser shows matches nothing at all; and banner names are red links to ?action=edit&redlink=1, which Miraheze's robots.txt disallows, so a href carrying a query is refused and the page URL stands in. No resetOffsets, and here the source did state an offset: -07:00, which is US Pacific and therefore shifts twice a year. That is the Fate/Grand Order gap arriving through a page that looks like it answered the question. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4dff14087f
commit
ecbf9c0422
@@ -43,6 +43,7 @@ const CASES: Array<{ adapter: Adapter; fixture: string }> = [
|
||||
{ adapter: adapter("fgo-fandom-events"), fixture: "fixtures/fgo/fandom-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("stellasora-stellasorawiki-events"), fixture: "fixtures/stellasora/stellasorawiki-events-2026-08-19" },
|
||||
];
|
||||
|
||||
async function runAdapter(adapter: Adapter, fixture: string) {
|
||||
@@ -1184,3 +1185,132 @@ describe("IOP Wiki (Girls' Frontline 2)", () => {
|
||||
).toThrow(/redesigned/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Stella Sora wiki", () => {
|
||||
const fixture = "fixtures/stellasora/stellasorawiki-events-2026-08-19";
|
||||
const ss = adapter("stellasora-stellasorawiki-events");
|
||||
|
||||
function parse(html: string) {
|
||||
return ss.parse(html, {
|
||||
now: NOW,
|
||||
sourceUrl: ss.url,
|
||||
sourceId: ss.id,
|
||||
game: ss.game,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The module as the template actually emits it — BEM underscores escaped as
|
||||
* `__`, which is the detail a selector written from a browser's view
|
||||
* of the page gets wrong.
|
||||
*/
|
||||
const banner = (name: string, href: string, start: string, end: string) =>
|
||||
`<div class="stellasora-home-banner">
|
||||
<div class="stellasora-home-banner__name"><a href="${href}">${name}</a></div>
|
||||
<div class="stellasora-home-banner__period">
|
||||
<time class="stellasora-time" datetime="${start}">x</time> —
|
||||
<time class="stellasora-time" datetime="${end}">y</time>
|
||||
</div></div>`;
|
||||
|
||||
const carded = (banners: string) =>
|
||||
`<div class="stellasora-home-card stellasora-home-card--current stellasora-home-current">
|
||||
<div class="stellasora-home-current__banners">${banners}</div></div>
|
||||
<div class="stellasora-home-card stellasora-home-card--navigation">
|
||||
<div class="stellasora-home-banner">
|
||||
<div class="stellasora-home-banner__name">Not a banner</div>
|
||||
<div><time datetime="2026-08-01T00:00-07:00">z</time>
|
||||
<time datetime="2026-12-01T00:00-07:00">z</time></div></div></div>`;
|
||||
|
||||
test("reads the four live banners, converting the stated offset", async () => {
|
||||
const events = await runAdapter(ss, fixture);
|
||||
expect(events.map((e) => e.title)).toEqual([
|
||||
"A Breezy Romance",
|
||||
"Afternoon Glimmer into the Green",
|
||||
"Bloom to the Bright Sun",
|
||||
"Tinges of Rainbow",
|
||||
]);
|
||||
// 2026-08-17T20:00-07:00 → 03:00Z the next day. Cross-checked against the
|
||||
// wiki's own `Banner_List`, which prints `2026-08-18 03:00:00` for this
|
||||
// banner — the agreement that shows the unzoned table is UTC, and is still
|
||||
// only evidence, which is why we read this page instead.
|
||||
const bloom = events.find((e) => e.title === "Bloom to the Bright Sun");
|
||||
expect(bloom?.startsAt).toBe("2026-08-18T03:00:00.000Z");
|
||||
expect(bloom?.endsAt).toBe("2026-09-08T02:59:00.000Z");
|
||||
for (const e of events) {
|
||||
expect(e.startPrecision).toBe("exact");
|
||||
expect(e.endPrecision).toBe("exact");
|
||||
expect(e.type).toBe("banner");
|
||||
}
|
||||
});
|
||||
|
||||
test("finds the module through its HTML-escaped class name", () => {
|
||||
// The template writes `__` where a browser shows `__`. A selector
|
||||
// written against the decoded name matches nothing and empties the lane
|
||||
// with no error anywhere, which is the failure this codebase ranks worst.
|
||||
const events = parse(
|
||||
carded(
|
||||
banner("A Breezy Romance", "/wiki/A_Breezy_Romance", "2026-08-03T21:00-07:00", "2026-08-24T12:59-07:00"),
|
||||
),
|
||||
);
|
||||
expect(events).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("stops at the next card rather than reading the whole page", () => {
|
||||
// Five more cards follow this one. An unbounded slice would publish
|
||||
// whichever of them grows a `<time>` element next.
|
||||
const events = parse(
|
||||
carded(
|
||||
banner("A Breezy Romance", "/wiki/A_Breezy_Romance", "2026-08-03T21:00-07:00", "2026-08-24T12:59-07:00"),
|
||||
),
|
||||
);
|
||||
expect(events.map((e) => e.title)).toEqual(["A Breezy Romance"]);
|
||||
});
|
||||
|
||||
test("refuses a red link's `?action=` href and falls back to the page", async () => {
|
||||
// Miraheze's robots.txt disallows `?action=`, and a create-page form is the
|
||||
// wrong place to send a reader. `holodori.ts` makes the same call.
|
||||
const events = await runAdapter(ss, fixture);
|
||||
const red = events.find((e) => e.title === "A Breezy Romance");
|
||||
expect(red?.sourceUrl).toBe("https://stellasora.miraheze.org/wiki/Main_Page");
|
||||
// The ones whose articles exist do get linked.
|
||||
expect(events.find((e) => e.title === "Tinges of Rainbow")?.sourceUrl).toBe(
|
||||
"https://stellasora.miraheze.org/wiki/Tinges_of_Rainbow/2026-08-17",
|
||||
);
|
||||
});
|
||||
|
||||
test("drops a banner whose datetime states no offset", () => {
|
||||
// The sibling `Banner_List` prints exactly this — a wall clock with no zone
|
||||
// anywhere on the page — and reading it as UTC is the assumption this
|
||||
// source was chosen to avoid.
|
||||
const events = parse(
|
||||
carded(banner("Zoneless", "/wiki/Zoneless", "2026-08-03T21:00", "2026-08-24T12:59")),
|
||||
);
|
||||
expect(events).toEqual([]);
|
||||
});
|
||||
|
||||
test("drops a banner missing one of its two timestamps", () => {
|
||||
// A start paired with an end read off the next banner would be a
|
||||
// confidently wrong date rather than a missing one.
|
||||
const events = parse(
|
||||
carded(
|
||||
`<div class="stellasora-home-banner">
|
||||
<div class="stellasora-home-banner__name">Half</div>
|
||||
<div><time datetime="2026-08-03T21:00-07:00">x</time></div></div>`,
|
||||
),
|
||||
);
|
||||
expect(events).toEqual([]);
|
||||
});
|
||||
|
||||
test("a redesign fails the source instead of emptying the lane", () => {
|
||||
expect(() =>
|
||||
parse('<div class="stellasora-home-card">no module here</div>'),
|
||||
).toThrow(/redesigned/);
|
||||
// The module present but its `<time>` children gone is equally a redesign.
|
||||
expect(() =>
|
||||
parse(
|
||||
`<div class="stellasora-home-current__banners">
|
||||
<div class="stellasora-home-banner">Aug 3, 2026 — Aug 24, 2026</div></div>`,
|
||||
),
|
||||
).toThrow(/redesigned/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user