Three rules cleaned in parallel passes — 187 fewer warnings, 0 TS
errors, 0 behaviour change beyond one incidental auth bugfix
flagged below.
storybook/no-redundant-story-name (23 → 0) — 14 stories files
Storybook v7+ infers the story name from the variable name, so
`name: 'Default'` next to `export const Default: Story = …` is
pure noise. Removed only when the name was redundant ;
preserved when the label was a French translation
('Par défaut', 'Chargement', 'Avec erreur', etc.) since those
are intentional.
react-refresh/only-export-components (25 → 0) — 21 files
Each warning marks a file that exports a React component AND a
hook / context / constant / barrel re-export. Suppressed
per-line with the suppression-with-justification pattern :
// eslint-disable-next-line react-refresh/only-export-components -- <kind>; refactor would split a tightly-coupled API
The justification matters — every comment names the specific
thing being co-located (hook / context / CVA constant / lazy
registry / route config / test util / backward-compat barrel).
Splitting these would create 21 new files for a HMR-only DX
win that's already a non-issue in practice.
@typescript-eslint/no-non-null-assertion (139 → 0) — 43 files
Distribution of fixes :
~85 cases : refactored to explicit guard
`if (!x) throw new Error('invariant: …')`
or hoisted into local with narrowing.
~36 cases : helper extraction (one tooltip test had 16
`wrapper!` patterns reduced to a single
`getWrapper()` helper).
~18 cases : suppressed with specific reason :
static literal arrays where index is provably
in bounds, mock fixtures with structural
guarantees, filter-then-map patterns where the
filter excludes the null branch.
One incidental find : services/api/auth.ts threw on missing
tokens but didn't guard `user` ; added the missing check while
refactoring the `user!` to a guard.
baseline post-commit : 921 warnings, 0 errors, 0 TS errors.
The remaining buckets are no-restricted-syntax (757, design-system
guardrail), no-explicit-any (115), exhaustive-deps (49).
CI --max-warnings will be lowered to 921 in the follow-up commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
762 lines
20 KiB
TypeScript
762 lines
20 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { renderHook, waitFor, act } from '@testing-library/react';
|
|
import { useTrackList } from './useTrackList';
|
|
import { getTracks } from '../services/trackListService';
|
|
import type { Track } from '../../player/types';
|
|
|
|
// Mock getTracks service
|
|
vi.mock('../services/trackListService', () => ({
|
|
getTracks: vi.fn(),
|
|
}));
|
|
|
|
// Mock useSearchParams from react-router-dom
|
|
const mockSetSearchParams = vi.fn();
|
|
const mockSearchParams = new URLSearchParams();
|
|
|
|
vi.mock('react-router-dom', async () => {
|
|
const actual = await vi.importActual('react-router-dom');
|
|
return {
|
|
...actual,
|
|
useSearchParams: () => [mockSearchParams, mockSetSearchParams],
|
|
};
|
|
});
|
|
|
|
const mockTracks: Track[] = [
|
|
{
|
|
id: 1,
|
|
title: 'Track A',
|
|
artist: 'Artist 1',
|
|
album: 'Album 1',
|
|
duration: 180,
|
|
url: 'https://example.com/track1.mp3',
|
|
genre: 'Rock',
|
|
},
|
|
{
|
|
id: 2,
|
|
title: 'Track B',
|
|
artist: 'Artist 2',
|
|
album: 'Album 2',
|
|
duration: 240,
|
|
url: 'https://example.com/track2.mp3',
|
|
genre: 'Pop',
|
|
},
|
|
{
|
|
id: 3,
|
|
title: 'Track C',
|
|
artist: 'Artist 1',
|
|
duration: 120,
|
|
url: 'https://example.com/track3.mp3',
|
|
genre: 'Rock',
|
|
},
|
|
];
|
|
|
|
describe('useTrackList', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
localStorage.clear();
|
|
mockSearchParams.forEach((_, key) => {
|
|
mockSearchParams.delete(key);
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
localStorage.clear();
|
|
});
|
|
|
|
describe('Client-side mode (useService = false)', () => {
|
|
it('should initialize with default values', () => {
|
|
const { result } = renderHook(() =>
|
|
useTrackList({ useService: false, autoLoad: false }),
|
|
);
|
|
|
|
expect(result.current.tracks).toEqual([]);
|
|
expect(result.current.displayMode).toBe('list');
|
|
expect(result.current.sortOptions.field).toBe('title');
|
|
expect(result.current.sortOptions.order).toBe('asc');
|
|
expect(result.current.filterOptions).toEqual({});
|
|
expect(result.current.isLoading).toBe(false);
|
|
expect(result.current.error).toBeNull();
|
|
expect(result.current.pagination).toEqual({ page: 1, limit: 20 });
|
|
});
|
|
|
|
it('should initialize with provided values', () => {
|
|
const { result } = renderHook(() =>
|
|
useTrackList({
|
|
useService: false,
|
|
autoLoad: false,
|
|
initialTracks: mockTracks,
|
|
initialDisplayMode: 'grid',
|
|
initialSortOptions: { field: 'duration', order: 'desc' },
|
|
initialFilterOptions: { genre: 'Rock' },
|
|
initialPagination: { page: 1, limit: 10 },
|
|
}),
|
|
);
|
|
|
|
expect(result.current.tracks.length).toBeGreaterThan(0);
|
|
expect(result.current.displayMode).toBe('grid');
|
|
expect(result.current.sortOptions.field).toBe('duration');
|
|
expect(result.current.sortOptions.order).toBe('desc');
|
|
expect(result.current.filterOptions.genre).toBe('Rock');
|
|
});
|
|
|
|
it('should filter tracks by genre', () => {
|
|
const { result } = renderHook(() =>
|
|
useTrackList({
|
|
useService: false,
|
|
autoLoad: false,
|
|
initialTracks: mockTracks,
|
|
}),
|
|
);
|
|
|
|
act(() => {
|
|
result.current.setFilterOptions({ genre: 'Rock' });
|
|
});
|
|
|
|
expect(result.current.filteredTracks).toHaveLength(2);
|
|
expect(
|
|
result.current.filteredTracks.every((t) => t.genre === 'Rock'),
|
|
).toBe(true);
|
|
});
|
|
|
|
it('should sort tracks by title', () => {
|
|
const { result } = renderHook(() =>
|
|
useTrackList({
|
|
useService: false,
|
|
autoLoad: false,
|
|
initialTracks: mockTracks,
|
|
}),
|
|
);
|
|
|
|
act(() => {
|
|
result.current.setSortField('title');
|
|
result.current.setSortOrder('asc');
|
|
});
|
|
|
|
expect(result.current.filteredTracks[0].title).toBe('Track A');
|
|
});
|
|
|
|
it('should paginate tracks', () => {
|
|
const { result } = renderHook(() =>
|
|
useTrackList({
|
|
useService: false,
|
|
autoLoad: false,
|
|
initialTracks: mockTracks,
|
|
initialPagination: { page: 1, limit: 2 },
|
|
}),
|
|
);
|
|
|
|
expect(result.current.filteredTracks).toHaveLength(2);
|
|
expect(result.current.total).toBe(3);
|
|
expect(result.current.totalPages).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe('Service mode (useService = true)', () => {
|
|
it('should load tracks from service on mount', async () => {
|
|
const mockResponse = {
|
|
data: mockTracks,
|
|
total: mockTracks.length,
|
|
page: 1,
|
|
limit: 20,
|
|
totalPages: 1,
|
|
};
|
|
|
|
vi.mocked(getTracks).mockResolvedValue(mockResponse);
|
|
|
|
const { result } = renderHook(() =>
|
|
useTrackList({ useService: true, autoLoad: true }),
|
|
);
|
|
|
|
expect(result.current.isLoading).toBe(true);
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isLoading).toBe(false);
|
|
});
|
|
|
|
expect(getTracks).toHaveBeenCalled();
|
|
expect(result.current.tracks).toEqual(mockTracks);
|
|
expect(result.current.total).toBe(mockTracks.length);
|
|
});
|
|
|
|
it('should not load tracks if autoLoad is false', () => {
|
|
renderHook(() => useTrackList({ useService: true, autoLoad: false }));
|
|
|
|
expect(getTracks).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should reload tracks when pagination changes', async () => {
|
|
const mockResponse1 = {
|
|
data: [mockTracks[0]],
|
|
total: mockTracks.length,
|
|
page: 1,
|
|
limit: 1,
|
|
totalPages: 3,
|
|
};
|
|
|
|
const mockResponse2 = {
|
|
data: [mockTracks[1]],
|
|
total: mockTracks.length,
|
|
page: 2,
|
|
limit: 1,
|
|
totalPages: 3,
|
|
};
|
|
|
|
vi.mocked(getTracks)
|
|
.mockResolvedValueOnce(mockResponse1)
|
|
.mockResolvedValueOnce(mockResponse2);
|
|
|
|
const { result } = renderHook(() =>
|
|
useTrackList({
|
|
useService: true,
|
|
autoLoad: true,
|
|
initialPagination: { page: 1, limit: 1 },
|
|
}),
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isLoading).toBe(false);
|
|
});
|
|
|
|
act(() => {
|
|
result.current.setPage(2);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(getTracks).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|
|
|
|
it('should reload tracks when filters change', async () => {
|
|
const mockResponse = {
|
|
data: [mockTracks[0], mockTracks[2]],
|
|
total: 2,
|
|
page: 1,
|
|
limit: 20,
|
|
totalPages: 1,
|
|
};
|
|
|
|
vi.mocked(getTracks).mockResolvedValue(mockResponse);
|
|
|
|
const { result } = renderHook(() =>
|
|
useTrackList({ useService: true, autoLoad: true }),
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isLoading).toBe(false);
|
|
});
|
|
|
|
act(() => {
|
|
result.current.setFilterOptions({ genre: 'Rock' });
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(getTracks).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
filters: { genre: 'Rock' },
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should reload tracks when sort changes', async () => {
|
|
const mockResponse = {
|
|
data: mockTracks,
|
|
total: mockTracks.length,
|
|
page: 1,
|
|
limit: 20,
|
|
totalPages: 1,
|
|
};
|
|
|
|
vi.mocked(getTracks).mockResolvedValue(mockResponse);
|
|
|
|
const { result } = renderHook(() =>
|
|
useTrackList({ useService: true, autoLoad: true }),
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isLoading).toBe(false);
|
|
});
|
|
|
|
act(() => {
|
|
result.current.setSortField('duration');
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(getTracks).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
sort: { field: 'duration', order: 'asc' },
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should handle loading state', async () => {
|
|
let resolvePromise: (value: any) => void;
|
|
const promise = new Promise((resolve) => {
|
|
resolvePromise = resolve;
|
|
});
|
|
|
|
vi.mocked(getTracks).mockReturnValue(promise as any);
|
|
|
|
const { result } = renderHook(() =>
|
|
useTrackList({ useService: true, autoLoad: true }),
|
|
);
|
|
|
|
expect(result.current.isLoading).toBe(true);
|
|
|
|
act(() => {
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- assigned synchronously in Promise executor above
|
|
resolvePromise!({
|
|
data: mockTracks,
|
|
total: mockTracks.length,
|
|
page: 1,
|
|
limit: 20,
|
|
totalPages: 1,
|
|
});
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isLoading).toBe(false);
|
|
});
|
|
});
|
|
|
|
it('should handle errors', async () => {
|
|
const error = new Error('Failed to load tracks');
|
|
vi.mocked(getTracks).mockRejectedValue(error);
|
|
|
|
const { result } = renderHook(() =>
|
|
useTrackList({ useService: true, autoLoad: true }),
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isLoading).toBe(false);
|
|
});
|
|
|
|
expect(result.current.error).toEqual(error);
|
|
expect(result.current.tracks).toEqual([]);
|
|
});
|
|
|
|
it('should manually load tracks', async () => {
|
|
const mockResponse = {
|
|
data: mockTracks,
|
|
total: mockTracks.length,
|
|
page: 1,
|
|
limit: 20,
|
|
totalPages: 1,
|
|
};
|
|
|
|
vi.mocked(getTracks).mockResolvedValue(mockResponse);
|
|
|
|
const { result } = renderHook(() =>
|
|
useTrackList({ useService: true, autoLoad: false }),
|
|
);
|
|
|
|
await act(async () => {
|
|
await result.current.loadTracks();
|
|
});
|
|
|
|
expect(getTracks).toHaveBeenCalled();
|
|
expect(result.current.tracks).toEqual(mockTracks);
|
|
});
|
|
|
|
it('should refresh tracks', async () => {
|
|
const mockResponse = {
|
|
data: mockTracks,
|
|
total: mockTracks.length,
|
|
page: 1,
|
|
limit: 20,
|
|
totalPages: 1,
|
|
};
|
|
|
|
vi.mocked(getTracks).mockResolvedValue(mockResponse);
|
|
|
|
const { result } = renderHook(() =>
|
|
useTrackList({ useService: true, autoLoad: true }),
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isLoading).toBe(false);
|
|
});
|
|
|
|
const callCount = vi.mocked(getTracks).mock.calls.length;
|
|
|
|
await act(async () => {
|
|
await result.current.refreshTracks();
|
|
});
|
|
|
|
expect(getTracks).toHaveBeenCalledTimes(callCount + 1);
|
|
});
|
|
|
|
it('should reset to page 1 when filters change', async () => {
|
|
const mockResponse = {
|
|
data: mockTracks,
|
|
total: mockTracks.length,
|
|
page: 1,
|
|
limit: 20,
|
|
totalPages: 1,
|
|
};
|
|
|
|
vi.mocked(getTracks).mockResolvedValue(mockResponse);
|
|
|
|
const { result } = renderHook(() =>
|
|
useTrackList({
|
|
useService: true,
|
|
autoLoad: true,
|
|
initialPagination: { page: 2, limit: 20 },
|
|
}),
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isLoading).toBe(false);
|
|
});
|
|
|
|
act(() => {
|
|
result.current.setFilterOptions({ genre: 'Rock' });
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.pagination.page).toBe(1);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('CRUD operations', () => {
|
|
it('should add track', () => {
|
|
const { result } = renderHook(() =>
|
|
useTrackList({ useService: false, autoLoad: false }),
|
|
);
|
|
|
|
act(() => {
|
|
result.current.addTrack(mockTracks[0]);
|
|
});
|
|
|
|
expect(result.current.tracks).toContain(mockTracks[0]);
|
|
});
|
|
|
|
it('should remove track', () => {
|
|
const { result } = renderHook(() =>
|
|
useTrackList({
|
|
useService: false,
|
|
autoLoad: false,
|
|
initialTracks: mockTracks,
|
|
}),
|
|
);
|
|
|
|
act(() => {
|
|
result.current.removeTrack(2);
|
|
});
|
|
|
|
expect(result.current.tracks).toHaveLength(2);
|
|
expect(result.current.tracks.find((t) => t.id === 2)).toBeUndefined();
|
|
});
|
|
|
|
it('should update track', () => {
|
|
const { result } = renderHook(() =>
|
|
useTrackList({
|
|
useService: false,
|
|
autoLoad: false,
|
|
initialTracks: mockTracks,
|
|
}),
|
|
);
|
|
|
|
const updatedTrack = { ...mockTracks[0], title: 'Updated Title' };
|
|
|
|
act(() => {
|
|
result.current.updateTrack(updatedTrack);
|
|
});
|
|
|
|
const updated = result.current.tracks.find(
|
|
(t) => t.id === updatedTrack.id,
|
|
);
|
|
expect(updated?.title).toBe('Updated Title');
|
|
});
|
|
});
|
|
|
|
describe('Pagination controls', () => {
|
|
it('should set page', () => {
|
|
const { result } = renderHook(() =>
|
|
useTrackList({ useService: false, autoLoad: false }),
|
|
);
|
|
|
|
act(() => {
|
|
result.current.setPage(3);
|
|
});
|
|
|
|
expect(result.current.pagination.page).toBe(3);
|
|
});
|
|
|
|
it('should set limit', () => {
|
|
const { result } = renderHook(() =>
|
|
useTrackList({ useService: false, autoLoad: false }),
|
|
);
|
|
|
|
act(() => {
|
|
result.current.setLimit(50);
|
|
});
|
|
|
|
expect(result.current.pagination.limit).toBe(50);
|
|
expect(result.current.pagination.page).toBe(1); // Should reset to page 1
|
|
});
|
|
|
|
it('should set pagination', () => {
|
|
const { result } = renderHook(() =>
|
|
useTrackList({ useService: false, autoLoad: false }),
|
|
);
|
|
|
|
act(() => {
|
|
result.current.setPagination({ page: 2, limit: 10 });
|
|
});
|
|
|
|
expect(result.current.pagination).toEqual({ page: 2, limit: 10 });
|
|
});
|
|
});
|
|
|
|
describe('Search', () => {
|
|
it('should set search query', () => {
|
|
const { result } = renderHook(() =>
|
|
useTrackList({ useService: false, autoLoad: false }),
|
|
);
|
|
|
|
act(() => {
|
|
result.current.setSearchQuery('test query');
|
|
});
|
|
|
|
// In client mode, search is handled in filteredTracks
|
|
expect(result.current.filteredTracks).toBeDefined();
|
|
});
|
|
|
|
it('should filter tracks by search query in client mode', () => {
|
|
const { result } = renderHook(() =>
|
|
useTrackList({
|
|
useService: false,
|
|
autoLoad: false,
|
|
initialTracks: mockTracks,
|
|
}),
|
|
);
|
|
|
|
act(() => {
|
|
result.current.setSearchQuery('Track A');
|
|
});
|
|
|
|
expect(result.current.filteredTracks).toHaveLength(1);
|
|
expect(result.current.filteredTracks[0].title).toBe('Track A');
|
|
});
|
|
});
|
|
|
|
describe('Persistence', () => {
|
|
it('should persist filters to localStorage when persistFilters is enabled', async () => {
|
|
const { result } = renderHook(() =>
|
|
useTrackList({
|
|
useService: false,
|
|
autoLoad: false,
|
|
persistFilters: true,
|
|
}),
|
|
);
|
|
|
|
act(() => {
|
|
result.current.setFilterOptions({ genre: 'Rock', artist: 'Artist 1' });
|
|
});
|
|
|
|
await waitFor(
|
|
() => {
|
|
const stored = localStorage.getItem('trackList_filters');
|
|
expect(stored).toBeTruthy();
|
|
if (stored) {
|
|
const parsed = JSON.parse(stored);
|
|
expect(parsed.genre).toBe('Rock');
|
|
expect(parsed.artist).toBe('Artist 1');
|
|
}
|
|
},
|
|
{ timeout: 2000 },
|
|
);
|
|
});
|
|
|
|
it('should persist sort options to localStorage when persistSort is enabled', async () => {
|
|
const { result } = renderHook(() =>
|
|
useTrackList({
|
|
useService: false,
|
|
autoLoad: false,
|
|
persistSort: true,
|
|
}),
|
|
);
|
|
|
|
act(() => {
|
|
result.current.setSortField('artist');
|
|
result.current.setSortOrder('desc');
|
|
});
|
|
|
|
await waitFor(
|
|
() => {
|
|
const stored = localStorage.getItem('trackList_sort');
|
|
expect(stored).toBeTruthy();
|
|
if (stored) {
|
|
const parsed = JSON.parse(stored);
|
|
expect(parsed.field).toBe('artist');
|
|
expect(parsed.order).toBe('desc');
|
|
}
|
|
},
|
|
{ timeout: 2000 },
|
|
);
|
|
});
|
|
|
|
it('should restore filters from localStorage on mount when persistFilters is enabled', () => {
|
|
localStorage.setItem(
|
|
'trackList_filters',
|
|
JSON.stringify({ genre: 'Pop', artist: 'Artist 2' }),
|
|
);
|
|
|
|
const { result } = renderHook(() =>
|
|
useTrackList({
|
|
useService: false,
|
|
autoLoad: false,
|
|
persistFilters: true,
|
|
}),
|
|
);
|
|
|
|
expect(result.current.filterOptions.genre).toBe('Pop');
|
|
expect(result.current.filterOptions.artist).toBe('Artist 2');
|
|
});
|
|
|
|
it('should restore sort options from localStorage on mount when persistSort is enabled', () => {
|
|
localStorage.setItem(
|
|
'trackList_sort',
|
|
JSON.stringify({ field: 'duration', order: 'desc' }),
|
|
);
|
|
|
|
const { result } = renderHook(() =>
|
|
useTrackList({
|
|
useService: false,
|
|
autoLoad: false,
|
|
persistSort: true,
|
|
}),
|
|
);
|
|
|
|
expect(result.current.sortOptions.field).toBe('duration');
|
|
expect(result.current.sortOptions.order).toBe('desc');
|
|
});
|
|
|
|
it('should use custom storage key prefix', async () => {
|
|
const { result } = renderHook(() =>
|
|
useTrackList({
|
|
useService: false,
|
|
autoLoad: false,
|
|
persistFilters: true,
|
|
storageKeyPrefix: 'customPrefix',
|
|
}),
|
|
);
|
|
|
|
act(() => {
|
|
result.current.setFilterOptions({ genre: 'Jazz' });
|
|
});
|
|
|
|
await waitFor(
|
|
() => {
|
|
const stored = localStorage.getItem('customPrefix_filters');
|
|
expect(stored).toBeTruthy();
|
|
if (stored) {
|
|
const parsed = JSON.parse(stored);
|
|
expect(parsed.genre).toBe('Jazz');
|
|
}
|
|
},
|
|
{ timeout: 2000 },
|
|
);
|
|
});
|
|
|
|
it('should sync filters with URL params when syncUrlParams is enabled', async () => {
|
|
const { result } = renderHook(() =>
|
|
useTrackList({
|
|
useService: false,
|
|
autoLoad: false,
|
|
syncUrlParams: true,
|
|
}),
|
|
);
|
|
|
|
act(() => {
|
|
result.current.setFilterOptions({ genre: 'Rock', artist: 'Artist 1' });
|
|
});
|
|
|
|
await waitFor(
|
|
() => {
|
|
expect(mockSetSearchParams).toHaveBeenCalled();
|
|
},
|
|
{ timeout: 2000 },
|
|
);
|
|
});
|
|
|
|
it('should sync sort options with URL params when syncUrlParams is enabled', async () => {
|
|
const { result } = renderHook(() =>
|
|
useTrackList({
|
|
useService: false,
|
|
autoLoad: false,
|
|
syncUrlParams: true,
|
|
}),
|
|
);
|
|
|
|
act(() => {
|
|
result.current.setSortField('artist');
|
|
result.current.setSortOrder('desc');
|
|
});
|
|
|
|
await waitFor(
|
|
() => {
|
|
expect(mockSetSearchParams).toHaveBeenCalled();
|
|
},
|
|
{ timeout: 2000 },
|
|
);
|
|
});
|
|
|
|
it('should load filters from URL params when syncUrlParams is enabled', () => {
|
|
mockSearchParams.set('genre', 'Pop');
|
|
mockSearchParams.set('artist', 'Artist 2');
|
|
mockSearchParams.set('year', '2020');
|
|
|
|
const { result } = renderHook(() =>
|
|
useTrackList({
|
|
useService: false,
|
|
autoLoad: false,
|
|
syncUrlParams: true,
|
|
}),
|
|
);
|
|
|
|
expect(result.current.filterOptions.genre).toBe('Pop');
|
|
expect(result.current.filterOptions.artist).toBe('Artist 2');
|
|
expect(result.current.filterOptions.year).toBe(2020);
|
|
});
|
|
|
|
it('should load sort options from URL params when syncUrlParams is enabled', () => {
|
|
mockSearchParams.set('sortField', 'duration');
|
|
mockSearchParams.set('sortOrder', 'desc');
|
|
|
|
const { result } = renderHook(() =>
|
|
useTrackList({
|
|
useService: false,
|
|
autoLoad: false,
|
|
syncUrlParams: true,
|
|
}),
|
|
);
|
|
|
|
expect(result.current.sortOptions.field).toBe('duration');
|
|
expect(result.current.sortOptions.order).toBe('desc');
|
|
});
|
|
|
|
it('should prioritize URL params over localStorage when both are enabled', () => {
|
|
localStorage.setItem(
|
|
'trackList_filters',
|
|
JSON.stringify({ genre: 'Rock' }),
|
|
);
|
|
mockSearchParams.set('genre', 'Pop');
|
|
|
|
const { result } = renderHook(() =>
|
|
useTrackList({
|
|
useService: false,
|
|
autoLoad: false,
|
|
persistFilters: true,
|
|
syncUrlParams: true,
|
|
}),
|
|
);
|
|
|
|
// URL params should take priority
|
|
expect(result.current.filterOptions.genre).toBe('Pop');
|
|
});
|
|
});
|
|
});
|