2025-12-03 21:56:50 +00:00
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
|
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
|
|
|
|
import { TrackHistory } from './TrackHistory';
|
|
|
|
|
import {
|
|
|
|
|
getTrackHistory,
|
|
|
|
|
TrackHistoryError,
|
|
|
|
|
TrackHistoryItem,
|
|
|
|
|
} from '../services/trackHistoryService';
|
|
|
|
|
import { useToast } from '@/hooks/useToast';
|
|
|
|
|
|
|
|
|
|
// Mock services
|
|
|
|
|
vi.mock('../services/trackHistoryService');
|
|
|
|
|
vi.mock('@/hooks/useToast');
|
|
|
|
|
|
|
|
|
|
describe('TrackHistory', () => {
|
|
|
|
|
const mockToast = {
|
|
|
|
|
success: vi.fn(),
|
|
|
|
|
error: vi.fn(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
vi.mocked(useToast).mockReturnValue(mockToast);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const mockHistoryItems: TrackHistoryItem[] = [
|
|
|
|
|
{
|
|
|
|
|
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(
|
2025-12-13 02:34:34 +00:00
|
|
|
() =>
|
|
|
|
|
new Promise((resolve) =>
|
|
|
|
|
setTimeout(
|
|
|
|
|
() =>
|
|
|
|
|
resolve({
|
|
|
|
|
history: mockHistoryItems,
|
|
|
|
|
total: 3,
|
|
|
|
|
limit: 50,
|
|
|
|
|
offset: 0,
|
|
|
|
|
}),
|
|
|
|
|
100,
|
|
|
|
|
),
|
|
|
|
|
),
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
render(<TrackHistory trackId={123} />);
|
|
|
|
|
|
|
|
|
|
expect(screen.getByRole('status')).toBeInTheDocument();
|
|
|
|
|
await waitFor(() => {
|
2025-12-13 02:34:34 +00:00
|
|
|
expect(getTrackHistory).toHaveBeenCalledWith(123, {
|
|
|
|
|
limit: 50,
|
|
|
|
|
offset: 0,
|
|
|
|
|
});
|
2025-12-03 21:56:50 +00:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should display history timeline', async () => {
|
|
|
|
|
vi.mocked(getTrackHistory).mockResolvedValue({
|
|
|
|
|
history: mockHistoryItems,
|
|
|
|
|
total: 3,
|
|
|
|
|
limit: 50,
|
|
|
|
|
offset: 0,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
render(<TrackHistory trackId={123} />);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
2025-12-13 02:34:34 +00:00
|
|
|
expect(
|
|
|
|
|
screen.getByText('Historique des modifications'),
|
|
|
|
|
).toBeInTheDocument();
|
2025-12-03 21:56:50 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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(() => {
|
2025-12-13 02:34:34 +00:00
|
|
|
expect(
|
|
|
|
|
screen.getByText('Aucune modification enregistrée'),
|
|
|
|
|
).toBeInTheDocument();
|
2025-12-03 21:56:50 +00:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should display error message on load error', async () => {
|
|
|
|
|
const error = new TrackHistoryError('Failed to load', 'SERVER', true);
|
|
|
|
|
vi.mocked(getTrackHistory).mockRejectedValue(error);
|
|
|
|
|
|
|
|
|
|
render(<TrackHistory trackId={123} />);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(screen.getByText('Failed to load')).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 () => {
|
2025-12-13 02:34:34 +00:00
|
|
|
const largeHistory: TrackHistoryItem[] = 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`,
|
|
|
|
|
}),
|
|
|
|
|
);
|
2025-12-03 21:56:50 +00:00
|
|
|
|
|
|
|
|
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(() => {
|
2025-12-13 02:34:34 +00:00
|
|
|
expect(getTrackHistory).toHaveBeenCalledWith(123, {
|
|
|
|
|
limit: 5,
|
|
|
|
|
offset: 5,
|
|
|
|
|
});
|
2025-12-03 21:56:50 +00:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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).mockResolvedValue({
|
|
|
|
|
history: mockHistoryItems.slice(0, 2),
|
|
|
|
|
total: 2,
|
|
|
|
|
limit: 2,
|
|
|
|
|
offset: 0,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
render(<TrackHistory trackId={123} limit={2} />);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
const nextButton = screen.getByText('Suivant');
|
|
|
|
|
expect(nextButton).toBeDisabled();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should display all action types correctly', async () => {
|
|
|
|
|
const allActions: TrackHistoryItem[] = [
|
2025-12-13 02:34:34 +00:00
|
|
|
{
|
|
|
|
|
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',
|
|
|
|
|
},
|
2025-12-03 21:56:50 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|