Skip to content

Commit 7636bf3

Browse files
committed
feat(table): Add FPS 24 to contest table (#2797)
1 parent 27c7069 commit 7636bf3

File tree

4 files changed

+485
-7
lines changed

4 files changed

+485
-7
lines changed
Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
# FPS24Provider テスト追加計画
2+
3+
**作成日**: 2025-11-06
4+
5+
**対象ブランチ**: #2797
6+
7+
**優先度**: High
8+
9+
---
10+
11+
## 参照ドキュメント
12+
13+
テストの書き方・スタイル・ベストプラクティスについては、以下を参照:
14+
15+
📖 [`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)
16+
17+
---
18+
19+
## 1. 概要
20+
21+
### 背景
22+
23+
`FPS24Provider``EDPCProvider``TDPCProvider` と同じ構造で、単一のコンテスト(`fps-24`)からなる問題集を提供します。
24+
25+
- **セクション範囲**: A ~ X(24文字)
26+
- **フォーマット**: 大文字アルファベット(A, B, C, ..., X)
27+
- **単一ソース**: `contest_id === 'fps-24'` で統一
28+
29+
### 目的
30+
31+
EDPC・TDPC テストと同等の粒度で、FPS24Provider の単体テスト 8 個を追加。
32+
33+
---
34+
35+
## 2. 仕様要件
36+
37+
| 項目 | 仕様 | 備考 |
38+
| ------------------ | --------------------- | ------------------------- |
39+
| **セクション範囲** | A ~ X | 24文字分 |
40+
| **ソート順序** | 昇順(A → B → ... X) | 必須 |
41+
| **フォーマット** | 大文字アルファベット | 例: A, B, X |
42+
| **単一ソース** | contest_id = 'fps-24' | EDPC・TDPC と同じパターン |
43+
44+
---
45+
46+
## 3. テストケース(8件)
47+
48+
### テスト1: フィルタリング
49+
50+
```typescript
51+
test('expects to filter tasks to include only fps-24 contest', () => {
52+
const provider = new FPS24Provider(ContestType.FPS_24);
53+
const mixedTasks = [
54+
{ contest_id: 'abc123', task_id: 'abc123_a', task_table_index: 'A' },
55+
{ contest_id: 'fps-24', task_id: 'fps-24_a', task_table_index: 'A' },
56+
{ contest_id: 'fps-24', task_id: 'fps-24_b', task_table_index: 'B' },
57+
{ contest_id: 'typical90', task_id: 'typical90_a', task_table_index: '001' },
58+
];
59+
const filtered = provider.filter(mixedTasks);
60+
61+
expect(filtered?.every((task) => task.contest_id === 'fps-24')).toBe(true);
62+
expect(filtered).not.toContainEqual(expect.objectContaining({ contest_id: 'abc123' }));
63+
expect(filtered).not.toContainEqual(expect.objectContaining({ contest_id: 'typical90' }));
64+
});
65+
```
66+
67+
---
68+
69+
### テスト2: メタデータ取得
70+
71+
```typescript
72+
test('expects to get correct metadata', () => {
73+
const provider = new FPS24Provider(ContestType.FPS_24);
74+
const metadata = provider.getMetadata();
75+
76+
expect(metadata.title).toBe('FPS 24 題');
77+
expect(metadata.abbreviationName).toBe('fps-24');
78+
});
79+
```
80+
81+
---
82+
83+
### テスト3: 表示設定
84+
85+
```typescript
86+
test('expects to get correct display configuration', () => {
87+
const provider = new FPS24Provider(ContestType.FPS_24);
88+
const displayConfig = provider.getDisplayConfig();
89+
90+
expect(displayConfig.isShownHeader).toBe(false);
91+
expect(displayConfig.isShownRoundLabel).toBe(false);
92+
expect(displayConfig.roundLabelWidth).toBe('');
93+
expect(displayConfig.tableBodyCellsWidth).toBe(
94+
'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',
95+
);
96+
expect(displayConfig.isShownTaskIndex).toBe(true);
97+
});
98+
```
99+
100+
---
101+
102+
### テスト4: ラウンドラベルフォーマット
103+
104+
```typescript
105+
test('expects to format contest round label correctly', () => {
106+
const provider = new FPS24Provider(ContestType.FPS_24);
107+
const label = provider.getContestRoundLabel('fps-24');
108+
109+
expect(label).toBe('');
110+
});
111+
```
112+
113+
---
114+
115+
### テスト5: テーブル生成
116+
117+
```typescript
118+
test('expects to generate correct table structure', () => {
119+
const provider = new FPS24Provider(ContestType.FPS_24);
120+
const tasks = [
121+
{ contest_id: 'fps-24', task_id: 'fps_24_a', task_table_index: 'A' },
122+
{ contest_id: 'fps-24', task_id: 'fps_24_b', task_table_index: 'B' },
123+
{ contest_id: 'fps-24', task_id: 'fps_24_x', task_table_index: 'X' },
124+
];
125+
const table = provider.generateTable(tasks);
126+
127+
expect(table).toHaveProperty('fps-24');
128+
expect(table['fps-24']).toHaveProperty('A');
129+
expect(table['fps-24']).toHaveProperty('B');
130+
expect(table['fps-24']).toHaveProperty('X');
131+
expect(table['fps-24']['A']).toEqual(expect.objectContaining({ task_id: 'fps-24_a' }));
132+
});
133+
```
134+
135+
---
136+
137+
### テスト6: ラウンド ID 取得
138+
139+
```typescript
140+
test('expects to get contest round IDs correctly', () => {
141+
const provider = new FPS24Provider(ContestType.FPS_24);
142+
const tasks = [
143+
{ contest_id: 'fps-24', task_id: 'fps_24_a', task_table_index: 'A' },
144+
{ contest_id: 'fps-24', task_id: 'fps_24_x', task_table_index: 'X' },
145+
];
146+
const roundIds = provider.getContestRoundIds(tasks);
147+
148+
expect(roundIds).toEqual(['fps-24']);
149+
});
150+
```
151+
152+
---
153+
154+
### テスト7: ヘッダー ID 取得(昇順)
155+
156+
```typescript
157+
test('expects to get header IDs for tasks correctly in ascending order', () => {
158+
const provider = new FPS24Provider(ContestType.FPS_24);
159+
const tasks = [
160+
{ contest_id: 'fps-24', task_id: 'fps_24_a', task_table_index: 'A' },
161+
{ contest_id: 'fps-24', task_id: 'fps_24_x', task_table_index: 'X' },
162+
{ contest_id: 'fps-24', task_id: 'fps_24_m', task_table_index: 'M' },
163+
{ contest_id: 'fps-24', task_id: 'fps_24_b', task_table_index: 'B' },
164+
];
165+
const headerIds = provider.getHeaderIdsForTask(tasks);
166+
167+
expect(headerIds).toEqual(['A', 'B', 'M', 'X']);
168+
});
169+
```
170+
171+
---
172+
173+
### テスト8: セクション範囲検証(A ~ X)
174+
175+
```typescript
176+
test('expects to handle section boundaries correctly (A-X)', () => {
177+
const provider = new FPS24Provider(ContestType.FPS_24);
178+
const tasks = [
179+
{ contest_id: 'fps-24', task_id: 'fps_24_a', task_table_index: 'A' },
180+
{ contest_id: 'fps-24', task_id: 'fps_24_x', task_table_index: 'X' },
181+
];
182+
const headerIds = provider.getHeaderIdsForTask(tasks);
183+
184+
expect(headerIds).toEqual(['A', 'X']);
185+
});
186+
```
187+
188+
---
189+
190+
## 4. モックデータ
191+
192+
追加先: `src/test/lib/utils/test_cases/contest_table_provider.ts`
193+
194+
```typescript
195+
export const taskResultsForFPS24Provider: TaskResults = [
196+
{
197+
contest_id: 'fps-24',
198+
task_id: 'fps_24_a',
199+
task_table_index: 'A',
200+
},
201+
{
202+
contest_id: 'fps-24',
203+
task_id: 'fps_24_b',
204+
task_table_index: 'B',
205+
},
206+
{
207+
contest_id: 'fps-24',
208+
task_id: 'fps_24_m',
209+
task_table_index: 'M',
210+
},
211+
{
212+
contest_id: 'fps-24',
213+
task_id: 'fps_24_x',
214+
task_table_index: 'X',
215+
},
216+
];
217+
```
218+
219+
---
220+
221+
## 5. テスト統合パターン
222+
223+
### 既存テスト構造(変更しない)
224+
225+
以下は変更対象外:
226+
227+
- Typical90 provider テスト
228+
- TessokuBook provider テスト
229+
- MathAndAlgorithm provider テスト
230+
231+
### 新規追加パターン
232+
233+
`describe.each()` に FPS24 を追加(EDPC・TDPC と同じ共通テストパターン):
234+
235+
```typescript
236+
describe.each([
237+
{
238+
providerClass: EDPCProvider,
239+
contestType: ContestType.EDPC,
240+
title: 'Educational DP Contest / DP まとめコンテスト',
241+
abbreviationName: 'edpc',
242+
label: 'EDPC provider',
243+
},
244+
{
245+
providerClass: TDPCProvider,
246+
contestType: ContestType.TDPC,
247+
title: 'Typical DP Contest',
248+
abbreviationName: 'tdpc',
249+
label: 'TDPC provider',
250+
},
251+
{
252+
providerClass: FPS24Provider,
253+
contestType: ContestType.FPS24,
254+
title: 'FPS 24 題',
255+
abbreviationName: 'fps-24',
256+
label: 'FPS24 provider',
257+
},
258+
])('$label', ({ providerClass, contestType, title, abbreviationName }) => {
259+
// 共通テスト: メタデータ、表示設定、ラウンドラベル
260+
});
261+
```
262+
263+
### FPS24 特有テスト
264+
265+
独立した `describe('FPS24 provider', ...)` ブロックで以下をテスト:
266+
267+
- フィルタリング機能
268+
- テーブル生成
269+
- ラウンド ID 取得
270+
- ヘッダー ID 取得(昇順)
271+
- セクション範囲検証(A ~ X)
272+
273+
---
274+
275+
## 6. 実装手順
276+
277+
**ステップ1**: ✅ モックデータを `src/test/lib/utils/test_cases/contest_table_provider.ts` に追加
278+
279+
**ステップ2**: ✅ `describe.each()` に FPS24 パラメータを追加(EDPC・TDPC と並べる)
280+
281+
**ステップ3**: ✅ FPS24 特有テスト 7 個を `src/test/lib/utils/contest_table_provider.test.ts` に追加
282+
283+
**ステップ4**: ✅ テスト実行・検証
284+
285+
```bash
286+
pnpm test:unit src/test/lib/utils/contest_table_provider.test.ts
287+
```
288+
289+
**ステップ5**: ✅ Lint チェック
290+
291+
```bash
292+
pnpm lint src/test/lib/utils/contest_table_provider.test.ts
293+
```
294+
295+
---
296+
297+
## 7. 注意点
298+
299+
1. **セクション形式**: 大文字アルファベット(A ~ X)であり、3桁数字ではない
300+
2. **コンテスト ID**: `contest_id === 'fps-24'` で統一(ハイフン含む)
301+
3. **単一ソース**: EDPC・TDPC と同様に、常に `contest_id === 'fps-24'`
302+
4. **ソート順序**: 文字列の辞書順ソート(`'A' < 'B' < ... < 'X'`
303+
304+
---
305+
306+
## 8. 参考資料
307+
308+
- PR #2286: FPS24Provider 実装(https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/pull/2286)
309+
- PR #2780: リファクタリング(https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/pull/2780)
310+
- 参照ドキュメント: `docs/dev-notes/2025-11-01/add_and_refactoring_tests_for_contest_table_provider/plan.md`
311+
312+
---
313+
314+
## 9. 実装結果・教訓
315+
316+
### ✅ 実装完了
317+
318+
**実施時間**: 12.4 秒(テスト実行 7.47 秒含む)
319+
320+
**実装内容**:
321+
322+
1. モックデータ追加: 4 個のサンプルタスク(`fps_24_a`, `fps_24_b`, `fps_24_m`, `fps_24_x`)を `contest_table_provider.ts` に追加
323+
2. classifyContest mock 拡張: `fps-24``ContestType.FPS_24` のマッピングを追加
324+
3. describe.each に FPS24 パラメータ追加: EDPC・TDPC と並べて共通テスト(メタデータ、表示設定、ラウンドラベル)を定義
325+
4. FPS24 特有テスト 7 個を実装: フィルタリング、テーブル生成、ラウンド ID 取得、ヘッダー ID 取得(昇順)、セクション範囲検証、空入力処理、混合コンテストタイプ処理
326+
327+
### 📚 得られた教訓
328+
329+
1. **既存のプリセット関数への影響**:新規プロバイダーを `prepareContestProviderPresets().dps()` に追加する際、既存テストケース(`expects to create DPs preset correctly`)が自動的に期待値が変わることに注意。既存テストを更新する必要がある
330+
331+
2. **共通テストパターンの有効性確認**:FPS24 が EDPC・TDPC と全く同じ構造(単一コンテスト ID、大文字アルファベット形式)であることから、`describe.each()` による共通テスト化が非常に効果的。テストコードの重複排除に成功
332+
333+
3. **アルファベット順ソートの正確性**:大文字アルファベット(A ~ X)のソートは JavaScript の標準文字列ソート(`sort()`)で正しく動作することを確認。ただし Unicode 順序に依存するため、テストケースで明示的に検証することは重要
334+
335+
4. **プリセット機能と外部ラベルの同期**`prepareContestProviderPresets().dps()` が返すグループ名・ボタンラベル・aria-label が既に FPS24 を含むよう更新されていたため、テストの期待値調整が必須。実装時はプリセット関数の実装と共にテストも確認すること

src/lib/utils/contest_table_provider.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,39 @@ export class TDPCProvider extends ContestTableProviderBase {
363363
}
364364
}
365365

366+
export class FPS24Provider extends ContestTableProviderBase {
367+
protected setFilterCondition(): (taskResult: TaskResult) => boolean {
368+
return (taskResult: TaskResult) => {
369+
if (classifyContest(taskResult.contest_id) !== this.contestType) {
370+
return false;
371+
}
372+
373+
return taskResult.contest_id === 'fps-24';
374+
};
375+
}
376+
377+
getMetadata(): ContestTableMetaData {
378+
return {
379+
title: 'FPS 24 題',
380+
abbreviationName: 'fps-24',
381+
};
382+
}
383+
384+
getDisplayConfig(): ContestTableDisplayConfig {
385+
return {
386+
isShownHeader: false,
387+
isShownRoundLabel: false,
388+
roundLabelWidth: '', // No specific width for task index in FPS 24
389+
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',
390+
isShownTaskIndex: true,
391+
};
392+
}
393+
394+
getContestRoundLabel(contestId: string): string {
395+
return '';
396+
}
397+
}
398+
366399
const regexForJoiFirstQualRound = /^(joi)(\d{4})(yo1)(a|b|c)$/i;
367400

368401
export class JOIFirstQualRoundProvider extends ContestTableProviderBase {
@@ -567,12 +600,13 @@ export const prepareContestProviderPresets = () => {
567600
* DP group (EDPC and TDPC)
568601
*/
569602
dps: () =>
570-
new ContestTableProviderGroup(`EDPC・TDPC`, {
571-
buttonLabel: 'EDPC・TDPC',
572-
ariaLabel: 'EDPC and TDPC contests',
603+
new ContestTableProviderGroup(`EDPC・TDPC・FPS 24`, {
604+
buttonLabel: 'EDPC・TDPC・FPS 24',
605+
ariaLabel: 'EDPC and TDPC and FPS 24 contests',
573606
}).addProviders(
574607
{ contestType: ContestType.EDPC, provider: new EDPCProvider(ContestType.EDPC) },
575608
{ contestType: ContestType.TDPC, provider: new TDPCProvider(ContestType.TDPC) },
609+
{ contestType: ContestType.FPS_24, provider: new FPS24Provider(ContestType.FPS_24) },
576610
),
577611

578612
JOIFirstQualRound: () =>

0 commit comments

Comments
 (0)