- Add can_edit_staff and can_edit_customers dangerous permissions - Move Site Builder, Services, Locations, Time Blocks, Payments to Settings permissions - Link Edit Others' Schedules and Edit Own Schedule permissions - Add permission checks to StaffViewSet (partial_update, toggle_active, verify_email) - Add permission checks to CustomerViewSet (update, partial_update, verify_email) - Fix CustomerViewSet permission key mismatch (can_access_customers) - Hide Edit/Verify buttons on Staff and Customers pages without permission - Make dangerous permissions section more visually distinct (darker red) - Fix StaffDashboard links to use correct paths (/dashboard/my-schedule) - Disable settings sub-permissions when Access Settings is unchecked 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
212 lines
6.9 KiB
TypeScript
212 lines
6.9 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { render, screen, fireEvent } from '@testing-library/react';
|
|
import LanguageSelector from '../LanguageSelector';
|
|
|
|
// Create mock function for changeLanguage
|
|
const mockChangeLanguage = vi.fn();
|
|
|
|
// Mock react-i18next
|
|
vi.mock('react-i18next', () => ({
|
|
useTranslation: () => ({
|
|
i18n: {
|
|
language: 'en',
|
|
changeLanguage: mockChangeLanguage,
|
|
},
|
|
}),
|
|
}));
|
|
|
|
// Mock i18n module
|
|
vi.mock('../../i18n', () => ({
|
|
supportedLanguages: [
|
|
{ code: 'en', name: 'English', flag: '🇺🇸' },
|
|
{ code: 'es', name: 'Español', flag: '🇪🇸' },
|
|
{ code: 'fr', name: 'Français', flag: '🇫🇷' },
|
|
],
|
|
}));
|
|
|
|
describe('LanguageSelector', () => {
|
|
beforeEach(() => {
|
|
mockChangeLanguage.mockClear();
|
|
});
|
|
|
|
describe('dropdown variant', () => {
|
|
it('renders dropdown button', () => {
|
|
render(<LanguageSelector />);
|
|
const button = screen.getByRole('button');
|
|
expect(button).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows current language flag by default', () => {
|
|
render(<LanguageSelector />);
|
|
expect(screen.getByText('🇺🇸')).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows current language name on larger screens', () => {
|
|
render(<LanguageSelector />);
|
|
expect(screen.getByText('English')).toBeInTheDocument();
|
|
});
|
|
|
|
it('opens dropdown on click', () => {
|
|
render(<LanguageSelector />);
|
|
const button = screen.getByRole('button');
|
|
fireEvent.click(button);
|
|
expect(screen.getByRole('listbox')).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows all languages when open', () => {
|
|
render(<LanguageSelector />);
|
|
const button = screen.getByRole('button');
|
|
fireEvent.click(button);
|
|
expect(screen.getByText('Español')).toBeInTheDocument();
|
|
expect(screen.getByText('Français')).toBeInTheDocument();
|
|
});
|
|
|
|
it('hides flag when showFlag is false', () => {
|
|
render(<LanguageSelector showFlag={false} />);
|
|
expect(screen.queryByText('🇺🇸')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('applies custom className', () => {
|
|
const { container } = render(<LanguageSelector className="custom-class" />);
|
|
expect(container.firstChild).toHaveClass('custom-class');
|
|
});
|
|
|
|
it('changes language when clicking a language option in dropdown', () => {
|
|
render(<LanguageSelector />);
|
|
const button = screen.getByRole('button');
|
|
fireEvent.click(button);
|
|
|
|
const spanishOption = screen.getByText('Español').closest('button');
|
|
expect(spanishOption).toBeInTheDocument();
|
|
|
|
fireEvent.click(spanishOption!);
|
|
|
|
expect(mockChangeLanguage).toHaveBeenCalledWith('es');
|
|
});
|
|
|
|
it('closes dropdown when language is selected', () => {
|
|
render(<LanguageSelector />);
|
|
const button = screen.getByRole('button');
|
|
fireEvent.click(button);
|
|
|
|
expect(screen.getByRole('listbox')).toBeInTheDocument();
|
|
|
|
const frenchOption = screen.getByText('Français').closest('button');
|
|
fireEvent.click(frenchOption!);
|
|
|
|
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('closes dropdown when clicking outside', () => {
|
|
render(<LanguageSelector />);
|
|
const button = screen.getByRole('button');
|
|
fireEvent.click(button);
|
|
|
|
expect(screen.getByRole('listbox')).toBeInTheDocument();
|
|
|
|
// Click outside the dropdown
|
|
fireEvent.mouseDown(document.body);
|
|
|
|
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('does not close dropdown when clicking inside dropdown', () => {
|
|
render(<LanguageSelector />);
|
|
const button = screen.getByRole('button');
|
|
fireEvent.click(button);
|
|
|
|
expect(screen.getByRole('listbox')).toBeInTheDocument();
|
|
|
|
const listbox = screen.getByRole('listbox');
|
|
fireEvent.mouseDown(listbox);
|
|
|
|
expect(screen.getByRole('listbox')).toBeInTheDocument();
|
|
});
|
|
|
|
it('toggles dropdown open/closed on button clicks', () => {
|
|
render(<LanguageSelector />);
|
|
const button = screen.getByRole('button');
|
|
|
|
// Open dropdown
|
|
fireEvent.click(button);
|
|
expect(screen.getByRole('listbox')).toBeInTheDocument();
|
|
|
|
// Close dropdown
|
|
fireEvent.click(button);
|
|
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('inline variant', () => {
|
|
it('renders all language buttons', () => {
|
|
render(<LanguageSelector variant="inline" />);
|
|
const buttons = screen.getAllByRole('button');
|
|
expect(buttons.length).toBe(3);
|
|
});
|
|
|
|
it('renders language names', () => {
|
|
render(<LanguageSelector variant="inline" />);
|
|
expect(screen.getByText(/English/)).toBeInTheDocument();
|
|
expect(screen.getByText(/Español/)).toBeInTheDocument();
|
|
expect(screen.getByText(/Français/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('highlights current language', () => {
|
|
render(<LanguageSelector variant="inline" />);
|
|
const englishButton = screen.getByText(/English/).closest('button');
|
|
expect(englishButton).toHaveClass('bg-brand-600');
|
|
});
|
|
|
|
it('shows flags by default', () => {
|
|
render(<LanguageSelector variant="inline" />);
|
|
expect(screen.getByText(/🇺🇸/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('changes language when clicking a language button', () => {
|
|
render(<LanguageSelector variant="inline" />);
|
|
|
|
const spanishButton = screen.getByText(/Español/).closest('button');
|
|
expect(spanishButton).toBeInTheDocument();
|
|
|
|
fireEvent.click(spanishButton!);
|
|
|
|
expect(mockChangeLanguage).toHaveBeenCalledWith('es');
|
|
});
|
|
|
|
it('calls changeLanguage with correct code for each language', () => {
|
|
render(<LanguageSelector variant="inline" />);
|
|
|
|
// Test English
|
|
const englishButton = screen.getByText(/English/).closest('button');
|
|
fireEvent.click(englishButton!);
|
|
expect(mockChangeLanguage).toHaveBeenCalledWith('en');
|
|
|
|
mockChangeLanguage.mockClear();
|
|
|
|
// Test French
|
|
const frenchButton = screen.getByText(/Français/).closest('button');
|
|
fireEvent.click(frenchButton!);
|
|
expect(mockChangeLanguage).toHaveBeenCalledWith('fr');
|
|
});
|
|
|
|
it('hides flags when showFlag is false', () => {
|
|
render(<LanguageSelector variant="inline" showFlag={false} />);
|
|
|
|
// Flags should not be visible
|
|
expect(screen.queryByText('🇺🇸')).not.toBeInTheDocument();
|
|
expect(screen.queryByText('🇪🇸')).not.toBeInTheDocument();
|
|
expect(screen.queryByText('🇫🇷')).not.toBeInTheDocument();
|
|
|
|
// But names should still be there
|
|
expect(screen.getByText('English')).toBeInTheDocument();
|
|
expect(screen.getByText('Español')).toBeInTheDocument();
|
|
expect(screen.getByText('Français')).toBeInTheDocument();
|
|
});
|
|
|
|
it('applies custom className', () => {
|
|
const { container } = render(<LanguageSelector variant="inline" className="custom-inline-class" />);
|
|
expect(container.firstChild).toHaveClass('custom-inline-class');
|
|
});
|
|
});
|
|
});
|