diff options
Diffstat (limited to 'scripts/test-scrape-window.ts')
| -rw-r--r-- | scripts/test-scrape-window.ts | 265 |
1 files changed, 265 insertions, 0 deletions
diff --git a/scripts/test-scrape-window.ts b/scripts/test-scrape-window.ts new file mode 100644 index 0000000..0bc6fee --- /dev/null +++ b/scripts/test-scrape-window.ts @@ -0,0 +1,265 @@ +/** + * 期間カバレッジテスト + * + * 各スクレーパーが SCRAPE_WINDOW_DAYS(65日)をカバーするために + * 必要な月のURLを全てフェッチしているかを検証する。 + * + * fetch をモックし、呼ばれた URL の月一覧を確認する。 + */ +import { test, describe, beforeEach, afterEach } from "node:test"; +import assert from "node:assert/strict"; + +// ---- テスト用ユーティリティ ---- + +function monthsInWindow(from: Date, windowDays: number): string[] { + const end = new Date(from); + end.setDate(end.getDate() + windowDays); + const months: string[] = []; + const d = new Date(from.getFullYear(), from.getMonth(), 1); + while (d <= end) { + months.push( + `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}` + ); + d.setMonth(d.getMonth() + 1); + } + return months; +} + +const WINDOW_DAYS = 65; + +/** fetch をモックし、呼ばれた URL 一覧を返す */ +function mockFetch(htmlByUrl: Record<string, string> = {}): { + fetchedUrls: string[]; + restore: () => void; +} { + const fetchedUrls: string[] = []; + const orig = global.fetch; + // @ts-ignore + global.fetch = async (url: string | URL) => { + const s = url.toString(); + fetchedUrls.push(s); + const html = htmlByUrl[s] ?? "<html><body></body></html>"; + return { + ok: true, + status: 200, + text: async () => html, + json: async () => ({ events: [] }), + } as unknown as Response; + }; + return { fetchedUrls, restore: () => { global.fetch = orig; } }; +} + +/** YYYY-MM 文字列が URL に含まれるか確認(複数フォーマット対応) */ +function urlCoversMonth(urls: string[], yyyymm: string): boolean { + const [y, m] = yyyymm.split("-"); + const mInt = parseInt(m, 10).toString(); // "06" → "6" + return urls.some( + (u) => + u.includes(`/${y}/${m}`) || + u.includes(`/${y}-${m}`) || + u.includes(`_${y}-${m}`) || // duo: index_2026-05.html + u.includes(`${y}${m}`) || + u.includes(`y=${y}&m=${mInt}`) || + u.includes(`y=${y}&m=${m}`) || + u.includes(encodeURIComponent(`${y}/${m}`)) || + u.includes(`date=${y}%2F${m}`) + ); +} + +// ---- ウィンドウ計算ロジック テスト ---- + +describe("monthsInWindow", () => { + test("65日窓は最大3ヶ月にまたがる(月初付近)", () => { + // 5/1 から65日 = 7/5 → May, Jun, Jul + const months = monthsInWindow(new Date("2026-05-01"), WINDOW_DAYS); + assert.deepEqual(months, ["2026-05", "2026-06", "2026-07"]); + }); + + test("65日窓は最大3ヶ月にまたがる(月末付近)", () => { + // 5/31 から65日 = 8/4 → May, Jun, Jul, Aug + const months = monthsInWindow(new Date("2026-05-31"), WINDOW_DAYS); + assert.deepEqual(months, ["2026-05", "2026-06", "2026-07", "2026-08"]); + }); + + test("年またぎ対応", () => { + // 11/15 から65日 = 2027/1/19 → Nov, Dec, Jan + const months = monthsInWindow(new Date("2026-11-15"), WINDOW_DAYS); + assert.deepEqual(months, ["2026-11", "2026-12", "2027-01"]); + }); +}); + +// ---- スクレーパー別 URL カバレッジテスト ---- + +describe("fever-shindaita: 3ヶ月分フェッチ", () => { + let restore: () => void; + let fetchedUrls: string[]; + + beforeEach(() => { + ({ fetchedUrls, restore } = mockFetch()); + }); + afterEach(() => restore()); + + test("今月・来月・再来月のURLをフェッチする", async () => { + const { scraper } = await import("../app/scrapers/fever-shindaita.js"); + await scraper.scrape(); + + const now = new Date(); + const expected = monthsInWindow(now, WINDOW_DAYS); + for (const m of expected) { + const [y, mo] = m.split("-"); + assert.ok( + fetchedUrls.some((u) => u.includes(`/${y}/${mo}/`)), + `fever-shindaita: ${m} のURLがフェッチされていない\nfetched: ${fetchedUrls.join(", ")}` + ); + } + }); +}); + +describe("liquid-room: 3ヶ月分フェッチ", () => { + let restore: () => void; + let fetchedUrls: string[]; + + beforeEach(() => { + ({ fetchedUrls, restore } = mockFetch()); + }); + afterEach(() => restore()); + + test("今月・来月・再来月のURLをフェッチする", async () => { + const { scraper } = await import("../app/scrapers/liquid-room.js"); + await scraper.scrape(); + + const now = new Date(); + const expected = monthsInWindow(now, WINDOW_DAYS); + for (const m of expected) { + assert.ok( + urlCoversMonth(fetchedUrls, m), + `liquid-room: ${m} のURLがフェッチされていない\nfetched: ${fetchedUrls.join(", ")}` + ); + } + }); +}); + +describe("club-quattro: 3ヶ月分フェッチ", () => { + let restore: () => void; + let fetchedUrls: string[]; + + beforeEach(() => { + ({ fetchedUrls, restore } = mockFetch()); + }); + afterEach(() => restore()); + + test("今月・来月・再来月のURLをフェッチする", async () => { + const { scraper } = await import("../app/scrapers/club-quattro.js"); + await scraper.scrape(); + + const now = new Date(); + const expected = monthsInWindow(now, WINDOW_DAYS); + for (const m of expected) { + assert.ok( + urlCoversMonth(fetchedUrls, m), + `club-quattro: ${m} のURLがフェッチされていない\nfetched: ${fetchedUrls.join(", ")}` + ); + } + }); +}); + +describe("meets-otsuka: 3ヶ月分フェッチ", () => { + let restore: () => void; + let fetchedUrls: string[]; + + beforeEach(() => { + ({ fetchedUrls, restore } = mockFetch()); + }); + afterEach(() => restore()); + + test("今月・来月・再来月のURLをフェッチする", async () => { + const { scraper } = await import("../app/scrapers/meets-otsuka.js"); + await scraper.scrape(); + + const now = new Date(); + const expected = monthsInWindow(now, WINDOW_DAYS); + for (const m of expected) { + assert.ok( + urlCoversMonth(fetchedUrls, m), + `meets-otsuka: ${m} のURLがフェッチされていない\nfetched: ${fetchedUrls.join(", ")}` + ); + } + }); +}); + +describe("nishieifuku-jam: 3ヶ月分フェッチ", () => { + let restore: () => void; + let fetchedUrls: string[]; + + beforeEach(() => { + ({ fetchedUrls, restore } = mockFetch()); + }); + afterEach(() => restore()); + + test("今月・来月・再来月のURLをフェッチする", async () => { + const { scraper } = await import("../app/scrapers/nishieifuku-jam.js"); + await scraper.scrape(); + + const now = new Date(); + const expected = monthsInWindow(now, WINDOW_DAYS); + for (const m of expected) { + assert.ok( + urlCoversMonth(fetchedUrls, m), + `nishieifuku-jam: ${m} のURLがフェッチされていない\nfetched: ${fetchedUrls.join(", ")}` + ); + } + }); +}); + +describe("shibuya-o: 全サブ会場 × 3ヶ月分フェッチ", () => { + let restore: () => void; + let fetchedUrls: string[]; + + beforeEach(() => { + ({ fetchedUrls, restore } = mockFetch()); + }); + afterEach(() => restore()); + + test("east/west/crest/nest × 今月・来月・再来月をフェッチする", async () => { + const { scraper } = await import("../app/scrapers/shibuya-o.js"); + await scraper.scrape(); + + const now = new Date(); + const expected = monthsInWindow(now, WINDOW_DAYS); + for (const subVenue of ["east", "west", "crest", "nest"]) { + for (const m of expected) { + assert.ok( + urlCoversMonth( + fetchedUrls.filter((u) => u.includes(`/${subVenue}/`)), + m + ), + `shibuya-o/${subVenue}: ${m} のURLがフェッチされていない` + ); + } + } + }); +}); + +describe("duo-music-exchange: 3ヶ月分フェッチ", () => { + let restore: () => void; + let fetchedUrls: string[]; + + beforeEach(() => { + ({ fetchedUrls, restore } = mockFetch()); + }); + afterEach(() => restore()); + + test("今月・来月・再来月のURLをフェッチする", async () => { + const { scraper } = await import("../app/scrapers/duo-music-exchange.js"); + await scraper.scrape(); + + const now = new Date(); + const expected = monthsInWindow(now, WINDOW_DAYS); + for (const m of expected) { + assert.ok( + urlCoversMonth(fetchedUrls, m), + `duo-music-exchange: ${m} のURLがフェッチされていない\nfetched: ${fetchedUrls.join(", ")}` + ); + } + }); +}); |
