From 4749dfbd701f27e45b549d58ad2de74549b4a226 Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Fri, 23 Jan 2026 14:12:18 +0000 Subject: [PATCH 1/3] docs: Summarize contest_table_provider in dev-notes (#2989) --- .../plan.md | 762 ------------------ .../plan.md | 351 -------- .../plan.md | 335 -------- .../plan.md | 482 ----------- .../plan.md | 133 --- .../plan.md | 699 ---------------- .../plan.md | 541 ------------- .../plan.md | 541 ------------- .../plan.md | 480 ----------- .../plan.md | 424 ---------- .../plan.md | 315 -------- .../how-to-add-contest-table-provider.md | 705 ++++++++++++++++ 12 files changed, 705 insertions(+), 5063 deletions(-) delete mode 100644 docs/dev-notes/2025-11-01/add_and_refactoring_tests_for_contest_table_provider/plan.md delete mode 100644 docs/dev-notes/2025-11-03/add_tests_for_contest_table_provider/plan.md delete mode 100644 docs/dev-notes/2025-11-06/add_tests_for_contest_table_provider/plan.md delete mode 100644 docs/dev-notes/2025-11-12/add_provider_key_for_contest_table_provider/plan.md delete mode 100644 docs/dev-notes/2025-11-14/add_tests_for_contest_table_provider/plan.md delete mode 100644 docs/dev-notes/2025-11-15/add_tests_for_contest_table_provider/plan.md delete mode 100644 docs/dev-notes/2025-11-19/add_tests_for_contest_table_provider/plan.md delete mode 100644 docs/dev-notes/2025-12-03/add_tests_for_contest_table_provider/plan.md delete mode 100644 docs/dev-notes/2025-12-10/add_tests_for_contest_table_provider/plan.md delete mode 100644 docs/dev-notes/2025-12-11/add_tests_for_contest_table_provider/plan.md delete mode 100644 docs/dev-notes/2025-12-17/add_tests_for_contest_table_provider/plan.md create mode 100644 docs/guides/how-to-add-contest-table-provider.md diff --git a/docs/dev-notes/2025-11-01/add_and_refactoring_tests_for_contest_table_provider/plan.md b/docs/dev-notes/2025-11-01/add_and_refactoring_tests_for_contest_table_provider/plan.md deleted file mode 100644 index 356cc5a19..000000000 --- a/docs/dev-notes/2025-11-01/add_and_refactoring_tests_for_contest_table_provider/plan.md +++ /dev/null @@ -1,762 +0,0 @@ -# ContestTableProvider テスト追加・リファクタリング計画 - -**作成日**: 2025-11-01 - -**対象ブランチ**: #2776 - -**優先度**: High(コアロジックの品質保証) - ---- - -## 1. 概要 - -### 背景 - -`src/lib/utils/contest_table_provider.ts` と関連するプロバイダー実装により、異なるコンテスト形式のテーブル生成ロジックが統一されました。 - -- 既存テスト: `src/test/lib/utils/contest_table_provider.test.ts` -- 新規プロバイダー: `TessokuBookProvider` - -### 目的 - -1. **TessokuBookProvider の単体テスト追加**(8+ テストケース) - - 複数コンテストの問題を扱う特殊な構造に対応 - - A01-A77、B01-B69、C01-C20 のセクション仕様を検証 - -2. **既存テストのリファクタリング** - - ABC テストの粒度をTypical90/EDPC と同等に強化 - - モックデータの一元管理 - - テスト可読性・保守性の向上 - -3. **テスト設計ガイドの確立** - - 新しいプロバイダー追加時のテンプレート - - fixtures 管理の標準化 - -### スコープ - -| 対象ファイル | 変更内容 | -| --------------------------------------------------------- | ---------------------------------------- | -| `src/test/lib/utils/test_cases/contest_table_provider.ts` | モックデータの追加・整理 | -| `src/test/lib/utils/contest_table_provider.test.ts` | TessokuBookProvider テスト追加 | -| `src/test/lib/utils/contest_table_provider.test.ts` | ABC テスト粒度の強化(リファクタリング) | - -**スコープ外**: - -- `task_results.test.ts` の直接修正(ただし教訓は最大限活用) -- E2E テスト -- 統合テスト - ---- - -## 2. TessokuBookProvider テスト仕様 - -### 2.1 概要 - -**Tessoku Book** は、複数のコンテスト(ABC、Typical90、数学アルゴリズム等)の問題を1つの問題集として統合したコンテスト。 - -```text -contest_id: 'tessoku-book' -task_id: 'math_and_algorithm_ai' | 'typical90_a' | 'abc007_3' | ... -task_table_index: 'A06' | 'A77' | 'B07' | 'B63' | 'C09' -``` - -### 2.2 仕様要件 - -| 項目 | 仕様 | 備考 | -| ------------------ | ----------------------------------------- | ------------------------ | -| **セクション範囲** | A01-A77、B01-B69、C01-C20 | 一部欠損あり(原典準拠) | -| **ソート順序** | 昇順(A01 → A77 → B01 → B69 → C01 → C20) | 必須 | -| **フォーマット** | 記号1文字 + 数字2文字(0 padding) | 例: A06、B63 | -| **複数ソース対応** | 異なる task_id(問題集のリンク) | DB 一意制約で保証 | - -### 2.3 テストケース(8+件) - -#### テスト1: フィルタリング - -```typescript -test('expects to filter tasks to include only tessoku-book contest', () => { - const provider = new TessokuBookProvider(ContestType.TESSOKU_BOOK); - const mixedTasks = [ - { contest_id: 'abc123', task_id: 'abc123_a', task_table_index: 'A' }, - { contest_id: 'tessoku-book', task_id: 'tesskoku_book_a', task_table_index: 'A01' }, - { contest_id: 'tessoku-book', task_id: 'math_and_algorithm_ai', task_table_index: 'A06' }, - { contest_id: 'tessoku-book', task_id: 'typical90_a', task_table_index: 'A77' }, - ]; - const filtered = provider.filter(mixedTasks); - - // 検証: contest_id === 'tessoku-book' のみ - expect(filtered?.every((task) => task.contest_id === 'tessoku-book')).toBe(true); - expect(filtered).not.toContainEqual(expect.objectContaining({ contest_id: 'abc123' })); -}); -``` - -**期待値**: `contest_id` が `tessoku-book` のタスクのみを返す -**検証方法**: `every()` + `not.toContainEqual()` - ---- - -#### テスト2: メタデータ取得 - -```typescript -test('expects to get correct metadata', () => { - const provider = new TessokuBookProvider(ContestType.TESSOKU_BOOK); - const metadata = provider.getMetadata(); - - expect(metadata.title).toBe('競技プログラミングの鉄則'); - expect(metadata.abbreviationName).toBe('tessoku-book'); -}); -``` - -**期待値**: タイトル、略称が正確 -**検証方法**: `toBe()` による厳密一致 - ---- - -#### テスト3: 表示設定 - -```typescript -test('expects to get correct display configuration', () => { - const provider = new TessokuBookProvider(ContestType.TESSOKU_BOOK); - const displayConfig = provider.getDisplayConfig(); - - expect(displayConfig.isShownHeader).toBe(false); - expect(displayConfig.isShownRoundLabel).toBe(false); - expect(displayConfig.roundLabelWidth).toBe(''); - expect(displayConfig.tableBodyCellsWidth).toBe( - 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 2xl:w-1/7 px-1 py-2', - ); - expect(displayConfig.isShownTaskIndex).toBe(true); -}); -``` - -**期待値**: ヘッダー・ラウンドラベル非表示、タスクインデックス表示 -**検証方法**: オブジェクト プロパティ照合 - ---- - -#### テスト4: ラウンドラベルフォーマット - -```typescript -test('expects to format contest round label correctly', () => { - const provider = new TessokuBookProvider(ContestType.TESSOKU_BOOK); - const label = provider.getContestRoundLabel('tessoku-book'); - - expect(label).toBe(''); -}); -``` - -**期待値**: 空文字列(ラウンド不要) -**検証方法**: `toBe('')` - ---- - -#### テスト5: テーブル生成(複数ソース対応) - -```typescript -test('expects to generate correct table structure with mixed problem sources', () => { - const provider = new TessokuBookProvider(ContestType.TESSOKU_BOOK); - const tasks = [ - { contest_id: 'tessoku-book', task_id: 'tesskoku_book_a', task_table_index: 'A01' }, - { contest_id: 'tessoku-book', task_id: 'math_and_algorithm_ai', task_table_index: 'A06' }, - { contest_id: 'tessoku-book', task_id: 'typical90_a', task_table_index: 'A77' }, - { contest_id: 'tessoku-book', task_id: 'math_and_algorithm_al', task_table_index: 'B07' }, - { contest_id: 'tessoku-book', task_id: 'abc007_3', task_table_index: 'B63' }, - { contest_id: 'tessoku-book', task_id: 'math_and_algorithm_ac', task_table_index: 'C09' }, - ]; - const table = provider.generateTable(tasks); - - expect(table).toHaveProperty('tessoku-book'); - expect(table['tessoku-book']).toHaveProperty('A06'); - expect(table['tessoku-book']['A06']).toEqual( - expect.objectContaining({ task_id: 'math_and_algorithm_ai' }), - ); -}); -``` - -**期待値**: `{ 'tessoku-book': { 'A06': {...}, 'A77': {...}, ... } }` 構造 -**検証方法**: `toHaveProperty()` + `objectContaining()` - ---- - -#### テスト6: ラウンド ID 取得 - -```typescript -test('expects to get contest round IDs correctly', () => { - const provider = new TessokuBookProvider(ContestType.TESSOKU_BOOK); - const tasks = [ - { contest_id: 'tessoku-book', task_id: 'math_and_algorithm_ai', task_table_index: 'A06' }, - { contest_id: 'tessoku-book', task_id: 'typical90_a', task_table_index: 'A77' }, - ]; - const roundIds = provider.getContestRoundIds(tasks); - - expect(roundIds).toEqual(['tessoku-book']); -}); -``` - -**期待値**: `['tessoku-book']`(単発コンテスト) -**検証方法**: `toEqual()` - ---- - -#### テスト7: ヘッダー ID 取得(昇順・複数ソース混在) - -```typescript -test('expects to get header IDs for tasks correctly in ascending order', () => { - const provider = new TessokuBookProvider(ContestType.TESSOKU_BOOK); - const tasks = [ - { contest_id: 'tessoku-book', task_id: 'tesskoku_book_a', task_table_index: 'A01' }, - { contest_id: 'tessoku-book', task_id: 'math_and_algorithm_ai', task_table_index: 'A06' }, - { contest_id: 'tessoku-book', task_id: 'typical90_a', task_table_index: 'A77' }, - { contest_id: 'tessoku-book', task_id: 'math_and_algorithm_al', task_table_index: 'B07' }, - { contest_id: 'tessoku-book', task_id: 'abc007_3', task_table_index: 'B63' }, - { contest_id: 'tessoku-book', task_id: 'math_and_algorithm_ac', task_table_index: 'C09' }, - ]; - const headerIds = provider.getHeaderIdsForTask(tasks); - - expect(headerIds).toEqual(['A06', 'A77', 'B07', 'B63', 'C09']); -}); -``` - -**期待値**: 昇順ソート済みの task_table_index 配列 -**検証方法**: `toEqual()` (順序重要) - ---- - -#### テスト8: ソート順序の厳密性(セクション境界) - -```typescript -test('expects to maintain proper sort order across all sections', () => { - const provider = new TessokuBookProvider(ContestType.TESSOKU_BOOK); - const tasks = [ - { contest_id: 'tessoku-book', task_id: 'math_and_algorithm_ac', task_table_index: 'C09' }, - { contest_id: 'tessoku-book', task_id: 'math_and_algorithm_ai', task_table_index: 'A06' }, - { contest_id: 'tessoku-book', task_id: 'abc007_3', task_table_index: 'B63' }, - ]; - const headerIds = provider.getHeaderIdsForTask(tasks); - - // A06 < B63 < C09 の順序を厳密に検証 - expect(headerIds).toEqual(['A06', 'B63', 'C09']); -}); -``` - -**期待値**: セクション間でのソート順序(A → B → C → 数字昇順) -**検証方法**: `toEqual()` - ---- - -#### テスト9: セクション範囲検証 - -```typescript -test('expects to handle section boundaries correctly (A01-A77, B01-B69, C01-C20)', () => { - const provider = new TessokuBookProvider(ContestType.TESSOKU_BOOK); - const tasks = [ - { contest_id: 'tessoku-book', task_id: 'tessoku_book_a', task_table_index: 'A01' }, - { contest_id: 'tessoku-book', task_id: 'typical90_a', task_table_index: 'A77' }, - { contest_id: 'tessoku-book', task_id: 'tessoku_book_bz', task_table_index: 'B01' }, - { contest_id: 'tessoku-book', task_id: 'tessoku_book_ep', task_table_index: 'B69' }, - { contest_id: 'tessoku-book', task_id: 'tessoku_book_ey', task_table_index: 'C01' }, - { contest_id: 'tessoku-book', task_id: 'tessoku_book_fr', task_table_index: 'C20' }, - ]; - const headerIds = provider.getHeaderIdsForTask(tasks); - - expect(headerIds).toEqual(['A01', 'A77', 'B01', 'B69', 'C01', 'C20']); -}); -``` - -**期待値**: 各セクションの境界値を正確に処理 -**検証方法**: 境界値テスト(`A01`, `A77`, `B69`, `C20`) - ---- - -#### テスト10: 空入力処理 - -```typescript -test('expects to handle empty task results', () => { - const provider = new TessokuBookProvider(ContestType.TESSOKU_BOOK); - const filtered = provider.filter([]); - - expect(filtered).toEqual([]); -}); -``` - -**期待値**: 空配列を空配列で返す -**検証方法**: `toEqual([])` - ---- - -#### テスト11: 混合コンテストタイプの排除 - -```typescript -test('expects to handle task results with different contest types', () => { - const provider = new TessokuBookProvider(ContestType.TESSOKU_BOOK); - const mixedTasks = [ - { contest_id: 'tessoku-book', task_id: 'math_and_algorithm_ai', task_table_index: 'A06' }, - { contest_id: 'abc123', task_id: 'abc123_a', task_table_index: 'A' }, - { contest_id: 'tessoku-book', task_id: 'typical90_a', task_table_index: 'A77' }, - { contest_id: 'typical90', task_id: 'typical90_b', task_table_index: 'B' }, - ]; - const filtered = provider.filter(mixedTasks); - - expect(filtered).toHaveLength(2); - expect(filtered?.every((task) => task.contest_id === 'tessoku-book')).toBe(true); -}); -``` - -**期待値**: `tessoku-book` のタスクのみ、他を完全に排除 -**検証方法**: `toHaveLength()` + `every()` - ---- - -### 2.4 モックデータ - -モックは `src/test/lib/utils/test_cases/contest_table_provider.ts` に統合管理: - -```typescript -export const taskResultsForTessokuBookProvider: TaskResults = [ - { - contest_id: 'tessoku-book', - task_id: 'math_and_algorithm_ai', - task_table_index: 'A06', - }, - { - contest_id: 'tessoku-book', - task_id: 'math_and_algorithm_o', - task_table_index: 'A27', - }, - { - contest_id: 'tessoku-book', - task_id: 'math_and_algorithm_aq', - task_table_index: 'A29', - }, - { - contest_id: 'tessoku-book', - task_id: 'math_and_algorithm_bn', - task_table_index: 'A39', - }, - { - contest_id: 'tessoku-book', - task_id: 'math_and_algorithm_an', - task_table_index: 'A63', - }, - { - contest_id: 'tessoku-book', - task_id: 'typical90_a', - task_table_index: 'A77', - }, - { - contest_id: 'tessoku-book', - task_id: 'math_and_algorithm_al', - task_table_index: 'B07', - }, - { - contest_id: 'tessoku-book', - task_id: 'dp_a', - task_table_index: 'B16', - }, - { - contest_id: 'tessoku-book', - task_id: 'math_and_algorithm_ap', - task_table_index: 'B28', - }, - { - contest_id: 'tessoku-book', - task_id: 'abc007_3', - task_table_index: 'B63', - }, - { - contest_id: 'tessoku-book', - task_id: 'math_and_algorithm_ac', - task_table_index: 'C09', - }, - { - contest_id: 'tessoku-book', - task_id: 'typical90_s', - task_table_index: 'C18', - }, -]; -``` - -**出典**: `prisma/contest_task_pairs.ts` - ---- - -## 3. 既存テストからの教訓活用 - -### 3.1 task_results.test.ts で得られた教訓 - -#### 教訓1: パラメータ化テストの効果 - -❌ **前**: Typical90、EDPC、TDPC で同じテストを3回記述 -✅ **後**: `describe.each()` でパラメータ化 - -```typescript -// 活用例: ABC テスト強化で採用 -describe.each([ - { round: 'abc378', expected: '378' }, - { round: 'abc001', expected: '1' }, -])('contests $round', ({ round, expected }) => { - test('formats label correctly', () => { - expect(provider.getContestRoundLabel(round)).toBe(expected); - }); -}); -``` - ---- - -#### 教訓2: モック関数の一元管理 - -❌ **前**: 各テストで独立した `vi.mock()` -✅ **後**: fixtures に集約 - -```typescript -// src/test/lib/utils/test_cases/contest_table_provider.ts -export const mockFunctions = { - classifyContest: vi.fn((contestId: string) => { ... }), - getContestNameLabel: vi.fn((contestId: string) => { ... }), -}; -``` - ---- - -#### 教訓3: 複数ソース対応テストの設計 - -❌ **前**: 単一の task*id のみテスト -✅ **後**: `math_and_algorithm*\_`、`typical90\_\_`、`abc*\_*` を混在テスト - -```typescript -// TessokuBook 用 -const tessokuTasks = [ - { task_id: 'math_and_algorithm_ai', ... }, - { task_id: 'typical90_a', ... }, - { task_id: 'abc007_3', ... }, -]; -``` - ---- - -#### 教訓4: エッジケースの明示的テスト - -❌ **前**: Happy path のみ -✅ **後**: セクション境界(A01、A77、B69、C20)を明示的テスト - -```typescript -test('expects to handle section boundaries correctly (A01-A77, B01-B69, C01-C20)', ...); -``` - ---- - -#### 教訓5: テスト粒度の統一化 - -❌ **前**: ABC は `getContestRoundLabel` のみ、Typical90 は `generateTable` も検証 -✅ **後**: すべてのプロバイダーで同等の粒度を適用 - -| テスト項目 | 粒度レベル | -| ---------------- | ---------------- | -| メタデータ取得 | ✓ 全プロバイダー | -| 表示設定 | ✓ 全プロバイダー | -| ラウンドラベル | ✓ 全プロバイダー | -| テーブル生成 | ✓ 全プロバイダー | -| ラウンド ID 取得 | ✓ 全プロバイダー | -| ヘッダー ID 取得 | ✓ 全プロバイダー | -| 空入力処理 | ✓ 全プロバイダー | -| 型混合処理 | ✓ 全プロバイダー | - ---- - -### 3.2 ABC テストの強化方針 - -#### 現状(リファクタリング前) - -```typescript -describe('ABC latest 20 rounds provider', () => { - test('expects to filter tasks to include only ABC contests', () => { ... }); - test('expects to limit results to the latest 20 rounds', () => { ... }); - test('expects to generate correct table structure', () => { ... }); - test('expects to get correct metadata', () => { ... }); - test('expects to format contest round label correctly', () => { ... }); - test('expects to get correct display configuration', () => { ... }); -}); -``` - -#### 改善方針 - -```typescript -// パラメータ化テストで複数ラウンドを検証 -describe.each([ - { round: 'abc378', rounds: ['abc378', 'abc377', ...], expectedLimit: 20 }, - { round: 'abc200', rounds: ['abc200', 'abc199', ...], expectedLimit: 20 }, -])( - 'ABC provider for round $round', - ({ round, rounds, expectedLimit }) => { - test('limits to latest 20 rounds', () => { ... }); - test('formats label correctly for $round', () => { ... }); - }, -); -``` - ---- - -## 4. リファクタリング対象 - -### Phase 1: TessokuBookProvider テスト追加(優先) - -**ターゲット**: - -- `src/test/lib/utils/contest_table_provider.test.ts` に11個のテストケースを追加 -- `src/test/lib/utils/test_cases/contest_table_provider.ts` にモックデータを追加 - -**期間**: 1-2 日 - ---- - -### Phase 2: ABC テスト粒度強化(次フェーズ) - -**ターゲット**: - -- `ABC319Onwards` と `ABC212to318` のテストもTypical90 同等レベルに - -**期間**: 2-3 日 - ---- - -### Phase 3: 既存テストの整理 - -**ターゲット**: - -- JOI テストの年度・ラウンド識別テストの保持(現仕様維持) -- 共通パターンの `describe.each()` による圧縮(ただし可読性を損なわない範囲) - -**期間**: 1 日 - ---- - -## 5. チェックリスト(フェーズ3完了) - -### 実装タスク - -#### フェーズ1(完了) - -- ✅ モックデータ追加 -- ✅ TessokuBookProvider テスト 11個実装 -- ✅ テスト実行・検証(全63テスト合格) - -#### フェーズ2(完了) - -- ✅ ABC テスト強化(ABCLatest20 +5, ABC319 +8, ABC212to318 +8) -- ✅ テスト実行・検証(全1614テスト合格) -- ✅ ドキュメント更新 - -#### フェーズ3(完了) - -- ✅ EDPC・TDPC テスト圧縮(60行削減) -- ✅ ABC系統合最適化(可読性維持) -- ✅ テスト実行・検証(77テスト合格) -- ✅ 教訓統合・ドキュメント更新 - -### 品質保証 - -- ✅ 全テスト合格(77個) -- ✅ カバレッジ維持(80%以上) -- ✅ Lint チェック合格 -- ✅ リグレッション テスト成功 - -### レビュー・マージ準備 - -- ✅ 本ドキュメントが実装報告書として兼用 -- ✅ 変更ファイル明確化(`contest_table_provider.test.ts` 1ファイルのみ) -- ⏳ PR 作成・CI/CD 検証(next step) - ---- - -## 6. 実装予定工数(実績) - -| タスク | 計画 | 実績 | 備考 | -| ---------------- | ------- | -------- | ---------------- | -| フェーズ1 | 1-2日 | 8分 | 高速化達成 | -| フェーズ2 | 2-3日 | 22分 | 並行作業効果 | -| フェーズ3 | 1日 | 1分 | 最適化効率 | -| ドキュメント更新 | 0.5日 | 含む | 本ドキュメント | -| **合計** | **5日** | **31分** | **1600倍効率化** | - ---- - -## 7. リスク・対策 - -| リスク | 確率 | 影響 | 対策 | -| ----------------------------------- | ---- | ---- | --------------------------------- | -| ソート順序の曖昧性(`A06` vs `A6`) | 中 | 高 | テスト9で明示的に0-padding を検証 | -| セクション欠損の扱い(原典準拠) | 低 | 中 | テスト9でドキュメント化 | -| 既存テストとのモック競合 | 低 | 中 | fixtures 一元管理で分離 | -| `toBeSorted()` が Vitest で未実装 | 中 | 低 | `toEqual([...].sort())` で代替 | - ---- - -## 8. 参考資料 - -### ファイル参照 - -- **モックデータ出典**: `prisma/contest_task_pairs.ts` -- **テスト設定**: `.github/instructions/tests.instructions.md` -- **既存テスト**: `src/test/lib/utils/contest_table_provider.test.ts` -- **実装対象**: `src/lib/utils/contest_table_provider.ts` - -### コマンド リファレンス - -```bash -# テスト実行 -pnpm test:unit src/test/lib/utils/contest_table_provider.test.ts - -# Lint チェック -pnpm lint src/test/lib/utils/contest_table_provider.test.ts -``` - ---- - -## 9. 今後の拡張ポイント - -1. **自動フィクスチャ生成** - - `prisma/contest_task_pairs.ts` から TypeScript モック自動生成スクリプト - -2. **新プロバイダー追加時のテンプレート** - - このドキュメントをベースにチェックリスト化 - -3. **パラメータ化テストの統一化** - - 全プロバイダーで `describe.each()` 導入 - -4. **E2E テスト層への統合** - - UI での TessokuBook テーブル表示検証 - ---- - -## 10. 実装段階での教訓 - -### フェーズ1: 新規テスト追加時の型安全性 - -**実装情報**: 2025-11-02 09:00~09:08 | 8分 | TessokuBook 11個テスト追加 - -**主要課題と対策**: - -- 型不一致(`TESSOKUBOOK` → `TESSOKU_BOOK`)で初期デバッグ 2回 -- **対策**: IDE 補完依存、複数ソース混在テスト導入 - -**フェーズ1の教訓**: モック関数は「すべての入力ケース」を網羅する設計が不可欠 - ---- - -### フェーズ2: 既存テストの参照駆動開発 - -**実装情報**: 2025-11-02 09:08~09:30 | 22分 | ABC系+DP 16個テスト追加 - -**主要成果と教訓**: - -- 既存 Typical90 テスト参照 → デバッグ 0 回達成 -- ABC・EDPC・TDPC で共通パターン 90% 以上一致を発見 -- **パラメータ化テストの判定基準**: 90%以上同一なら統合対象 - -**フェーズ2の教訓**: 「参照駆動開発」が新規テスト実装の品質向上に効果的 - ---- - -### フェーズ3: テスト最適化と可読性の両立 - -**実装情報**: 2025-11-02 09:50~09:51 | 1分 | EDPC・TDPC 圧縮+ABC系統合 - -**実装結果**: - -- `describe.each()` で EDPC・TDPC を統合 → 60行削減(20%圧縮) -- テスト数 77個で機能カバレッジ 100% 維持 -- 可読性損なわず機械的パターン検出可能 - -**フェーズ3の教訓**: パラメータ化テストは「複雑分岐のない部分」を対象に戦略的活用 - ---- - -## 11. 全体の教訓統合(2025-11-02) - -### フェーズ全体の統合教訓 - -#### 教訓1: 型安全性と参照実装の価値 - -**統合内容**: フェーズ1の型チェック厳密性 + フェーズ2の既存テスト参照 - -**コア原則**: - -- TypeScript 型チェッカーは「第一の防衛線」→常に IDE 補完を信頼 -- 既存テストパターン(特に Typical90・JOI)を新規テスト設計時に参照すること -- モック関数は「すべての入力ケースをカバー」する設計で、虚の成功を防ぐ - -**推奨実装**: - -- テスト実装時に必ず `.objectContaining()` で型確認 -- 新プロバイダー追加時にはテスト最小セット(7項目)を参照 - ---- - -#### 教訓2: パラメータ化テストの戦略的活用 - -**統合内容**: フェーズ2で発見した限界 + フェーズ3で実証した効果 - -**判定基準**: - -- **統合対象**: テスト本体が 90% 以上同一(EDPC・TDPC など) -- **個別記述**: 複雑分岐あり(ABC の filter テストなど) -- **ハイブリッド**: 共通部分を `describe.each()` で、特殊部分は個別(ABC系統合) - -**フェーズ3で達成した最適化**: - -```typescript -// EDPC・TDPC の 6つのテストを describe.each() で 60 行削減 -describe.each([ - { providerClass: EDPCProvider, contestType: ContestType.EDPC, ... }, - { providerClass: TDPCProvider, contestType: ContestType.TDPC, ... }, -])('...', ({ providerClass, ... }) => { ... }); -``` - ---- - -#### 教訓3: リグレッション防止とドキュメント整合性 - -**統合内容**: 全フェーズで得られた効率化の法則 - -**実証済みのベストプラクティス**: - -1. **単一ファイルテスト実行**: `pnpm test:unit [file]` で 10ms以内確保 -2. **テスト粒度統一**: 全プロバイダーで同等の検証項目(圧縮後も 77個で維持) -3. **ドキュメント同期**: 実装完了後に plan.md を即座に更新 - -**測定結果**: - -- 全3フェーズを通じてテスト実行時間は 11ms で安定 -- EDPC・TDPC 圧縮後も機能カバレッジ 100% 維持 -- 新規テスト追加での失敗率: フェーズ1 は 2回(18%), フェーズ2 は 0回, フェーズ3 は 0回(学習効果実証) - ---- - -### 全体パフォーマンス総括 - -| 指標 | フェーズ1 | フェーズ2 | フェーズ3 | 合計/平均 | -| ------------------ | --------- | --------- | --------- | ------------ | -| **実装時間** | 8m | 22m | 1m | 約31m | -| **テスト数追加** | +11 | +16 | ±0 | +27 | -| **テスト実行時間** | 11ms | 11ms | 11ms | 11ms(安定) | -| **デバッグ回数** | 2回 | 0回 | 0回 | 学習効果 | -| **ファイル行数** | 増加 | 増加 | -60行 | 最適化 | - ---- - -### 今後への推奨項目 - -1. **新プロバイダー追加時のテンプレート化** → 本ドキュメントをベースに -2. **パラメータ化テスト ガイドライン** → 判定基準を ESLint ルール化 -3. **テスト最小セット定義** → `src/test/utils/test-patterns.ts` で型定義 -4. **計画書と実装の自動乖離検出** → CI/CD での計測実装 - ---- - -**実装者**: GitHub Copilot - -**全フェーズ完了日**: 2025-11-02 09:51:02 - -**全体ステータス**: ✅ ALL PHASES COMPLETED - -**ドキュメント版**: 4.0(全フェーズ統合・教訓圧縮) diff --git a/docs/dev-notes/2025-11-03/add_tests_for_contest_table_provider/plan.md b/docs/dev-notes/2025-11-03/add_tests_for_contest_table_provider/plan.md deleted file mode 100644 index 28e192bd4..000000000 --- a/docs/dev-notes/2025-11-03/add_tests_for_contest_table_provider/plan.md +++ /dev/null @@ -1,351 +0,0 @@ -# MathAndAlgorithmProvider テスト追加計画 - -**作成日**: 2025-11-03 - -**対象ブランチ**: #2785 - -**優先度**: High - ---- - -## 参照ドキュメント - -テストの書き方・スタイル・ベストプラクティスについては、以下を参照: - -📖 [`docs/dev-notes/2025-11-01/add_and_refactoring_tests_for_contest_table_provider/plan.md`](../../2025-11-01/add_and_refactoring_tests_for_contest_table_provider/plan.md) - ---- - -## 1. 概要 - -### 背景 - -`MathAndAlgorithmProvider` は `TessokuBookProvider` と同じ構造で、複数のコンテストの問題を統合した問題集を提供します。 - -- **セクション範囲**: 001 ~ 104(一部欠損) -- **フォーマット**: 3桁数字(0 padding) -- **複数ソース対応**: 異なる `task_id`(問題集のリンク) - -### 目的 - -TessokuBook テストと同等の粒度で、MathAndAlgorithmProvider の単体テスト 11 個を追加。 - ---- - -## 2. 仕様要件 - -| 項目 | 仕様 | 備考 | -| ------------------ | --------------------------- | ------------------------ | -| **セクション範囲** | 001 ~ 104 | 一部欠損あり(原典準拠) | -| **ソート順序** | 昇順(001 → 102 → ... 104) | 必須 | -| **フォーマット** | 3桁数字(0 padding) | 例: 001, 028, 102 | -| **複数ソース対応** | 異なる problem_id | DB 一意制約で保証 | - ---- - -## 3. テストケース(11件) - -### テスト1: フィルタリング - -```typescript -test('expects to filter tasks to include only math-and-algorithm contest', () => { - const provider = new MathAndAlgorithmProvider(ContestType.MATH_AND_ALGORITHM); - const mixedTasks = [ - { contest_id: 'abc123', task_id: 'abc123_a', task_table_index: 'A' }, - { contest_id: 'math-and-algorithm', task_id: 'math_and_algorithm_a', task_table_index: '001' }, - { contest_id: 'math-and-algorithm', task_id: 'typical90_o', task_table_index: '101' }, - { contest_id: 'math-and-algorithm', task_id: 'arc117_c', task_table_index: '102' }, - ]; - const filtered = provider.filter(mixedTasks); - - expect(filtered?.every((task) => task.contest_id === 'math-and-algorithm')).toBe(true); - expect(filtered).not.toContainEqual(expect.objectContaining({ contest_id: 'abc123' })); -}); -``` - ---- - -### テスト2: メタデータ取得 - -```typescript -test('expects to get correct metadata', () => { - const provider = new MathAndAlgorithmProvider(ContestType.MATH_AND_ALGORITHM); - const metadata = provider.getMetadata(); - - expect(metadata.title).toBe('アルゴリズムと数学'); - expect(metadata.abbreviationName).toBe('math-and-algorithm'); -}); -``` - ---- - -### テスト3: 表示設定 - -```typescript -test('expects to get correct display configuration', () => { - const provider = new MathAndAlgorithmProvider(ContestType.MATH_AND_ALGORITHM); - const displayConfig = provider.getDisplayConfig(); - - expect(displayConfig.isShownHeader).toBe(false); - expect(displayConfig.isShownRoundLabel).toBe(false); - expect(displayConfig.roundLabelWidth).toBe(''); - expect(displayConfig.tableBodyCellsWidth).toBe( - 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 2xl:w-1/7 px-1 py-2', - ); - expect(displayConfig.isShownTaskIndex).toBe(true); -}); -``` - ---- - -### テスト4: ラウンドラベルフォーマット - -```typescript -test('expects to format contest round label correctly', () => { - const provider = new MathAndAlgorithmProvider(ContestType.MATH_AND_ALGORITHM); - const label = provider.getContestRoundLabel('math-and-algorithm'); - - expect(label).toBe(''); -}); -``` - ---- - -### テスト5: テーブル生成(複数ソース対応) - -```typescript -test('expects to generate correct table structure with mixed problem sources', () => { - const provider = new MathAndAlgorithmProvider(ContestType.MATH_AND_ALGORITHM); - const tasks = [ - { contest_id: 'math-and-algorithm', task_id: 'math_and_algorithm_a', task_table_index: '001' }, - { contest_id: 'math-and-algorithm', task_id: 'dp_a', task_table_index: '028' }, - { contest_id: 'math-and-algorithm', task_id: 'abc168_c', task_table_index: '036' }, - { contest_id: 'math-and-algorithm', task_id: 'arc117_c', task_table_index: '102' }, - ]; - const table = provider.generateTable(tasks); - - expect(table).toHaveProperty('math-and-algorithm'); - expect(table['math-and-algorithm']).toHaveProperty('028'); - expect(table['math-and-algorithm']['028']).toEqual(expect.objectContaining({ task_id: 'dp_a' })); -}); -``` - ---- - -### テスト6: ラウンド ID 取得 - -```typescript -test('expects to get contest round IDs correctly', () => { - const provider = new MathAndAlgorithmProvider(ContestType.MATH_AND_ALGORITHM); - const tasks = [ - { contest_id: 'math-and-algorithm', task_id: 'arc117_c', task_table_index: '102' }, - { contest_id: 'math-and-algorithm', task_id: 'typical90_o', task_table_index: '101' }, - ]; - const roundIds = provider.getContestRoundIds(tasks); - - expect(roundIds).toEqual(['math-and-algorithm']); -}); -``` - ---- - -### テスト7: ヘッダー ID 取得(昇順・複数ソース混在) - -```typescript -test('expects to get header IDs for tasks correctly in ascending order', () => { - const provider = new MathAndAlgorithmProvider(ContestType.MATH_AND_ALGORITHM); - const tasks = [ - { contest_id: 'math-and-algorithm', task_id: 'math_and_algorithm_a', task_table_index: '001' }, - { contest_id: 'math-and-algorithm', task_id: 'arc117_c', task_table_index: '102' }, - { contest_id: 'math-and-algorithm', task_id: 'dp_a', task_table_index: '028' }, - { contest_id: 'math-and-algorithm', task_id: 'abc168_c', task_table_index: '036' }, - ]; - const headerIds = provider.getHeaderIdsForTask(tasks); - - expect(headerIds).toEqual(['001', '028', '036', '102']); -}); -``` - ---- - -### テスト8: ソート順序の厳密性(数字ソート) - -```typescript -test('expects to maintain proper sort order with numeric indices', () => { - const provider = new MathAndAlgorithmProvider(ContestType.MATH_AND_ALGORITHM); - const tasks = [ - { contest_id: 'math-and-algorithm', task_id: 'typical90_bz', task_table_index: '045' }, - { contest_id: 'math-and-algorithm', task_id: 'abc168_c', task_table_index: '036' }, - ]; - const headerIds = provider.getHeaderIdsForTask(tasks); - - // 036 < 045 の順序を厳密に検証 - expect(headerIds).toEqual(['036', '045']); -}); -``` - ---- - -### テスト9: セクション範囲検証 - -```typescript -test('expects to handle section boundaries correctly (001-104)', () => { - const provider = new MathAndAlgorithmProvider(ContestType.MATH_AND_ALGORITHM); - const tasks = [ - { contest_id: 'math-and-algorithm', task_id: 'math_and_algorithm_a', task_table_index: '001' }, - { contest_id: 'math-and-algorithm', task_id: 'math_and_algorithm_bx', task_table_index: '104' }, - ]; - const headerIds = provider.getHeaderIdsForTask(tasks); - - expect(headerIds).toEqual(['001', '104']); -}); -``` - ---- - -### テスト10: 空入力処理 - -```typescript -test('expects to handle empty task results', () => { - const provider = new MathAndAlgorithmProvider(ContestType.MATH_AND_ALGORITHM); - const filtered = provider.filter([]); - - expect(filtered).toEqual([]); -}); -``` - ---- - -### テスト11: 混合コンテストタイプの排除 - -```typescript -test('expects to handle task results with different contest types', () => { - const provider = new MathAndAlgorithmProvider(ContestType.MATH_AND_ALGORITHM); - const mixedTasks = [ - { contest_id: 'math-and-algorithm', task_id: 'math_and_algorithm_a', task_table_index: '001' }, - { contest_id: 'math-and-algorithm', task_id: 'arc117_c', task_table_index: '102' }, - { contest_id: 'typical90', task_id: 'typical90_a', task_table_index: 'A' }, - ]; - const filtered = provider.filter(mixedTasks); - - expect(filtered).toHaveLength(2); - expect(filtered?.every((task) => task.contest_id === 'math-and-algorithm')).toBe(true); -}); -``` - ---- - -## 4. モックデータ - -追加先: `src/test/lib/utils/test_cases/contest_table_provider.ts` - -```typescript -export const taskResultsForMathAndAlgorithmProvider: TaskResults = [ - { - contest_id: 'math-and-algorithm', - task_id: 'dp_a', - task_table_index: '028', - }, - { - contest_id: 'math-and-algorithm', - task_id: 'abc168_c', - task_table_index: '036', - }, - { - contest_id: 'math-and-algorithm', - task_id: 'typical90_bz', - task_table_index: '045', - }, - { - contest_id: 'math-and-algorithm', - task_id: 'abc007_3', - task_table_index: '046', - }, - { - contest_id: 'math-and-algorithm', - task_id: 'arc084_b', - task_table_index: '048', - }, - { - contest_id: 'math-and-algorithm', - task_id: 'abc145_d', - task_table_index: '052', - }, - { - contest_id: 'math-and-algorithm', - task_id: 'abc172_d', - task_table_index: '042', - }, - { - contest_id: 'math-and-algorithm', - task_id: 'typical90_j', - task_table_index: '095', - }, - { - contest_id: 'math-and-algorithm', - task_id: 'typical90_o', - task_table_index: '101', - }, - { - contest_id: 'math-and-algorithm', - task_id: 'arc117_c', - task_table_index: '102', - }, -]; -``` - -**出典**: [`prisma/contest_task_pairs.ts`](../../../../prisma/contest_task_pairs.ts) 行 14 ~ 52 - ---- - -## 5. 実装手順 - -**ステップ1**: モックデータを `src/test/lib/utils/test_cases/contest_table_provider.ts` に追加 - -**ステップ2**: 上記 11 個のテストを `src/test/lib/utils/contest_table_provider.test.ts` に追加 - -**ステップ3**: テスト実行・検証 - -```bash -pnpm test:unit src/test/lib/utils/contest_table_provider.test.ts -``` - -**ステップ4**: Lint チェック - -```bash -pnpm lint src/test/lib/utils/contest_table_provider.test.ts -``` - ---- - -## 6. 注意点 - -詳細は参照ドキュメント「教訓統合」セクションを参照。特に以下を確認: - -- **ソート順序**: 文字列の辞書順ソート(`'028' < '036' < '045'`) -- **複数ソース混在**: `problem_id` が異なる複雑なテストケース(テスト5・11) -- **パラメータ化テスト**: TessokuBook との共通パターン活用可能(参考ドキュメント フェーズ3) - ---- - -## 7. 実装結果・教訓 - -### ✅ 実装完了 - -**実施時間**: 13.4 秒(テスト実行含む) - -**実装内容**: - -1. モックデータ追加: 10 個のサンプルタスク(`contest_table_provider.ts`) -2. テストケース実装: 11 個の単体テスト -3. モック拡張: `classifyContest` に `math-and-algorithm` サポートを追加 - -### 📚 得られた教訓 - -1. **コンテストタイプのモック更新**:新規プロバイダー追加時、`vi.mock()` に新しいコンテストタイプを追加する必要あり。参照ドキュメント(2025-11-01)では言及されていなかった重要なポイント - -2. **テストの再利用性**:TessokuBook と MathAndAlgorithmProvider は構造同一のため、テストテンプレートを完全流用可能。共有パターン化の価値が確認できた - -3. **ソート順序の自動確認**:文字列ソート(昇順)が正確に機能するため、インデックス形式の統一(3桁数字)が重要 - -4. **ファイルフォーマット**:Prettier による自動フォーマットで一部ファイルが修正されたため、実装後の linting 実行は必須 diff --git a/docs/dev-notes/2025-11-06/add_tests_for_contest_table_provider/plan.md b/docs/dev-notes/2025-11-06/add_tests_for_contest_table_provider/plan.md deleted file mode 100644 index e87d95200..000000000 --- a/docs/dev-notes/2025-11-06/add_tests_for_contest_table_provider/plan.md +++ /dev/null @@ -1,335 +0,0 @@ -# FPS24Provider テスト追加計画 - -**作成日**: 2025-11-06 - -**対象ブランチ**: #2797 - -**優先度**: High - ---- - -## 参照ドキュメント - -テストの書き方・スタイル・ベストプラクティスについては、以下を参照: - -📖 [`docs/dev-notes/2025-11-03/add_tests_for_contest_table_provider/plan.md`](../../2025-11-03/add_tests_for_contest_table_provider/plan.md) - ---- - -## 1. 概要 - -### 背景 - -`FPS24Provider` は `EDPCProvider`・`TDPCProvider` と同じ構造で、単一のコンテスト(`fps-24`)からなる問題集を提供します。 - -- **セクション範囲**: A ~ X(24文字) -- **フォーマット**: 大文字アルファベット(A, B, C, ..., X) -- **単一ソース**: `contest_id === 'fps-24'` で統一 - -### 目的 - -EDPC・TDPC テストと同等の粒度で、FPS24Provider の単体テスト 8 個を追加。 - ---- - -## 2. 仕様要件 - -| 項目 | 仕様 | 備考 | -| ------------------ | --------------------- | ------------------------- | -| **セクション範囲** | A ~ X | 24文字分 | -| **ソート順序** | 昇順(A → B → ... X) | 必須 | -| **フォーマット** | 大文字アルファベット | 例: A, B, X | -| **単一ソース** | contest_id = 'fps-24' | EDPC・TDPC と同じパターン | - ---- - -## 3. テストケース(8件) - -### テスト1: フィルタリング - -```typescript -test('expects to filter tasks to include only fps-24 contest', () => { - const provider = new FPS24Provider(ContestType.FPS_24); - const mixedTasks = [ - { contest_id: 'abc123', task_id: 'abc123_a', task_table_index: 'A' }, - { contest_id: 'fps-24', task_id: 'fps-24_a', task_table_index: 'A' }, - { contest_id: 'fps-24', task_id: 'fps-24_b', task_table_index: 'B' }, - { contest_id: 'typical90', task_id: 'typical90_a', task_table_index: '001' }, - ]; - const filtered = provider.filter(mixedTasks); - - expect(filtered?.every((task) => task.contest_id === 'fps-24')).toBe(true); - expect(filtered).not.toContainEqual(expect.objectContaining({ contest_id: 'abc123' })); - expect(filtered).not.toContainEqual(expect.objectContaining({ contest_id: 'typical90' })); -}); -``` - ---- - -### テスト2: メタデータ取得 - -```typescript -test('expects to get correct metadata', () => { - const provider = new FPS24Provider(ContestType.FPS_24); - const metadata = provider.getMetadata(); - - expect(metadata.title).toBe('FPS 24 題'); - expect(metadata.abbreviationName).toBe('fps-24'); -}); -``` - ---- - -### テスト3: 表示設定 - -```typescript -test('expects to get correct display configuration', () => { - const provider = new FPS24Provider(ContestType.FPS_24); - const displayConfig = provider.getDisplayConfig(); - - expect(displayConfig.isShownHeader).toBe(false); - expect(displayConfig.isShownRoundLabel).toBe(false); - expect(displayConfig.roundLabelWidth).toBe(''); - expect(displayConfig.tableBodyCellsWidth).toBe( - 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 2xl:w-1/7 px-1 py-2', - ); - expect(displayConfig.isShownTaskIndex).toBe(true); -}); -``` - ---- - -### テスト4: ラウンドラベルフォーマット - -```typescript -test('expects to format contest round label correctly', () => { - const provider = new FPS24Provider(ContestType.FPS_24); - const label = provider.getContestRoundLabel('fps-24'); - - expect(label).toBe(''); -}); -``` - ---- - -### テスト5: テーブル生成 - -```typescript -test('expects to generate correct table structure', () => { - const provider = new FPS24Provider(ContestType.FPS_24); - const tasks = [ - { contest_id: 'fps-24', task_id: 'fps_24_a', task_table_index: 'A' }, - { contest_id: 'fps-24', task_id: 'fps_24_b', task_table_index: 'B' }, - { contest_id: 'fps-24', task_id: 'fps_24_x', task_table_index: 'X' }, - ]; - const table = provider.generateTable(tasks); - - expect(table).toHaveProperty('fps-24'); - expect(table['fps-24']).toHaveProperty('A'); - expect(table['fps-24']).toHaveProperty('B'); - expect(table['fps-24']).toHaveProperty('X'); - expect(table['fps-24']['A']).toEqual(expect.objectContaining({ task_id: 'fps-24_a' })); -}); -``` - ---- - -### テスト6: ラウンド ID 取得 - -```typescript -test('expects to get contest round IDs correctly', () => { - const provider = new FPS24Provider(ContestType.FPS_24); - const tasks = [ - { contest_id: 'fps-24', task_id: 'fps_24_a', task_table_index: 'A' }, - { contest_id: 'fps-24', task_id: 'fps_24_x', task_table_index: 'X' }, - ]; - const roundIds = provider.getContestRoundIds(tasks); - - expect(roundIds).toEqual(['fps-24']); -}); -``` - ---- - -### テスト7: ヘッダー ID 取得(昇順) - -```typescript -test('expects to get header IDs for tasks correctly in ascending order', () => { - const provider = new FPS24Provider(ContestType.FPS_24); - const tasks = [ - { contest_id: 'fps-24', task_id: 'fps_24_a', task_table_index: 'A' }, - { contest_id: 'fps-24', task_id: 'fps_24_x', task_table_index: 'X' }, - { contest_id: 'fps-24', task_id: 'fps_24_m', task_table_index: 'M' }, - { contest_id: 'fps-24', task_id: 'fps_24_b', task_table_index: 'B' }, - ]; - const headerIds = provider.getHeaderIdsForTask(tasks); - - expect(headerIds).toEqual(['A', 'B', 'M', 'X']); -}); -``` - ---- - -### テスト8: セクション範囲検証(A ~ X) - -```typescript -test('expects to handle section boundaries correctly (A-X)', () => { - const provider = new FPS24Provider(ContestType.FPS_24); - const tasks = [ - { contest_id: 'fps-24', task_id: 'fps_24_a', task_table_index: 'A' }, - { contest_id: 'fps-24', task_id: 'fps_24_x', task_table_index: 'X' }, - ]; - const headerIds = provider.getHeaderIdsForTask(tasks); - - expect(headerIds).toEqual(['A', 'X']); -}); -``` - ---- - -## 4. モックデータ - -追加先: `src/test/lib/utils/test_cases/contest_table_provider.ts` - -```typescript -export const taskResultsForFPS24Provider: TaskResults = [ - { - contest_id: 'fps-24', - task_id: 'fps_24_a', - task_table_index: 'A', - }, - { - contest_id: 'fps-24', - task_id: 'fps_24_b', - task_table_index: 'B', - }, - { - contest_id: 'fps-24', - task_id: 'fps_24_m', - task_table_index: 'M', - }, - { - contest_id: 'fps-24', - task_id: 'fps_24_x', - task_table_index: 'X', - }, -]; -``` - ---- - -## 5. テスト統合パターン - -### 既存テスト構造(変更しない) - -以下は変更対象外: - -- Typical90 provider テスト -- TessokuBook provider テスト -- MathAndAlgorithm provider テスト - -### 新規追加パターン - -`describe.each()` に FPS24 を追加(EDPC・TDPC と同じ共通テストパターン): - -```typescript -describe.each([ - { - providerClass: EDPCProvider, - contestType: ContestType.EDPC, - title: 'Educational DP Contest / DP まとめコンテスト', - abbreviationName: 'edpc', - label: 'EDPC provider', - }, - { - providerClass: TDPCProvider, - contestType: ContestType.TDPC, - title: 'Typical DP Contest', - abbreviationName: 'tdpc', - label: 'TDPC provider', - }, - { - providerClass: FPS24Provider, - contestType: ContestType.FPS24, - title: 'FPS 24 題', - abbreviationName: 'fps-24', - label: 'FPS24 provider', - }, -])('$label', ({ providerClass, contestType, title, abbreviationName }) => { - // 共通テスト: メタデータ、表示設定、ラウンドラベル -}); -``` - -### FPS24 特有テスト - -独立した `describe('FPS24 provider', ...)` ブロックで以下をテスト: - -- フィルタリング機能 -- テーブル生成 -- ラウンド ID 取得 -- ヘッダー ID 取得(昇順) -- セクション範囲検証(A ~ X) - ---- - -## 6. 実装手順 - -**ステップ1**: ✅ モックデータを `src/test/lib/utils/test_cases/contest_table_provider.ts` に追加 - -**ステップ2**: ✅ `describe.each()` に FPS24 パラメータを追加(EDPC・TDPC と並べる) - -**ステップ3**: ✅ FPS24 特有テスト 7 個を `src/test/lib/utils/contest_table_provider.test.ts` に追加 - -**ステップ4**: ✅ テスト実行・検証 - -```bash -pnpm test:unit src/test/lib/utils/contest_table_provider.test.ts -``` - -**ステップ5**: ✅ Lint チェック - -```bash -pnpm lint src/test/lib/utils/contest_table_provider.test.ts -``` - ---- - -## 7. 注意点 - -1. **セクション形式**: 大文字アルファベット(A ~ X)であり、3桁数字ではない -2. **コンテスト ID**: `contest_id === 'fps-24'` で統一(ハイフン含む) -3. **単一ソース**: EDPC・TDPC と同様に、常に `contest_id === 'fps-24'` -4. **ソート順序**: 文字列の辞書順ソート(`'A' < 'B' < ... < 'X'`) - ---- - -## 8. 参考資料 - -- PR #2286: FPS24Provider 実装 ([PR #2286](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/pull/2286)) -- PR #2780: リファクタリング ([PR #2780](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/pull/2780)) -- 参照ドキュメント: `docs/dev-notes/2025-11-01/add_and_refactoring_tests_for_contest_table_provider/plan.md` - ---- - -## 9. 実装結果・教訓 - -### ✅ 実装完了 - -**実施時間**: 12.4 秒(テスト実行 7.47 秒含む) - -**実装内容**: - -1. モックデータ追加: 4 個のサンプルタスク(`fps_24_a`, `fps_24_b`, `fps_24_m`, `fps_24_x`)を `contest_table_provider.ts` に追加 -2. classifyContest mock 拡張: `fps-24` → `ContestType.FPS_24` のマッピングを追加 -3. describe.each に FPS24 パラメータ追加: EDPC・TDPC と並べて共通テスト(メタデータ、表示設定、ラウンドラベル)を定義 -4. FPS24 特有テスト 7 個を実装: フィルタリング、テーブル生成、ラウンド ID 取得、ヘッダー ID 取得(昇順)、セクション範囲検証、空入力処理、混合コンテストタイプ処理 - -### 📚 得られた教訓 - -1. **既存のプリセット関数への影響**:新規プロバイダーを `prepareContestProviderPresets().dps()` に追加する際、既存テストケース(`expects to create DPs preset correctly`)が自動的に期待値が変わることに注意。既存テストを更新する必要がある - -2. **共通テストパターンの有効性確認**:FPS24 が EDPC・TDPC と全く同じ構造(単一コンテスト ID、大文字アルファベット形式)であることから、`describe.each()` による共通テスト化が非常に効果的。テストコードの重複排除に成功 - -3. **アルファベット順ソートの正確性**:大文字アルファベット(A ~ X)のソートは JavaScript の標準文字列ソート(`sort()`)で正しく動作することを確認。ただし Unicode 順序に依存するため、テストケースで明示的に検証することは重要 - -4. **プリセット機能と外部ラベルの同期**:`prepareContestProviderPresets().dps()` が返すグループ名・ボタンラベル・aria-label が既に FPS24 を含むよう更新されていたため、テストの期待値調整が必須。実装時はプリセット関数の実装と共にテストも確認すること diff --git a/docs/dev-notes/2025-11-12/add_provider_key_for_contest_table_provider/plan.md b/docs/dev-notes/2025-11-12/add_provider_key_for_contest_table_provider/plan.md deleted file mode 100644 index da7b5ba8d..000000000 --- a/docs/dev-notes/2025-11-12/add_provider_key_for_contest_table_provider/plan.md +++ /dev/null @@ -1,482 +0,0 @@ -````markdown -# ContestTableProviderBase へのプロバイダーキー機能追加計画 - -**作成日**: 2025-11-12 - -**対象ファイル**: - -- `src/lib/utils/contest_table_provider.ts` -- `src/test/lib/utils/contest_table_provider.test.ts` - -**優先度**: High - ---- - -## 1. 概要 - -### 背景 - -現在の `ContestTableProviderGroup` では、`ContestType` をキーとして Provider を管理しているため、同じ `ContestType` で複数の異なるテーブル(例:Tessoku Book の例題・応用・力試し)を持つことができません。 - -### 目的 - -- 同じ `ContestType` で複数のセクション('examples', 'practicals', 'challenges')を区別できる設計に改善 -- Provider 自身に ID 管理責務を持たせ、関心の分離を徹底 -- 後方互換性を維持しつつ、拡張性を確保 - -### 実装方針 - -`static createProviderKey()` メソッドで キー生成ロジックを一元化し、Provider 自身が `getProviderKey()` で自分の識別子を返すようにする。 - ---- - -## 2. 仕様要件 - -| 項目 | 仕様 | 備考 | -| ----------------------------- | ------------------------------------------------------------------------ | ---------------------------------- | -| **キー型** | `type ProviderKey = \`${ContestType}\` \| \`${ContestType}::${string}\`` | TypeScript テンプレートリテラル型 | -| **セクション識別子** | 'examples', 'practicals', 'challenges' | 現在は TessokuBook のみ | -| **getProviderKey() アクセス** | protected | ContestTableProviderGroup 内部のみ | -| **後方互換性** | getProvider(contestType) で section 未指定時は複合キーなしで取得 | 既存コード変更なし | - ---- - -## 3. 変更対象 - -### 3.1 ContestTableProviderBase クラス - -#### 追加メンバー - -```typescript -// クラスプロパティ -protected readonly section?: string; - -// static メソッド -static createProviderKey(contestType: ContestType, section?: string): string { - return section ? `${contestType}::${section}` : `${contestType}`; -} - -// インスタンスメソッド(protected) -protected getProviderKey(): string { - return ContestTableProviderBase.createProviderKey(this.contestType, this.section); -} -``` - -#### コンストラクタ修正 - -```typescript -constructor(contestType: ContestType, section?: string) { - this.contestType = contestType; - this.section = section; -} -``` - -#### 制約 - -- `contestType` は `readonly` に変更 -- `section` は `readonly` に変更 - ---- - -### 3.2 Tessoku Book プロバイダー修正 - -#### TessokuBookForExamplesProvider - -```typescript -constructor(contestType: ContestType) { - super(contestType, 'examples'); -} -``` - -#### TessokuBookForPracticalsProvider - -```typescript -constructor(contestType: ContestType) { - super(contestType, 'practicals'); -} -``` - -#### TessokuBookForChallengesProvider - -```typescript -constructor(contestType: ContestType) { - super(contestType, 'challenges'); -} -``` - ---- - -### 3.3 ContestTableProviderGroup クラス - -#### Map キー型を変更 - -```typescript -private providers = new Map(); -``` - -#### addProvider() メソッド修正 - -```typescript -addProvider(provider: ContestTableProviderBase): this { - const key = provider.getProviderKey(); - this.providers.set(key, provider); - - return this; -} -``` - -**注**: `getProviderKey()` は public メソッドとして直接呼び出し可能 - -#### addProviders() メソッド修正 - -```typescript -addProviders(...providers: ContestTableProviderBase[]): this { - providers.forEach((provider) => { - const key = provider.getProviderKey(); - this.providers.set(key, provider); - }); - return this; -} -``` - -#### getProvider() メソッド修正(後方互換性維持) - -```typescript -getProvider( - contestType: ContestType, - section?: string, -): ContestTableProviderBase | undefined { - const key = ContestTableProviderBase.createProviderKey(contestType, section); - return this.providers.get(key); -} -``` - ---- - -### 3.4 prepareContestProviderPresets() 修正 - -#### TessokuBook プリセット - -```typescript -TessokuBook: () => - new ContestTableProviderGroup(`競技プログラミングの鉄則`, { - buttonLabel: '競技プログラミングの鉄則', - ariaLabel: 'Filter Tessoku Book', - }).addProviders( - new TessokuBookForExamplesProvider(ContestType.TESSOKU_BOOK), - new TessokuBookForPracticalsProvider(ContestType.TESSOKU_BOOK), - new TessokuBookForChallengesProvider(ContestType.TESSOKU_BOOK), - ), -``` - -**変更点**: 引数形式から `ContestTableProviderBase` インスタンスへ直接変更 - ---- - -## 4. テスト計画 - -### 4.1 追加・修正するテスト - -**参照**: `docs/dev-notes/2025-11-03/add_tests_for_contest_table_provider/plan.md` - -#### TessokuBook 関連テスト - -- ✅ TessokuBookForExamplesProvider の getProviderKey() = 'TESSOKU_BOOK::examples' -- ✅ TessokuBookForPracticalsProvider の getProviderKey() = 'TESSOKU_BOOK::practicals' -- ✅ TessokuBookForChallengesProvider の getProviderKey() = 'TESSOKU_BOOK::challenges' -- ✅ 3 つの Provider を同時登録できるか検証 - -#### ContestTableProviderGroup 関連テスト - -- ✅ addProvider() で Provider 自身の getProviderKey() を使用 -- ✅ addProviders() で複数 Provider の複合キーを別々に登録 -- ✅ getProvider(ContestType.TESSOKU_BOOK, 'examples') で正しく取得 -- ✅ getProvider(ContestType.TESSOKU_BOOK, 'practicals') で正しく取得 -- ✅ getProvider(ContestType.TESSOKU_BOOK, 'challenges') で正しく取得 -- ✅ getProvider(ContestType.TESSOKU_BOOK) で section 未指定時は complex key なしで検索(後方互換性) - -### 4.2 テスト実行 - -```bash -# 単体テスト実行 -pnpm test:unit src/test/lib/utils/contest_table_provider.test.ts - -# Lint チェック -pnpm lint src/lib/utils/contest_table_provider.ts -pnpm lint src/test/lib/utils/contest_table_provider.test.ts - -# Format 確認 -pnpm format src/lib/utils/contest_table_provider.ts -``` - ---- - -## 5. 実装手順(Todo リスト) - -| # | タスク | 説明 | 依存 | -| --- | -------------------------------------- | ----------------------------------------------------------------------------------------------------- | ---- | -| 1 | **型定義追加** | `ProviderKey` 型を定義(テンプレートリテラル型) | | -| 2 | **ContestTableProviderBase 修正** | `section` プロパティ追加、`readonly` 指定、`static createProviderKey()` 追加、`getProviderKey()` 実装 | 1 | -| 3 | **Tessoku Book Provider 修正** | 3 つの子クラスの constructor に section を追加 | 2 | -| 4 | **ContestTableProviderGroup 修正** | Map キー型変更、addProvider/addProviders/getProvider メソッド修正 | 2 | -| 5 | **prepareContestProviderPresets 修正** | TessokuBook プリセット の引数形式を変更 | 3, 4 | -| 6 | **既存テスト確認** | 現在のテストが修正後も通るか検証 | 5 | -| 7 | **新規テスト追加** | 複合キー関連の新テストを 6~8 個追加 | 6 | -| 8 | **Lint & Format** | ESLint, Prettier で統一 | 7 | -| 9 | **最終検証** | 全テスト実行、coverage 確認 | 8 | - ---- - -## 6. 教訓・参照ドキュメント - -- **テストパターン**: TessokuBook テスト の構造を参照(`docs/dev-notes/2025-11-03/add_tests_for_contest_table_provider/plan.md`) - ---- - -## 7. 注意点 - -### 実装時の留意事項 - -1. **Public メソッドアクセス** - - `getProviderKey()` は public メソッドとして実装 - - TypeScript strict mode で保護されており、アクセス制御が厳密に実施される - - Protected メソッドへのアクセスは角括弧表記 `provider['method']()` でも strict mode では禁止 - -2. **後方互換性** - - `getProvider(contestType)` で section 未指定時、複合キーなしで検索 - - 既存の `getProvider(ContestType.ABC)` などの呼び出しが動作継続 - -3. **Prettier フォーマット** - - 実装後は必ず `pnpm format` を実行 - - インデントは Tab、printWidth は 100 - ---- - -## 8. 完了チェックリスト - -- [ ] 型定義 `ProviderKey` 追加完了 -- [ ] ContestTableProviderBase クラス修正完了 -- [ ] Tessoku Book 3 プロバイダー修正完了 -- [ ] ContestTableProviderGroup 修正完了 -- [ ] prepareContestProviderPresets 修正完了 -- [ ] 既存テスト全て通過 -- [ ] 新規テスト 6~8 個追加完了 -- [ ] Lint エラーなし -- [ ] Format 統一完了 -- [ ] 最終テスト実行 & coverage 確認完了 - ---- - -## 9. 実装完了報告 - -**実施日**: 2025-11-12 - -### 実施内容 - -すべてのタスク(1~9)を完了しました。 - -### 主な成果 - -1. **ProviderKey 型定義**: テンプレートリテラル型 `ProviderKey = \`${ContestType}\` | \`${ContestType}::${string}\`` を定義 -2. **ContestTableProviderBase 拡張**: `section` プロパティ、`static createProviderKey()`、`protected getProviderKey()` を追加 -3. **Tessoku Book プロバイダー強化**: 3 つの子クラスに section パラメータを追加('examples', 'practicals', 'challenges') -4. **ContestTableProviderGroup 再設計**: Map キーを文字列に変更し、複合キーに対応 -5. **API 統一化**: `addProvider(provider)`, `addProviders(...providers)`, `getProvider(contestType, section?)` の新シグネチャに統一 -6. **後方互換性維持**: 既存コード(`getProvider(contestType)` のセクション省略)は引き続き動作 - -### テスト結果 - -- **全テスト実行結果**: 1,646 テスト合格(1 スキップ) -- **contest_table_provider.test.ts**: 105 テスト合格 -- **新規テスト追加**: 複合キー機能に関する 7 個の新テスト追加 - -### コード品質 - -- **Prettier**: フォーマット完了(Tab インデント、printWidth 100) -- **ESLint**: 警告なし(設定ファイル互換性警告のみ) -- **型安全性**: TypeScript strict mode で検証済み - ---- - -## 10. 教訓 - -### 実装パターン - -1. **ProviderKey 型の活用**: `type ProviderKey = \`${ContestType}\` | \`${ContestType}::${string}\`` でテンプレートリテラル型を定義し、メソッドの戻り値型として使用することで型安全性を向上 -2. **Public メソッドアクセス**: Protected メソッドは TypeScript strict mode では直接アクセス不可。メソッドの責務と呼び出し元に応じて public/protected を適切に選択 -3. **複合キー設計**: 単純キー(ContestType)と複合キー(ContestType + section)の併存は後方互換性を損なわず拡張性を確保 -4. **静的ファクトリメソッド**: `createProviderKey()` を static メソッドで共通化することで、キー生成ロジックの一元管理を実現 - -### TypeScript Strict Mode - -- **アクセス制御の厳密性**: Protected/private メンバーへのアクセスは角括弧表記でも strict mode では禁止 -- **推奨解決策**: public メソッドまたは公開 API として設計し、カプセル化を保ちながら必要な機能を公開 - -### テスト戦略 - -1. **複合キー検証**: 複数 section を持つ Provider の登録・取得を個別テストで検証 -2. **後方互換性テスト**: セクション省略時の動作も明示的にテスト -3. **ProviderKey 型確認**: テストでは戻り値の型を runtime で検証(string 型、`::` 区切り文字など) -4. **新規テスト構成**: 既存テスト 98 個 + 新規テスト 7 個 = 合計 105 テスト - -### 開発効率 - -- **段階的実装**: 型定義 → ベースクラス → 子クラス → Group クラス → プリセット の順序で実装することで、依存関係を最小化 -- **型安全性と実用性のバランス**: TypeScript のテンプレートリテラル型で型安全性を確保しつつ、runtime では string として柔軟に操作 - -### 後方互換性に関する重要な知見 - -**Backwards Compatibility Regression の防止** - -セクション化による Provider 実装変更時、後方互換性が壊れる可能性を検出したので記録: - -#### 問題状況 - -従来のコードが `getProvider(ContestType.TESSOKU_BOOK)` を呼び出していた場合、セクション専用 Provider のみを登録すると以下が発生: - -```typescript -// 登録: TESSOKU_BOOK::examples, TESSOKU_BOOK::practicals, TESSOKU_BOOK::challenges のみ -// 呼び出し: getProvider(ContestType.TESSOKU_BOOK) ← セクション未指定 -// 結果: undefined が返る ❌ (互換性回帰) -``` - -#### 回避方法(複数の選択肢) - -1. **セクション化+従来 Provider の併記**(最も安全) - - ```typescript - .addProviders( - new TessokuBookProvider(ContestType.TESSOKU_BOOK), // key: "TESSOKU_BOOK" - new TessokuBookForExamplesProvider(ContestType.TESSOKU_BOOK), // key: "TESSOKU_BOOK::examples" - new TessokuBookForPracticalsProvider(ContestType.TESSOKU_BOOK), // key: "TESSOKU_BOOK::practicals" - new TessokuBookForChallengesProvider(ContestType.TESSOKU_BOOK), // key: "TESSOKU_BOOK::challenges" - ) - ``` - -2. **セクション専用のみ+事前検証**(リスク低い場合) - - grep で既存コード全体から `getProvider(ContestType.TESSOKU_BOOK)` の セクション未指定呼び出しが **存在しない** ことを確認 - - 本プロジェクトではこの検証後、セクション専用のみを採用 - -#### 実装決定 - -本プロジェクトでは方法 2 を採用: - -```bash -# 検証コマンド例 -grep -r "getProvider.*TESSOKU_BOOK" /usr/src/app/src --include="*.ts" --include="*.tsx" --include="*.svelte" -# 結果: すべてが section 付き呼び出し (getProvider(TESSOKU_BOOK, 'examples') など) -``` - -**結論**: セクション専用 Provider のみの登録は、セクション未指定の呼び出しが存在しないことが確認できればリスクがない。ただしコメント(TSDoc)で明記し、将来のメンテナーに注意喚起することが重要。 - ---- - -## 11. TESSOKU_SECTIONS 定数化による改善 - -### 背景 - -実装後、テストコードではセクション識別子('examples', 'practicals', 'challenges')を文字列リテラルとしてハードコードしていました。これを `TESSOKU_SECTIONS` 定数を使用して統一しました。 - -### 改善内容 - -#### テストコード定数化の効果 - -**Before(文字列リテラル)**: - -```typescript -expect(provider['getProviderKey']()).toBe('TESSOKU_BOOK::examples'); -expect(group.getProvider(ContestType.TESSOKU_BOOK, 'practicals')).toBe(practicalsProvider); -``` - -**After(TESSOKU_SECTIONS 定数)**: - -```typescript -import { TESSOKU_SECTIONS } from '$lib/types/contest_table_provider'; - -expect(provider['getProviderKey']()).toBe(`TESSOKU_BOOK::${TESSOKU_SECTIONS.EXAMPLES}`); -expect(group.getProvider(ContestType.TESSOKU_BOOK, TESSOKU_SECTIONS.PRACTICALS)).toBe( - practicalsProvider, -); -``` - -### メリット - -1. **型安全性の向上** - - 定数値の変更時、すべての利用箇所が自動的に追従 - - タイポ防止:`'exapmles'` のようなスペルミスを事前防止 - -2. **保守性の向上** - - セクション値の定義を一元管理(`contest_table_provider.ts` の `TESSOKU_SECTIONS`) - - 値の意図が明確(`EXAMPLES`, `PRACTICALS`, `CHALLENGES` はセマンティック) - -3. **ドキュメント性の向上** - - テストコード自体がドキュメント化 - - セクションの有効値が明示的にわかる - -4. **リファクタリングの容易性** - - セクション名変更時、`TESSOKU_SECTIONS` 定義を変更するだけで全体を更新 - - IDE のリファクタリング機能で安全な置き換えが可能 - -### テスト実装例 - -```typescript -import { TESSOKU_SECTIONS } from '$lib/types/contest_table_provider'; - -describe('TessokuBook provider keys with TESSOKU_SECTIONS', () => { - test('expects createProviderKey to generate correct composite key with TESSOKU_SECTIONS.EXAMPLES', () => { - const key = ContestTableProviderBase.createProviderKey( - ContestType.TESSOKU_BOOK, - TESSOKU_SECTIONS.EXAMPLES, - ); - expect(key).toBe(`TESSOKU_BOOK::${TESSOKU_SECTIONS.EXAMPLES}`); - }); - - test('expects getProvider with TESSOKU_SECTIONS.PRACTICALS to retrieve correct provider', () => { - const provider = group.getProvider(ContestType.TESSOKU_BOOK, TESSOKU_SECTIONS.PRACTICALS); - expect(provider).toBe(practicalsProvider); - }); -}); -``` - -### 型安全性の強化 - -`TESSOKU_SECTIONS` は `as const` で定義されているため: - -```typescript -export const TESSOKU_SECTIONS = { - EXAMPLES: 'examples', - PRACTICALS: 'practicals', - CHALLENGES: 'challenges', -} as const; -``` - -- **値型の推論**: TypeScript は各プロパティの値を `'examples' | 'practicals' | 'challenges'` のリテラル型で推論 -- **プロパティアクセスの型安全**: `TESSOKU_SECTIONS.EXAMPLES` は常に `'examples'` 型に確定 -- **存在しないプロパティへのアクセス**: `TESSOKU_SECTIONS.INVALID` は型エラーになる - -### テスト結果 - -定数化後のテスト実行結果: - -- **全テスト**: 105 テスト合格 -- **セクション関連テスト**: 7 テスト(すべて定数化対応) -- **タイポエラー**: 0 件(定数化により未然に防止) - -### 推奨事項 - -1. **セクション定義の一元管理** - - 新しいセクションを追加する場合、必ず `TESSOKU_SECTIONS` に定義してから使用 - - 他の定数との一貫性を保つ - -2. **テスト・本番コード統一** - - テストコードとアプリケーションコード両方で `TESSOKU_SECTIONS` を利用 - - 値の乖離を防止 - -3. **ドキュメント更新** - - `TESSOKU_SECTIONS` の意図・用途を TSDoc でコメント化 - - 今後のメンテナーへの情報伝達を確実に - ---- - -**状態**: ✅ 完了(TESSOKU_SECTIONS 定数化、型安全性向上、保守性向上を達成) -```` diff --git a/docs/dev-notes/2025-11-14/add_tests_for_contest_table_provider/plan.md b/docs/dev-notes/2025-11-14/add_tests_for_contest_table_provider/plan.md deleted file mode 100644 index a542d6d63..000000000 --- a/docs/dev-notes/2025-11-14/add_tests_for_contest_table_provider/plan.md +++ /dev/null @@ -1,133 +0,0 @@ -# ABC126ToABC211Provider テスト追加計画 - -**作成日**: 2025-11-14 - -**対象ブランチ**: #2830 - -**優先度**: High - ---- - -## 参照ドキュメント - -テストの書き方・スタイルについては、以下を参照: - -📖 [`docs/dev-notes/2025-11-03/add_tests_for_contest_table_provider/plan.md`](../../2025-11-03/add_tests_for_contest_table_provider/plan.md) - ---- - -## 実装チェックリスト - -### 1. テスト設計 ✅ - -- [x] フィルタリングテスト(ABC126~211範囲内のみ抽出) -- [x] コンテストタイプ判別テスト(ABC型のみ) -- [x] メタデータ取得テスト -- [x] ディスプレイ設定テスト -- [x] ラウンドラベルフォーマットテスト -- [x] エッジケーステスト(空入力など) -- [x] 混合コンテストタイプ対応テスト - -### 2. モックデータ準備 - -- [x] `src/test/lib/utils/test_cases/contest_table_provider.ts` に ABC126~211 データを追加 -- [x] ABC126, ABC150, ABC211 の 3 コンテストでサンプルデータを作成 -- [x] task_table_index は A, B, C, D, E, F に対応 - -### 3. テスト実装 - -- [x] 既存テスト(`ABC212ToABC318Provider` など)を参考に記述 -- [x] `ABC126ToABC211Provider` をテストファイルにインポート -- [x] `describe.each()` に ABC126ToABC211Provider を追加(displayConfig 共通化) - -### 4. テスト リファクタリング - -- [x] `describe.each()` に ABC126ToABC211 を追加:displayConfig, label format, empty results テストを共通化 -- [x] ABC126ToABC211 個別テストから重複テストを削除:5 個削除 → 4 個に削減 - -### 5. 実装後の検証 - -- [x] テスト実行: `pnpm test:unit src/test/lib/utils/contest_table_provider.test.ts` → **115 テスト全てパス** -- [x] Lint チェック: `pnpm format` → **フォーマット完了** -- [x] 全テスト合格確認 → **✅ PASS** - ---- - -## テスト仕様 - -### 対象プロバイダー - -`ABC126ToABC211Provider` - -### フィルタ範囲 - -- **最小**: ABC 126 -- **最大**: ABC 211 -- **対象数**: 86 コンテスト - -### 表示設定 - -| 項目 | 値 | -| --------------------- | ------------------------------------------------------- | -| `isShownHeader` | `true` | -| `isShownRoundLabel` | `true` | -| `roundLabelWidth` | `'xl:w-16'` | -| `tableBodyCellsWidth` | `'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 px-1 py-1'` | -| `isShownTaskIndex` | `false` | - ---- - -## 実装結果・教訓 - -### ✅ 実装完了 - -**実施時間**: 約 5 分(リファクタリング含む) - -**実装内容**: - -1. モックデータ追加: ABC126, ABC150, ABC211 の 3 コンテスト分(9 タスク) -2. `describe.each()` に ABC126ToABC211Provider を追加 -3. ABC126ToABC211 個別テストから重複テストを削除(5→4) -4. テストコード削減: 39 行削除、DRY 原則に従う - -### 📚 得られた教訓 - -1. **パラメータ化テストの活用**: `describe.each()` で同一構造のプロバイダーをシェアすることで、メンテナンス性向上とコード削減が実現。新規プロバイダー追加時は同じ表示設定を持つ場合、`describe.each()` への統合を検討すべき - -2. **テスト重複の排除**: 共通テスト(displayConfig, label format, empty results)と固有テスト(フィルタリング範囲)の分離により、テストの意図が明確化され、保守が容易に - -3. **プロバイダー設計の統一性**: ABC126~211 と ABC212~318 が同じ displayConfig を持つことは、プロバイダー実装の設計が一貫していることを示す。新規プロバイダー追加時は既存設計との整合性を確認することが重要 - ---- - -## 改善提案(実装完了)✅ - -### `describe.each()` パターンへの統合 - -**実施内容**: - -`ABC126ToABC211Provider` を `describe.each()` に追加し、displayConfig などの共通テストをシェア。 - -**変更内容**: - -```typescript -// 追加されたパラメータ -{ - providerClass: ABC126ToABC211Provider, - label: '126 to 211', - displayConfig: { - roundLabelWidth: 'xl:w-16', - tableBodyCellsWidth: 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 px-1 py-1', - }, -}, -``` - -**ABC126ToABC211 個別テストから削除**: - -- `test('expects to get correct display configuration', ...)` -- `test('expects to format contest round label correctly', ...)` -- `test('expects to handle empty task results', ...)` -- `test('expects to generate correct table structure', ...)` -- `test('expects to get contest round IDs correctly', ...)` - -**結果**: テストコード 39 行削減、テスト数 115(変わらず)、DRY 原則に従う構造へ改善 diff --git a/docs/dev-notes/2025-11-15/add_tests_for_contest_table_provider/plan.md b/docs/dev-notes/2025-11-15/add_tests_for_contest_table_provider/plan.md deleted file mode 100644 index cc48661d8..000000000 --- a/docs/dev-notes/2025-11-15/add_tests_for_contest_table_provider/plan.md +++ /dev/null @@ -1,699 +0,0 @@ -````markdown -# ARC104OnwardsProvider テスト追加計画 - -**作成日**: 2025-11-15 - -**対象ブランチ**: #2835 - -**優先度**: High - ---- - -## 参照ドキュメント - -テストの書き方・スタイルについては、以下を参照: - -📖 [`docs/dev-notes/2025-11-01/add_and_refactoring_tests_for_contest_table_provider/plan.md`](../../2025-11-01/add_and_refactoring_tests_for_contest_table_provider/plan.md) - -📖 [`docs/dev-notes/2025-11-03/add_tests_for_contest_table_provider/plan.md`](../../2025-11-03/add_tests_for_contest_table_provider/plan.md) - -📖 [`docs/dev-notes/2025-11-14/add_tests_for_contest_table_provider/plan.md`](../../2025-11-14/add_tests_for_contest_table_provider/plan.md) - ---- - -## 実装チェックリスト - -### 1. テスト設計 📋 - -- [ ] フィルタリングテスト(ARC104~999範囲内のみ抽出) -- [ ] コンテストタイプ判別テスト(ARC型のみ) -- [ ] メタデータ取得テスト -- [ ] ディスプレイ設定テスト -- [ ] ラウンドラベルフォーマットテスト -- [ ] エッジケーステスト(空入力など) -- [ ] 混合コンテストタイプ対応テスト -- [ ] 複数問題パターンテスト(4問、5問、6問、7問+F2) - -### 2. モックデータ準備 - -- [ ] `src/test/lib/utils/test_cases/contest_table_provider.ts` に ARC104+ データを追加 -- [ ] ARC104(6問: A, B, C, D, E, F) -- [ ] ARC120(7問: A, B, C, D, E, F, F2)- 例外的ケース -- [ ] ARC204(4問: A, B, C, D) -- [ ] ARC208(5問: A, B, C, D, E) - -### 3. テスト実装 - -- [ ] 既存テスト(`ABC212ToABC318Provider` など)を参考に記述 -- [ ] `ARC104OnwardsProvider` をテストファイルにインポート -- [ ] `describe.each()` に ARC104OnwardsProvider を追加(displayConfig 共通化) -- [ ] ARC104Onwards個別テストで複数問題パターンの検証を追加 - -### 4. テスト リファクタリング - -- [ ] displayConfig 共通テストを `describe.each()` で統合 -- [ ] ARC104Onwards固有テスト(フィルタリング範囲、複数パターン)を実装 - -### 5. 実装後の検証 - -- [ ] テスト実行: `pnpm test:unit src/test/lib/utils/contest_table_provider.test.ts` -- [ ] Lint チェック: `pnpm format` -- [ ] 全テスト合格確認 - ---- - -## 1. テスト対象プロバイダー - -### ARC104OnwardsProvider - -| 項目 | 仕様 | 備考 | -| ---------------- | -------------------- | ------------------ | -| **範囲** | ARC 104 ~ 999 | 開始日: 2020/10/03 | -| **問題数** | 4~7問 | ラウンドにより変動 | -| **フォーマット** | A, B, C, D, E, F, F2 | 標準は6問(F迄) | - ---- - -## 2. 問題パターン仕様 - -### パターン1: 4問コンテスト(ARC204) - -``` -task_table_index: A, B, C, D -``` - -**用例**: 一部の特殊ラウンド - ---- - -### パターン2: 5問コンテスト(ARC208, 209) - -``` -task_table_index: A, B, C, D, E -``` - -**用例**: 比較的新しいラウンドの一部 - ---- - -### パターン3: 6問コンテスト(標準) - -``` -task_table_index: A, B, C, D, E, F -``` - -**用例**: ARC104, ARC150など大多数のラウンド - ---- - -### パターン4: 7問コンテスト(ARC120のみ) - -``` -task_table_index: A, B, C, D, E, F, F2 -``` - -**用例**: ARC120(非常に例外的) - ---- - -## 3. 表示設定(displayConfig) - -| 項目 | 値 | -| --------------------- | ------------------------------------------------------- | -| `isShownHeader` | `true` | -| `isShownRoundLabel` | `true` | -| `roundLabelWidth` | `'xl:w-16'` | -| `tableBodyCellsWidth` | `'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 px-1 py-1'` | -| `isShownTaskIndex` | `false` | - -**備考**: `ABC126ToABC211Provider` と `ABC212ToABC318Provider` と同じ設定 - ---- - -## 4. テストケース仕様(12-14件) - -### 4.1 共通テスト(describe.each()統合) - -#### テスト1: displayConfig - -```typescript -test('expects to get correct display configuration', () => { - const provider = new ARC104OnwardsProvider(ContestType.ARC); - const config = provider.getDisplayConfig(); - - expect(config.isShownHeader).toBe(true); - expect(config.isShownRoundLabel).toBe(true); - expect(config.roundLabelWidth).toBe('xl:w-16'); - expect(config.tableBodyCellsWidth).toBe('w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 px-1 py-1'); - expect(config.isShownTaskIndex).toBe(false); -}); -``` - -**期待値**: ABC等と同じ設定 -**検証方法**: `toBe()` による厳密一致 - ---- - -#### テスト2: ラウンドラベルフォーマット - -```typescript -test('expects to format contest round label correctly', () => { - const provider = new ARC104OnwardsProvider(ContestType.ARC); - const label = provider.getContestRoundLabel('arc378'); - - expect(label).toBe('378'); -}); -``` - -**期待値**: 「ARC」プレフィックス削除後の数字のみ -**検証方法**: `toBe()` - ---- - -#### テスト3: 空入力処理 - -```typescript -test('expects to handle empty task results', () => { - const provider = new ARC104OnwardsProvider(ContestType.ARC); - const filtered = provider.filter([] as TaskResults); - - expect(filtered).toEqual([] as TaskResults); -}); -``` - -**期待値**: 空配列を空配列で返す -**検証方法**: `toEqual([])` - ---- - -### 4.2 ARC104Onwards 固有テスト - -#### テスト4: フィルタリング(範囲検証) - -```typescript -test('expects to filter tasks to include only ARC104 and later', () => { - const provider = new ARC104OnwardsProvider(ContestType.ARC); - const filtered = provider.filter(mockTaskResults); - - expect(filtered.every((task) => task.contest_id.startsWith('arc'))).toBe(true); - expect( - filtered.every((task) => { - const round = getContestRound(task.contest_id, 'arc'); - return round >= 104 && round <= 999; - }), - ).toBe(true); -}); -``` - -**期待値**: ARC104~999範囲内のみ -**検証方法**: `every()` + 数値範囲チェック - ---- - -#### テスト5: メタデータ取得 - -```typescript -test('expects to get correct metadata', () => { - const provider = new ARC104OnwardsProvider(ContestType.ARC); - const metadata = provider.getMetadata(); - - expect(metadata.title).toBe('AtCoder Regular Contest 104 〜 '); - expect(metadata.abbreviationName).toBe('arc104Onwards'); -}); -``` - -**期待値**: タイトル、略称が正確 -**検証方法**: `toBe()` - ---- - -#### テスト6: テーブル生成(複数パターン対応) - -```typescript -test('expects to generate correct table structure', () => { - const provider = new ARC104OnwardsProvider(ContestType.ARC); - const tasks = [ - { contest_id: 'arc104', task_id: 'arc104_a', task_table_index: 'A' }, - { contest_id: 'arc104', task_id: 'arc104_b', task_table_index: 'B' }, - { contest_id: 'arc204', task_id: 'arc204_a', task_table_index: 'A' }, - { contest_id: 'arc204', task_id: 'arc204_d', task_table_index: 'D' }, - ]; - const table = provider.generateTable(tasks as TaskResults); - - expect(table).toHaveProperty('arc104'); - expect(table).toHaveProperty('arc204'); - expect(table['arc104']).toHaveProperty('A'); - expect(table['arc204']['D']).toEqual(expect.objectContaining({ task_id: 'arc204_d' })); -}); -``` - -**期待値**: `{ 'arc104': { 'A': {...}, 'B': {...} }, 'arc204': { 'A': {...}, 'D': {...} } }` -**検証方法**: `toHaveProperty()` + `objectContaining()` - ---- - -#### テスト7: ラウンド ID 取得 - -```typescript -test('expects to get contest round IDs correctly', () => { - const provider = new ARC104OnwardsProvider(ContestType.ARC); - const tasks = [ - { contest_id: 'arc204', task_id: 'arc204_a', task_table_index: 'A' }, - { contest_id: 'arc120', task_id: 'arc120_a', task_table_index: 'A' }, - { contest_id: 'arc104', task_id: 'arc104_a', task_table_index: 'A' }, - ]; - const roundIds = provider.getContestRoundIds(tasks as TaskResults); - - expect(roundIds).toEqual(['arc204', 'arc120', 'arc104']); -}); -``` - -**期待値**: 降順ソート(新しい順) -**検証方法**: `toEqual()` - ---- - -#### テスト8: ヘッダー ID 取得(複数問題対応) - -```typescript -test('expects to get header IDs for tasks correctly', () => { - const provider = new ARC104OnwardsProvider(ContestType.ARC); - const tasks = [ - { contest_id: 'arc104', task_id: 'arc104_a', task_table_index: 'A' }, - { contest_id: 'arc104', task_id: 'arc104_f', task_table_index: 'F' }, - { contest_id: 'arc104', task_id: 'arc104_e', task_table_index: 'E' }, - ]; - const headerIds = provider.getHeaderIdsForTask(tasks as TaskResults); - - expect(headerIds).toEqual(['A', 'E', 'F']); -}); -``` - -**期待値**: 昇順ソート済みのタスク一覧 -**検証方法**: `toEqual()` - ---- - -#### テスト9: 4問パターン(ARC204) - -```typescript -test('expects to handle 4-problem contest pattern (ARC204)', () => { - const provider = new ARC104OnwardsProvider(ContestType.ARC); - const tasks = [ - { contest_id: 'arc204', task_id: 'arc204_a', task_table_index: 'A' }, - { contest_id: 'arc204', task_id: 'arc204_b', task_table_index: 'B' }, - { contest_id: 'arc204', task_id: 'arc204_c', task_table_index: 'C' }, - { contest_id: 'arc204', task_id: 'arc204_d', task_table_index: 'D' }, - ]; - const filtered = provider.filter(tasks as TaskResults); - const headerIds = provider.getHeaderIdsForTask(filtered); - - expect(filtered).toHaveLength(4); - expect(headerIds).toEqual(['A', 'B', 'C', 'D']); -}); -``` - -**期待値**: 4問(A, B, C, D)のみ取得 -**検証方法**: `toHaveLength()` + `toEqual()` - ---- - -#### テスト10: 5問パターン(ARC208) - -```typescript -test('expects to handle 5-problem contest pattern (ARC208)', () => { - const provider = new ARC104OnwardsProvider(ContestType.ARC); - const tasks = [ - { contest_id: 'arc208', task_id: 'arc208_a', task_table_index: 'A' }, - { contest_id: 'arc208', task_id: 'arc208_b', task_table_index: 'B' }, - { contest_id: 'arc208', task_id: 'arc208_c', task_table_index: 'C' }, - { contest_id: 'arc208', task_id: 'arc208_d', task_table_index: 'D' }, - { contest_id: 'arc208', task_id: 'arc208_e', task_table_index: 'E' }, - ]; - const filtered = provider.filter(tasks as TaskResults); - const headerIds = provider.getHeaderIdsForTask(filtered); - - expect(filtered).toHaveLength(5); - expect(headerIds).toEqual(['A', 'B', 'C', 'D', 'E']); -}); -``` - -**期待値**: 5問(A, B, C, D, E)のみ取得 -**検証方法**: `toHaveLength()` + `toEqual()` - ---- - -#### テスト11: 7問パターン+F2(ARC120) - -```typescript -test('expects to handle 7-problem contest pattern with F2 (ARC120)', () => { - const provider = new ARC104OnwardsProvider(ContestType.ARC); - const tasks = [ - { contest_id: 'arc120', task_id: 'arc120_a', task_table_index: 'A' }, - { contest_id: 'arc120', task_id: 'arc120_b', task_table_index: 'B' }, - { contest_id: 'arc120', task_id: 'arc120_c', task_table_index: 'C' }, - { contest_id: 'arc120', task_id: 'arc120_d', task_table_index: 'D' }, - { contest_id: 'arc120', task_id: 'arc120_e', task_table_index: 'E' }, - { contest_id: 'arc120', task_id: 'arc120_f', task_table_index: 'F' }, - { contest_id: 'arc120', task_id: 'arc120_f2', task_table_index: 'F2' }, - ]; - const filtered = provider.filter(tasks as TaskResults); - const headerIds = provider.getHeaderIdsForTask(filtered); - - expect(filtered).toHaveLength(7); - expect(headerIds).toEqual(['A', 'B', 'C', 'D', 'E', 'F', 'F2']); -}); -``` - -**期待値**: 7問(A, B, C, D, E, F, F2)すべて取得 -**検証方法**: `toHaveLength()` + `toEqual()` - ---- - -#### テスト12: 混合コンテストタイプの排除 - -```typescript -test('expects to handle task results with different contest types', () => { - const provider = new ARC104OnwardsProvider(ContestType.ARC); - const mixedTasks = [ - { contest_id: 'arc200', task_id: 'arc200_a', task_table_index: 'A' }, - { contest_id: 'abc123', task_id: 'abc123_a', task_table_index: 'A' }, - { contest_id: 'arc104', task_id: 'arc104_a', task_table_index: 'A' }, - { contest_id: 'typical90', task_id: 'typical90_a', task_table_index: '001' }, - ]; - const filtered = provider.filter(mixedTasks as TaskResults); - - expect(filtered).toHaveLength(2); - expect(filtered?.every((task) => task.contest_id.startsWith('arc'))).toBe(true); -}); -``` - -**期待値**: `arc` で始まるタスクのみ、他を完全に排除 -**検証方法**: `toHaveLength()` + `every()` - ---- - -#### テスト13: 範囲外コンテストの排除(ARC103以下) - -```typescript -test('expects to exclude contests below ARC104', () => { - const provider = new ARC104OnwardsProvider(ContestType.ARC); - const mixedTasks = [ - { contest_id: 'arc100', task_id: 'arc100_a', task_table_index: 'A' }, - { contest_id: 'arc103', task_id: 'arc103_a', task_table_index: 'A' }, - { contest_id: 'arc104', task_id: 'arc104_a', task_table_index: 'A' }, - { contest_id: 'arc105', task_id: 'arc105_a', task_table_index: 'A' }, - ]; - const filtered = provider.filter(mixedTasks as TaskResults); - - expect(filtered).toHaveLength(2); - expect( - filtered?.every((task) => { - const round = getContestRound(task.contest_id, 'arc'); - return round >= 104; - }), - ).toBe(true); -}); -``` - -**期待値**: ARC104以上のみ(ARC100、103は除外) -**検証方法**: `toHaveLength()` + `every()` - ---- - -#### テスト14: 提供元ヘッダー ID 順序(昇順確認) - -```typescript -test('expects to maintain proper alphabetical/numeric sort order', () => { - const provider = new ARC104OnwardsProvider(ContestType.ARC); - const tasks = [ - { contest_id: 'arc104', task_id: 'arc104_f', task_table_index: 'F' }, - { contest_id: 'arc104', task_id: 'arc104_c', task_table_index: 'C' }, - { contest_id: 'arc104', task_id: 'arc104_a', task_table_index: 'A' }, - { contest_id: 'arc104', task_id: 'arc104_f2', task_table_index: 'F2' }, - ]; - const headerIds = provider.getHeaderIdsForTask(tasks as TaskResults); - - expect(headerIds).toEqual(['A', 'C', 'F', 'F2']); -}); -``` - -**期待値**: A → C → F → F2 の正確な昇順 -**検証方法**: `toEqual()` - ---- - -## 5. モックデータ設計 - -### 5.1 追加先 - -`src/test/lib/utils/test_cases/contest_table_provider.ts` - -### 5.2 構成 - -#### パターンA: ARC104(6問、標準) - -```typescript -const [arc104_a, arc104_b, arc104_c, arc104_d, arc104_e, arc104_f] = createContestTasks('arc104', [ - { taskTableIndex: 'A', statusName: AC }, - { taskTableIndex: 'B', statusName: AC }, - { taskTableIndex: 'C', statusName: AC_WITH_EDITORIAL }, - { taskTableIndex: 'D', statusName: TRYING }, - { taskTableIndex: 'E', statusName: PENDING }, - { taskTableIndex: 'F', statusName: AC }, -]); -``` - ---- - -#### パターンB: ARC120(7問、F2含む) - -```typescript -const [arc120_a, arc120_b, arc120_c, arc120_d, arc120_e, arc120_f, arc120_f2] = createContestTasks( - 'arc120', - [ - { taskTableIndex: 'A', statusName: AC }, - { taskTableIndex: 'B', statusName: AC }, - { taskTableIndex: 'C', statusName: AC }, - { taskTableIndex: 'D', statusName: AC_WITH_EDITORIAL }, - { taskTableIndex: 'E', statusName: TRYING }, - { taskTableIndex: 'F', statusName: PENDING }, - { taskTableIndex: 'F2', statusName: AC }, - ], -); -``` - ---- - -#### パターンC: ARC204(4問) - -```typescript -const [arc204_a, arc204_b, arc204_c, arc204_d] = createContestTasks('arc204', [ - { taskTableIndex: 'A', statusName: AC }, - { taskTableIndex: 'B', statusName: AC_WITH_EDITORIAL }, - { taskTableIndex: 'C', statusName: TRYING }, - { taskTableIndex: 'D', statusName: PENDING }, -]); -``` - ---- - -#### パターンD: ARC208(5問) - -```typescript -const [arc208_a, arc208_b, arc208_c, arc208_d, arc208_e] = createContestTasks('arc208', [ - { taskTableIndex: 'A', statusName: AC }, - { taskTableIndex: 'B', statusName: AC }, - { taskTableIndex: 'C', statusName: AC_WITH_EDITORIAL }, - { taskTableIndex: 'D', statusName: TRYING }, - { taskTableIndex: 'E', statusName: PENDING }, -]); -``` - ---- - -### 5.3 エクスポート - -```typescript -export const taskResultsForARC104OnwardsProvider: TaskResults = [ - arc104_a, - arc104_b, - arc104_c, - arc104_d, - arc104_e, - arc104_f, - arc120_a, - arc120_b, - arc120_c, - arc120_d, - arc120_e, - arc120_f, - arc120_f2, - arc204_a, - arc204_b, - arc204_c, - arc204_d, - arc208_a, - arc208_b, - arc208_c, - arc208_d, - arc208_e, -]; -``` - ---- - -## 6. 実装手順 - -**ステップ1**: モックデータを `src/test/lib/utils/test_cases/contest_table_provider.ts` に追加 - -**ステップ2**: `src/test/lib/utils/contest_table_provider.test.ts` に以下を追加 - -- `describe.each()` に `ARC104OnwardsProvider` を追加(displayConfig等共通テスト) -- `describe('ARC 104 Onwards')` セクションで固有テスト14個を実装 - -**ステップ3**: テスト実行・検証 - -```bash -pnpm test:unit src/test/lib/utils/contest_table_provider.test.ts -``` - -**ステップ4**: Lint チェック - -```bash -pnpm format -``` - ---- - -## 7. 注意点 - -### 7.1 ソート順序 - -文字列の辞書順ソート(`'A' < 'B' < 'C' < 'D' < 'E' < 'F' < 'F2'`) - -**補足**: `'F'` < `'F2'` であることが重要(`'F2'` は2文字) - ---- - -### 7.2 複数パターン対応 - -- 4問(ARC204): A, B, C, D -- 5問(ARC208, 209): A, B, C, D, E -- 6問(標準): A, B, C, D, E, F -- 7問(ARC120のみ): A, B, C, D, E, F, F2 - -各パターンで独立したテストを用意し、複数パターン同時の場合も検証 - ---- - -### 7.3 範囲検証 - -ARC104 ~ 999の範囲内を厳密に検証 - -- `arc103` 以下: 除外 -- `arc104` 以上: 含包 - ---- - -### 7.4 displayConfig 共通化 - -`ABC126ToABC211Provider` と `ABC212ToABC318Provider` と同じ displayConfig を持つため、`describe.each()` に統合可能 - -| 項目 | 値 | -| --------------------- | ------------------------------------------------------- | -| `isShownHeader` | `true` | -| `isShownRoundLabel` | `true` | -| `roundLabelWidth` | `'xl:w-16'` | -| `tableBodyCellsWidth` | `'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 px-1 py-1'` | -| `isShownTaskIndex` | `false` | - ---- - -## 8. テスト数想定 - -| カテゴリ | 個数 | 備考 | -| --------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -| 共通テスト(describe.each()統合) | 3-4 | displayConfig, ラウンドラベル, 空入力など | -| ARC104Onwards固有テスト | 11-12 | 1. フィルタリング 2. メタデータ 3. テーブル生成 4. ラウンドID 5. ヘッダーID 6-8. 複数問題パターン(4,5,7問) 9. 型混合処理 10. 範囲外除外 11. ソート順序 | -| **合計** | **14-16** | | - ---- - -## 9. 実装パターン参考 - -### 参照実装: ABC212ToABC318Provider - -```typescript -describe.each([ - { - providerClass: ABC212ToABC318Provider, - label: '212 to 318', - displayConfig: { - roundLabelWidth: 'xl:w-16', - tableBodyCellsWidth: 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 px-1 py-1', - }, - }, -])('$label', ({ providerClass, displayConfig }) => { - test('expects to get correct display configuration', () => { - const provider = new providerClass(ContestType.ABC); - const config = provider.getDisplayConfig(); - - expect(config.isShownHeader).toBe(true); - expect(config.isShownRoundLabel).toBe(true); - expect(config.roundLabelWidth).toBe(displayConfig.roundLabelWidth); - expect(config.tableBodyCellsWidth).toBe(displayConfig.tableBodyCellsWidth); - expect(config.isShownTaskIndex).toBe(false); - }); - - // 他のテスト... -}); - -// ARC104Onwards固有テスト -describe('ARC 104 Onwards', () => { - test('expects to filter tasks to include only ARC104 and later', () => { - // ... - }); - - test('expects to handle 4-problem contest pattern (ARC204)', () => { - // ... - }); - - // 他のテスト... -}); -``` - ---- - -## 10. 教訓・ベストプラクティス - -### 参照ドキュメントから得られた知見 - -1. **複数パターン対応**: ARC104Onwards の複数問題パターン(4, 5, 6, 7問)は、各々を明示的にテストすることで、将来的な問題数変動に対応できる設計 - -2. **F2の例外処理**: ARC120のみの例外ケースを専用テストで検証することで、メンテナンス性が向上 - -3. **displayConfig の統一**: ABC系プロバイダーと ARC系プロバイダーが同じ表示設定を持つことで、UI 層での一貫性が保証される - -4. **テスト粒度の統一**: フィルタリング、メタデータ、表示設定、テーブル生成など、すべてのプロバイダーで等しいテスト粒度を適用 - -5. **ソート順序の厳密化**: 文字列ソートと数値ソートの違いを理解し、期待値を正確に指定することが重要 - ---- - -## 11. 実装完了記録 - -**実装日**: 2025-11-15 - -**テスト結果**: 127 テスト全合格 - -### 実装時の学習 - -1. **モック関数の完全性**: テスト対象が新しいコンテストタイプ(ARC)を扱う場合、モック関数(`classifyContest`, `getContestNameLabel`)にもそのコンテストタイプの処理を追加する必要がある - -2. **describe.each() の型安全性**: 複数のプロバイダーを単一の describe.each() でテストする場合、プロバイダーごとに異なるコンテストタイプ(ABC vs ARC)を使用する場合は分離したほうがシンプル - -3. **複数パターンテストの有効性**: 4, 5, 6, 7 問のパターンを個別テストすることで、将来の仕様変更にも対応しやすい設計が実現 - -4. **小規模な mock データの活用**: 固有テスト用に限定的な mock データセット(`taskResultsForARC104OnwardsProvider`)を用意することで、テスト意図が明確になる -```` diff --git a/docs/dev-notes/2025-11-19/add_tests_for_contest_table_provider/plan.md b/docs/dev-notes/2025-11-19/add_tests_for_contest_table_provider/plan.md deleted file mode 100644 index d6989aa9e..000000000 --- a/docs/dev-notes/2025-11-19/add_tests_for_contest_table_provider/plan.md +++ /dev/null @@ -1,541 +0,0 @@ -# AGC001OnwardsProvider テスト追加計画 - -**作成日**: 2025-11-19 - -**対象ブランチ**: #2837 - -**優先度**: High - ---- - -## 参照ドキュメント - -テストの書き方・スタイルについては、以下を参照: - -📖 [`docs/dev-notes/2025-11-15/add_tests_for_contest_table_provider/plan.md`](../../2025-11-15/add_tests_for_contest_table_provider/plan.md) (ARC104OnwardsProvider) - -**本ドキュメントは ARC版の差分版です。基本構造は ARC版に準じます。** - ---- - -## 実装チェックリスト - -### 1. テスト設計 📋 - -- [ ] フィルタリングテスト(AGC001~999範囲内のみ抽出) -- [ ] コンテストタイプ判別テスト(AGC型のみ) -- [ ] メタデータ取得テスト -- [ ] ディスプレイ設定テスト -- [ ] ラウンドラベルフォーマットテスト -- [ ] エッジケーステスト(空入力など) -- [ ] 混合コンテストタイプ対応テスト -- [ ] **複数問題パターンテスト(4問、5問、6問、7問)** - -### 2. モックデータ準備 - -- [ ] `src/test/lib/utils/test_cases/contest_table_provider.ts` に AGC001+ データを追加 -- [ ] AGC001(6問: A, B, C, D, E, F)- 標準パターン -- [ ] AGC002(6問: A, B, C, D, E, F)- 標準パターン -- [ ] AGC009(5問: A, B, C, D, E)- 例外パターン -- [ ] AGC028(7問: A, B, C, D, E, F, F2)- 2025年11時点で、唯一の7問パターン -- [ ] AGC073(4問: A, B, C, D)- 2025年11時点で、唯一の4問パターン -- [ ] AGC074(5問: A, B, C, D, E)- AGC067以降の5問パターン - -### 3. テスト実装 - -- [ ] 既存テスト(ARC104OnwardsProvider)を参考に記述 -- [ ] `AGC001OnwardsProvider` をテストファイルにインポート -- [ ] `describe.each()` に AGC001OnwardsProvider を追加(displayConfig 共通化) -- [ ] AGC001Onwards個別テストで複数問題パターンの検証を追加 - -### 4. テスト リファクタリング - -- [ ] displayConfig 共通テストを `describe.each()` で統合 -- [ ] AGC001Onwards固有テスト(フィルタリング範囲、複数パターン)を実装 - -### 5. 実装後の検証 - -- [ ] テスト実行: `pnpm test:unit src/test/lib/utils/contest_table_provider.test.ts` -- [ ] Lint チェック: `pnpm format` -- [ ] 全テスト合格確認 - ---- - -## 1. テスト対象プロバイダー - -### AGC001OnwardsProvider - -| 項目 | 仕様 | 備考 | -| ---------------- | -------------------- | ------------------ | -| **範囲** | AGC 001 ~ 999 | 開始日: 2016/07/16 | -| **問題数** | 4~7問 | ラウンドにより変動 | -| **フォーマット** | A, B, C, D, E, F, F2 | 標準は6問(F迄) | - ---- - -## 2. 問題パターン仕様 - -### パターン1: 4問コンテスト(AGC073) - -```text -task_table_index: A, B, C, D -``` - -**用例**: AGC073(唯一) - ---- - -### パターン2: 5問コンテスト(AGC009、AGC067~) - -```text -task_table_index: A, B, C, D, E -``` - -**用例**: AGC009(歴史的)、AGC067以降(標準) - ---- - -### パターン3: 6問コンテスト(標準) - -```text -task_table_index: A, B, C, D, E, F -``` - -**用例**: AGC001, AGC002, AGC010~AGC066 など大多数のラウンド - ---- - -### パターン4: 7問コンテスト(AGC028のみ) - -```text -task_table_index: A, B, C, D, E, F, F2 -``` - -**用例**: AGC028(非常に例外的) - ---- - -## 3. 表示設定(displayConfig) - -| 項目 | 値 | -| --------------------- | ------------------------------------------------------- | -| `isShownHeader` | `true` | -| `isShownRoundLabel` | `true` | -| `roundLabelWidth` | `'xl:w-16'` | -| `tableBodyCellsWidth` | `'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 px-1 py-1'` | -| `isShownTaskIndex` | `false` | - -**備考**: `ARC104OnwardsProvider` と同じ設定 - ---- - -## 4. テストケース仕様 - -> 詳細は [`docs/dev-notes/2025-11-15/add_tests_for_contest_table_provider/plan.md`](../../2025-11-15/add_tests_for_contest_table_provider/plan.md) の「4. テストケース仕様」を参照。 -> -> AGC版では以下の差分のみ記載: - -### 4.1 共通テスト(describe.each()統合) - -ARC版と同様(displayConfig, ラウンドラベルフォーマット, 空入力処理) - -### 4.2 AGC001Onwards 固有テスト(差分) - -#### テスト: フィルタリング(範囲検証) - -```typescript -test('expects to filter tasks to include only AGC001 and later', () => { - const provider = new AGC001OnwardsProvider(ContestType.AGC); - const filtered = provider.filter(mockTaskResults); - - expect(filtered.every((task) => task.contest_id.startsWith('agc'))).toBe(true); - expect( - filtered.every((task) => { - const round = getContestRound(task.contest_id, 'agc'); - return round >= 1 && round <= 999; - }), - ).toBe(true); -}); -``` - -**期待値**: AGC001~999範囲内のみ -**参照**: ARC版テスト4を AGC用に適応 - ---- - -#### テスト: メタデータ取得 - -```typescript -test('expects to get correct metadata', () => { - const provider = new AGC001OnwardsProvider(ContestType.AGC); - const metadata = provider.getMetadata(); - - expect(metadata.title).toBe('AtCoder Grand Contest 001 〜 '); - expect(metadata.abbreviationName).toBe('agc001Onwards'); -}); -``` - -**参照**: ARC版テスト5 - ---- - -#### テスト: 4問パターン(AGC073) - -```typescript -test('expects to handle 4-problem contest pattern (AGC073)', () => { - const provider = new AGC001OnwardsProvider(ContestType.AGC); - const tasks = [ - { contest_id: 'agc073', task_id: 'agc073_a', task_table_index: 'A' }, - { contest_id: 'agc073', task_id: 'agc073_b', task_table_index: 'B' }, - { contest_id: 'agc073', task_id: 'agc073_c', task_table_index: 'C' }, - { contest_id: 'agc073', task_id: 'agc073_d', task_table_index: 'D' }, - ]; - const filtered = provider.filter(tasks as TaskResults); - const headerIds = provider.getHeaderIdsForTask(filtered); - - expect(filtered).toHaveLength(4); - expect(headerIds).toEqual(['A', 'B', 'C', 'D']); -}); -``` - -**参照**: ARC版テスト9を AGCに適用 - ---- - -#### テスト: 5問パターン(AGC009・AGC074) - -```typescript -test('expects to handle 5-problem contest pattern (AGC009, AGC074)', () => { - const provider = new AGC001OnwardsProvider(ContestType.AGC); - const tasks = [ - { contest_id: 'agc009', task_id: 'agc009_a', task_table_index: 'A' }, - { contest_id: 'agc009', task_id: 'agc009_b', task_table_index: 'B' }, - { contest_id: 'agc009', task_id: 'agc009_c', task_table_index: 'C' }, - { contest_id: 'agc009', task_id: 'agc009_d', task_table_index: 'D' }, - { contest_id: 'agc009', task_id: 'agc009_e', task_table_index: 'E' }, - ]; - const filtered = provider.filter(tasks as TaskResults); - const headerIds = provider.getHeaderIdsForTask(filtered); - - expect(filtered).toHaveLength(5); - expect(headerIds).toEqual(['A', 'B', 'C', 'D', 'E']); -}); -``` - -**参照**: ARC版テスト10を AGCに適用 - ---- - -#### テスト: 7問パターン+F2(AGC028) - -```typescript -test('expects to handle 7-problem contest pattern with F2 (AGC028)', () => { - const provider = new AGC001OnwardsProvider(ContestType.AGC); - const tasks = [ - { contest_id: 'agc028', task_id: 'agc028_a', task_table_index: 'A' }, - { contest_id: 'agc028', task_id: 'agc028_b', task_table_index: 'B' }, - { contest_id: 'agc028', task_id: 'agc028_c', task_table_index: 'C' }, - { contest_id: 'agc028', task_id: 'agc028_d', task_table_index: 'D' }, - { contest_id: 'agc028', task_id: 'agc028_e', task_table_index: 'E' }, - { contest_id: 'agc028', task_id: 'agc028_f', task_table_index: 'F' }, - { contest_id: 'agc028', task_id: 'agc028_f2', task_table_index: 'F2' }, - ]; - const filtered = provider.filter(tasks as TaskResults); - const headerIds = provider.getHeaderIdsForTask(filtered); - - expect(filtered).toHaveLength(7); - expect(headerIds).toEqual(['A', 'B', 'C', 'D', 'E', 'F', 'F2']); -}); -``` - -**参照**: ARC版テスト11を AGCに適用 - ---- - -#### その他のテスト - -- テスト: 混合コンテストタイプの排除 -- テスト: 範囲外コンテストの排除(AGC000以下) -- テスト: ソート順序(昇順確認) -- テスト: テーブル生成 -- テスト: ラウンド ID 取得 -- テスト: ヘッダー ID 取得 - -**参照**: ARC版テスト6, 7, 8, 12, 13, 14 - ---- - -## 5. モックデータ設計 - -### 5.1 追加先 - -`src/test/lib/utils/test_cases/contest_table_provider.ts` - -### 5.2 構成 - -#### パターンA: AGC001(6問、標準) - -```typescript -const [agc001_a, agc001_b, agc001_c, agc001_d, agc001_e, agc001_f] = createContestTasks('agc001', [ - { taskTableIndex: 'A', statusName: AC }, - { taskTableIndex: 'B', statusName: AC }, - { taskTableIndex: 'C', statusName: AC_WITH_EDITORIAL }, - { taskTableIndex: 'D', statusName: TRYING }, - { taskTableIndex: 'E', statusName: PENDING }, - { taskTableIndex: 'F', statusName: PENDING }, -]); -``` - ---- - -#### パターンB: AGC002(6問、標準) - -```typescript -const [agc002_a, agc002_b, agc002_c, agc002_d, agc002_e, agc002_f] = createContestTasks('agc002', [ - { taskTableIndex: 'A', statusName: AC }, - { taskTableIndex: 'B', statusName: AC }, - { taskTableIndex: 'C', statusName: AC }, - { taskTableIndex: 'D', statusName: AC_WITH_EDITORIAL }, - { taskTableIndex: 'E', statusName: TRYING }, - { taskTableIndex: 'F', statusName: PENDING }, -]); -``` - ---- - -#### パターンC: AGC009(5問、歴史的例外) - -```typescript -const [agc009_a, agc009_b, agc009_c, agc009_d, agc009_e] = createContestTasks('agc009', [ - { taskTableIndex: 'A', statusName: AC }, - { taskTableIndex: 'B', statusName: AC }, - { taskTableIndex: 'C', statusName: AC_WITH_EDITORIAL }, - { taskTableIndex: 'D', statusName: TRYING }, - { taskTableIndex: 'E', statusName: PENDING }, -]); -``` - ---- - -#### パターンD: AGC028(7問、F2含む) - -```typescript -const [agc028_a, agc028_b, agc028_c, agc028_d, agc028_e, agc028_f, agc028_f2] = createContestTasks( - 'agc028', - [ - { taskTableIndex: 'A', statusName: AC }, - { taskTableIndex: 'B', statusName: AC }, - { taskTableIndex: 'C', statusName: AC }, - { taskTableIndex: 'D', statusName: AC_WITH_EDITORIAL }, - { taskTableIndex: 'E', statusName: TRYING }, - { taskTableIndex: 'F', statusName: PENDING }, - { taskTableIndex: 'F2', statusName: PENDING }, - ], -); -``` - ---- - -#### パターンE: AGC073(4問) - -```typescript -const [agc073_a, agc073_b, agc073_c, agc073_d] = createContestTasks('agc073', [ - { taskTableIndex: 'A', statusName: AC }, - { taskTableIndex: 'B', statusName: AC_WITH_EDITORIAL }, - { taskTableIndex: 'C', statusName: TRYING }, - { taskTableIndex: 'D', statusName: PENDING }, -]); -``` - ---- - -#### パターンF: AGC074(5問、AGC067以降の標準) - -```typescript -const [agc074_a, agc074_b, agc074_c, agc074_d, agc074_e] = createContestTasks('agc074', [ - { taskTableIndex: 'A', statusName: AC }, - { taskTableIndex: 'B', statusName: AC }, - { taskTableIndex: 'C', statusName: AC_WITH_EDITORIAL }, - { taskTableIndex: 'D', statusName: TRYING }, - { taskTableIndex: 'E', statusName: PENDING }, -]); -``` - ---- - -### 5.3 エクスポート - -```typescript -export const taskResultsForAGC001OnwardsProvider: TaskResults = [ - agc001_a, - agc001_b, - agc001_c, - agc001_d, - agc001_e, - agc001_f, - agc002_a, - agc002_b, - agc002_c, - agc002_d, - agc002_e, - agc002_f, - agc009_a, - agc009_b, - agc009_c, - agc009_d, - agc009_e, - agc028_a, - agc028_b, - agc028_c, - agc028_d, - agc028_e, - agc028_f, - agc028_f2, - agc073_a, - agc073_b, - agc073_c, - agc073_d, - agc074_a, - agc074_b, - agc074_c, - agc074_d, - agc074_e, -]; -``` - ---- - -## 6. 実装手順 - -**ステップ1**: モックデータを `src/test/lib/utils/test_cases/contest_table_provider.ts` に追加 - -**ステップ2**: `src/test/lib/utils/contest_table_provider.test.ts` に以下を追加 - -- `describe.each()` に `AGC001OnwardsProvider` を追加(displayConfig等共通テスト) -- `describe('AGC 001 Onwards')` セクションで固有テスト(14個以上)を実装 - -**ステップ3**: テスト実行・検証 - -```bash -pnpm test:unit src/test/lib/utils/contest_table_provider.test.ts -``` - -**ステップ4**: Lint チェック - -```bash -pnpm format -``` - ---- - -## 7. AGC固有の注意点 - -### 7.1 複数例外パターンの網羅 - -AGCは以下4つの問題数パターンを持つため、各パターンを明示的にテストすることが重要: - -- 4問(AGC073) -- 5問(AGC009、AGC067~) -- 6問(〜AGC066の標準) -- 7問+F2(AGC028) - -### 7.2 モックデータの多様性 - -AGC001, AGC002(標準6問)、AGC009(歴史的5問)、AGC028(特殊7問)、AGC073(4問)、AGC074(新5問)の6パターンを用意することで、仕様変更に対応しやすい設計 - -### 7.3 displayConfig の確認 - -ARC104OnwardsProvider と同一 - ---- - -## 8. テスト数想定 - -| カテゴリ | 個数 | 備考 | -| --------------------------------- | --------- | ----------------------------------------------------- | -| 共通テスト(describe.each()統合) | 3-4 | displayConfig, ラウンドラベル, 空入力など | -| AGC001Onwards固有テスト | 14-16 | パターン4つ+その他(フィルタリング、メタデータなど) | -| **合計** | **17-20** | ARC版(14-16)より若干多い(パターン数増加のため) | - ---- - -## 9. 参考: 歴史的背景 - -- **AGC001-AGC008**: 基本は6問 -- **AGC009**: 例外的に5問 -- **AGC010-AGC027**: 基本は6問 -- **AGC028**: 例外的に7問(F2含む) -- **AGC029-AGC066**: 基本は6問 -- **AGC067-AGC072**: 基本は5問(仕様変更) -- **AGC073**: 例外的に4問 -- **AGC074以降**: 標準5問 - ---- - -## 10. 実装前確認事項 - -### 確認日: 2025-11-19 - -#### Q1: 既存テストファイルの存在状況 - -**結果**: ✅ Yes - -- `src/test/lib/utils/contest_table_provider.test.ts` は存在 -- ARC104OnwardsProvider のテストが既に実装済み(約150行) -- テストパターン: フィルタリング、メタデータ、4/5/6/7問パターン等 - -**参照**: Lines 385-530 の "ARC 104 Onwards" describe ブロック - ---- - -#### Q2: モックデータファイルの存在状況 - -**結果**: ✅ Yes - -- `src/test/lib/utils/test_cases/contest_table_provider.ts` は存在 -- 複数のABC、ARC、Typical90等のモックデータが既に定義 -- `taskResultsForARC104OnwardsProvider` がエクスポート済み - -**参照**: Lines 1-151(以降も続く)で各コンテストタイプのデータ定義 - ---- - -#### Q3: AGC001OnwardsProvider の実装状況 - -**結果**: ✅ Yes - -- `src/lib/utils/contest_table_provider.ts` Lines 287-310 に実装済み -- 実装内容: - - `setFilterCondition()`: AGC001~AGC999のフィルタリング - - `getMetadata()`: タイトル 'AtCoder Grand Contest 001 〜 ' - - `getContestRoundLabel()`: コンテスト名ラベル生成 - - ヘルパー関数 `parseContestRound()` で丸め処理 - ---- - -## 11. 実装完了記録 - -**実装日**: 2025-11-19 - -**テスト結果**: ✅ All tests passed (142 tests passed) - -**実装時の学習**: - -1. **モック設定の重要性**: テストファイルの`vi.mock()`セクションでは、被テストのコードが使用するすべての依存関数に対応する必要がある。AGC対応の際、モックに`classifyContest`と`getContestNameLabel`のAGC処理が不足していたため、フィルタリングが機能しなかった。 - -2. **複数パターン対応のテスト設計**: AGCは4/5/6/7問の4つのパターンを持つため、各パターンを個別にテストすることで、仕様変更に対応しやすいテストスイートを実現できた。モックデータ(agc001, agc002, agc009, agc028, agc073, agc074)を6つのコンテストで用意することで、パターンごとの検証が明確になった。 - -3. **ARC版との差分適用**: ARC104OnwardsProvider(2025-11-15計画)のテスト実装を参考にすることで、同様の構造のAGC001OnwardsProviderテストをスムーズに実装できた。既存実装パターンを活用することで、開発効率が大幅に向上した。 - -4. **テスト駆動による品質確認**: 計画で指定された全要件(フィルタリング、メタデータ、displayConfig、4/5/6/7問パターン、ソート順序、エッジケース等)に対してテストを実装することで、実装の正確性を機械的に検証できた。 - -**成果物**: - -- AGC001OnwardsProvider用モックデータ6パターン(33個のテスク)追加 -- AGC001Onwards固有テスト18個追加(16個実装 + displayConfig・ラウンドラベル共通テスト) -- テストモック更新(classifyContest, getContestNameLabelにAGC対応追加) - -**次ステップ**: - -- 他のコンテストプロバイダー(例: ABC系列、Typical90など)への同様テスト実装をスケール -- テストカバレッジの継続的監視と改善 diff --git a/docs/dev-notes/2025-12-03/add_tests_for_contest_table_provider/plan.md b/docs/dev-notes/2025-12-03/add_tests_for_contest_table_provider/plan.md deleted file mode 100644 index a458b7b28..000000000 --- a/docs/dev-notes/2025-12-03/add_tests_for_contest_table_provider/plan.md +++ /dev/null @@ -1,541 +0,0 @@ -# ABC042~125 & ARC058~103 テスト追加計画 - -**作成日**: 2025-12-03 - -**対象ブランチ**: #2836 - -**優先度**: High - ---- - -## 概要 - -Issue #2836 で実装された `ABC042ToABC125Provider` と `ARC058ToARC103Provider` に対するテストを追加する計画。 - -**対象ファイル**: - -- **Provider実装**: [`src/lib/utils/contest_table_provider.ts`](../../../../../src/lib/utils/contest_table_provider.ts) - - `ABC042ToABC125Provider` (299行目~) - - `ARC058ToARC103Provider` (435行目~) -- **テストファイル**: [`src/test/lib/utils/contest_table_provider.test.ts`](../../../../../src/test/lib/utils/contest_table_provider.test.ts) - -**参照ドキュメント**: - -- [`docs/dev-notes/2025-11-19/add_tests_for_contest_table_provider/plan.md`](../../2025-11-19/add_tests_for_contest_table_provider/plan.md) - AGC001OnwardsProvider のテスト設計 -- Issue #2837 -- [`prisma/contest_task_pairs.ts`](../../../../../prisma/contest_task_pairs.ts) - 共有問題の対応関係 - ---- - -## 特徴と仕様 - -### ABC042~125 の仕様 - -- **範囲**: ABC042 ~ ABC125 -- **特徴**: 大部分が ARC との同日開催(ABC051、054 など一部は単独開催) -- **共有問題**: ABC042、043 のみ ARC側で登録 - - ABC042:C・D 問題 = ARC058 の C・D 問題(task_id は `arc058_a`, `arc058_b`) - - ABC043:C・D 問題 = ARC059 の C・D 問題(task_id は `arc059_a`, `arc059_b`) -- **その他**: ABC045~125 の共有問題は ABC側で登録されている - -### ARC058~103 の仕様 - -- **範囲**: ARC058 ~ ARC103 -- **特徴**: この範囲のすべての Round で ABC と同日開催 -- **共有問題**: C・D 問題が ABC側で登録されている - - ARC060 の C・D = ABC044 の C・D(task_id は `arc060_a`, `arc060_b`) - - ARC061 の C・D = ABC045 の C・D(task_id は `arc061_a`, `arc061_b`) - -### 表示設定 - -両プロバイダーとも以下で統一: - -- `tableBodyCellsWidth: 'w-1/2 md:w-1/3 lg:w-1/4 px-1 py-1'` -- `isShownHeader: true` -- `isShownRoundLabel: true` -- `roundLabelWidth: 'xl:w-16'` -- `isShownTaskIndex: false` - ---- - -## テスト設計 - -### 2.1 ABC042ToABC125Provider テスト - -**ファイル**: `src/test/lib/utils/contest_table_provider.test.ts` - -**配置**: AGC 001 Onwards の直前(現在の "ABC 126 to ABC 211" の後) - -#### テスト2.1.1: フィルタリング(Range検証) - -```typescript -test('expects to filter tasks within ABC042-125 range', () => { - const provider = new ABC042ToABC125Provider(ContestType.ABC); - const mixed = [ - { contest_id: 'abc041', task_id: 'abc041_a', task_table_index: 'A' }, - { contest_id: 'abc042', task_id: 'abc042_a', task_table_index: 'A' }, - { contest_id: 'abc125', task_id: 'abc125_a', task_table_index: 'A' }, - { contest_id: 'abc126', task_id: 'abc126_a', task_table_index: 'A' }, - ]; - - const filtered = provider.filter(mixed as TaskResult[]); - - expect(filtered).toHaveLength(2); - expect(filtered.map((t) => t.contest_id)).toEqual(['abc042', 'abc125']); -}); -``` - -#### テスト2.1.2: コンテストタイプ判別 - -```typescript -test('expects to filter only ABC-type contests', () => { - const provider = new ABC042ToABC125Provider(ContestType.ABC); - const mixed = [ - { contest_id: 'abc042', task_id: 'abc042_a', task_table_index: 'A' }, - { contest_id: 'abc042', task_id: 'arc058_a', task_table_index: 'C' }, - { contest_id: 'arc058', task_id: 'arc058_a', task_table_index: 'C' }, - ]; - - const filtered = provider.filter(mixed as TaskResult[]); - - expect(filtered).toHaveLength(2); - expect(filtered[0].contest_id).toBe('abc042'); - expect(filtered[1].contest_id).toBe('abc042'); -}); -``` - -#### テスト2.1.3: メタデータ取得 - -```typescript -test('expects to return correct metadata', () => { - const provider = new ABC042ToABC125Provider(ContestType.ABC); - const metadata = provider.getMetadata(); - - expect(metadata.title).toBe('AtCoder Beginner Contest 042 〜 125(ARC 同時開催が大半)'); - expect(metadata.abbreviationName).toBe('fromAbc042ToAbc125'); -}); -``` - -#### テスト2.1.4: ディスプレイ設定 - -```typescript -test('expects to return correct display config', () => { - const provider = new ABC042ToABC125Provider(ContestType.ABC); - const config = provider.getDisplayConfig(); - - expect(config.isShownHeader).toBe(true); - expect(config.isShownRoundLabel).toBe(true); - expect(config.tableBodyCellsWidth).toBe('w-1/2 md:w-1/3 lg:w-1/4 px-1 py-1'); - expect(config.roundLabelWidth).toBe('xl:w-16'); - expect(config.isShownTaskIndex).toBe(false); -}); -``` - -#### テスト2.1.5: ラウンドラベルフォーマット - -```typescript -test('expects to format contest round label correctly', () => { - const provider = new ABC042ToABC125Provider(ContestType.ABC); - - expect(provider.getContestRoundLabel('abc042')).toBe('042'); - expect(provider.getContestRoundLabel('abc125')).toBe('125'); -}); -``` - -#### テスト2.1.6: テーブル生成 - -```typescript -test('expects to generate correct table structure', () => { - const provider = new ABC042ToABC125Provider(ContestType.ABC); - const mockTasks = [ - { contest_id: 'abc042', task_id: 'abc042_a', task_table_index: 'A' }, - { contest_id: 'abc042', task_id: 'abc042_b', task_table_index: 'B' }, - { contest_id: 'abc042', task_id: 'arc058_a', task_table_index: 'C' }, - { contest_id: 'abc042', task_id: 'arc058_b', task_table_index: 'D' }, - ]; - - const table = provider.generateTable(mockTasks as TaskResult[]); - - expect(table).toHaveProperty('abc042'); - expect(table.abc042).toHaveProperty('A'); - expect(table.abc042).toHaveProperty('B'); - expect(table.abc042).toHaveProperty('C'); - expect(table.abc042).toHaveProperty('D'); -}); -``` - -#### テスト2.1.7: ラウンド ID 取得 - -```typescript -test('expects to return correct round id', () => { - const provider = new ABC042ToABC125Provider(ContestType.ABC); - - expect(provider.getContestRoundIds('abc042')).toBe('abc042'); - expect(provider.getContestRoundIds('abc125')).toBe('abc125'); -}); -``` - -#### テスト2.1.8: ヘッダー ID 取得 - -```typescript -test('expects to return correct header id', () => { - const provider = new ABC042ToABC125Provider(ContestType.ABC); - - expect(provider.getHeaderIdsForTask()).toMatch(/fromAbc042ToAbc125/); -}); -``` - -#### テスト2.1.9: 空入力処理 - -```typescript -test('expects to handle empty input', () => { - const provider = new ABC042ToABC125Provider(ContestType.ABC); - - const table = provider.generateTable([] as TaskResult[]); - - expect(table).toEqual({}); -}); -``` - -#### テスト2.1.10: 混合コンテストタイプ排除 - -```typescript -test('expects to exclude non-ABC contests even if in range', () => { - const provider = new ABC042ToABC125Provider(ContestType.ABC); - const mixed = [ - { contest_id: 'abc042', task_id: 'abc042_a', task_table_index: 'A' }, - { contest_id: 'abc043', task_id: 'abc043_a', task_table_index: 'A' }, - { contest_id: 'arc058', task_id: 'arc058_a', task_table_index: 'C' }, - { contest_id: 'arc059', task_id: 'arc059_a', task_table_index: 'C' }, - ]; - - const filtered = provider.filter(mixed as TaskResult[]); - - expect(filtered).toHaveLength(2); - expect(filtered.every((t) => t.contest_id.startsWith('abc'))).toBe(true); -}); -``` - -#### テスト2.1.11: 共有問題の正しい処理(ABC042の特殊性) - -```typescript -test('expects to generate correct table structure with shared problems (ABC042 with ARC058)', () => { - const provider = new ABC042ToABC125Provider(ContestType.ABC); - const mockAbc042Tasks = [ - { contest_id: 'abc042', task_id: 'abc042_a', task_table_index: 'A' }, - { contest_id: 'abc042', task_id: 'abc042_b', task_table_index: 'B' }, - { contest_id: 'abc042', task_id: 'arc058_a', task_table_index: 'C' }, - { contest_id: 'abc042', task_id: 'arc058_b', task_table_index: 'D' }, - ]; - - const table = provider.generateTable(mockAbc042Tasks as TaskResult[]); - - expect(table).toHaveProperty('abc042'); - expect(table.abc042).toHaveProperty('A'); // ABC専用 - expect(table.abc042).toHaveProperty('B'); // ABC専用 - expect(table.abc042).toHaveProperty('C'); // 共有問題(ARC側のtask_id) - expect(table.abc042).toHaveProperty('D'); // 共有問題(ARC側のtask_id) -}); -``` - -#### テスト2.1.12: 単独開催と同時開催の混在処理 - -```typescript -test('expects to handle both solo and concurrent contests correctly', () => { - const provider = new ABC042ToABC125Provider(ContestType.ABC); - const mixed = [ - // ABC042:ARC058と同時開催 - { contest_id: 'abc042', task_id: 'abc042_a', task_table_index: 'A' }, - { contest_id: 'abc042', task_id: 'arc058_a', task_table_index: 'C' }, - // ABC051:単独開催(ARC同時開催なし) - { contest_id: 'abc051', task_id: 'abc051_c', task_table_index: 'C' }, - { contest_id: 'abc051', task_id: 'abc051_d', task_table_index: 'D' }, - ]; - - const filtered = provider.filter(mixed as TaskResult[]); - const table = provider.generateTable(filtered); - - expect(table).toHaveProperty('abc042'); - expect(table).toHaveProperty('abc051'); - expect(table.abc042).toHaveProperty('C'); // 共有問題 - expect(table.abc051).toHaveProperty('C'); - expect(table.abc051).toHaveProperty('D'); -}); -``` - ---- - -### 2.2 ARC058ToARC103Provider テスト - -**ファイル**: `src/test/lib/utils/contest_table_provider.test.ts` - -**配置**: ARC 104 Onwards の直後 - -#### テスト2.2.1: フィルタリング(Range検証) - -```typescript -test('expects to filter tasks within ARC058-103 range', () => { - const provider = new ARC058ToARC103Provider(ContestType.ARC); - const mixed = [ - { contest_id: 'arc057', task_id: 'arc057_a', task_table_index: 'A' }, - { contest_id: 'arc058', task_id: 'arc058_a', task_table_index: 'C' }, - { contest_id: 'arc103', task_id: 'arc103_a', task_table_index: 'C' }, - { contest_id: 'arc104', task_id: 'arc104_a', task_table_index: 'A' }, - ]; - - const filtered = provider.filter(mixed as TaskResult[]); - - expect(filtered).toHaveLength(2); - expect(filtered.map((t) => t.contest_id)).toEqual(['arc058', 'arc103']); -}); -``` - -#### テスト2.2.2: コンテストタイプ判別 - -```typescript -test('expects to filter only ARC-type contests', () => { - const provider = new ARC058ToARC103Provider(ContestType.ARC); - const mixed = [ - { contest_id: 'arc058', task_id: 'arc058_a', task_table_index: 'C' }, - { contest_id: 'arc058', task_id: 'arc058_f', task_table_index: 'F' }, - { contest_id: 'abc042', task_id: 'abc042_a', task_table_index: 'A' }, - ]; - - const filtered = provider.filter(mixed as TaskResult[]); - - expect(filtered).toHaveLength(2); - expect(filtered[0].contest_id).toBe('arc058'); - expect(filtered[1].contest_id).toBe('arc058'); -}); -``` - -#### テスト2.2.3: メタデータ取得 - -```typescript -test('expects to return correct metadata', () => { - const provider = new ARC058ToARC103Provider(ContestType.ARC); - const metadata = provider.getMetadata(); - - expect(metadata.title).toBe('AtCoder Regular Contest 058 〜 103(ABC 同時開催)'); - expect(metadata.abbreviationName).toBe('fromArc058ToArc103'); -}); -``` - -#### テスト2.2.4: ディスプレイ設定 - -```typescript -test('expects to return correct display config', () => { - const provider = new ARC058ToARC103Provider(ContestType.ARC); - const config = provider.getDisplayConfig(); - - expect(config.isShownHeader).toBe(true); - expect(config.isShownRoundLabel).toBe(true); - expect(config.tableBodyCellsWidth).toBe('w-1/2 md:w-1/3 lg:w-1/4 px-1 py-1'); - expect(config.roundLabelWidth).toBe('xl:w-16'); - expect(config.isShownTaskIndex).toBe(false); -}); -``` - -#### テスト2.2.5: ラウンドラベルフォーマット - -```typescript -test('expects to format contest round label correctly', () => { - const provider = new ARC058ToARC103Provider(ContestType.ARC); - - expect(provider.getContestRoundLabel('arc058')).toBe('058'); - expect(provider.getContestRoundLabel('arc103')).toBe('103'); -}); -``` - -#### テスト2.2.6: テーブル生成 - -```typescript -test('expects to generate correct table structure', () => { - const provider = new ARC058ToARC103Provider(ContestType.ARC); - const mockTasks = [ - { contest_id: 'arc058', task_id: 'arc058_a', task_table_index: 'C' }, - { contest_id: 'arc058', task_id: 'arc058_b', task_table_index: 'D' }, - { contest_id: 'arc058', task_id: 'arc058_c', task_table_index: 'E' }, - { contest_id: 'arc058', task_id: 'arc058_d', task_table_index: 'F' }, - ]; - - const table = provider.generateTable(mockTasks as TaskResult[]); - - expect(table).toHaveProperty('arc058'); - expect(table.arc058).toHaveProperty('C'); - expect(table.arc058).toHaveProperty('D'); - expect(table.arc058).toHaveProperty('E'); - expect(table.arc058).toHaveProperty('F'); -}); -``` - -#### テスト2.2.7: ラウンド ID 取得 - -```typescript -test('expects to return correct round id', () => { - const provider = new ARC058ToARC103Provider(ContestType.ARC); - - expect(provider.getContestRoundIds('arc058')).toBe('arc058'); - expect(provider.getContestRoundIds('arc103')).toBe('arc103'); -}); -``` - -#### テスト2.2.8: ヘッダー ID 取得 - -```typescript -test('expects to return correct header id', () => { - const provider = new ARC058ToARC103Provider(ContestType.ARC); - - expect(provider.getHeaderIdsForTask()).toMatch(/fromArc058ToArc103/); -}); -``` - -#### テスト2.2.9: 空入力処理 - -```typescript -test('expects to handle empty input', () => { - const provider = new ARC058ToARC103Provider(ContestType.ARC); - - const table = provider.generateTable([] as TaskResult[]); - - expect(table).toEqual({}); -}); -``` - -#### テスト2.2.10: 混合コンテストタイプ排除 - -```typescript -test('expects to exclude non-ARC contests even if in range', () => { - const provider = new ARC058ToARC103Provider(ContestType.ARC); - const mixed = [ - { contest_id: 'arc058', task_id: 'arc058_a', task_table_index: 'C' }, - { contest_id: 'arc059', task_id: 'arc059_a', task_table_index: 'C' }, - { contest_id: 'abc042', task_id: 'abc042_a', task_table_index: 'A' }, - { contest_id: 'abc043', task_id: 'abc043_a', task_table_index: 'A' }, - ]; - - const filtered = provider.filter(mixed as TaskResult[]); - - expect(filtered).toHaveLength(2); - expect(filtered.every((t) => t.contest_id.startsWith('arc'))).toBe(true); -}); -``` - -#### テスト2.2.11: 共有問題の正しい処理(ARC058の構成) - -```typescript -test('expects to generate correct table structure with shared and exclusive problems (ARC058)', () => { - const provider = new ARC058ToARC103Provider(ContestType.ARC); - const mockArc058Tasks = [ - { contest_id: 'arc058', task_id: 'arc058_a', task_table_index: 'C' }, - { contest_id: 'arc058', task_id: 'arc058_b', task_table_index: 'D' }, - { contest_id: 'arc058', task_id: 'arc058_c', task_table_index: 'E' }, - { contest_id: 'arc058', task_id: 'arc058_d', task_table_index: 'F' }, - ]; - - const table = provider.generateTable(mockArc058Tasks as TaskResult[]); - - expect(table).toHaveProperty('arc058'); - expect(table.arc058).toHaveProperty('C'); // ABC側と共有 - expect(table.arc058).toHaveProperty('D'); // ABC側と共有 - expect(table.arc058).toHaveProperty('E'); // ARC専用 - expect(table.arc058).toHaveProperty('F'); // ARC専用 -}); -``` - -#### テスト2.2.12: ABC同時開催パターンの検証 - -```typescript -test('expects to correctly handle ARC58-103 where all rounds are concurrent with ABC', () => { - const provider = new ARC058ToARC103Provider(ContestType.ARC); - - // ARC058(ABC042と同時開催) - const arc058Tasks = [ - { contest_id: 'arc058', task_id: 'arc058_a', task_table_index: 'C' }, - { contest_id: 'arc058', task_id: 'arc058_b', task_table_index: 'D' }, - ]; - - // ARC060(ABC044と同時開催) - const arc060Tasks = [ - { contest_id: 'arc060', task_id: 'arc060_a', task_table_index: 'C' }, - { contest_id: 'arc060', task_id: 'arc060_b', task_table_index: 'D' }, - ]; - - const mixed = [...arc058Tasks, ...arc060Tasks]; - const filtered = provider.filter(mixed as TaskResult[]); - const table = provider.generateTable(filtered); - - expect(filtered).toHaveLength(4); - expect(table).toHaveProperty('arc058'); - expect(table).toHaveProperty('arc060'); -}); -``` - ---- - -## 実装手順 - -1. **モックデータ作成**: 各プロバイダーのテストに必要なモック TaskResult を定義 -2. **ABC042ToABC125Provider テスト実装**: テスト2.1.1 ~ 2.1.12 を順次実装 -3. **ARC058ToARC103Provider テスト実装**: テスト2.2.1 ~ 2.2.12 を順次実装 -4. **テスト実行**: `pnpm test` ですべてのテストが通ることを確認 -5. **カバレッジ確認**: 両プロバイダーで 100% のカバレッジを達成 - ---- - -## 実装時の注意事項 - -### any の使用を避ける - -モックデータ型指定時は、`as TaskResult[]` ではなく、以下のように型安全に実装すること: - -```typescript -const mockTasks: TaskResult[] = [ - // ... 正しい型定義 -]; -``` - -### 共有問題表現の統一 - -- ABC042/043:ARC側の task_id を使用(`arc058_a`, `arc059_a` など) -- ARC058~103:すべてのコンテストで ABC と共有する C・D 問題あり - -### テーブル構造の検証 - -`generateTable` の戻り値は以下の構造: - -```typescript -type ContestTable = { - [contestId: string]: { - [taskIndex: string]: TaskResult; - }; -}; -``` - ---- - -## 参考資料 - -- [AGC001OnwardsProvider テスト](../../2025-11-19/add_tests_for_contest_table_provider/plan.md) - 基本パターン -- [`src/lib/utils/contest_table_provider.ts`](../../../../../src/lib/utils/contest_table_provider.ts) - 実装コード -- [`prisma/contest_task_pairs.ts`](../../../../../prisma/contest_task_pairs.ts) - 共有問題対応関係 - ---- - -## 実装結果と教訓 - -### 実装完了 - -- **ABC042ToABC125Provider**: 12個のテストを実装・パス ✓ -- **ARC058ToARC103Provider**: 12個のテストを実装・パス ✓ -- **合計テスト数**: 166個のテストがすべてパス - -### 教訓 - -1. **メソッド名の確認が重要**: `getRoundId()` や `getHeaderId()` などのメソッドは実装されていないため、`getContestRoundIds()` や `getMetadata().abbreviationName` など、実装済みのメソッドを使うべき。事前に ContestTableProviderBase の実装を確認してからテストを書くこと。 - -2. **Range検証とType検証を分離**: フィルタリングのテストでは、Range(範囲)の検証とContestType(コンテストタイプ)の検証を別々のテストケースで行うと、問題の原因特定が容易になる。 - -3. **共有問題のテストは実装の詳細を反映**: ABC042/043や ARC058~103の共有問題は、テスト時も実装の動作に合わせて task_id を正しく設定すること。これにより、実装と テストの一貫性が保証される。 - -4. **Plan.md の役割の重要性**: 実装前に計画書を詳細に作成・確認することで、「どのメソッドを使うべきか」「どのような入力パターンをカバーすべきか」が明確になり、実装効率が大幅に向上する。 diff --git a/docs/dev-notes/2025-12-10/add_tests_for_contest_table_provider/plan.md b/docs/dev-notes/2025-12-10/add_tests_for_contest_table_provider/plan.md deleted file mode 100644 index b80f5b0b0..000000000 --- a/docs/dev-notes/2025-12-10/add_tests_for_contest_table_provider/plan.md +++ /dev/null @@ -1,480 +0,0 @@ -# ABC001~041 & ARC001~057 テスト追加計画 - -**作成日**: 2025-12-10 - -**対象ブランチ**: #2838 - -**優先度**: High - ---- - -## 概要 - -Issue #2838 で実装された `ABC001ToABC041Provider` と `ARC001ToARC057Provider` に対するテストを追加する計画。 - -**対象ファイル**: - -- **Provider実装**: [`src/lib/utils/contest_table_provider.ts`](../../../../../src/lib/utils/contest_table_provider.ts) - - `ABC001ToABC041Provider` (318行目~) - - `ARC001ToARC057Provider` (401行目~) -- **テストファイル**: [`src/test/lib/utils/contest_table_provider.test.ts`](../../../../../src/test/lib/utils/contest_table_provider.test.ts) - -**参照ドキュメント**: - -- [`docs/dev-notes/2025-12-03/add_tests_for_contest_table_provider/plan.md`](../../2025-12-03/add_tests_for_contest_table_provider/plan.md) - ABC042~125 & ARC058~103 のテスト設計(参照パターン) -- [`prisma/tasks.ts`](../../../../../prisma/tasks.ts) - タスク ID のフォーマット確認 - ---- - -## 特徴と仕様 - -### ABC001~041 の仕様 - -- **範囲**: ABC001 ~ ABC041 -- **特徴**: レーティング導入前のコンテスト -- **4問構成**: すべてのラウンドで A・B・C・D の4問 -- **ID フォーマット**: - - ABC001~019: `abc001_1`, `abc001_2`, `abc001_3`, `abc001_4` (数字サフィックス) - - ABC020~041: `abc020_a`, `abc020_b`, `abc020_c`, `abc020_d` (英字サフィックス) -- **共有問題**: なし(ARC001~057 と共有問題がない) - -### ARC001~057 の仕様 - -- **範囲**: ARC001 ~ ARC057 -- **特徴**: レーティング導入前のコンテスト -- **4問構成**: すべてのラウンドで A・B・C・D の4問 -- **ID フォーマット**: - - ARC001~034: `arc001_1`, `arc001_2`, `arc001_3`, `arc001_4` (数字サフィックス) - - ARC035~057: `arc035_a`, `arc035_b`, `arc035_c`, `arc035_d` (英字サフィックス) -- **共有問題**: なし(ABC001~041 と共有問題がない) - -### 表示設定 - -両プロバイダーとも以下で統一: - -- `tableBodyCellsWidth: 'w-1/2 md:w-1/3 lg:w-1/4 px-1 py-1'` -- `isShownHeader: true` -- `isShownRoundLabel: true` -- `roundLabelWidth: 'xl:w-16'` -- `isShownTaskIndex: false` - ---- - -## テスト設計 - -### 2.1 ABC001ToABC041Provider テスト - -**ファイル**: `src/test/lib/utils/contest_table_provider.test.ts` - -**配置**: ARC058ToARC103Provider テストの後、AGC001OnwardsProvider テストの前 - -テストケースは [`docs/dev-notes/2025-12-03/add_tests_for_contest_table_provider/plan.md`](../../2025-12-03/add_tests_for_contest_table_provider/plan.md) の テスト2.1.1~2.1.10 を参照して実装。共有問題がないため、テスト2.1.11・2.1.12 は不要。 - -#### テスト2.1.1: フィルタリング(Range検証) - -ABC001~041 の範囲のみをフィルタリングすることを確認。古い ID フォーマット(数字サフィックス)と新しい ID フォーマット(英字サフィックス)の両方を含める。 - -```typescript -test('expects to filter tasks within ABC001-41 range', () => { - const provider = new ABC001ToABC041Provider(ContestType.ABC); - const mixed = [ - { contest_id: 'abc000', task_id: 'abc000_1', task_table_index: 'A' }, - { contest_id: 'abc001', task_id: 'abc001_1', task_table_index: 'A' }, // 古いフォーマット - { contest_id: 'abc020', task_id: 'abc020_a', task_table_index: 'A' }, // 新しいフォーマット - { contest_id: 'abc041', task_id: 'abc041_a', task_table_index: 'A' }, - { contest_id: 'abc042', task_id: 'abc042_a', task_table_index: 'A' }, - ]; - - const filtered = provider.filter(mixed as TaskResult[]); - - expect(filtered).toHaveLength(3); - expect(filtered.map((t) => t.contest_id)).toEqual(['abc001', 'abc020', 'abc041']); -}); -``` - -#### テスト2.1.2: コンテストタイプ判別(ABC のみ、ARC との分離) - -ABC-type のみをフィルタリングし、ARC のコンテストが混ざらないことを確認。 - -```typescript -test('expects to filter only ABC-type contests and exclude ARC', () => { - const provider = new ABC001ToABC041Provider(ContestType.ABC); - const mixed = [ - { contest_id: 'abc001', task_id: 'abc001_1', task_table_index: 'A' }, - { contest_id: 'arc001', task_id: 'arc001_1', task_table_index: 'A' }, - { contest_id: 'abc041', task_id: 'abc041_a', task_table_index: 'A' }, - { contest_id: 'arc057', task_id: 'arc057_a', task_table_index: 'A' }, - ]; - - const filtered = provider.filter(mixed as TaskResult[]); - - expect(filtered).toHaveLength(2); - expect(filtered.every((t) => t.contest_id.startsWith('abc'))).toBe(true); -}); -``` - -#### テスト2.1.3: メタデータ取得 - -```typescript -test('expects to return correct metadata', () => { - const provider = new ABC001ToABC041Provider(ContestType.ABC); - const metadata = provider.getMetadata(); - - expect(metadata.title).toBe('AtCoder Beginner Contest 001 〜 041(レーティング導入前)'); - expect(metadata.abbreviationName).toBe('fromAbc001ToAbc041'); -}); -``` - -#### テスト2.1.4: ディスプレイ設定 - -```typescript -test('expects to return correct display config', () => { - const provider = new ABC001ToABC041Provider(ContestType.ABC); - const config = provider.getDisplayConfig(); - - expect(config.isShownHeader).toBe(true); - expect(config.isShownRoundLabel).toBe(true); - expect(config.tableBodyCellsWidth).toBe('w-1/2 md:w-1/3 lg:w-1/4 px-1 py-1'); - expect(config.roundLabelWidth).toBe('xl:w-16'); - expect(config.isShownTaskIndex).toBe(false); -}); -``` - -#### テスト2.1.5: ラウンドラベルフォーマット - -```typescript -test('expects to format contest round label correctly', () => { - const provider = new ABC001ToABC041Provider(ContestType.ABC); - - expect(provider.getContestRoundLabel('abc001')).toBe('001'); - expect(provider.getContestRoundLabel('abc041')).toBe('041'); -}); -``` - -#### テスト2.1.6: テーブル生成(古い ID フォーマット) - -古いフォーマット(数字サフィックス)での正しいテーブル生成を確認。 - -```typescript -test('expects to generate correct table structure with old ID format (numeric suffix)', () => { - const provider = new ABC001ToABC041Provider(ContestType.ABC); - const mockTasks = [ - { contest_id: 'abc001', task_id: 'abc001_1', task_table_index: 'A' }, - { contest_id: 'abc001', task_id: 'abc001_2', task_table_index: 'B' }, - { contest_id: 'abc001', task_id: 'abc001_3', task_table_index: 'C' }, - { contest_id: 'abc001', task_id: 'abc001_4', task_table_index: 'D' }, - ]; - - const table = provider.generateTable(mockTasks as TaskResult[]); - - expect(table).toHaveProperty('abc001'); - expect(table.abc001).toHaveProperty('A'); - expect(table.abc001).toHaveProperty('B'); - expect(table.abc001).toHaveProperty('C'); - expect(table.abc001).toHaveProperty('D'); -}); -``` - -#### テスト2.1.7: テーブル生成(新しい ID フォーマット) - -新しいフォーマット(英字サフィックス)での正しいテーブル生成を確認。 - -```typescript -test('expects to generate correct table structure with new ID format (alphabet suffix)', () => { - const provider = new ABC001ToABC041Provider(ContestType.ABC); - const mockTasks = [ - { contest_id: 'abc020', task_id: 'abc020_a', task_table_index: 'A' }, - { contest_id: 'abc020', task_id: 'abc020_b', task_table_index: 'B' }, - { contest_id: 'abc020', task_id: 'abc020_c', task_table_index: 'C' }, - { contest_id: 'abc020', task_id: 'abc020_d', task_table_index: 'D' }, - ]; - - const table = provider.generateTable(mockTasks as TaskResult[]); - - expect(table).toHaveProperty('abc020'); - expect(table.abc020).toHaveProperty('A'); - expect(table.abc020).toHaveProperty('B'); - expect(table.abc020).toHaveProperty('C'); - expect(table.abc020).toHaveProperty('D'); -}); -``` - -#### テスト2.1.8: コンテスト ID 取得 - -```typescript -test('expects to return correct contest round ids', () => { - const provider = new ABC001ToABC041Provider(ContestType.ABC); - const mockTasks = [ - { contest_id: 'abc001', task_id: 'abc001_1', task_table_index: 'A' }, - { contest_id: 'abc020', task_id: 'abc020_a', task_table_index: 'A' }, - { contest_id: 'abc041', task_id: 'abc041_a', task_table_index: 'A' }, - ]; - - const ids = provider.getContestRoundIds(mockTasks as TaskResult[]); - - expect(ids).toContain('abc001'); - expect(ids).toContain('abc020'); - expect(ids).toContain('abc041'); -}); -``` - -#### テスト2.1.9: ヘッダー ID 取得 - -```typescript -test('expects to return correct header ids', () => { - const provider = new ABC001ToABC041Provider(ContestType.ABC); - const mockTasks = [ - { contest_id: 'abc001', task_id: 'abc001_1', task_table_index: 'A' }, - { contest_id: 'abc001', task_id: 'abc001_2', task_table_index: 'B' }, - { contest_id: 'abc001', task_id: 'abc001_3', task_table_index: 'C' }, - { contest_id: 'abc001', task_id: 'abc001_4', task_table_index: 'D' }, - ]; - - const ids = provider.getHeaderIdsForTask(mockTasks as TaskResult[]); - - expect(ids).toEqual(['A', 'B', 'C', 'D']); -}); -``` - -#### テスト2.1.10: 空入力処理 - -```typescript -test('expects to handle empty input gracefully', () => { - const provider = new ABC001ToABC041Provider(ContestType.ABC); - - const filteredEmpty = provider.filter([] as TaskResult[]); - const tableEmpty = provider.generateTable([] as TaskResult[]); - const idsEmpty = provider.getContestRoundIds([] as TaskResult[]); - const headerIdsEmpty = provider.getHeaderIdsForTask([] as TaskResult[]); - - expect(filteredEmpty).toEqual([]); - expect(tableEmpty).toEqual({}); - expect(idsEmpty).toEqual([]); - expect(headerIdsEmpty).toEqual([]); -}); -``` - ---- - -### 2.2 ARC001ToARC057Provider テスト - -**ファイル**: `src/test/lib/utils/contest_table_provider.test.ts` - -**配置**: ABC001ToABC041Provider テストの後、AGC001OnwardsProvider テストの前 - -テストケースは [`docs/dev-notes/2025-12-03/add_tests_for_contest_table_provider/plan.md`](../../2025-12-03/add_tests_for_contest_table_provider/plan.md) の テスト2.2.1~2.2.10 を参照して実装。共有問題がないため、テスト2.2.11・2.2.12 は不要。 - -各テストはパターンとして ABC001ToABC041Provider と同等ですが、以下の点で異なります: - -- Provider クラス: `ARC001ToARC057Provider` -- コンテスト ID の範囲: `arc001` ~ `arc057` -- ID フォーマット: - - ARC001~034: 数字サフィックス (`arc001_1`, `arc001_2`, ...) - - ARC035~057: 英字サフィックス (`arc035_a`, `arc035_b`, ...) -- メタデータ: - - title: `'AtCoder Regular Contest 001 〜 057(レーティング導入前)'` - - abbreviationName: `'fromArc001ToArc057'` - -#### テスト2.2.1: フィルタリング(Range検証) - -ARC001~057 の範囲のみをフィルタリングすることを確認。古い ID フォーマット(数字サフィックス)と新しい ID フォーマット(英字サフィックス)の両方を含める。 - -```typescript -test('expects to filter tasks within ARC001-57 range', () => { - const provider = new ARC001ToARC057Provider(ContestType.ARC); - const mixed = [ - { contest_id: 'arc000', task_id: 'arc000_1', task_table_index: 'A' }, - { contest_id: 'arc001', task_id: 'arc001_1', task_table_index: 'A' }, // 古いフォーマット - { contest_id: 'arc035', task_id: 'arc035_a', task_table_index: 'A' }, // 新しいフォーマット - { contest_id: 'arc057', task_id: 'arc057_a', task_table_index: 'A' }, - { contest_id: 'arc058', task_id: 'arc058_a', task_table_index: 'A' }, - ]; - - const filtered = provider.filter(mixed as TaskResult[]); - - expect(filtered).toHaveLength(3); - expect(filtered.map((t) => t.contest_id)).toEqual(['arc001', 'arc035', 'arc057']); -}); -``` - -#### テスト2.2.2: コンテストタイプ判別(ARC のみ、ABC との分離) - -ARC-type のみをフィルタリングし、ABC のコンテストが混ざらないことを確認。 - -```typescript -test('expects to filter only ARC-type contests and exclude ABC', () => { - const provider = new ARC001ToARC057Provider(ContestType.ARC); - const mixed = [ - { contest_id: 'arc001', task_id: 'arc001_1', task_table_index: 'A' }, - { contest_id: 'abc001', task_id: 'abc001_1', task_table_index: 'A' }, - { contest_id: 'arc057', task_id: 'arc057_a', task_table_index: 'A' }, - { contest_id: 'abc041', task_id: 'abc041_a', task_table_index: 'A' }, - ]; - - const filtered = provider.filter(mixed as TaskResult[]); - - expect(filtered).toHaveLength(2); - expect(filtered.every((t) => t.contest_id.startsWith('arc'))).toBe(true); -}); -``` - -#### テスト2.2.3~2.2.10 - -ABC001ToABC041Provider のテスト2.1.3~2.1.10 と同等の構成で実装。 - -詳細は [`docs/dev-notes/2025-12-03/add_tests_for_contest_table_provider/plan.md`](../../2025-12-03/add_tests_for_contest_table_provider/plan.md) のテスト2.2.3~2.2.10 を参照。 - ---- - -## テスト実装時の注意点 - -### 1. モックデータの準備 - -`src/test/lib/utils/test_cases/contest_table_provider.ts` に、以下のモックデータを追加する必要があります: - -- ABC001~041 (古いフォーマット・新しいフォーマットの両方) -- ARC001~057 (古いフォーマット・新しいフォーマットの両方) - -### 2. task_table_index の取り扱い - -古い ID フォーマット(`abc001_1`)でも、`task_table_index` は必ず `'A'`, `'B'`, `'C'`, `'D'` になることに注意。 - -### 3. 共有問題への対応 - -ABC001~041 と ARC001~057 には共有問題がないため、共有問題に関するテストケースは不要です。 - -### 4. テストの配置 - -既存テストの構成を崩さないよう、以下の順序で配置: - -1. ABC 関連テスト(ABCLatest20Rounds → ABC319Onwards → ABC212ToABC318 → ABC126ToABC211 → ABC042ToABC125 → **ABC001ToABC041**) -2. ARC 関連テスト(ARC104Onwards → ARC058ToARC103 → **ARC001ToARC057**) -3. AGC001OnwardsProvider テスト以降 - ---- - -## 実装を開始する前の確認事項(設計パターン) - -### Q1. 参照ドキュメント - -計画では `docs/dev-notes/2025-12-03/add_tests_for_contest_table_provider/plan.md` のテスト設計を参照していますが、このファイルで ABC042ToABC125 & ARC058ToARC103 のテストが既に実装されていますか? - -**A1. YES** - 既に実装済み。参照パターンとして利用可。 - -### Q2. モックデータファイル - -`src/test/lib/utils/test_cases/contest_table_provider.ts` は既に存在していますか? - -**A2. YES** - 既に存在。新しいテストケース用のモックデータを追加する。 - -### Q3. 現在の実装状態 - -`src/lib/utils/contest_table_provider.ts` で ABC001ToABC041Provider と ARC001ToARC057Provider は既に実装されていますか? - -**A3. YES** - 既に実装済み(Issue #2838)。テスト追加のみ。 - ---- - -## 実装完了と教訓 - -### 実装結果 - -**テスト数**: - -- ABC001ToABC041Provider: 10テスト -- ARC001ToARC057Provider: 10テスト -- 合計新規追加: 20テスト -- 全体テストパス数: 186テスト(既存166テスト) - -**実装内容**: - -1. テストケースデータを `src/test/lib/utils/test_cases/contest_table_provider.ts` に追加 - - ABC001, ABC019, ABC020, ABC041(数字・英字サフィックスの両方をカバー) - - ARC001, ARC034, ARC035, ARC057(数字・英字サフィックスの両方をカバー) - -2. テストを `src/test/lib/utils/contest_table_provider.test.ts` に追加 - - ABC042ToABC125 テストの直後に ABC001ToABC041Provider テスト - - ARC058ToARC103 テストの直後に ARC001ToARC057Provider テスト - -### テスト設計パターンの活用 - -参照ドキュメント(2025-12-03のテスト設計)を活用することで、以下のベストプラクティスを実装: - -1. **フィルタリング検証**: 正しい範囲のコンテストのみを抽出していることを確認 -2. **コンテストタイプ判別**: ABC/ARC の分離が正確に機能していることを確認 -3. **メタデータ検証**: title と abbreviationName が正確であることを確認 -4. **ディスプレイ設定**: UI関連の設定が統一されていることを確認 -5. **ラウンドラベルフォーマット**: 3桁のゼロパディングが正確であることを確認 -6. **テーブル生成(両フォーマット)**: 古いフォーマット(数字)と新しいフォーマット(英字)の両方に対応していることを確認 -7. **ID取得**: コンテスト ID とヘッダー ID の取得が正確であることを確認 -8. **エッジケース処理**: 空入力の処理が正確であることを確認 - -### 実装上の重要なポイント - -1. **レーティング導入前コンテストの特性**: ABC001-041 と ARC001-057 はレーティング導入前のコンテストで、以下の特性を持つ - - ID フォーマットの移行期がある(ABC020 から、ARC035 から) - - 共有問題がない - - すべて 4 問構成 - -2. **テストケースデータの設計**: モックデータは以下の観点を網羅 - - 新旧 ID フォーマットの両方を含める - - 複数のステータス(AC, AC_WITH_EDITORIAL, TRYING, PENDING)を含める - - 範囲の開始・中盤・終了を網羅 - -3. **テスト配置の重要性**: テスト実行順序と配置 - - ABC関連: ABCLatest20Rounds → ... → ABC042ToABC125 → **ABC001ToABC041** → ARC104Onwards - - ARC関連: ARC104Onwards → ARC058ToARC103 → **ARC001ToARC057** → AGC001Onwards - - 既存テストの構成を維持することが重要 - -### 学習した設計パターン - -今後、新しいコンテスト期間のプロバイダーをテストする際に活用可能なパターン: - -```typescript -// 1. テストケースデータ: 新旧フォーマットを含める -const [xxx_old_format] = createContestTasks('xxxNNN', [ - { taskId: 'xxxnnn_1', taskTableIndex: 'A', statusName: AC }, - // ... old format -]); - -const [xxx_new_format] = createContestTasks('xxxNNN', [ - { taskId: 'xxxnnn_a', taskTableIndex: 'A', statusName: AC }, - // ... new format -]); - -// 2. テスト: 確認すべき項目を系統的に検証 -describe('XXXToYYYProvider', () => { - test('expects to filter tasks within XXX-YYY range', () => { - /* ... */ - }); - test('expects to filter only appropriate contest type', () => { - /* ... */ - }); - test('expects to return correct metadata', () => { - /* ... */ - }); - test('expects to return correct display config', () => { - /* ... */ - }); - test('expects to format contest round label correctly', () => { - /* ... */ - }); - test('expects to generate correct table structure with old ID format', () => { - /* ... */ - }); - test('expects to generate correct table structure with new ID format', () => { - /* ... */ - }); - test('expects to return correct contest round ids', () => { - /* ... */ - }); - test('expects to return correct header ids', () => { - /* ... */ - }); - test('expects to handle empty input gracefully', () => { - /* ... */ - }); -}); -``` - -このパターンにより、新しいプロバイダーの網羅的なテストを体系的に実装できる。 diff --git a/docs/dev-notes/2025-12-11/add_tests_for_contest_table_provider/plan.md b/docs/dev-notes/2025-12-11/add_tests_for_contest_table_provider/plan.md deleted file mode 100644 index 2b33e3bc5..000000000 --- a/docs/dev-notes/2025-12-11/add_tests_for_contest_table_provider/plan.md +++ /dev/null @@ -1,424 +0,0 @@ -# ABSProvider 単体テスト追加計画 - -**作成日**: 2025-12-11 - -**対象ブランチ**: #2919 - -**優先度**: High - ---- - -## 概要 - -`ABSProvider`(AtCoder Beginners Selection)に対する単体テストを追加する計画。 - -**対象ファイル**: - -- **Provider実装**: [`src/lib/utils/contest_table_provider.ts`](../../../../../src/lib/utils/contest_table_provider.ts) - - `ABSProvider` (136行目~) -- **テストファイル**: [`src/test/lib/utils/contest_table_provider.test.ts`](../../../../../src/test/lib/utils/contest_table_provider.test.ts) -- **テストケースファイル**: [`src/test/lib/utils/test_cases/contest_table_provider.ts`](../../../../../src/test/lib/utils/test_cases/contest_table_provider.ts) - -**参照ドキュメント**: - -- [`docs/dev-notes/2025-12-03/add_tests_for_contest_table_provider/plan.md`](../../2025-12-03/add_tests_for_contest_table_provider/plan.md) - テスト設計パターンの参考 -- [`prisma/tasks.ts`](../../../../../prisma/tasks.ts) - practice_1 及び各問題のタスク定義 -- [`prisma/contest_task_pairs.ts`](../../../../../prisma/contest_task_pairs.ts) - ABS の問題対応関係(11問) - ---- - -## ABSProvider の仕様と特徴 - -### 基本情報 - -- **名称**: AtCoder Beginners Selection -- **contest_id**: `'abs'` -- **ContestType**: `ContestType.ABS` - -### 問題構成 - -ABSは入門者向けのコンテンツで、異なるコンテストから選ばれた**11問**で構成されている: - -| 問題番号 | task_id | 元のcontest_id | problem_index | -| -------- | ---------- | -------------- | ------------- | -| 1 | practice_1 | abs | A | -| 2 | abc086_a | abc086 | B | -| 3 | abc081_a | abc081 | C | -| 4 | abc081_b | abc081 | D | -| 5 | abc087_b | abc087 | E | -| 6 | abc083_b | abc083 | F | -| 7 | abc088_b | abc088 | G | -| 8 | abc085_b | abc085 | H | -| 9 | abc085_c | abc085 | I | -| 10 | arc065_a | abc049 | J | -| 11 | arc089_a | abc086 | K | - -**重要な特徴**: - -- **複数コンテスト由来**: 同じ問題IDを別のコンテストで使用している問題が大半(abc081, abc085など2問ずつ、その他は各1問) -- **難易度順**: A~K の順序が入門者向けコンテンツとして意図した難易度順になっている -- **contest_idの単一化**: すべての問題のcontest_idは'abs'で統一される - -### ディスプレイ設定 - -ABSProviderは他のプロバイダーと異なる設定を持つ: - -```typescript -{ - isShownHeader: false, // ヘッダーを非表示 - isShownRoundLabel: false, // ラウンドラベルを非表示 - isShownTaskIndex: false, // タスクインデックスを非表示 - tableBodyCellsWidth: 'w-1/2 md:w-1/3 lg:w-1/4 px-1 py-2', - roundLabelWidth: '', // ラウンドラベル幅なし -} -``` - ---- - -## テスト設計 - -### テストファイル配置 - -**ファイル**: `src/test/lib/utils/contest_table_provider.test.ts` - -**配置**: 「ABC Latest 20 Rounds」セクションの直前(ABC providers セクション内の最初) - -### テストデータ構築 - -#### テストケースファイルでの準備 - -`src/test/lib/utils/test_cases/contest_table_provider.ts`に以下を追加: - -```typescript -/** - * Test data for ABSProvider (AtCoder Beginners Selection) - * 11 problems from various contests, problem_index from A to K - */ -export const taskResultsForABS: TaskResults = [ - createContestTasksForABS('practice_1', 'abs', 'A'), - createContestTasksForABS('abc086_a', 'abs', 'B'), - createContestTasksForABS('abc081_a', 'abs', 'C'), - createContestTasksForABS('abc081_b', 'abs', 'D'), - createContestTasksForABS('abc087_b', 'abs', 'E'), - createContestTasksForABS('abc083_b', 'abs', 'F'), - createContestTasksForABS('abc088_b', 'abs', 'G'), - createContestTasksForABS('abc085_b', 'abs', 'H'), - createContestTasksForABS('abc085_c', 'abs', 'I'), - createContestTasksForABS('arc065_a', 'abs', 'J'), - createContestTasksForABS('arc089_a', 'abs', 'K'), -]; - -function createContestTasksForABS( - taskId: string, - contestId: string, - taskTableIndex: string, -): TaskResult { - return createTaskResultWithTaskTableIndex(contestId, taskId, taskTableIndex, AC); -} -``` - -### テストケース詳細 - -#### テスト1.1: フィルタリング(contest_id検証) - -contest_id='abs' のタスクのみをフィルタリングすることを確認。 - -```typescript -test('expects to filter tasks with contest_id "abs"', () => { - const provider = new ABSProvider(ContestType.ABS); - const mixed = [ - { contest_id: 'abs', task_id: 'practice_1', task_table_index: 'A' }, - { contest_id: 'abs', task_id: 'abc086_a', task_table_index: 'B' }, - { contest_id: 'abc086', task_id: 'abc086_a', task_table_index: 'A' }, - { contest_id: 'abc081', task_id: 'abc081_a', task_table_index: 'A' }, - ]; - - const filtered = provider.filter(mixed as TaskResults); - - expect(filtered).toHaveLength(2); - expect(filtered.every((task) => task.contest_id === 'abs')).toBe(true); -}); -``` - -#### テスト1.2: コンテストタイプ判別 - -ContestType.ABS のみをフィルタリングすることを確認。 - -```typescript -test('expects to filter only ABS-type contests', () => { - const provider = new ABSProvider(ContestType.ABS); - const mixed = [ - { contest_id: 'abs', task_id: 'practice_1', task_table_index: 'A' }, - { contest_id: 'abc378', task_id: 'abc378_a', task_table_index: 'A' }, - { contest_id: 'arc100', task_id: 'arc100_a', task_table_index: 'A' }, - ]; - - const filtered = provider.filter(mixed as TaskResults); - - expect(filtered).toHaveLength(1); - expect(filtered[0].contest_id).toBe('abs'); -}); -``` - -#### テスト1.3: メタデータ取得 - -```typescript -test('expects to return correct metadata', () => { - const provider = new ABSProvider(ContestType.ABS); - const metadata = provider.getMetadata(); - - expect(metadata.title).toBe('AtCoder Beginners Selection'); - expect(metadata.abbreviationName).toBe('abs'); -}); -``` - -#### テスト1.4: ディスプレイ設定確認 - -ディスプレイ設定がABS固有の値であることを確認。 - -```typescript -test('expects to return correct display config with ABS-specific settings', () => { - const provider = new ABSProvider(ContestType.ABS); - const config = provider.getDisplayConfig(); - - expect(config.isShownHeader).toBe(false); - expect(config.isShownRoundLabel).toBe(false); - expect(config.isShownTaskIndex).toBe(false); - expect(config.tableBodyCellsWidth).toBe('w-1/2 md:w-1/3 lg:w-1/4 px-1 py-2'); - expect(config.roundLabelWidth).toBe(''); -}); -``` - -#### テスト1.5: ラウンドラベルフォーマット - -ABSではラウンドラベルが空文字列で返されることを確認。 - -```typescript -test('expects to return empty string for contest round label', () => { - const provider = new ABSProvider(ContestType.ABS); - - expect(provider.getContestRoundLabel('abs')).toBe(''); - expect(provider.getContestRoundLabel('abc086')).toBe(''); -}); -``` - -#### テスト1.6: テーブル生成(11問全数) - -ABSの全11問がテーブルに含まれることを確認。 - -```typescript -test('expects to generate correct table structure with all 11 problems', () => { - const provider = new ABSProvider(ContestType.ABS); - const table = provider.generateTable(taskResultsForABS); - - expect(table).toHaveProperty('abs'); - expect(Object.keys(table.abs)).toHaveLength(11); - expect(Object.keys(table.abs)).toEqual(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K']); -}); -``` - -#### テスト1.7: ラウンドID取得 - -```typescript -test('expects to return correct contest round ids', () => { - const provider = new ABSProvider(ContestType.ABS); - const roundIds = provider.getContestRoundIds(taskResultsForABS); - - expect(roundIds).toEqual(['abs']); -}); -``` - -#### テスト1.8: ヘッダーID取得 - -```typescript -test('expects to return correct header ids for all problems', () => { - const provider = new ABSProvider(ContestType.ABS); - const headerIds = provider.getHeaderIdsForTask(taskResultsForABS); - - expect(headerIds).toEqual(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K']); -}); -``` - -#### テスト1.9: 複数コンテスト由来の確認 - -ABS内の問題が複数の異なるコンテスト由来であることを確認(重要な特徴)。 - -```typescript -test('expects to verify that ABS problems come from multiple different contests', () => { - const provider = new ABSProvider(ContestType.ABS); - const filtered = provider.filter(taskResultsForABS); - - // task_id から元のcontest_idを抽出 - const sourceContests = new Set( - filtered.map((task) => { - const match = task.task_id.match(/^(arc|abc)\d+/); - return match ? match[0] : null; - }), - ); - - // practice_1 以外の10問から8つの異なるcontest由来 - expect(sourceContests.size).toBeGreaterThanOrEqual(8); - expect(Array.from(sourceContests)).toContain('abc086'); - expect(Array.from(sourceContests)).toContain('abc081'); - expect(Array.from(sourceContests)).toContain('arc089'); - expect(Array.from(sourceContests)).toContain('arc065'); -}); -``` - -#### テスト1.10: 空入力ハンドリング - -```typescript -test('expects to handle empty input gracefully', () => { - const provider = new ABSProvider(ContestType.ABS); - - const filteredEmpty = provider.filter([] as TaskResults); - const tableEmpty = provider.generateTable([] as TaskResults); - const idsEmpty = provider.getContestRoundIds([] as TaskResults); - const headerIdsEmpty = provider.getHeaderIdsForTask([] as TaskResults); - - expect(filteredEmpty).toEqual([]); - expect(tableEmpty).toEqual({}); - expect(idsEmpty).toEqual([]); - expect(headerIdsEmpty).toEqual([]); -}); -``` - ---- - -## 実装手順 - -### ステップ1: テストデータ準備 - -`src/test/lib/utils/test_cases/contest_table_provider.ts`に以下を追加: - -- `createContestTasksForABS` ヘルパー関数 -- `taskResultsForABS` 定数(11問のテストデータ) - -### ステップ2: テストケース作成 - -`src/test/lib/utils/contest_table_provider.test.ts`の「ABC Latest 20 Rounds」セクション直前に新しい`describe('ABS')`セクションを追加: - -- テスト1.1~1.10 を実装 -- インポート句に`ABSProvider`を追加 -- インポート句に`taskResultsForABS`を追加 - -### ステップ3: テスト実行と検証 - -```bash -pnpm test:unit src/test/lib/utils/contest_table_provider.test.ts -``` - ---- - -## 確認事項テンプレート(今後の参考用) - -ContestTableProviderの新規テスト追加時に確認すべき事項: - -### データソース確認 - -- [ ] `prisma/tasks.ts`で当該contest_idのエントリを確認 -- [ ] `prisma/contest_task_pairs.ts`で共有問題の有無を確認 -- [ ] 問題数、ID フォーマット(数字/英字サフィックス)を把握 - -### ContestType確認 - -- [ ] 対応する`ContestType`が`src/lib/types/contest.ts`に存在するか確認 -- [ ] Providerクラスに正しく指定されているか確認 - -### テスト項目標準化 - -- 基本的なフィルタリング(contest_id/type検証) -- メタデータ取得(title, abbreviationName) -- ディスプレイ設定確認(isShownHeader, isShownRoundLabel等) -- ラウンドラベルフォーマット -- テーブル生成(問題数確認) -- ラウンドID取得 -- ヘッダーID取得 -- 当該Providerの特徴的な検証(共有問題確認、複数由来確認等) -- 空入力ハンドリング - -### テストデータ構築方法 - -- `createTaskResultWithTaskTableIndex` ヘルパーを使用 -- 専用の`taskResultsFor[ProviderName]`定数を作成 -- contest_id, task_id, task_table_indexを明確に指定 - ---- - -## 参考資料 - -- **既存のプロバイダーテスト**: ABC042ToABC125Provider, ABC001ToABC041Provider などを参考 -- **テスト設計パターン**: [`docs/dev-notes/2025-12-03/add_tests_for_contest_table_provider/plan.md`](../../2025-12-03/add_tests_for_contest_table_provider/plan.md) - ---- - -## 実装結果 - -**実施日**: 2025-12-11 - -**ステータス**: ✅ 完了(全テストパス) - -### 実装サマリー - -計画通りにABSProviderの単体テスト10項目をすべて実装し、全196テストがパスしました。 - -**実装内容**: - -- `src/test/lib/utils/test_cases/contest_table_provider.ts` に `taskResultsForABS` 定数を追加(11問分のテストデータ) -- `src/test/lib/utils/contest_table_provider.test.ts` の「ABC Latest 20 Rounds」セクション直前に `describe('ABS')` ブロックを追加 -- テスト1.1~テスト1.10 をすべて実装 - -### 発見事項と解決策 - -#### Issue: Mock関数のcontestId分類が不完全 - -**問題**: テストで mock されている `classifyContest` 関数が `'abs'` に対応していなかったため、ABSProviderの filter メソッドが正常に機能しませんでした。 - -**原因**: mockの `classifyContest` に 'abs' の分類ロジックがなく、`ContestType.OTHERS` にデフォルト分類されていました。 - -**解決方法**: mock関数に以下の条件を最初に追加しました: - -```typescript -if (contestId === 'abs') { - return ContestType.ABS; -} -``` - -**教訓**: - -- **新しいContestType対応時は、テストの mock 関数も同時に更新する必要があります** - - テストケースの実装だけでなく、テストヘルパー・mock関数の整備も確認すべき - - classifyContest のような汎用utility関数の mock は、すべてのContestType対応を列挙する形で保守する必要がある - -### テスト実行結果 - -```text -✓ src/test/lib/utils/contest_table_provider.test.ts (196 tests) 24ms - - Test Files 1 passed (1) - Tests 196 passed (196) -``` - -**新規テスト**(ABS関連): 10テスト - -- テスト1.1: フィルタリング(contest_id検証) ✅ -- テスト1.2: コンテストタイプ判別 ✅ -- テスト1.3: メタデータ取得 ✅ -- テスト1.4: ディスプレイ設定確認 ✅ -- テスト1.5: ラウンドラベルフォーマット ✅ -- テスト1.6: テーブル生成(11問全数) ✅ -- テスト1.7: ラウンドID取得 ✅ -- テスト1.8: ヘッダーID取得 ✅ -- テスト1.9: 複数コンテスト由来の確認 ✅ -- テスト1.10: 空入力ハンドリング ✅ - -### 気づき - -1. **ContestType追加時のチェックリスト**: 新しいContestTypeを追加する際は、以下の3つのセットで更新する必要があります: - - ✅ ContestType enum の定義 - - ✅ Provider クラスの実装 - - ✅ **テストの mock 関数の更新(見落としやすい!)** - -2. **計画通りの実装が重要**: このドキュメントで定義したテストケース(10項目)を順番に実装することで、スムーズに完了できました - -3. **複数コンテスト由来の問題テストが有効**: テスト1.9で複数元のコンテスト由来を検証することで、ABSの特徴的な仕様を確認できました diff --git a/docs/dev-notes/2025-12-17/add_tests_for_contest_table_provider/plan.md b/docs/dev-notes/2025-12-17/add_tests_for_contest_table_provider/plan.md deleted file mode 100644 index 8b799d625..000000000 --- a/docs/dev-notes/2025-12-17/add_tests_for_contest_table_provider/plan.md +++ /dev/null @@ -1,315 +0,0 @@ -# ACLPracticeProvider 単体テスト追加計画 - -**作成日**: 2025-12-17 - -**対象ブランチ**: #2962 - -**優先度**: High - ---- - -## 概要 - -`ACLPracticeProvider`(AtCoder Library Practice Contest)に対する単体テストを追加する計画。 - -**対象ファイル**: - -- **Provider実装**: [`src/lib/utils/contest_table_provider.ts`](../../../../../src/lib/utils/contest_table_provider.ts) - - `ACLPracticeProvider` (773行目~) -- **テストファイル**: [`src/test/lib/utils/contest_table_provider.test.ts`](../../../../../src/test/lib/utils/contest_table_provider.test.ts) -- **テストケースファイル**: [`src/test/lib/utils/test_cases/contest_table_provider.ts`](../../../../../src/test/lib/utils/test_cases/contest_table_provider.ts) - -**参照ドキュメント**: - -- [`docs/dev-notes/2025-12-11/add_tests_for_contest_table_provider/plan.md`](../../2025-12-11/add_tests_for_contest_table_provider/plan.md) - ABSProvider テスト設計パターンの参考 -- [`prisma/tasks.ts`](../../../../../prisma/tasks.ts) - practice2 及び各問題のタスク定義 -- [`src/lib/utils/contest.ts`](../../../../../src/lib/utils/contest.ts) - contest_id 'practice2' → ContestType.ACL_PRACTICE の判別ロジック - ---- - -## ACLPracticeProvider の仕様と特徴 - -### 基本情報 - -- **名称**: AtCoder Library Practice Contest -- **contest_id**: `'practice2'` -- **ContestType**: `ContestType.ACL_PRACTICE` - -### 問題構成 - -ACL Practice は高度なアルゴリズム技法を学ぶための教育的コンテンツで、**12問**で構成されている: - -| 問題番号 | task_id | 難易度 | problem_index | -| -------- | ----------- | ------ | ------------- | -| 1 | practice2_a | Q3 | A | -| 2 | practice2_b | Q1 | B | -| 3 | practice2_c | D2 | C | -| 4 | practice2_d | D2 | D | -| 5 | practice2_e | D3 | E | -| 6 | practice2_f | D2 | F | -| 7 | practice2_g | D2 | G | -| 8 | practice2_h | D2 | H | -| 9 | practice2_i | D2 | I | -| 10 | practice2_j | D1 | J | -| 11 | practice2_k | D2 | K | -| 12 | practice2_l | D2 | L | - -**重要な特徴**: - -- **単一コンテスト由来**: すべての問題が同じコンテスト 'practice2' に属する(複数コンテスト由来の問題はない) -- **難易度順**: A~L の順序が段階的難易度を表している -- **contest_idの統一**: すべての問題のcontest_idは'practice2'で統一される - -### ディスプレイ設定 - -ACLPracticeProviderは EDPCProvider と同じ設定を持つ: - -```typescript -{ - isShownHeader: false, // ヘッダーを非表示 - isShownRoundLabel: false, // ラウンドラベルを非表示 - isShownTaskIndex: true, // タスクインデックスを表示 - tableBodyCellsWidth: 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 2xl:w-1/7 px-1 py-2', - roundLabelWidth: '', // ラウンドラベル幅なし -} -``` - ---- - -## テスト設計 - -### テストファイル配置 - -**ファイル**: `src/test/lib/utils/contest_table_provider.test.ts` - -**配置**: 「JOI First Qual Round provider」セクションの直前(1981行目付近) - -### テストデータ構築 - -#### テストケースファイルでの準備 - -`src/test/lib/utils/test_cases/contest_table_provider.ts`に以下を追加: - -```typescript -/** - * Test data for ACLPracticeProvider (AtCoder Library Practice Contest) - * 12 problems with progressive difficulty, problem_index from A to L - * Test data includes varied submission statuses: - */ -export const taskResultsForACLPracticeProvider: TaskResults = [ - createContestTasksForACLPractice('practice2_a', 'practice2', 'A', AC), - createContestTasksForACLPractice('practice2_b', 'practice2', 'B', AC), - createContestTasksForACLPractice('practice2_c', 'practice2', 'C', AC_WITH_EDITORIAL), - createContestTasksForACLPractice('practice2_d', 'practice2', 'D', AC_WITH_EDITORIAL), - createContestTasksForACLPractice('practice2_e', 'practice2', 'E', TRYING), - createContestTasksForACLPractice('practice2_f', 'practice2', 'F', AC_WITH_EDITORIAL), - createContestTasksForACLPractice('practice2_g', 'practice2', 'G', AC_WITH_EDITORIAL), - createContestTasksForACLPractice('practice2_h', 'practice2', 'H', TRYING), - createContestTasksForACLPractice('practice2_i', 'practice2', 'I', TRYING), - createContestTasksForACLPractice('practice2_j', 'practice2', 'J', AC), - createContestTasksForACLPractice('practice2_k', 'practice2', 'K', PENDING), - createContestTasksForACLPractice('practice2_l', 'practice2', 'L', AC_WITH_EDITORIAL), -]; - -function createContestTasksForACLPractice( - taskId: string, - contestId: string, - taskTableIndex: string, - statusName: string, -): TaskResult { - return createTaskResultWithTaskTableIndex(contestId, taskId, taskTableIndex, statusName); -} -``` - -### テストケース詳細 - -#### テスト1.1: フィルタリング(contest_id検証) - -contest_id='practice2' のタスクのみをフィルタリングすることを確認。 - -```typescript -test('expects to filter tasks with contest_id "practice2"', () => { - const provider = new ACLPracticeProvider(ContestType.ACL_PRACTICE); - const mixed = [ - { contest_id: 'practice2', task_id: 'practice2_a', task_table_index: 'A' }, - { contest_id: 'practice2', task_id: 'practice2_l', task_table_index: 'L' }, - { contest_id: 'dp', task_id: 'dp_a', task_table_index: 'A' }, - { contest_id: 'abc123', task_id: 'abc123_a', task_table_index: 'A' }, - ]; - - const filtered = provider.filter(mixed as TaskResults); - - expect(filtered).toHaveLength(2); - expect(filtered.every((task) => task.contest_id === 'practice2')).toBe(true); -}); -``` - -#### テスト1.2: コンテストタイプ判別 - -ContestType.ACL_PRACTICE のみをフィルタリングすることを確認。 - -```typescript -test('expects to filter only ACL_PRACTICE-type contests', () => { - const provider = new ACLPracticeProvider(ContestType.ACL_PRACTICE); - const mixed = [ - { contest_id: 'practice2', task_id: 'practice2_a', task_table_index: 'A' }, - { contest_id: 'dp', task_id: 'dp_a', task_table_index: 'A' }, - { contest_id: 'abc378', task_id: 'abc378_a', task_table_index: 'A' }, - ]; - - const filtered = provider.filter(mixed as TaskResults); - - expect(filtered).toHaveLength(1); - expect(filtered[0].contest_id).toBe('practice2'); -}); -``` - -#### テスト1.3: メタデータ取得 - -```typescript -test('expects to return correct metadata', () => { - const provider = new ACLPracticeProvider(ContestType.ACL_PRACTICE); - const metadata = provider.getMetadata(); - - expect(metadata.title).toBe('AtCoder Library Practice Contest'); - expect(metadata.abbreviationName).toBe('aclPractice'); -}); -``` - -#### テスト1.4: ディスプレイ設定確認 - -ディスプレイ設定が ACL Practice 固有の値であることを確認。 - -```typescript -test('expects to return correct display config with ACL Practice-specific settings', () => { - const provider = new ACLPracticeProvider(ContestType.ACL_PRACTICE); - const config = provider.getDisplayConfig(); - - expect(config.isShownHeader).toBe(false); - expect(config.isShownRoundLabel).toBe(false); - expect(config.isShownTaskIndex).toBe(true); - expect(config.tableBodyCellsWidth).toBe( - 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 2xl:w-1/7 px-1 py-2', - ); - expect(config.roundLabelWidth).toBe(''); -}); -``` - -#### テスト1.5: ラウンドラベルフォーマット - -ACL Practice ではラウンドラベルが空文字列で返されることを確認。 - -```typescript -test('expects to return empty string for contest round label', () => { - const provider = new ACLPracticeProvider(ContestType.ACL_PRACTICE); - - expect(provider.getContestRoundLabel('practice2')).toBe(''); -}); -``` - -#### テスト1.6: テストケースデータ検証 - -準備されたテストケースデータが正しく構成されていることを確認。 - -```typescript -test('expects test data to have 12 tasks with correct properties', () => { - expect(taskResultsForACLPracticeProvider).toHaveLength(12); - expect(taskResultsForACLPracticeProvider.every((task) => task.contest_id === 'practice2')).toBe( - true, - ); - - const expectedIndices = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L']; - const actualIndices = taskResultsForACLPracticeProvider.map((task) => task.task_table_index); - - expect(actualIndices).toEqual(expectedIndices); -}); -``` - -#### テスト1.7: フィルタリング統合テスト - -実際のテストケースデータを使用して、フィルタリング機能を検証。 - -```typescript -test('expects to filter test data correctly', () => { - const provider = new ACLPracticeProvider(ContestType.ACL_PRACTICE); - const allTasks = [...taskResultsForACLPracticeProvider, ...someOtherContestTasks]; - - const filtered = provider.filter(allTasks); - - expect(filtered).toHaveLength(12); - expect(filtered).toEqual(taskResultsForACLPracticeProvider); -}); -``` - ---- - -## 実装ステップ - -### ステップ 1: テストケースデータの追加 - -`src/test/lib/utils/test_cases/contest_table_provider.ts` に以下を追加: - -1. `taskResultsForACLPracticeProvider` 定数の定義 -2. `createContestTasksForACLPractice` ヘルパー関数の定義 - -### ステップ 2: テストケースのエクスポート - -`src/test/lib/utils/test_cases/contest_table_provider.ts` のエクスポート一覧に `taskResultsForACLPracticeProvider` を追加 - -### ステップ 3: テストスイートの追加 - -`src/test/lib/utils/contest_table_provider.test.ts` に以下を追加: - -1. インポート文に `taskResultsForACLPracticeProvider` を追加 -2. 「JOI First Qual Round provider」セクションの直前に「ACL Practice Provider」セクションを追加 -3. 上記のテストケース(1.1~1.7)を実装 - -### ステップ 4: テスト実行と検証 - -```bash -pnpm test:unit src/test/lib/utils/contest_table_provider.test.ts -``` - -すべてのテストが PASSすることを確認 - ---- - -## 注記 - -- テストケースのステータス分布は現実的な使用パターンを反映:初期問題は解けているが、高度な問題は挑戦中 -- ACLPracticeProvider は EDPCProvider と同じ表示設定を共有するため、比較的シンプルなテスト設計が可能 -- テストは JOI First Qual Round provider の前に配置されることで、テストスイートの論理的な順序を保つ - ---- - -## 実装後の教訓 - -### ✅ 実装完了 - -- **テスト結果**: 203/203 PASS ✅ -- テストケースデータ追加 + テストスイート実装完了 - -### 📌 重要な学習ポイント - -#### 1. **モック関数の漏れは必ず発生する** ⚠️ - -- `classifyContest` モックに `practice2` → `ContestType.ACL_PRACTICE` を忘れずに追加すること -- **毎回チェックリスト**: - - [ ] 新しい contest_id に対応するモック処理を追加したか - - [ ] テストデータの contest_id とモックの対応が一致しているか - - [ ] 初回実行で失敗した場合、モック定義を最優先で確認すること - -#### 2. **既存ヘルパー関数の活用** - -- `createContestTasks` 関数を使用することで、テストデータの一貫性を維持 -- 手動で TaskResult を構築するより、ヘルパー関数を優先する - -#### 3. **統合テストの重要性** - -- 単体テスト(個別コンテスト)だけでなく、同一問題で複数コンテストが混在するテストも必須 -- フィルタリングの正確性確保には、他のコンテストデータとの組み合わせが効果的 - -#### 4. **テスト配置の順序** - -- 新しいテストスイートは既存のセクション構成を考慮して配置 -- 論理的な順序(年代順・難易度順など)を保つことで保守性が向上 diff --git a/docs/guides/how-to-add-contest-table-provider.md b/docs/guides/how-to-add-contest-table-provider.md new file mode 100644 index 000000000..aa6b94f8e --- /dev/null +++ b/docs/guides/how-to-add-contest-table-provider.md @@ -0,0 +1,705 @@ +# ContestTableProvider 実装・テストガイド + +## 概要 + +競技プログラミングコンテストサイト の各コンテスト種別に対応した `ContestTableProvider` を新規に実装・テストするときのガイドです。 + +**対象者**: JavaScript の基本知識がある開発者 + +**スコープ**: Provider クラス実装 + 単体テスト設計 + +--- + +## Test Driven Development (TDD) 設計ガイド + +新しい Provider を実装する際は、**テストファースト** のアプローチを推奨します。 + +### 実装フロー + +1. **テスト設計フェーズ** + - モックデータを定義 + - テストケースをリスト化 + - 各テストの期待値を明確化 + +2. **テスト実装フェーズ** + - テストコードを先に記述 + - Provider クラスはスケルトンで作成(メソッドは未実装でOK) + - **スケルトン Provider を `prepareContestProviderPresets()` と `contestTableProviderGroups` に登録・エクスポート**(テスト実行可能な環境構築) + +3. **Provider 実装フェーズ** + - 登録済みの Provider クラスに実装を追加 + - テストが RED → GREEN になるまで段階的に実装 + - `setFilterCondition()`、`getMetadata()`、`getDisplayConfig()`、`getContestRoundLabel()` を完成 + +### 利点 + +- **要件の明確化**: テスト設計時点で仕様が固まる +- **品質保証**: 実装中にテストで即座に検証 +- **保守性**: テストが仕様書として機能 + +### 実装フェーズ詳細 + +#### ステップ1: Provider クラス作成(スケルトン) + +テスト実装フェーズで使用するスケルトン Provider クラスを作成: + +**要件**: + +- `ContestTableProviderBase` を継承 +- `protected contestType` を指定 +- 抽象メソッドは暫定実装(テストが実行可能な状態まで) + +**例**: + +```typescript +export class MyNewProvider extends ContestTableProviderBase { + protected contestType = ContestType.MY_NEW; + + protected setFilterCondition(): (taskResult: TaskResult) => boolean { + return () => true; // 暫定実装 + } + + getMetadata(): ContestTableMetaData { + return { title: '', abbreviationName: '' }; // 暫定実装 + } + + getDisplayConfig(): ContestTableDisplayConfig { + return { + /* 暫定実装 */ + }; + } + + getContestRoundLabel(): string { + return ''; // 暫定実装 + } +} +``` + +#### ステップ2: Provider 登録・エクスポート(テスト実行環境構築) + +**ファイル**: `src/lib/utils/contest_table_provider.ts` + +`prepareContestProviderPresets()` 関数内に新規 Provider を追加: + +```typescript +function prepareContestProviderPresets() { + return { + // ... 既存の Provider + myNewProvider: () => + new ContestTableProviderGroup().addProvider(new MyNewProvider(ContestType.MY_NEW)), + }; +} +``` + +次に、`contestTableProviderGroups` オブジェクトに新規 Provider グループをエクスポート: + +```typescript +export const contestTableProviderGroups: Record = { + // ... 既存の Provider グループ + myNewProvider: prepareContestProviderPresets().myNewProvider(), +}; +``` + +**重要**: この登録がないとテストが実行されず、RED 状態が継続します。 + +#### ステップ3: Provider 実装フェーズ(本体実装) + +登録済みの Provider クラスに実装を追加: + +**要件**: + +- `setFilterCondition()`: 条件関数を返すメソッド +- `getMetadata()`: `title`、`abbreviationName` を返す +- `getDisplayConfig()`: 表示設定を返す +- `getContestRoundLabel()`: ラウンドラベルを返す + +**例**: + +```typescript +export class MyNewProvider extends ContestTableProviderBase { + protected contestType = ContestType.MY_NEW; + + protected setFilterCondition(): (taskResult: TaskResult) => boolean { + return (taskResult: TaskResult) => taskResult.contest_id === 'my-contest'; + } + + getMetadata(): ContestTableMetaData { + return { + title: 'My New Contest', + abbreviationName: 'myNew', + }; + } + + getDisplayConfig(): ContestTableDisplayConfig { + return { + isShownHeader: true, + isShownRoundLabel: false, + roundLabelWidth: '', + tableBodyCellsWidth: 'w-1/2 md:w-1/3 lg:w-1/4 px-1 py-2', + isShownTaskIndex: false, + }; + } + + getContestRoundLabel(contestId: string): string { + return ''; + } +} +``` + +テストを実行しながら段階的に実装を完成させてください: + +```bash +pnpm test:unit src/test/lib/utils/contest_table_provider.test.ts +``` + +--- + +## 実装パターン + +### パターン1: 範囲フィルタ型(ABC / ARC / AGC) + +**特徴**: + +- 範囲内の contest_id をフィルタリング +- contest_id から数字を抽出して範囲判定 +- すべての ARC/AGC はこのパターン +- 表示: コンテストのラウンド名、ヘッダー +- 非表示: 問題名の前にある問題 id + +**実装例**(ABC 001-041): + +```typescript +class ABC001ToABC041Provider extends ContestTableProviderBase { + protected contestType = ContestType.ABC; + + filter(tasks: TaskResults): TaskResults | null { + return tasks.filter((task) => { + const round = this.extractRound(task.contest_id, 'abc'); + return round >= 1 && round <= 41; + }); + } + + getMetadata(): ContestTableMetaData { + return { + title: 'AtCoder Beginner Contest 001 〜 041(レーティング導入前)', + abbreviationName: 'fromAbc001ToAbc041', + }; + } + + getDisplayConfig(): ContestTableDisplayConfig { + return { + isShownHeader: true, + isShownRoundLabel: true, + roundLabelWidth: 'xl:w-16', + tableBodyCellsWidth: 'w-1/2 md:w-1/3 lg:w-1/4 px-1 py-1', + isShownTaskIndex: false, + }; + } + + getContestRoundLabel(contestId: string): string { + const contestNameLabel = getContestNameLabel(contestId); + return contestNameLabel.replace('ABC ', ''); + } +} +``` + +**注意**: + +- 他の ABC 範囲も同じパターン(ARC、AGC も同様) + +--- + +### パターン2: 単一ソース型(EDPC / TDPC / FPS_24 / ACL_PRACTICE) + +**特徴**: + +- 単一の contest_id のみをフィルタリング +- セクションは固定フォーマット(A~Z など) +- 表示: 問題名の前にある問題 id +- 非表示: コンテストのラウンド名、ヘッダー + +**実装例**(EDPC): + +```typescript +class EDPCProvider extends ContestTableProviderBase { + protected contestType = ContestType.EDPC; + + filter(tasks: TaskResults): TaskResults | null { + return tasks.filter((task) => task.contest_id === 'dp'); + } + + getMetadata(): ContestTableMetaData { + return { + title: 'AtCoder Educational DP Contest', + abbreviationName: 'edpc', + }; + } + + getDisplayConfig(): ContestTableDisplayConfig { + return { + isShownHeader: false, + isShownRoundLabel: false, + roundLabelWidth: '', + tableBodyCellsWidth: 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 2xl:w-1/7 px-1 py-2', + isShownTaskIndex: true, + }; + } + + getContestRoundLabel(contestId: string): string { + return ''; + } +} +``` + +**注意**: + +- TDPC、FPS_24 も同じパターン(contest_id と メタデータだけ異なる) + +--- + +### パターン3: 複合ソース型(ABS / TESSOKU_BOOK / MATH_AND_ALGORITHM) + +**特徴**: + +- 複数の異なる contest/task_id を1つのテーブルに表示 +- task_table_index が共通フォーマット(A~K、001~104 など) +- セクション分割可能(Tessoku Book のみ) + +**実装例**(ABS): + +```typescript +class ABSProvider extends ContestTableProviderBase { + protected contestType = ContestType.ABS; + + filter(tasks: TaskResults): TaskResults | null { + return tasks.filter((task) => task.contest_id === 'abs'); + } + + getMetadata(): ContestTableMetaData { + return { + title: 'AtCoder Beginners Selection', + abbreviationName: 'abs', + }; + } + + getDisplayConfig(): ContestTableDisplayConfig { + return { + isShownHeader: false, + isShownRoundLabel: false, + roundLabelWidth: '', + tableBodyCellsWidth: 'w-1/2 md:w-1/3 lg:w-1/4 px-1 py-2', + isShownTaskIndex: false, + }; + } + + getContestRoundLabel(contestId: string): string { + return ''; + } + + getHeaderIdsForTask(tasks: TaskResults): string[] { + return ( + this.filter(tasks) + ?.map((task) => task.task_table_index) + .sort() ?? [] + ); + } +} +``` + +**Tessoku Book(セクション分割あり)**: + +```typescript +// 基底クラス: TessokuBookProvider +export class TessokuBookProvider extends ContestTableProviderBase { + protected setFilterCondition(): (taskResult: TaskResult) => boolean { + return (taskResult: TaskResult) => { + return classifyContest(taskResult.contest_id) === this.contestType; + }; + } + + getMetadata(): ContestTableMetaData { + return { + title: '競技プログラミングの鉄則', + abbreviationName: 'tessoku-book', + }; + } +} + +// 例題: A01~A77 +export class TessokuBookForExamplesProvider extends TessokuBookProvider { + constructor(contestType: ContestType) { + super(contestType, 'examples'); + } + + protected setFilterCondition(): (taskResult: TaskResult) => boolean { + return (taskResult: TaskResult) => { + return ( + classifyContest(taskResult.contest_id) === this.contestType && + taskResult.task_table_index.startsWith('A') + ); + }; + } + + getMetadata(): ContestTableMetaData { + return { + title: '競技プログラミングの鉄則(A. 例題)', + abbreviationName: 'tessoku-book-for-examples', + }; + } +} + +// 応用: B01~B69 +export class TessokuBookForPracticalsProvider extends TessokuBookProvider { + constructor(contestType: ContestType) { + super(contestType, 'practicals'); + } + + protected setFilterCondition(): (taskResult: TaskResult) => boolean { + return (taskResult: TaskResult) => { + return ( + classifyContest(taskResult.contest_id) === this.contestType && + taskResult.task_table_index.startsWith('B') + ); + }; + } + + getMetadata(): ContestTableMetaData { + return { + title: '競技プログラミングの鉄則(B. 応用問題)', + abbreviationName: 'tessoku-book-for-practicals', + }; + } +} + +// 力試し: C01~C20 +export class TessokuBookForChallengesProvider extends TessokuBookProvider { + constructor(contestType: ContestType) { + super(contestType, 'challenges'); + } + + protected setFilterCondition(): (taskResult: TaskResult) => boolean { + return (taskResult: TaskResult) => { + return ( + classifyContest(taskResult.contest_id) === this.contestType && + taskResult.task_table_index.startsWith('C') + ); + }; + } + + getMetadata(): ContestTableMetaData { + return { + title: '競技プログラミングの鉄則(C. 力試し問題)', + abbreviationName: 'tessoku-book-for-challenges', + }; + } +} +``` + +--- + +## 各コンテスト種別の特有仕様 + +### 範囲フィルタ型 + +| コンテスト | 範囲 | フォーマット | 問題数 | ラベル表示 | 特有の注意 | +| ----------- | -------- | ------------ | ------------- | ---------- | ------------------------- | +| ABC 001-041 | 001~041 | 001, 041 | 4問(A-D) | あり | 古いID形式混在 | +| ABC 042-125 | 042~125 | 042, 125 | 4問(A-D) | あり | 共有問題あり(ARC) | +| ABC 126-211 | 126~211 | 126, 211 | 6問(A-F) | あり | 標準形式 | +| ABC 212-318 | 212~318 | 212, 318 | 8問(A-Ex/H) | あり | 標準形式 | +| ABC 319- | 319~ | 319 | 7問(A-G) | あり | 標準形式 | +| ARC 001-057 | 001~057 | 001, 057 | 4問(A-D) | あり | 古いID形式 | +| ARC 058-103 | 058~103 | 058, 103 | 4問(C-F) | あり | 共有問題あり (ABC) | +| ARC 104- | 104~ | 104 | 4~6問 | あり | - | +| AGC 001- | 001~ | 001 | 4~7問 | あり | 7問パターン(AGC028のみ) | + +### 単一ソース型 + +| コンテスト | contest_id | セクション範囲 | フォーマット | ヘッダー表示 | 問題インデックス表示 | +| ------------ | ------------- | -------------- | ------------ | ------------ | -------------------- | +| EDPC | `'dp'` | A~Z(26問) | A, B, ..., Z | なし | あり | +| TDPC | `'tdpc'` | A~Z(26問) | A, B, ..., Z | なし | あり | +| FPS_24 | `'fps-24'` | A~X(24問) | A, B, ..., X | なし | あり | +| ACL_PRACTICE | `'practice2'` | A~L(12問) | A, B, ..., L | なし | あり | + +### 複合ソース型 + +| コンテスト | contest_id | 問題数 | セクション範囲 | フォーマット | セクション分割 | 複数コンテスト | ヘッダー表示 | インデックス表示 | 特有の注意 | +| ------------------ | ---------------------- | ------ | ---------------------------- | ------------------ | -------------- | -------------- | ------------ | ---------------- | -------------------- | +| ABS | `'abs'` | 11問 | A~K | A, B, ..., K | なし | あり(11個) | なし | なし | task_idが複雑 | +| TESSOKU_BOOK | `'tessoku-book'` | 166問 | A(01-77), B(01-69), C(01-20) | A01, A77, B63, C20 | あり | あり | なし | あり | セクション判定が複雑 | +| MATH_AND_ALGORITHM | `'math-and-algorithm'` | 104問 | 001~104 | 001, 028, 104 | なし | あり | なし | あり | 範囲内に欠損あり | + +--- + +## テスト設計ガイド + +### テストケース設計 + +#### 全 Provider 共通の必須テスト項目 + +1. **基本的なフィルタリング**: contest_id / 型の検証 +2. **メタデータ取得**: title、abbreviationName +3. **ディスプレイ設定確認**: isShownHeader、isShownRoundLabel 等 +4. **ラウンドラベルフォーマット**: getContestRoundLabel() +5. **テーブル生成**: generateTable() の構造(問題数確認) +6. **ラウンドID取得**: getContestRoundIds() +7. **ヘッダーID取得**: getHeaderIdsForTask() +8. **当該Provider の特徴的な検証**: 共有問題確認、複数由来確認等 +9. **空入力ハンドリング**: 空配列での動作 + +#### パターン固有テスト + +- **範囲フィルタ型**: 範囲境界値(最小値、最大値、範囲外)の検証、共有問題がないか確認 +- **複合ソース型**: 複数 contest_id の混在検証、セクション分割ロジック(該当する場合) + +### モックデータの準備と確認事項 + +#### ステップ1: データソース確認 + +テスト設計開始前に、以下のファイルで当該コンテストの仕様を把握してください: + +**ファイル**: `prisma/tasks.ts` + +- 当該 contest_id のエントリを確認 +- 問題ID フォーマット(数字 `001` / 英字サフィックス `A`)を把握 +- 複数コンテスト由来の問題がないか確認 + +**ファイル**: `prisma/contest_task_pairs.ts` + +- 共有問題の有無を確認(同一の問題が複数コンテストで使用されているか) +- 複合ソース型の場合、各問題の元のコンテストを特定 + +**ファイル**: `prisma/schema.prisma` + +- `task_table_index` フィールドのフォーマットを確認 + +#### ステップ2: ContestType 確認 + +- **ファイル**: `src/lib/types/contest.ts` + - 対応する `ContestType` が定義されているか確認 + - 定義されていない場合は、先に ContestType を追加 + +- **Provider 実装**の確認 + - `protected contestType` が正しく指定されているか + - `classifyContest()` ユーティリティ関数に分類ロジックが実装されているか確認 + +#### ステップ3: テストデータ構築 + +**ファイル**: `src/test/lib/utils/test_cases/contest_table_provider.ts` + +以下のヘルパー関数を使用してテストデータを作成してください: + +```typescript +// 既存のヘルパー関数(汎用) +export function createTaskResultWithTaskTableIndex( + contestId: string, + taskId: string, + taskTableIndex: string, + submissionStatus: SubmissionStatus, +): TaskResult { + return { + contest_id: contestId, + task_id: taskId, + task_table_index: taskTableIndex, + submission_status: submissionStatus, + // ... その他のプロパティ + }; +} + +// 新規 Provider 用テストデータ定数 +export const taskResultsForNewProvider: TaskResults = [ + createTaskResultWithTaskTableIndex('contest_id', 'task_id_1', 'A', AC), + createTaskResultWithTaskTableIndex('contest_id', 'task_id_2', 'B', AC), + // ... 問題数分のエントリ +]; +``` + +**重要なポイント**: + +- `createTaskResultWithTaskTableIndex` を使用して、contest_id、task_id、task_table_index を明確に指定 +- テストデータは「全 Provider 共通の必須テスト項目」すべてをカバーする構成にする +- 複数コンテスト由来の問題がある場合は、そのパターンも明示的に含める +- `AC` 以外のステータスが必要な場合は、テストケースごとに明示的に指定 + +### テスト実装の最小構成 + +#### Vitest の基本概念 + +- `describe()`: テストスイートをグループ化 +- `test()` または `it()`: 個別のテストケース +- `expect()`: アサーション(期待値の検証) +- `vi.mock()`: モック関数の定義 +- `beforeEach()`: 各テスト前に実行する初期化処理 + +#### Vitest を使用したテスト例 + +```typescript +import { describe, test, expect, vi } from 'vitest'; + +describe('NewProvider', () => { + // モック定義 + beforeEach(() => { + vi.mock('src/lib/utils/contest', () => ({ + classifyContest: vi.fn((contestId) => { + if (contestId === 'new-contest') return ContestType.NEW; + // ... その他の処理 + }), + })); + }); + + test('expects to filter tasks correctly', () => { + const provider = new NewProvider(ContestType.NEW); + const filtered = provider.filter(mockTasks); + expect(filtered?.every((t) => /* condition */)).toBe(true); + }); + + test('expects to return correct metadata', () => { + const provider = new NewProvider(ContestType.NEW); + expect(provider.getMetadata().title).toBe('Expected Title'); + }); + + test('expects to return correct display config', () => { + const provider = new NewProvider(ContestType.NEW); + const config = provider.getDisplayConfig(); + expect(config.isShownHeader).toBe(true); + }); +}); +``` + +--- + +## 教訓: よくあるミス + +### 1. **モック定義の漏れ**(最頻出) + +**問題**: テストで新しい contest_id を使うと、`classifyContest()` モックに対応定義がなく失敗 + +**対策チェックリスト**: + +- [ ] 新規 contest_id をテストで使用する場合、`classifyContest` モックに追加したか +- [ ] テストデータと モック定義が対応しているか +- [ ] 初回実行で失敗したら、**モック定義を最優先で確認** + +**例**: + +```typescript +vi.mock('src/lib/utils/contest', () => ({ + classifyContest: vi.fn((contestId) => { + if (contestId === 'practice2') return ContestType.ACL_PRACTICE; + if (contestId === 'new-contest') return ContestType.NEW; + // ... + }), +})); +``` + +### 2. **ソート順序の不安定** + +**問題**: 数字ソート(001, 028, 036, 102)と文字ソート(A, B, M, X)の混在で誤り + +**対策**: + +- 数字フォーマット(001など)は **文字列として数値ソート** +- 英字フォーマット(A, Bなど)は **localeCompare() を使用** + +```typescript +// ❌ 間違い +const sorted = indices.sort(); + +// ✅ 正解(数字フォーマット) +const sorted = indices.sort((a, b) => { + const aNum = parseInt(a, 10); + const bNum = parseInt(b, 10); + return aNum - bNum; +}); + +// ✅ 正解(英字フォーマット) +const sorted = indices.sort((a, b) => a.localeCompare(b)); +``` + +### 3. **複数コンテスト由来の問題を見落とし** + +**問題**: ABC042-125 の共有問題(ARC との同日開催)で task_id が arc58_a なのに contest_id は abc042 のケースを処理し忘れ + +**対策**: + +- 範囲フィルタ型で複数コンテスト由来の問題がないか確認 +- テストケースに **混合パターン** を明示的に含める + +```typescript +test('expects to filter only ABC-type contests', () => { + const mixed = [ + { contest_id: 'abc042', task_id: 'abc042_a', task_table_index: 'A' }, // ABC側 + { contest_id: 'abc042', task_id: 'arc058_a', task_table_index: 'C' }, // 共有問題 + { contest_id: 'arc058', task_id: 'arc058_a', task_table_index: 'C' }, // ARC側 + ]; + // ARC側は除外、ABC042側の2つだけが返る +}); +``` + +### 4. **セクション判定ロジックの複雑化** + +**問題**: Tessoku Book の A01~A77, B01~B69, C01~C20 で正規表現を間違える + +**対策**: + +- 正規表現は **段階的にテスト** +- 範囲の上限が2桁(A77, B69)と可変の場合、明示的に記述 + +```typescript +// Tessoku Book 例題(A01~A30) +const isExample = /^A(0[1-9]|[12][0-9]|30)$/.test(index); + +// 応用(A31~A77) +const isPractical = /^A(3[1-9]|[4-6][0-9]|7[0-7])$/.test(index); +``` + +### 5. **パラメータ化テスト(describe.each)での誤り** + +**問題**: 複数 Provider で displayConfig が共通だが、1つだけ異なる値を設定するとテスト全体が失敗 + +**対策**: + +- `describe.each()` で共通テストは共有、固有テストは別ブロック +- 表示設定が異なる Provider は `describe.each()` から **除外** + +```typescript +describe.each([ + { Provider: EDPCProvider, ... }, + { Provider: TDPCProvider, ... }, + // ただし displayConfig が異なる場合は個別 describe ブロック +])('...', ({ Provider, ... }) => { + test('shared test', () => { ... }); +}); + +describe('CustomProvider with unique displayConfig', () => { + test('custom test', () => { ... }); +}); +``` + +--- + +## 参考資料 + +### GitHub Issues + +- [#2919](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2919) - ABS +- [#2830](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2830) - ABC126ToABC211Provider +- [#2836](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2836) - ABC042~125 & ARC058~103 +- [#2835](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2835) - ARC104OnwardsProvider +- [#2837](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2837) - AGC001OnwardsProvider +- [#2838](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2838) - ABC001~041 & ARC001~057 +- [#2776](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2776) - TessokuBookProvider +- [#2785](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2785) - MathAndAlgorithmProvider +- [#2797](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2797) - FPS24Provider +- [#2962](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2962) - ACLPracticeProvider + +### 実装ファイル + +- [Provider 実装](../src/lib/utils/contest_table_provider.ts) +- [単体テスト](../src/test/lib/utils/contest_table_provider.test.ts) +- [モックデータ](../src/test/lib/utils/test_cases/contest_table_provider.ts) + +--- + +**最終更新**: 2026-01-23 From 20def92429055753fd65b8e59de8323fe8a9dc52 Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Fri, 23 Jan 2026 14:16:31 +0000 Subject: [PATCH 2/3] chore: Fix typo (#2989) --- docs/guides/how-to-add-contest-table-provider.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/how-to-add-contest-table-provider.md b/docs/guides/how-to-add-contest-table-provider.md index aa6b94f8e..889106743 100644 --- a/docs/guides/how-to-add-contest-table-provider.md +++ b/docs/guides/how-to-add-contest-table-provider.md @@ -692,7 +692,7 @@ describe('CustomProvider with unique displayConfig', () => { - [#2776](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2776) - TessokuBookProvider - [#2785](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2785) - MathAndAlgorithmProvider - [#2797](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2797) - FPS24Provider -- [#2962](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2962) - ACLPracticeProvider +- [#2920](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2920) - ACLPracticeProvider ### 実装ファイル From 63fee7aa9a140edeeaea3d0362a134ced3ef521c Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Sat, 24 Jan 2026 01:46:29 +0000 Subject: [PATCH 3/3] chore(docs): Fix method name and signature (#2989) --- .../how-to-add-contest-table-provider.md | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/docs/guides/how-to-add-contest-table-provider.md b/docs/guides/how-to-add-contest-table-provider.md index 889106743..ca60b6927 100644 --- a/docs/guides/how-to-add-contest-table-provider.md +++ b/docs/guides/how-to-add-contest-table-provider.md @@ -111,7 +111,7 @@ export const contestTableProviderGroups: Record boolean { + return (taskResult: TaskResult) => { + if (classifyContest(taskResult.contest_id) !== this.contestType) { + return false; + } - filter(tasks: TaskResults): TaskResults | null { - return tasks.filter((task) => { - const round = this.extractRound(task.contest_id, 'abc'); - return round >= 1 && round <= 41; - }); + const contestRound = parseContestRound(taskResult.contest_id, 'abc'); + return contestRound >= 1 && contestRound <= 41; + }; } getMetadata(): ContestTableMetaData { @@ -222,15 +224,19 @@ class ABC001ToABC041Provider extends ContestTableProviderBase { ```typescript class EDPCProvider extends ContestTableProviderBase { - protected contestType = ContestType.EDPC; + protected setFilterCondition(): (taskResult: TaskResult) => boolean { + return (taskResult: TaskResult) => { + if (classifyContest(taskResult.contest_id) !== this.contestType) { + return false; + } - filter(tasks: TaskResults): TaskResults | null { - return tasks.filter((task) => task.contest_id === 'dp'); + return taskResult.contest_id === 'dp'; + }; } getMetadata(): ContestTableMetaData { return { - title: 'AtCoder Educational DP Contest', + title: 'Educational DP Contest / DP まとめコンテスト', abbreviationName: 'edpc', }; } @@ -269,10 +275,10 @@ class EDPCProvider extends ContestTableProviderBase { ```typescript class ABSProvider extends ContestTableProviderBase { - protected contestType = ContestType.ABS; - - filter(tasks: TaskResults): TaskResults | null { - return tasks.filter((task) => task.contest_id === 'abs'); + protected setFilterCondition(): (taskResult: TaskResult) => boolean { + return (taskResult: TaskResult) => { + return classifyContest(taskResult.contest_id) === this.contestType; + }; } getMetadata(): ContestTableMetaData {