test: add unit tests for new components to improve coverage
- Add tests for useClickOutside hook - Add tests for UserDropdown component - Add tests for TicketPrizeDisplay component - Add tests for TicketTableRow component - Increase test count from 181 to 222 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c78b68ca4f
commit
0dd11b572d
211
__tests__/components/UserDropdown.test.tsx
Normal file
211
__tests__/components/UserDropdown.test.tsx
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
/**
|
||||
* Tests for the UserDropdown component
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { UserDropdown } from '@/components/UserDropdown';
|
||||
|
||||
// Mock next/link
|
||||
jest.mock('next/link', () => {
|
||||
return ({ children, href }: { children: React.ReactNode; href: string }) => (
|
||||
<a href={href}>{children}</a>
|
||||
);
|
||||
});
|
||||
|
||||
describe('UserDropdown', () => {
|
||||
const mockUser = {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john.doe@example.com',
|
||||
};
|
||||
|
||||
const mockOnLogout = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render user initials', () => {
|
||||
render(
|
||||
<UserDropdown
|
||||
user={mockUser}
|
||||
profilePath="/profil"
|
||||
onLogout={mockOnLogout}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('JD')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render user name and email', () => {
|
||||
render(
|
||||
<UserDropdown
|
||||
user={mockUser}
|
||||
profilePath="/profil"
|
||||
onLogout={mockOnLogout}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
||||
expect(screen.getByText('john.doe@example.com')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should toggle dropdown on click', () => {
|
||||
render(
|
||||
<UserDropdown
|
||||
user={mockUser}
|
||||
profilePath="/profil"
|
||||
onLogout={mockOnLogout}
|
||||
/>
|
||||
);
|
||||
|
||||
// Dropdown should be closed initially
|
||||
expect(screen.queryByText('Profil')).not.toBeInTheDocument();
|
||||
|
||||
// Click to open - get the main trigger button (contains user name)
|
||||
const triggerButton = screen.getByText('John Doe').closest('button');
|
||||
fireEvent.click(triggerButton!);
|
||||
expect(screen.getByText('Profil')).toBeInTheDocument();
|
||||
expect(screen.getByText('Deconnexion')).toBeInTheDocument();
|
||||
|
||||
// Click to close
|
||||
fireEvent.click(triggerButton!);
|
||||
expect(screen.queryByText('Profil')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render profile link with correct path', () => {
|
||||
render(
|
||||
<UserDropdown
|
||||
user={mockUser}
|
||||
profilePath="/admin/profil"
|
||||
onLogout={mockOnLogout}
|
||||
/>
|
||||
);
|
||||
|
||||
const triggerButton = screen.getByText('John Doe').closest('button');
|
||||
fireEvent.click(triggerButton!);
|
||||
|
||||
const profileLink = screen.getByText('Profil').closest('a');
|
||||
expect(profileLink).toHaveAttribute('href', '/admin/profil');
|
||||
});
|
||||
|
||||
it('should call onLogout when logout button is clicked', () => {
|
||||
render(
|
||||
<UserDropdown
|
||||
user={mockUser}
|
||||
profilePath="/profil"
|
||||
onLogout={mockOnLogout}
|
||||
/>
|
||||
);
|
||||
|
||||
const triggerButton = screen.getByText('John Doe').closest('button');
|
||||
fireEvent.click(triggerButton!);
|
||||
fireEvent.click(screen.getByText('Deconnexion'));
|
||||
|
||||
expect(mockOnLogout).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should have onClick handler on profile link', () => {
|
||||
render(
|
||||
<UserDropdown
|
||||
user={mockUser}
|
||||
profilePath="/profil"
|
||||
onLogout={mockOnLogout}
|
||||
/>
|
||||
);
|
||||
|
||||
const triggerButton = screen.getByText('John Doe').closest('button');
|
||||
fireEvent.click(triggerButton!);
|
||||
|
||||
const profileLink = screen.getByText('Profil').closest('a');
|
||||
expect(profileLink).toBeInTheDocument();
|
||||
expect(profileLink).toHaveAttribute('href', '/profil');
|
||||
});
|
||||
|
||||
it('should apply blue accent color by default', () => {
|
||||
const { container } = render(
|
||||
<UserDropdown
|
||||
user={mockUser}
|
||||
profilePath="/profil"
|
||||
onLogout={mockOnLogout}
|
||||
/>
|
||||
);
|
||||
|
||||
const avatar = container.querySelector('.bg-blue-600');
|
||||
expect(avatar).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply green accent color when specified', () => {
|
||||
const { container } = render(
|
||||
<UserDropdown
|
||||
user={mockUser}
|
||||
profilePath="/profil"
|
||||
onLogout={mockOnLogout}
|
||||
accentColor="green"
|
||||
/>
|
||||
);
|
||||
|
||||
const avatar = container.querySelector('.bg-green-600');
|
||||
expect(avatar).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle null user gracefully', () => {
|
||||
const { container } = render(
|
||||
<UserDropdown
|
||||
user={null}
|
||||
profilePath="/profil"
|
||||
onLogout={mockOnLogout}
|
||||
/>
|
||||
);
|
||||
|
||||
// Should render empty initials
|
||||
const avatarDiv = container.querySelector('.bg-blue-600');
|
||||
expect(avatarDiv).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle user with missing firstName', () => {
|
||||
render(
|
||||
<UserDropdown
|
||||
user={{ lastName: 'Doe', email: 'test@test.com' }}
|
||||
profilePath="/profil"
|
||||
onLogout={mockOnLogout}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('D')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(
|
||||
<UserDropdown
|
||||
user={mockUser}
|
||||
profilePath="/profil"
|
||||
onLogout={mockOnLogout}
|
||||
className="custom-class"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
it('should rotate chevron icon when dropdown is open', () => {
|
||||
const { container } = render(
|
||||
<UserDropdown
|
||||
user={mockUser}
|
||||
profilePath="/profil"
|
||||
onLogout={mockOnLogout}
|
||||
/>
|
||||
);
|
||||
|
||||
const chevron = container.querySelector('.w-4.h-4.text-gray-500');
|
||||
expect(chevron).not.toHaveClass('rotate-180');
|
||||
|
||||
const triggerButton = screen.getByText('John Doe').closest('button');
|
||||
fireEvent.click(triggerButton!);
|
||||
|
||||
const rotatedChevron = container.querySelector('.rotate-180');
|
||||
expect(rotatedChevron).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
73
__tests__/components/ui/TicketPrizeDisplay.test.tsx
Normal file
73
__tests__/components/ui/TicketPrizeDisplay.test.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* Tests for the TicketPrizeDisplay component
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { TicketPrizeDisplay } from '@/components/ui/TicketPrizeDisplay';
|
||||
|
||||
describe('TicketPrizeDisplay', () => {
|
||||
it('should render prize name for INFUSEUR', () => {
|
||||
render(<TicketPrizeDisplay prizeType="INFUSEUR" />);
|
||||
expect(screen.getByText('Infuseur à thé')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render prize name for THE_SIGNATURE', () => {
|
||||
render(<TicketPrizeDisplay prizeType="THE_SIGNATURE" />);
|
||||
expect(screen.getByText('Thé signature 100g')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render prize name for COFFRET_DECOUVERTE', () => {
|
||||
render(<TicketPrizeDisplay prizeType="COFFRET_DECOUVERTE" />);
|
||||
expect(screen.getByText('Coffret découverte 39€')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render prize name for COFFRET_PRESTIGE', () => {
|
||||
render(<TicketPrizeDisplay prizeType="COFFRET_PRESTIGE" />);
|
||||
expect(screen.getByText('Coffret prestige 69€')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render prize name for THE_GRATUIT', () => {
|
||||
render(<TicketPrizeDisplay prizeType="THE_GRATUIT" />);
|
||||
expect(screen.getByText('Thé gratuit en magasin')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render icon for each prize type', () => {
|
||||
const { container } = render(<TicketPrizeDisplay prizeType="INFUSEUR" />);
|
||||
const svg = container.querySelector('svg');
|
||||
expect(svg).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should return null for unknown prize type', () => {
|
||||
const { container } = render(<TicketPrizeDisplay prizeType="UNKNOWN_PRIZE" />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(
|
||||
<TicketPrizeDisplay prizeType="INFUSEUR" className="custom-class" />
|
||||
);
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
it('should render with prize color styling', () => {
|
||||
const { container } = render(<TicketPrizeDisplay prizeType="INFUSEUR" />);
|
||||
const iconContainer = container.querySelector('.rounded-full');
|
||||
expect(iconContainer).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have flex layout with gap', () => {
|
||||
const { container } = render(<TicketPrizeDisplay prizeType="INFUSEUR" />);
|
||||
expect(container.firstChild).toHaveClass('flex');
|
||||
expect(container.firstChild).toHaveClass('items-center');
|
||||
expect(container.firstChild).toHaveClass('gap-3');
|
||||
});
|
||||
|
||||
it('should render prize name with correct text styling', () => {
|
||||
render(<TicketPrizeDisplay prizeType="INFUSEUR" />);
|
||||
const nameElement = screen.getByText('Infuseur à thé');
|
||||
expect(nameElement).toHaveClass('text-sm');
|
||||
expect(nameElement).toHaveClass('font-medium');
|
||||
});
|
||||
});
|
||||
194
__tests__/components/ui/TicketTableRow.test.tsx
Normal file
194
__tests__/components/ui/TicketTableRow.test.tsx
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
/**
|
||||
* Tests for the TicketTableRow component
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { TicketTableRow } from '@/components/ui/TicketTableRow';
|
||||
import { Ticket } from '@/types';
|
||||
|
||||
describe('TicketTableRow', () => {
|
||||
const mockTicket: Ticket = {
|
||||
id: '1',
|
||||
code: 'ABC123',
|
||||
status: 'PENDING',
|
||||
playedAt: '2024-01-15T10:30:00Z',
|
||||
claimedAt: null,
|
||||
prize: {
|
||||
id: '1',
|
||||
type: 'INFUSEUR',
|
||||
name: 'Infuseur à thé',
|
||||
value: 10,
|
||||
},
|
||||
};
|
||||
|
||||
it('should render ticket code', () => {
|
||||
render(
|
||||
<table>
|
||||
<tbody>
|
||||
<TicketTableRow ticket={mockTicket} />
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
expect(screen.getByText('ABC123')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render ticket status badge', () => {
|
||||
render(
|
||||
<table>
|
||||
<tbody>
|
||||
<TicketTableRow ticket={mockTicket} />
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
expect(screen.getByText('En attente')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render prize display when prize exists', () => {
|
||||
render(
|
||||
<table>
|
||||
<tbody>
|
||||
<TicketTableRow ticket={mockTicket} />
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
expect(screen.getByText('Infuseur à thé')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render played date', () => {
|
||||
render(
|
||||
<table>
|
||||
<tbody>
|
||||
<TicketTableRow ticket={mockTicket} />
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
// French date format
|
||||
expect(screen.getByText(/15\/01\/2024|15 janvier 2024/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show claimed date when showClaimedDate is true', () => {
|
||||
const ticketWithClaim: Ticket = {
|
||||
...mockTicket,
|
||||
status: 'CLAIMED',
|
||||
claimedAt: '2024-01-20T14:00:00Z',
|
||||
};
|
||||
|
||||
render(
|
||||
<table>
|
||||
<tbody>
|
||||
<TicketTableRow ticket={ticketWithClaim} showClaimedDate />
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
// Should have an extra column for claimed date
|
||||
const cells = screen.getAllByRole('cell');
|
||||
expect(cells.length).toBe(5); // code, prize, status, playedAt, claimedAt
|
||||
});
|
||||
|
||||
it('should not show claimed date column when showClaimedDate is false', () => {
|
||||
render(
|
||||
<table>
|
||||
<tbody>
|
||||
<TicketTableRow ticket={mockTicket} showClaimedDate={false} />
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
const cells = screen.getAllByRole('cell');
|
||||
expect(cells.length).toBe(4); // code, prize, status, playedAt
|
||||
});
|
||||
|
||||
it('should show dash when playedAt is null', () => {
|
||||
const ticketWithoutDate: Ticket = {
|
||||
...mockTicket,
|
||||
playedAt: null,
|
||||
};
|
||||
|
||||
render(
|
||||
<table>
|
||||
<tbody>
|
||||
<TicketTableRow ticket={ticketWithoutDate} />
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
expect(screen.getByText('-')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show dash when claimedAt is null with showClaimedDate', () => {
|
||||
render(
|
||||
<table>
|
||||
<tbody>
|
||||
<TicketTableRow ticket={mockTicket} showClaimedDate />
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
const dashes = screen.getAllByText('-');
|
||||
expect(dashes.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should render CLAIMED status correctly', () => {
|
||||
const claimedTicket: Ticket = {
|
||||
...mockTicket,
|
||||
status: 'CLAIMED',
|
||||
};
|
||||
|
||||
render(
|
||||
<table>
|
||||
<tbody>
|
||||
<TicketTableRow ticket={claimedTicket} />
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Réclamé')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle ticket without prize', () => {
|
||||
const ticketWithoutPrize: Ticket = {
|
||||
...mockTicket,
|
||||
prize: undefined,
|
||||
};
|
||||
|
||||
render(
|
||||
<table>
|
||||
<tbody>
|
||||
<TicketTableRow ticket={ticketWithoutPrize} />
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
expect(screen.getByText('ABC123')).toBeInTheDocument();
|
||||
// Should not crash and prize cell should be empty
|
||||
});
|
||||
|
||||
it('should have hover styling on row', () => {
|
||||
render(
|
||||
<table>
|
||||
<tbody>
|
||||
<TicketTableRow ticket={mockTicket} />
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
const row = screen.getByRole('row');
|
||||
expect(row).toHaveClass('hover:bg-gradient-to-r');
|
||||
});
|
||||
|
||||
it('should render code with monospace font', () => {
|
||||
render(
|
||||
<table>
|
||||
<tbody>
|
||||
<TicketTableRow ticket={mockTicket} />
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
const codeElement = screen.getByText('ABC123');
|
||||
expect(codeElement).toHaveClass('font-mono');
|
||||
});
|
||||
});
|
||||
103
__tests__/hooks/useClickOutside.test.ts
Normal file
103
__tests__/hooks/useClickOutside.test.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
/**
|
||||
* Tests for the useClickOutside hook
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useClickOutside } from '@/hooks/useClickOutside';
|
||||
import { createRef } from 'react';
|
||||
|
||||
describe('useClickOutside', () => {
|
||||
let callback: jest.Mock;
|
||||
let ref: React.RefObject<HTMLDivElement>;
|
||||
let element: HTMLDivElement;
|
||||
|
||||
beforeEach(() => {
|
||||
callback = jest.fn();
|
||||
element = document.createElement('div');
|
||||
document.body.appendChild(element);
|
||||
ref = { current: element };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(element);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should call callback when clicking outside the element', () => {
|
||||
renderHook(() => useClickOutside(ref, callback, true));
|
||||
|
||||
const outsideElement = document.createElement('div');
|
||||
document.body.appendChild(outsideElement);
|
||||
|
||||
const event = new MouseEvent('mousedown', { bubbles: true });
|
||||
outsideElement.dispatchEvent(event);
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
|
||||
document.body.removeChild(outsideElement);
|
||||
});
|
||||
|
||||
it('should not call callback when clicking inside the element', () => {
|
||||
renderHook(() => useClickOutside(ref, callback, true));
|
||||
|
||||
const event = new MouseEvent('mousedown', { bubbles: true });
|
||||
element.dispatchEvent(event);
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not call callback when disabled', () => {
|
||||
renderHook(() => useClickOutside(ref, callback, false));
|
||||
|
||||
const outsideElement = document.createElement('div');
|
||||
document.body.appendChild(outsideElement);
|
||||
|
||||
const event = new MouseEvent('mousedown', { bubbles: true });
|
||||
outsideElement.dispatchEvent(event);
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
document.body.removeChild(outsideElement);
|
||||
});
|
||||
|
||||
it('should handle null ref gracefully', () => {
|
||||
const nullRef = createRef<HTMLDivElement>();
|
||||
|
||||
// Should not throw when ref is null
|
||||
expect(() => {
|
||||
renderHook(() => useClickOutside(nullRef, callback, true));
|
||||
|
||||
const event = new MouseEvent('mousedown', { bubbles: true });
|
||||
document.body.dispatchEvent(event);
|
||||
}).not.toThrow();
|
||||
|
||||
// Callback should NOT be called when ref is null (ref.current is falsy)
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should remove event listener on unmount', () => {
|
||||
const removeEventListenerSpy = jest.spyOn(document, 'removeEventListener');
|
||||
|
||||
const { unmount } = renderHook(() => useClickOutside(ref, callback, true));
|
||||
unmount();
|
||||
|
||||
expect(removeEventListenerSpy).toHaveBeenCalledWith('mousedown', expect.any(Function));
|
||||
|
||||
removeEventListenerSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should use default enabled value of true', () => {
|
||||
renderHook(() => useClickOutside(ref, callback));
|
||||
|
||||
const outsideElement = document.createElement('div');
|
||||
document.body.appendChild(outsideElement);
|
||||
|
||||
const event = new MouseEvent('mousedown', { bubbles: true });
|
||||
outsideElement.dispatchEvent(event);
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
|
||||
document.body.removeChild(outsideElement);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user