veza/apps/web/src/features/tracks/components/TrackGrid.test.tsx
senke 95e31646cb feat(ui): Sidebar refactor, premium skeletons, ContentFadeIn transitions
- Sidebar: useSidebarNavigation hook, ARIA, token-based layout
- Layout: lg:ml-main-expanded/collapsed (replace arbitrary ml-64)
- TrackCardSkeleton + PlaylistCardSkeleton: KŌDŌ tokens, min-heights for CLS
- ContentFadeIn: 200ms fade-in with --ease-out
- TrackGrid, PlaylistList, LibraryPage: integrate skeletons + fade-in
- Player: player-bar subcomponents, useAudioAnalyser
- Tests: TrackGrid wrapper (QueryClient, ToastProvider)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 22:51:51 +01:00

463 lines
15 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ToastProvider } from '@/components/feedback/ToastProvider';
import { TrackGrid } from './TrackGrid';
import type { Track } from '../../player/types';
function createWrapper() {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>
<ToastProvider>{children}</ToastProvider>
</QueryClientProvider>
);
}
const mockTracks: Track[] = [
{
id: 1,
title: 'Track 1',
artist: 'Artist 1',
album: 'Album 1',
duration: 180,
url: 'https://example.com/track1.mp3',
cover: 'https://example.com/cover1.jpg',
genre: 'Rock',
},
{
id: 2,
title: 'Track 2',
artist: 'Artist 2',
duration: 240,
url: 'https://example.com/track2.mp3',
},
];
describe('TrackGrid', () => {
it('should render track grid', () => {
render(<TrackGrid tracks={mockTracks} />, { wrapper: createWrapper() });
expect(
screen.getByRole('grid', { name: 'Grille de pistes' }),
).toBeInTheDocument();
});
it('should display all tracks', () => {
render(<TrackGrid tracks={mockTracks} />, { wrapper: createWrapper() });
expect(screen.getByText('Track 1')).toBeInTheDocument();
expect(screen.getByText('Track 2')).toBeInTheDocument();
});
it('should display empty state when no tracks', () => {
render(<TrackGrid tracks={[]} />);
expect(screen.getByText('Aucune piste disponible')).toBeInTheDocument();
});
it('should call onTrackClick when track is clicked', async () => {
const user = userEvent.setup();
const mockOnTrackClick = vi.fn();
render(<TrackGrid tracks={mockTracks} onTrackClick={mockOnTrackClick} />, {
wrapper: createWrapper(),
});
// TrackCard utilise un role="button" pour la carte
const track1 = screen.getByText('Track 1').closest('[role="button"]');
if (track1) {
await user.click(track1);
expect(mockOnTrackClick).toHaveBeenCalledWith(mockTracks[0]);
} else {
// Si pas de role="button", chercher dans le conteneur parent
const track1Text = screen.getByText('Track 1');
await user.click(track1Text.closest('div')!);
expect(mockOnTrackClick).toHaveBeenCalledWith(mockTracks[0]);
}
});
it('should use correct number of columns', () => {
const { container } = render(<TrackGrid tracks={mockTracks} columns={3} />, {
wrapper: createWrapper(),
});
const grid = container.querySelector('[role="grid"]');
expect(grid).toHaveClass('grid-cols-2', 'sm:grid-cols-2', 'md:grid-cols-3');
});
it('should display cover images when showCover is true', () => {
render(<TrackGrid tracks={mockTracks} showCover={true} />, {
wrapper: createWrapper(),
});
const images = screen.getAllByAltText(/Cover de/);
expect(images.length).toBeGreaterThan(0);
});
it('should apply custom className', () => {
const { container } = render(
<TrackGrid tracks={mockTracks} className="custom-class" />,
{ wrapper: createWrapper() },
);
expect(container.firstChild).toHaveClass('custom-class');
});
it('should display skeleton when isLoading is true', () => {
render(<TrackGrid tracks={[]} isLoading={true} />);
expect(
screen.getByRole('status', { name: /Chargement des pistes/i }),
).toBeInTheDocument();
});
it('should display empty state when no tracks', () => {
render(<TrackGrid tracks={[]} />);
expect(screen.getByText('Aucune piste disponible')).toBeInTheDocument();
});
it('should display custom empty message', () => {
render(<TrackGrid tracks={[]} emptyMessage="Pas de résultats" />);
expect(screen.getByText('Pas de résultats')).toBeInTheDocument();
});
it('should use TrackCard component for each track', () => {
render(<TrackGrid tracks={mockTracks} />, { wrapper: createWrapper() });
expect(screen.getAllByText('Track 1').length).toBeGreaterThan(0);
expect(screen.getAllByText('Track 2').length).toBeGreaterThan(0);
});
it('should support different column counts', () => {
const { container: container2 } = render(
<TrackGrid tracks={mockTracks} columns={2} />,
{ wrapper: createWrapper() },
);
const grid2 = container2.querySelector('[role="grid"]');
expect(grid2).toHaveClass('grid-cols-2', 'sm:grid-cols-2');
const { container: container4 } = render(
<TrackGrid tracks={mockTracks} columns={4} />,
);
const grid4 = container4.querySelector('[role="grid"]');
expect(grid4).toHaveClass(
'grid-cols-2',
'sm:grid-cols-2',
'md:grid-cols-3',
'lg:grid-cols-4',
);
const { container: container6 } = render(
<TrackGrid tracks={mockTracks} columns={6} />,
{ wrapper: createWrapper() },
);
const grid6 = container6.querySelector('[role="grid"]');
expect(grid6).toHaveClass(
'grid-cols-2',
'sm:grid-cols-2',
'md:grid-cols-3',
'lg:grid-cols-4',
'xl:grid-cols-5',
'2xl:grid-cols-6',
);
});
it('should support different card sizes', () => {
const { rerender } = render(
<TrackGrid tracks={mockTracks} cardSize="sm" />,
{ wrapper: createWrapper() },
);
expect(screen.getByText('Track 1')).toBeInTheDocument();
rerender(<TrackGrid tracks={mockTracks} cardSize="lg" />);
expect(screen.getByText('Track 1')).toBeInTheDocument();
});
it('should pass isLiked and isPlaying to TrackCard', () => {
render(
<TrackGrid
tracks={mockTracks}
isLiked={(id) => id === 1}
isPlaying={(id) => id === 2}
onTrackLike={vi.fn()}
/>,
{ wrapper: createWrapper() },
);
// Les cartes devraient recevoir les props correctes
expect(screen.getByText('Track 1')).toBeInTheDocument();
expect(screen.getByText('Track 2')).toBeInTheDocument();
});
it('should support auto columns mode', () => {
const { container } = render(
<TrackGrid tracks={mockTracks} autoColumns={true} />,
{ wrapper: createWrapper() },
);
const grid = container.querySelector('[role="grid"]');
expect(grid).toHaveClass(
'grid-cols-2',
'sm:grid-cols-2',
'md:grid-cols-3',
'lg:grid-cols-4',
'xl:grid-cols-5',
'2xl:grid-cols-6',
);
});
it('should support single column on mobile', () => {
const { container } = render(
<TrackGrid tracks={mockTracks} columns={4} mobileColumns={1} />,
{ wrapper: createWrapper() },
);
const grid = container.querySelector('[role="grid"]');
expect(grid).toHaveClass('grid-cols-1', 'sm:grid-cols-2');
});
it('should support different gap sizes', () => {
const { container: containerSm } = render(
<TrackGrid tracks={mockTracks} gap="sm" />,
{ wrapper: createWrapper() },
);
const gridSm = containerSm.querySelector('[role="grid"]');
expect(gridSm).toHaveClass('gap-2', 'sm:gap-3');
const { container: containerMd } = render(
<TrackGrid tracks={mockTracks} gap="md" />,
{ wrapper: createWrapper() },
);
const gridMd = containerMd.querySelector('[role="grid"]');
expect(gridMd).toHaveClass('gap-4', 'sm:gap-4');
const { container: containerLg } = render(
<TrackGrid tracks={mockTracks} gap="lg" />,
{ wrapper: createWrapper() },
);
const gridLg = containerLg.querySelector('[role="grid"]');
expect(gridLg).toHaveClass('gap-4', 'sm:gap-6');
});
it('should use responsive breakpoints for different column counts', () => {
// Test 2 colonnes
const { container: container2 } = render(
<TrackGrid tracks={mockTracks} columns={2} />,
{ wrapper: createWrapper() },
);
const grid2 = container2.querySelector('[role="grid"]');
expect(grid2).toHaveClass('grid-cols-2', 'sm:grid-cols-2');
// Test 3 colonnes
const { container: container3 } = render(
<TrackGrid tracks={mockTracks} columns={3} />,
{ wrapper: createWrapper() },
);
const grid3 = container3.querySelector('[role="grid"]');
expect(grid3).toHaveClass(
'grid-cols-2',
'sm:grid-cols-2',
'md:grid-cols-3',
);
// Test 4 colonnes
const { container: container4 } = render(
<TrackGrid tracks={mockTracks} columns={4} />,
{ wrapper: createWrapper() },
);
const grid4 = container4.querySelector('[role="grid"]');
expect(grid4).toHaveClass(
'grid-cols-2',
'sm:grid-cols-2',
'md:grid-cols-3',
'lg:grid-cols-4',
);
// Test 5 colonnes
const { container: container5 } = render(
<TrackGrid tracks={mockTracks} columns={5} />,
{ wrapper: createWrapper() },
);
const grid5 = container5.querySelector('[role="grid"]');
expect(grid5).toHaveClass(
'grid-cols-2',
'sm:grid-cols-2',
'md:grid-cols-3',
'lg:grid-cols-4',
'xl:grid-cols-5',
);
// Test 6 colonnes
const { container: container6 } = render(
<TrackGrid tracks={mockTracks} columns={6} />,
{ wrapper: createWrapper() },
);
const grid6 = container6.querySelector('[role="grid"]');
expect(grid6).toHaveClass(
'grid-cols-2',
'sm:grid-cols-2',
'md:grid-cols-3',
'lg:grid-cols-4',
'xl:grid-cols-5',
'2xl:grid-cols-6',
);
});
it('should apply responsive gap to skeleton loader', () => {
const { container } = render(
<TrackGrid tracks={[]} isLoading={true} gap="lg" />,
);
const grid = container.querySelector('.grid');
expect(grid).toHaveClass('gap-4', 'sm:gap-6');
});
it('should use mobileColumns with autoColumns', () => {
const { container } = render(
<TrackGrid tracks={mockTracks} autoColumns={true} mobileColumns={1} />,
{ wrapper: createWrapper() },
);
const grid = container.querySelector('[role="grid"]');
expect(grid).toHaveClass('grid-cols-1', 'sm:grid-cols-2');
});
it('should display density selector when showDensitySelector is true', () => {
render(<TrackGrid tracks={mockTracks} showDensitySelector={true} />, {
wrapper: createWrapper(),
});
expect(screen.getByRole('radiogroup')).toBeInTheDocument();
});
it('should not display density selector by default', () => {
render(<TrackGrid tracks={mockTracks} />, { wrapper: createWrapper() });
expect(screen.queryByRole('radiogroup')).not.toBeInTheDocument();
});
it('should apply compact density settings', () => {
const { container } = render(
<TrackGrid tracks={mockTracks} density="compact" />,
{ wrapper: createWrapper() },
);
const grid = container.querySelector('[role="grid"]');
// En mode compact, gap devrait être 'sm'
expect(grid).toHaveClass('gap-2', 'sm:gap-3');
});
it('should apply normal density settings', () => {
const { container } = render(
<TrackGrid tracks={mockTracks} density="normal" />,
{ wrapper: createWrapper() },
);
const grid = container.querySelector('[role="grid"]');
// En mode normal, gap devrait être 'md'
expect(grid).toHaveClass('gap-4', 'sm:gap-4');
});
it('should apply comfortable density settings', () => {
const { container } = render(
<TrackGrid tracks={mockTracks} density="comfortable" />,
{ wrapper: createWrapper() },
);
const grid = container.querySelector('[role="grid"]');
// En mode comfortable, gap devrait être 'lg'
expect(grid).toHaveClass('gap-4', 'sm:gap-6');
});
it('should call onDensityChange when density selector changes', async () => {
const user = userEvent.setup();
const mockOnDensityChange = vi.fn();
render(
<TrackGrid
tracks={mockTracks}
showDensitySelector={true}
onDensityChange={mockOnDensityChange}
/>,
{ wrapper: createWrapper() },
);
const compactButton = screen.getByLabelText(/Compact/);
await user.click(compactButton);
expect(mockOnDensityChange).toHaveBeenCalledWith('compact');
});
it('should persist density preference in localStorage', async () => {
const storageKey = 'testDensityKey';
localStorage.clear();
const { rerender } = render(
<TrackGrid
tracks={mockTracks}
persistDensity={true}
densityStorageKey={storageKey}
density="compact"
/>,
{ wrapper: createWrapper() },
);
// Attendre que useEffect synchronise densityProp avec densityState puis persiste
await waitFor(
() => {
expect(localStorage.getItem(storageKey)).toBe('compact');
},
{ timeout: 1000 },
);
rerender(
<TrackGrid
tracks={mockTracks}
persistDensity={true}
densityStorageKey={storageKey}
density="comfortable"
/>,
);
// Attendre que useEffect synchronise et persiste la nouvelle valeur
await waitFor(
() => {
expect(localStorage.getItem(storageKey)).toBe('comfortable');
},
{ timeout: 1000 },
);
});
it('should load density from localStorage on mount when persistDensity is enabled', () => {
const storageKey = 'testDensityKey';
localStorage.setItem(storageKey, 'comfortable');
render(
<TrackGrid
tracks={mockTracks}
persistDensity={true}
densityStorageKey={storageKey}
showDensitySelector={true} // Nécessaire pour activer useDensity
/>,
{ wrapper: createWrapper() },
);
// Vérifier que le sélecteur de densité est présent
expect(screen.getByRole('radiogroup')).toBeInTheDocument();
// La densité devrait être chargée depuis localStorage dans l'état initial
// et le sélecteur devrait l'afficher comme sélectionnée
// Note: Le test vérifie que le sélecteur est présent et que la valeur est chargée
// La densité est chargée dans l'initializer de useState, donc elle devrait être disponible immédiatement
const comfortableButton = screen.getByLabelText(/Confortable/);
// Le bouton devrait exister, mais peut-être pas encore sélectionné si useDensity n'est pas activé
expect(comfortableButton).toBeInTheDocument();
localStorage.removeItem(storageKey);
});
it('should use density prop over localStorage', () => {
const storageKey = 'testDensityKey';
localStorage.setItem(storageKey, 'comfortable');
const { container } = render(
<TrackGrid
tracks={mockTracks}
persistDensity={true}
densityStorageKey={storageKey}
density="compact"
/>,
{ wrapper: createWrapper() },
);
const grid = container.querySelector('[role="grid"]');
// Devrait utiliser la prop 'compact' plutôt que localStorage
expect(grid).toHaveClass('gap-2', 'sm:gap-3');
localStorage.removeItem(storageKey);
});
});