Skip to content

Commit cb793eb

Browse files
KATO-Hiroclaude
andcommitted
fix(votes): sort tasks before filtering to preserve stable order
Previously sort() was applied after filter(), which mutated a derived array on every keystroke. Pre-sort via sortedTasks derived state ensures stable order regardless of search input. Also strengthen test: use typical90 fixture to isolate contest name label matching, and add order-preservation test. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 109782c commit cb793eb

2 files changed

Lines changed: 16 additions & 7 deletions

File tree

src/routes/votes/+page.svelte

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,8 @@
2929
3030
let search = $state('');
3131
32-
const filteredTasks = $derived(
33-
filterTasksBySearch(data.tasks, search, MAX_SEARCH_RESULTS).sort(compareByContestIdAndTaskId),
34-
);
32+
const sortedTasks = $derived([...data.tasks].sort(compareByContestIdAndTaskId));
33+
const filteredTasks = $derived(filterTasksBySearch(sortedTasks, search, MAX_SEARCH_RESULTS));
3534
</script>
3635

3736
<div class="container mx-auto w-5/6">

src/test/lib/utils/task_filter.test.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ const tasks = [
77
{ title: 'AGC 060 C - No Majority', task_id: 'agc060_c', contest_id: 'agc060' },
88
{ title: 'ABC 301 A - Overall Winner', task_id: 'abc301_a', contest_id: 'abc301' },
99
{ title: 'ABC 302 A - Attack', task_id: 'abc302_a', contest_id: 'abc302' },
10+
// title intentionally omits '典型' so only getContestNameLabel matches '競プロ典型 90 問'
11+
{ title: 'Shortest Path', task_id: 'typical90_a', contest_id: 'typical90' },
1012
];
1113

1214
describe('filterTasksBySearch', () => {
@@ -32,11 +34,11 @@ describe('filterTasksBySearch', () => {
3234
expect(result[0].task_id).toBe('agc060_c');
3335
});
3436

35-
test('matches by contest name label (e.g. "ABC 300")', () => {
36-
// getContestNameLabel('abc300') => 'ABC 300'
37-
const result = filterTasksBySearch(tasks, 'ABC 300', 20);
37+
test('matches by contest name label when title does not contain the label', () => {
38+
// getContestNameLabel('typical90') => '競プロ典型 90 問'; title is 'Shortest Path' — no overlap
39+
const result = filterTasksBySearch(tasks, '競プロ典型 90 問', 20);
3840
expect(result).toHaveLength(1);
39-
expect(result[0].task_id).toBe('abc300_a');
41+
expect(result[0].task_id).toBe('typical90_a');
4042
});
4143

4244
test('returns empty array when no tasks match', () => {
@@ -49,6 +51,14 @@ describe('filterTasksBySearch', () => {
4951
expect(result).toHaveLength(2);
5052
});
5153

54+
test('preserves input order — sorting is the caller responsibility', () => {
55+
// tasks are declared as: abc300, arc150, agc060, abc301, abc302
56+
// abc30x matches: abc300, abc301, abc302 (in that input order)
57+
// With limit=3, the first 2 from input order should be returned when limit=2
58+
const result = filterTasksBySearch(tasks, 'abc30', 3);
59+
expect(result.map((t) => t.task_id)).toEqual(['abc300_a', 'abc301_a', 'abc302_a']);
60+
});
61+
5262
test('returns all matched results when count is less than limit', () => {
5363
const result = filterTasksBySearch(tasks, 'arc', 20);
5464
expect(result).toHaveLength(1);

0 commit comments

Comments
 (0)