import { render, screen, fireEvent, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom/vitest';
import { Tooltip } from './tooltip';
describe('Tooltip Component', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
});
afterEach(() => {
vi.runOnlyPendingTimers();
vi.useRealTimers();
});
it('renders children correctly', () => {
render(
,
);
expect(screen.getByText('Hover me')).toBeInTheDocument();
});
it('does not show tooltip initially', () => {
render(
,
);
expect(screen.queryByText('Tooltip text')).not.toBeInTheDocument();
});
it('shows tooltip on hover after delay', async () => {
const { container } = render(
,
);
const wrapper = container.querySelector('.relative.inline-block');
fireEvent.mouseEnter(wrapper!);
await act(async () => {
vi.advanceTimersByTime(310);
});
expect(screen.getByText('Tooltip text')).toBeInTheDocument();
});
it('hides tooltip when mouse leaves', async () => {
const { container } = render(
,
);
const wrapper = container.querySelector('.relative.inline-block');
fireEvent.mouseEnter(wrapper!);
await act(async () => {
vi.advanceTimersByTime(10);
});
expect(screen.getByText('Tooltip text')).toBeInTheDocument();
fireEvent.mouseLeave(wrapper!);
await act(async () => {
vi.advanceTimersByTime(250);
});
// Le tooltip peut être monté mais invisible, vérifier qu'il n'est pas visible
const tooltip = screen.queryByText('Tooltip text');
if (tooltip) {
expect(tooltip).toHaveClass('opacity-0');
}
});
it('shows tooltip immediately when delay is 0', async () => {
const { container } = render(
,
);
const wrapper = container.querySelector('.relative.inline-block');
fireEvent.mouseEnter(wrapper!);
await act(async () => {
vi.advanceTimersByTime(10);
});
expect(screen.getByText('Tooltip text')).toBeInTheDocument();
});
it('applies top position by default', async () => {
const { container } = render(
,
);
const wrapper = container.querySelector('.relative.inline-block');
fireEvent.mouseEnter(wrapper!);
await act(async () => {
vi.advanceTimersByTime(10);
});
const tooltip = screen.getByText('Tooltip text');
expect(tooltip.className).toMatch(/bottom-full|top-full/);
});
it('applies bottom position', async () => {
const { container } = render(
,
);
const wrapper = container.querySelector('.relative.inline-block');
fireEvent.mouseEnter(wrapper!);
await act(async () => {
vi.advanceTimersByTime(10);
});
const tooltip = screen.getByText('Tooltip text');
expect(tooltip.closest('.top-full')).toBeInTheDocument();
});
it('applies left position', async () => {
const { container } = render(
,
);
const wrapper = container.querySelector('.relative.inline-block');
fireEvent.mouseEnter(wrapper!);
await act(async () => {
vi.advanceTimersByTime(10);
});
const tooltip = screen.getByText('Tooltip text');
expect(tooltip.className).toMatch(/left-full|right-full/);
});
it('applies right position', async () => {
const { container } = render(
,
);
const wrapper = container.querySelector('.relative.inline-block');
fireEvent.mouseEnter(wrapper!);
await act(async () => {
vi.advanceTimersByTime(10);
});
const tooltip = screen.getByText('Tooltip text');
expect(tooltip.closest('.left-full')).toBeInTheDocument();
});
it('does not show tooltip when disabled', async () => {
render(
,
);
const button = screen.getByText('Hover me');
fireEvent.mouseEnter(button);
await act(async () => {
vi.advanceTimersByTime(10);
});
expect(screen.queryByText('Tooltip text')).not.toBeInTheDocument();
});
it('renders React node as content', async () => {
const { container } = render(
Custom content} delay={0}>
,
);
const wrapper = container.querySelector('.relative.inline-block');
fireEvent.mouseEnter(wrapper!);
await act(async () => {
vi.advanceTimersByTime(10);
});
expect(screen.getByText('Custom content')).toBeInTheDocument();
});
it('applies custom className', async () => {
const { container } = render(
,
);
const wrapper = container.querySelector('.relative.inline-block');
fireEvent.mouseEnter(wrapper!);
await act(async () => {
vi.advanceTimersByTime(10);
});
const tooltip = screen.getByText('Tooltip text');
expect(tooltip.closest('.custom-tooltip')).toBeInTheDocument();
});
it('has correct ARIA role', async () => {
const { container } = render(
,
);
const wrapper = container.querySelector('.relative.inline-block');
fireEvent.mouseEnter(wrapper!);
await act(async () => {
vi.advanceTimersByTime(10);
});
const tooltip = screen.getByRole('tooltip');
expect(tooltip).toBeInTheDocument();
});
it('cancels tooltip display if mouse leaves before delay', async () => {
const { container } = render(
,
);
const wrapper = container.querySelector('.relative.inline-block');
fireEvent.mouseEnter(wrapper!);
await act(async () => {
vi.advanceTimersByTime(200);
});
fireEvent.mouseLeave(wrapper!);
await act(async () => {
vi.advanceTimersByTime(200);
});
expect(screen.queryByText('Tooltip text')).not.toBeInTheDocument();
});
describe('Triggers', () => {
it('shows tooltip on click when trigger is click', async () => {
render(
,
);
const button = screen.getByText('Click me');
await act(async () => {
fireEvent.click(button);
});
expect(screen.getByText('Click tooltip')).toBeInTheDocument();
});
it('toggles tooltip on click when trigger is click', async () => {
render(
,
);
const button = screen.getByText('Click me');
await act(async () => {
fireEvent.click(button);
});
expect(screen.getByText('Click tooltip')).toBeInTheDocument();
await act(async () => {
fireEvent.click(button);
});
await act(async () => {
vi.advanceTimersByTime(300);
});
// Le tooltip est monté mais invisible, vérifier qu'il n'est pas visible
const tooltip = screen.queryByText('Click tooltip');
// Le tooltip peut être monté mais invisible, ou complètement démonté
// On vérifie qu'il n'est pas visible (opacity-0 ou pas présent)
if (tooltip) {
expect(tooltip).toHaveClass('opacity-0');
}
});
it('shows tooltip on focus when trigger is focus', async () => {
render(
,
);
const input = screen.getByPlaceholderText('Focus me');
fireEvent.focus(input);
await act(async () => {
vi.advanceTimersByTime(10);
});
expect(screen.getByText('Focus tooltip')).toBeInTheDocument();
});
it('hides tooltip on blur when trigger is focus', async () => {
render(
,
);
const input = screen.getByPlaceholderText('Focus me');
fireEvent.focus(input);
await act(async () => {
vi.advanceTimersByTime(10);
});
expect(screen.getByText('Focus tooltip')).toBeInTheDocument();
fireEvent.blur(input);
await act(async () => {
vi.advanceTimersByTime(300);
});
// Le tooltip est monté mais invisible, vérifier qu'il n'est pas visible
const tooltip = screen.queryByText('Focus tooltip');
// Le tooltip peut être monté mais invisible, ou complètement démonté
// On vérifie qu'il n'est pas visible (opacity-0 ou pas présent)
if (tooltip) {
expect(tooltip).toHaveClass('opacity-0');
}
});
});
describe('Advanced features', () => {
it('shows arrow when showArrow is true', async () => {
const { container } = render(
,
);
const wrapper = container.querySelector('.relative.inline-block');
fireEvent.mouseEnter(wrapper!);
await act(async () => {
vi.advanceTimersByTime(10);
});
const tooltip = screen.getByText('With arrow');
const arrow = tooltip.parentElement?.querySelector('.border-4');
expect(arrow).toBeInTheDocument();
});
it('hides arrow when showArrow is false', async () => {
const { container } = render(
,
);
const wrapper = container.querySelector('.relative.inline-block');
fireEvent.mouseEnter(wrapper!);
await act(async () => {
vi.advanceTimersByTime(10);
});
const tooltip = screen.getByText('No arrow');
const arrow = tooltip.parentElement?.querySelector('.border-4');
expect(arrow).not.toBeInTheDocument();
});
it('applies maxWidth style', async () => {
render(
,
);
const button = screen.getByText('Hover me');
fireEvent.mouseEnter(button);
await act(async () => {
vi.advanceTimersByTime(10);
});
const tooltip = screen.getByText('Limited width');
expect(tooltip).toHaveStyle({ maxWidth: '200px' });
});
it('renders rich content with HTML elements', async () => {
const { container } = render(
Rich content
With multiple elements
}
delay={0}
>
,
);
const wrapper = container.querySelector('.relative.inline-block');
fireEvent.mouseEnter(wrapper!);
await act(async () => {
vi.advanceTimersByTime(10);
});
expect(screen.getByText('Rich content')).toBeInTheDocument();
expect(screen.getByText('With multiple elements')).toBeInTheDocument();
});
});
});