summaryrefslogtreecommitdiff
path: root/scripts/test-scrape-window.ts
diff options
context:
space:
mode:
authoryyamashita <yyamashita@mosquit.one>2026-05-10 23:22:17 +0900
committeryyamashita <yyamashita@mosquit.one>2026-05-10 23:22:17 +0900
commitb56e79b5b288b7c9e2fef396b303afc32c9baf5d (patch)
tree28080f7f019889659ef1682f4d3661ed9650da54 /scripts/test-scrape-window.ts
parent05d2b35a85a46dde9a1264d3002ba86e02e3d5eb (diff)
Fix multi-month scrape coverage and add duo MUSIC EXCHANGE
- Extend 8 scrapers (liquid-room, shibuya-o, club-quattro, meets-otsuka, nishieifuku-jam, fever-shindaita, fad-yokohama, and new duo-music-exchange) to fetch 3 calendar months instead of 1-2, covering the full 65-day window - Add duo MUSIC EXCHANGE scraper (渋谷, ~700 cap, /schedule/YYYY/index_YYYY-MM.html) - Add npm test: Node.js built-in test runner verifies each scraper fetches all required month URLs via mocked fetch (10 tests, no extra deps) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'scripts/test-scrape-window.ts')
-rw-r--r--scripts/test-scrape-window.ts265
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(", ")}`
+ );
+ }
+ });
+});