feat: Visual masterpiece - true light mode & premium UI
🎨 **True Light/Dark Mode**
- Implemented proper light mode with inverted color scheme
- Smooth theme transitions (0.3s ease)
- Light mode colors: white backgrounds, dark text, vibrant accents
- System theme detection with proper class application
🌈 **Enhanced Theme System**
- 4 color themes work in both light and dark modes
- Cyber (cyan/magenta), Ocean (blue/teal), Forest (green/lime), Sunset (orange/purple)
- Theme-specific glassmorphism effects
- Proper contrast in light mode
✨ **Premium Animations**
- Float, glow-pulse, slide-in, scale-in, rotate-in animations
- Smooth page transitions
- Hover effects with depth (lift, glow, scale)
- Micro-interactions on all interactive elements
🎯 **Visual Polish**
- Enhanced glassmorphism for light/dark modes
- Custom scrollbar with theme colors
- Beautiful text selection
- Focus indicators for accessibility
- Premium utility classes
🔧 **Technical Improvements**
- Updated UIStore to properly apply light/dark classes
- Added data-theme attribute for CSS targeting
- Smooth scroll behavior
- Optimized transitions
The app is now a visual masterpiece with perfect light/dark mode support!
2026-01-11 01:32:21 +00:00
|
|
|
import { render, screen, fireEvent } from '@testing-library/react';
|
2025-12-03 21:56:50 +00:00
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
|
|
|
import userEvent from '@testing-library/user-event';
|
feat: Visual masterpiece - true light mode & premium UI
🎨 **True Light/Dark Mode**
- Implemented proper light mode with inverted color scheme
- Smooth theme transitions (0.3s ease)
- Light mode colors: white backgrounds, dark text, vibrant accents
- System theme detection with proper class application
🌈 **Enhanced Theme System**
- 4 color themes work in both light and dark modes
- Cyber (cyan/magenta), Ocean (blue/teal), Forest (green/lime), Sunset (orange/purple)
- Theme-specific glassmorphism effects
- Proper contrast in light mode
✨ **Premium Animations**
- Float, glow-pulse, slide-in, scale-in, rotate-in animations
- Smooth page transitions
- Hover effects with depth (lift, glow, scale)
- Micro-interactions on all interactive elements
🎯 **Visual Polish**
- Enhanced glassmorphism for light/dark modes
- Custom scrollbar with theme colors
- Beautiful text selection
- Focus indicators for accessibility
- Premium utility classes
🔧 **Technical Improvements**
- Updated UIStore to properly apply light/dark classes
- Added data-theme attribute for CSS targeting
- Smooth scroll behavior
- Optimized transitions
The app is now a visual masterpiece with perfect light/dark mode support!
2026-01-11 01:32:21 +00:00
|
|
|
import '@testing-library/jest-dom/vitest';
|
2025-12-03 21:56:50 +00:00
|
|
|
import { Tooltip } from './tooltip';
|
|
|
|
|
|
|
|
|
|
describe('Tooltip Component', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
vi.useFakeTimers();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
vi.runOnlyPendingTimers();
|
|
|
|
|
vi.useRealTimers();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders children correctly', () => {
|
|
|
|
|
render(
|
|
|
|
|
<Tooltip content="Tooltip text">
|
|
|
|
|
<button>Hover me</button>
|
2025-12-13 02:34:34 +00:00
|
|
|
</Tooltip>,
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText('Hover me')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('does not show tooltip initially', () => {
|
|
|
|
|
render(
|
|
|
|
|
<Tooltip content="Tooltip text">
|
|
|
|
|
<button>Hover me</button>
|
2025-12-13 02:34:34 +00:00
|
|
|
</Tooltip>,
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(screen.queryByText('Tooltip text')).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows tooltip on hover after delay', () => {
|
|
|
|
|
render(
|
|
|
|
|
<Tooltip content="Tooltip text" delay={300}>
|
|
|
|
|
<button>Hover me</button>
|
2025-12-13 02:34:34 +00:00
|
|
|
</Tooltip>,
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const button = screen.getByText('Hover me');
|
|
|
|
|
fireEvent.mouseEnter(button);
|
|
|
|
|
|
|
|
|
|
// Avancer le temps pour déclencher le délai (300ms) et le setTimeout imbriqué (10ms)
|
|
|
|
|
vi.advanceTimersByTime(310);
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText('Tooltip text')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('hides tooltip when mouse leaves', () => {
|
|
|
|
|
render(
|
|
|
|
|
<Tooltip content="Tooltip text" delay={0}>
|
|
|
|
|
<button>Hover me</button>
|
2025-12-13 02:34:34 +00:00
|
|
|
</Tooltip>,
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const button = screen.getByText('Hover me');
|
|
|
|
|
fireEvent.mouseEnter(button);
|
|
|
|
|
|
|
|
|
|
// Avancer pour le setTimeout(..., 0)
|
|
|
|
|
vi.advanceTimersByTime(10);
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText('Tooltip text')).toBeInTheDocument();
|
|
|
|
|
|
|
|
|
|
fireEvent.mouseLeave(button);
|
|
|
|
|
// Avancer pour la fin de l'animation (200ms)
|
|
|
|
|
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', () => {
|
|
|
|
|
render(
|
|
|
|
|
<Tooltip content="Tooltip text" delay={0}>
|
|
|
|
|
<button>Hover me</button>
|
2025-12-13 02:34:34 +00:00
|
|
|
</Tooltip>,
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const button = screen.getByText('Hover me');
|
|
|
|
|
fireEvent.mouseEnter(button);
|
|
|
|
|
|
|
|
|
|
// Avancer pour le setTimeout(..., 0)
|
|
|
|
|
vi.advanceTimersByTime(10);
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText('Tooltip text')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('applies top position by default', () => {
|
|
|
|
|
render(
|
|
|
|
|
<Tooltip content="Tooltip text" delay={0}>
|
|
|
|
|
<button>Hover me</button>
|
2025-12-13 02:34:34 +00:00
|
|
|
</Tooltip>,
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const button = screen.getByText('Hover me');
|
|
|
|
|
fireEvent.mouseEnter(button);
|
|
|
|
|
|
|
|
|
|
vi.advanceTimersByTime(10);
|
|
|
|
|
|
|
|
|
|
const tooltip = screen.getByText('Tooltip text');
|
|
|
|
|
expect(tooltip.closest('.bottom-full')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('applies bottom position', () => {
|
|
|
|
|
render(
|
|
|
|
|
<Tooltip content="Tooltip text" position="bottom" delay={0}>
|
|
|
|
|
<button>Hover me</button>
|
2025-12-13 02:34:34 +00:00
|
|
|
</Tooltip>,
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const button = screen.getByText('Hover me');
|
|
|
|
|
fireEvent.mouseEnter(button);
|
|
|
|
|
|
|
|
|
|
vi.advanceTimersByTime(10);
|
|
|
|
|
|
|
|
|
|
const tooltip = screen.getByText('Tooltip text');
|
|
|
|
|
expect(tooltip.closest('.top-full')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('applies left position', () => {
|
|
|
|
|
render(
|
|
|
|
|
<Tooltip content="Tooltip text" position="left" delay={0}>
|
|
|
|
|
<button>Hover me</button>
|
2025-12-13 02:34:34 +00:00
|
|
|
</Tooltip>,
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const button = screen.getByText('Hover me');
|
|
|
|
|
fireEvent.mouseEnter(button);
|
|
|
|
|
|
|
|
|
|
vi.advanceTimersByTime(10);
|
|
|
|
|
|
|
|
|
|
const tooltip = screen.getByText('Tooltip text');
|
|
|
|
|
expect(tooltip.closest('.right-full')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('applies right position', () => {
|
|
|
|
|
render(
|
|
|
|
|
<Tooltip content="Tooltip text" position="right" delay={0}>
|
|
|
|
|
<button>Hover me</button>
|
2025-12-13 02:34:34 +00:00
|
|
|
</Tooltip>,
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const button = screen.getByText('Hover me');
|
|
|
|
|
fireEvent.mouseEnter(button);
|
|
|
|
|
|
|
|
|
|
vi.advanceTimersByTime(10);
|
|
|
|
|
|
|
|
|
|
const tooltip = screen.getByText('Tooltip text');
|
|
|
|
|
expect(tooltip.closest('.left-full')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('does not show tooltip when disabled', () => {
|
|
|
|
|
render(
|
|
|
|
|
<Tooltip content="Tooltip text" disabled delay={0}>
|
|
|
|
|
<button>Hover me</button>
|
2025-12-13 02:34:34 +00:00
|
|
|
</Tooltip>,
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const button = screen.getByText('Hover me');
|
|
|
|
|
fireEvent.mouseEnter(button);
|
|
|
|
|
|
|
|
|
|
vi.advanceTimersByTime(10);
|
|
|
|
|
|
|
|
|
|
expect(screen.queryByText('Tooltip text')).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders React node as content', () => {
|
|
|
|
|
render(
|
|
|
|
|
<Tooltip content={<span>Custom content</span>} delay={0}>
|
|
|
|
|
<button>Hover me</button>
|
2025-12-13 02:34:34 +00:00
|
|
|
</Tooltip>,
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const button = screen.getByText('Hover me');
|
|
|
|
|
fireEvent.mouseEnter(button);
|
|
|
|
|
|
|
|
|
|
vi.advanceTimersByTime(10);
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText('Custom content')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('applies custom className', () => {
|
|
|
|
|
render(
|
|
|
|
|
<Tooltip content="Tooltip text" className="custom-tooltip" delay={0}>
|
|
|
|
|
<button>Hover me</button>
|
2025-12-13 02:34:34 +00:00
|
|
|
</Tooltip>,
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const button = screen.getByText('Hover me');
|
|
|
|
|
fireEvent.mouseEnter(button);
|
|
|
|
|
|
|
|
|
|
vi.advanceTimersByTime(10);
|
|
|
|
|
|
|
|
|
|
const tooltip = screen.getByText('Tooltip text');
|
|
|
|
|
expect(tooltip.closest('.custom-tooltip')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('has correct ARIA role', () => {
|
|
|
|
|
render(
|
|
|
|
|
<Tooltip content="Tooltip text" delay={0}>
|
|
|
|
|
<button>Hover me</button>
|
2025-12-13 02:34:34 +00:00
|
|
|
</Tooltip>,
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const button = screen.getByText('Hover me');
|
|
|
|
|
fireEvent.mouseEnter(button);
|
|
|
|
|
|
|
|
|
|
vi.advanceTimersByTime(10);
|
|
|
|
|
|
|
|
|
|
const tooltip = screen.getByRole('tooltip');
|
|
|
|
|
expect(tooltip).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('cancels tooltip display if mouse leaves before delay', () => {
|
|
|
|
|
render(
|
|
|
|
|
<Tooltip content="Tooltip text" delay={300}>
|
|
|
|
|
<button>Hover me</button>
|
2025-12-13 02:34:34 +00:00
|
|
|
</Tooltip>,
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const button = screen.getByText('Hover me');
|
|
|
|
|
fireEvent.mouseEnter(button);
|
|
|
|
|
|
|
|
|
|
// Avancer le temps mais pas assez pour déclencher l'affichage
|
|
|
|
|
vi.advanceTimersByTime(200);
|
|
|
|
|
|
|
|
|
|
fireEvent.mouseLeave(button);
|
|
|
|
|
|
|
|
|
|
// Avancer le reste du temps
|
|
|
|
|
vi.advanceTimersByTime(200);
|
|
|
|
|
|
|
|
|
|
expect(screen.queryByText('Tooltip text')).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Triggers', () => {
|
|
|
|
|
it('shows tooltip on click when trigger is click', async () => {
|
|
|
|
|
const user = userEvent.setup({ delay: null });
|
|
|
|
|
render(
|
|
|
|
|
<Tooltip content="Click tooltip" trigger="click" delay={0}>
|
|
|
|
|
<button>Click me</button>
|
2025-12-13 02:34:34 +00:00
|
|
|
</Tooltip>,
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const button = screen.getByText('Click me');
|
|
|
|
|
await user.click(button);
|
|
|
|
|
|
|
|
|
|
vi.advanceTimersByTime(10);
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText('Click tooltip')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('toggles tooltip on click when trigger is click', async () => {
|
|
|
|
|
const user = userEvent.setup({ delay: null });
|
|
|
|
|
render(
|
|
|
|
|
<Tooltip content="Click tooltip" trigger="click" delay={0}>
|
|
|
|
|
<button>Click me</button>
|
2025-12-13 02:34:34 +00:00
|
|
|
</Tooltip>,
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const button = screen.getByText('Click me');
|
2025-12-13 02:34:34 +00:00
|
|
|
|
2025-12-03 21:56:50 +00:00
|
|
|
// Premier clic
|
|
|
|
|
await user.click(button);
|
|
|
|
|
vi.advanceTimersByTime(10);
|
|
|
|
|
expect(screen.getByText('Click tooltip')).toBeInTheDocument();
|
|
|
|
|
|
|
|
|
|
// Deuxième clic pour fermer
|
|
|
|
|
await user.click(button);
|
|
|
|
|
vi.advanceTimersByTime(300);
|
2025-12-13 02:34:34 +00:00
|
|
|
|
2025-12-03 21:56:50 +00:00
|
|
|
// 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', () => {
|
|
|
|
|
render(
|
|
|
|
|
<Tooltip content="Focus tooltip" trigger="focus" delay={0}>
|
|
|
|
|
<input type="text" placeholder="Focus me" />
|
2025-12-13 02:34:34 +00:00
|
|
|
</Tooltip>,
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const input = screen.getByPlaceholderText('Focus me');
|
|
|
|
|
fireEvent.focus(input);
|
|
|
|
|
|
|
|
|
|
vi.advanceTimersByTime(10);
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText('Focus tooltip')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('hides tooltip on blur when trigger is focus', () => {
|
|
|
|
|
render(
|
|
|
|
|
<Tooltip content="Focus tooltip" trigger="focus" delay={0}>
|
|
|
|
|
<input type="text" placeholder="Focus me" />
|
2025-12-13 02:34:34 +00:00
|
|
|
</Tooltip>,
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const input = screen.getByPlaceholderText('Focus me');
|
|
|
|
|
fireEvent.focus(input);
|
|
|
|
|
vi.advanceTimersByTime(10);
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText('Focus tooltip')).toBeInTheDocument();
|
|
|
|
|
|
|
|
|
|
fireEvent.blur(input);
|
|
|
|
|
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', () => {
|
|
|
|
|
render(
|
|
|
|
|
<Tooltip content="With arrow" showArrow delay={0}>
|
|
|
|
|
<button>Hover me</button>
|
2025-12-13 02:34:34 +00:00
|
|
|
</Tooltip>,
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const button = screen.getByText('Hover me');
|
|
|
|
|
fireEvent.mouseEnter(button);
|
|
|
|
|
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', () => {
|
|
|
|
|
render(
|
|
|
|
|
<Tooltip content="No arrow" showArrow={false} delay={0}>
|
|
|
|
|
<button>Hover me</button>
|
2025-12-13 02:34:34 +00:00
|
|
|
</Tooltip>,
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const button = screen.getByText('Hover me');
|
|
|
|
|
fireEvent.mouseEnter(button);
|
|
|
|
|
vi.advanceTimersByTime(10);
|
|
|
|
|
|
|
|
|
|
const tooltip = screen.getByText('No arrow');
|
|
|
|
|
const arrow = tooltip.parentElement?.querySelector('.border-4');
|
|
|
|
|
expect(arrow).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('applies maxWidth style', () => {
|
|
|
|
|
render(
|
|
|
|
|
<Tooltip content="Limited width" maxWidth={200} delay={0}>
|
|
|
|
|
<button>Hover me</button>
|
2025-12-13 02:34:34 +00:00
|
|
|
</Tooltip>,
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const button = screen.getByText('Hover me');
|
|
|
|
|
fireEvent.mouseEnter(button);
|
|
|
|
|
vi.advanceTimersByTime(10);
|
|
|
|
|
|
|
|
|
|
const tooltip = screen.getByText('Limited width');
|
|
|
|
|
expect(tooltip).toHaveStyle({ maxWidth: '200px' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders rich content with HTML elements', () => {
|
|
|
|
|
render(
|
|
|
|
|
<Tooltip
|
|
|
|
|
content={
|
|
|
|
|
<div>
|
|
|
|
|
<strong>Rich content</strong>
|
|
|
|
|
<p>With multiple elements</p>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
delay={0}
|
|
|
|
|
>
|
|
|
|
|
<button>Hover me</button>
|
2025-12-13 02:34:34 +00:00
|
|
|
</Tooltip>,
|
2025-12-03 21:56:50 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const button = screen.getByText('Hover me');
|
|
|
|
|
fireEvent.mouseEnter(button);
|
|
|
|
|
vi.advanceTimersByTime(10);
|
|
|
|
|
|
|
|
|
|
expect(screen.getByText('Rich content')).toBeInTheDocument();
|
|
|
|
|
expect(screen.getByText('With multiple elements')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|