veza/apps/web/src/features/tracks/components/TrackHistory.test.tsx
senke c0bcb711df fix(e2e): align CI Go version to 1.24 for v0.101
fix(web): resolve lint errors for v0.101
- eslint: add ignores (e2e, scripts, playwright-report, generated types)
- eslint: add browser globals, disable react-hooks in stories
- fix empty catch blocks (Cart, MarketplacePage, RolesPage, SettingsPage)
- fix PlayerExpanded: move useEffect before early return
- fix TrackHistory.test: rename type import to avoid no-redeclare
2026-02-19 16:27:10 +01:00

337 lines
8.6 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { TrackHistory } from './TrackHistory';
import {
getTrackHistory,
TrackHistoryError,
} from '@/features/tracks/services/trackHistoryService';
import type { TrackHistory as TrackHistoryType } from '@/features/tracks/services/trackHistoryService';
import { useToast } from '@/hooks/useToast';
// Mock services (path must match hook: track-history/useTrackHistory imports ../../services/trackHistoryService)
vi.mock('@/features/tracks/services/trackHistoryService', () => ({
getTrackHistory: vi.fn(),
TrackHistoryError: class TrackHistoryError extends Error {
constructor(
message: string,
public code: string,
public retryable: boolean,
public originalError?: unknown,
) {
super(message);
this.name = 'TrackHistoryError';
}
},
}));
vi.mock('@/hooks/useToast');
describe('TrackHistory', () => {
const mockToast = {
success: vi.fn(),
error: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(useToast).mockReturnValue(mockToast);
});
const mockHistoryItems: TrackHistoryType[] = [
{
id: 1,
track_id: 123,
user_id: 1,
action: 'created',
old_value: undefined,
new_value: '{"title": "Test Track"}',
created_at: '2024-01-01T00:00:00Z',
},
{
id: 2,
track_id: 123,
user_id: 1,
action: 'updated',
old_value: '{"title": "Test Track"}',
new_value: '{"title": "Updated Track"}',
created_at: '2024-01-02T00:00:00Z',
},
{
id: 3,
track_id: 123,
user_id: 1,
action: 'published',
old_value: undefined,
new_value: undefined,
created_at: '2024-01-03T00:00:00Z',
},
];
it('should render loading state initially', async () => {
vi.mocked(getTrackHistory).mockImplementation(
() =>
new Promise((resolve) =>
setTimeout(
() =>
resolve({
history: mockHistoryItems,
total: 3,
limit: 50,
offset: 0,
}),
100,
),
),
);
render(<TrackHistory trackId={123} />);
expect(screen.getByRole('status')).toBeInTheDocument();
await waitFor(() => {
expect(getTrackHistory).toHaveBeenCalledWith(123, {
limit: 50,
offset: 0,
});
});
});
it('should display history timeline', async () => {
vi.mocked(getTrackHistory).mockResolvedValue({
history: mockHistoryItems,
total: 3,
limit: 50,
offset: 0,
});
render(<TrackHistory trackId={123} />);
await waitFor(() => {
expect(
screen.getByText('Historique des modifications'),
).toBeInTheDocument();
});
expect(screen.getByText('Créé')).toBeInTheDocument();
expect(screen.getByText('Modifié')).toBeInTheDocument();
expect(screen.getByText('Publié')).toBeInTheDocument();
});
it('should display empty state when no history', async () => {
vi.mocked(getTrackHistory).mockResolvedValue({
history: [],
total: 0,
limit: 50,
offset: 0,
});
render(<TrackHistory trackId={123} />);
await waitFor(() => {
expect(
screen.getByText('Aucune modification enregistrée'),
).toBeInTheDocument();
});
});
it('should display error message on load error', async () => {
const loadError = new TrackHistoryError('Failed to load', 'SERVER', true);
vi.mocked(getTrackHistory).mockImplementation(() =>
Promise.reject(loadError),
);
render(<TrackHistory trackId="123" />);
const message = await screen.findByText('Failed to load', {}, { timeout: 3000 });
expect(message).toBeInTheDocument();
});
it('should display old and new values when available', async () => {
vi.mocked(getTrackHistory).mockResolvedValue({
history: [mockHistoryItems[1]], // Updated action with values
total: 1,
limit: 50,
offset: 0,
});
render(<TrackHistory trackId={123} />);
await waitFor(() => {
expect(screen.getByText('Ancienne valeur:')).toBeInTheDocument();
expect(screen.getByText('Nouvelle valeur:')).toBeInTheDocument();
});
});
it('should handle pagination', async () => {
const largeHistory: TrackHistory[] = Array.from(
{ length: 10 },
(_, i) => ({
id: i + 1,
track_id: 123,
user_id: 1,
action: 'updated' as const,
created_at: `2024-01-0${i + 1}T00:00:00Z`,
}),
);
vi.mocked(getTrackHistory).mockResolvedValue({
history: largeHistory.slice(0, 5),
total: 10,
limit: 5,
offset: 0,
});
render(<TrackHistory trackId={123} limit={5} />);
await waitFor(() => {
expect(screen.getByText(/Affichage 1 - 5 sur 10/)).toBeInTheDocument();
});
const nextButton = screen.getByText('Suivant');
expect(nextButton).toBeInTheDocument();
expect(nextButton).not.toBeDisabled();
// Mock next page response
vi.mocked(getTrackHistory).mockResolvedValue({
history: largeHistory.slice(5, 10),
total: 10,
limit: 5,
offset: 5,
});
fireEvent.click(nextButton);
await waitFor(() => {
expect(getTrackHistory).toHaveBeenCalledWith(123, {
limit: 5,
offset: 5,
});
});
});
it('should disable previous button on first page', async () => {
vi.mocked(getTrackHistory).mockResolvedValue({
history: mockHistoryItems.slice(0, 2),
total: 10,
limit: 2,
offset: 0,
});
render(<TrackHistory trackId={123} limit={2} />);
await waitFor(() => {
const previousButton = screen.getByText('Précédent');
expect(previousButton).toBeDisabled();
});
});
it('should disable next button on last page', async () => {
vi.mocked(getTrackHistory)
.mockResolvedValueOnce({
history: mockHistoryItems.slice(0, 2),
total: 3,
limit: 2,
offset: 0,
})
.mockResolvedValueOnce({
history: mockHistoryItems.slice(2, 3),
total: 3,
limit: 2,
offset: 2,
});
render(<TrackHistory trackId={123} limit={2} />);
await waitFor(() => {
expect(screen.getByText(/Affichage 1 - 2 sur 3/)).toBeInTheDocument();
});
const nextButton = screen.getByText('Suivant');
fireEvent.click(nextButton);
await waitFor(() => {
const nextButtonAfter = screen.getByText('Suivant');
expect(nextButtonAfter).toBeDisabled();
});
});
it('should display all action types correctly', async () => {
const allActions: TrackHistory[] = [
{
id: 1,
track_id: 123,
user_id: 1,
action: 'created',
created_at: '2024-01-01T00:00:00Z',
},
{
id: 2,
track_id: 123,
user_id: 1,
action: 'updated',
created_at: '2024-01-02T00:00:00Z',
},
{
id: 3,
track_id: 123,
user_id: 1,
action: 'deleted',
created_at: '2024-01-03T00:00:00Z',
},
{
id: 4,
track_id: 123,
user_id: 1,
action: 'published',
created_at: '2024-01-04T00:00:00Z',
},
{
id: 5,
track_id: 123,
user_id: 1,
action: 'unpublished',
created_at: '2024-01-05T00:00:00Z',
},
{
id: 6,
track_id: 123,
user_id: 1,
action: 'restored',
created_at: '2024-01-06T00:00:00Z',
},
];
vi.mocked(getTrackHistory).mockResolvedValue({
history: allActions,
total: 6,
limit: 50,
offset: 0,
});
render(<TrackHistory trackId={123} />);
await waitFor(() => {
expect(screen.getByText('Créé')).toBeInTheDocument();
expect(screen.getByText('Modifié')).toBeInTheDocument();
expect(screen.getByText('Supprimé')).toBeInTheDocument();
expect(screen.getByText('Publié')).toBeInTheDocument();
expect(screen.getByText('Dépublié')).toBeInTheDocument();
expect(screen.getByText('Restauré')).toBeInTheDocument();
});
});
it('should format dates correctly', async () => {
vi.mocked(getTrackHistory).mockResolvedValue({
history: [mockHistoryItems[0]],
total: 1,
limit: 50,
offset: 0,
});
render(<TrackHistory trackId={123} />);
await waitFor(() => {
// Check that date is formatted (should contain month name in French)
const dateElement = screen.getByText(/janvier/i);
expect(dateElement).toBeInTheDocument();
});
});
});