Add staff permission controls for editing staff and customers
- 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>
This commit is contained in:
@@ -0,0 +1,576 @@
|
||||
/**
|
||||
* Unit tests for WidgetConfigModal component
|
||||
*
|
||||
* Tests cover:
|
||||
* - Component rendering and visibility
|
||||
* - Modal open/close behavior
|
||||
* - Widget list display
|
||||
* - Widget toggle functionality
|
||||
* - Active widget highlighting
|
||||
* - Reset layout functionality
|
||||
* - Widget icons display
|
||||
* - Internationalization (i18n)
|
||||
* - Accessibility
|
||||
* - Backdrop click handling
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import WidgetConfigModal from '../WidgetConfigModal';
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'dashboard.configureWidgets': 'Configure Widgets',
|
||||
'dashboard.configureWidgetsDescription': 'Choose which widgets to display on your dashboard',
|
||||
'dashboard.resetToDefault': 'Reset to Default',
|
||||
'dashboard.done': 'Done',
|
||||
// Widget titles
|
||||
'dashboard.widgetTitles.appointmentsMetric': 'Total Appointments',
|
||||
'dashboard.widgetTitles.customersMetric': 'Active Customers',
|
||||
'dashboard.widgetTitles.servicesMetric': 'Services',
|
||||
'dashboard.widgetTitles.resourcesMetric': 'Resources',
|
||||
'dashboard.widgetTitles.revenueChart': 'Revenue',
|
||||
'dashboard.widgetTitles.appointmentsChart': 'Appointments Trend',
|
||||
'dashboard.widgetTitles.openTickets': 'Open Tickets',
|
||||
'dashboard.widgetTitles.recentActivity': 'Recent Activity',
|
||||
'dashboard.widgetTitles.capacityUtilization': 'Capacity Utilization',
|
||||
'dashboard.widgetTitles.noShowRate': 'No-Show Rate',
|
||||
'dashboard.widgetTitles.customerBreakdown': 'New vs Returning',
|
||||
// Widget descriptions
|
||||
'dashboard.widgetDescriptions.appointmentsMetric': 'Shows appointment count with weekly and monthly growth',
|
||||
'dashboard.widgetDescriptions.customersMetric': 'Shows customer count with weekly and monthly growth',
|
||||
'dashboard.widgetDescriptions.servicesMetric': 'Shows number of services offered',
|
||||
'dashboard.widgetDescriptions.resourcesMetric': 'Shows number of resources available',
|
||||
'dashboard.widgetDescriptions.revenueChart': 'Weekly revenue bar chart',
|
||||
'dashboard.widgetDescriptions.appointmentsChart': 'Weekly appointments line chart',
|
||||
'dashboard.widgetDescriptions.openTickets': 'Shows open support tickets requiring attention',
|
||||
'dashboard.widgetDescriptions.recentActivity': 'Timeline of recent business events',
|
||||
'dashboard.widgetDescriptions.capacityUtilization': 'Shows how booked your resources are this week',
|
||||
'dashboard.widgetDescriptions.noShowRate': 'Percentage of appointments marked as no-show',
|
||||
'dashboard.widgetDescriptions.customerBreakdown': 'Customer breakdown this month',
|
||||
};
|
||||
return translations[key] || key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('WidgetConfigModal', () => {
|
||||
const mockOnClose = vi.fn();
|
||||
const mockOnToggleWidget = vi.fn();
|
||||
const mockOnResetLayout = vi.fn();
|
||||
|
||||
const defaultProps = {
|
||||
isOpen: true,
|
||||
onClose: mockOnClose,
|
||||
activeWidgets: ['appointments-metric', 'customers-metric', 'revenue-chart'],
|
||||
onToggleWidget: mockOnToggleWidget,
|
||||
onResetLayout: mockOnResetLayout,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Modal Visibility', () => {
|
||||
it('should render when isOpen is true', () => {
|
||||
render(<WidgetConfigModal {...defaultProps} />);
|
||||
expect(screen.getByText('Configure Widgets')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render when isOpen is false', () => {
|
||||
render(<WidgetConfigModal {...defaultProps} isOpen={false} />);
|
||||
expect(screen.queryByText('Configure Widgets')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should return null when not open', () => {
|
||||
const { container } = render(<WidgetConfigModal {...defaultProps} isOpen={false} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Modal Header', () => {
|
||||
it('should render modal title', () => {
|
||||
render(<WidgetConfigModal {...defaultProps} />);
|
||||
const title = screen.getByText('Configure Widgets');
|
||||
expect(title).toBeInTheDocument();
|
||||
expect(title).toHaveClass('text-lg', 'font-semibold');
|
||||
});
|
||||
|
||||
it('should render close button in header', () => {
|
||||
const { container } = render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
// Close button (X icon) should be present
|
||||
const closeButtons = container.querySelectorAll('button');
|
||||
expect(closeButtons.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should call onClose when header close button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const { container } = render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
// Find the X button in header
|
||||
const buttons = container.querySelectorAll('button');
|
||||
const closeButton = Array.from(buttons).find(btn =>
|
||||
btn.querySelector('svg')
|
||||
) as HTMLElement;
|
||||
|
||||
if (closeButton) {
|
||||
await user.click(closeButton);
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Modal Content', () => {
|
||||
it('should render description text', () => {
|
||||
render(<WidgetConfigModal {...defaultProps} />);
|
||||
expect(screen.getByText('Choose which widgets to display on your dashboard')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render all widget options', () => {
|
||||
render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
// Check for widget titles
|
||||
expect(screen.getByText('Total Appointments')).toBeInTheDocument();
|
||||
expect(screen.getByText('Active Customers')).toBeInTheDocument();
|
||||
expect(screen.getByText('Services')).toBeInTheDocument();
|
||||
expect(screen.getByText('Resources')).toBeInTheDocument();
|
||||
expect(screen.getByText('Revenue')).toBeInTheDocument();
|
||||
expect(screen.getByText('Appointments Trend')).toBeInTheDocument();
|
||||
expect(screen.getByText('Open Tickets')).toBeInTheDocument();
|
||||
expect(screen.getByText('Recent Activity')).toBeInTheDocument();
|
||||
expect(screen.getByText('Capacity Utilization')).toBeInTheDocument();
|
||||
expect(screen.getByText('No-Show Rate')).toBeInTheDocument();
|
||||
expect(screen.getByText('New vs Returning')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render widget descriptions', () => {
|
||||
render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('Shows appointment count with weekly and monthly growth')).toBeInTheDocument();
|
||||
expect(screen.getByText('Timeline of recent business events')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render widget icons', () => {
|
||||
const { container } = render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
// Should have multiple SVG icons
|
||||
const svgs = container.querySelectorAll('svg');
|
||||
expect(svgs.length).toBeGreaterThan(10); // At least one per widget
|
||||
});
|
||||
});
|
||||
|
||||
describe('Widget Selection', () => {
|
||||
it('should highlight active widgets', () => {
|
||||
const { container } = render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
// Active widgets should have brand-500 border
|
||||
const activeWidgets = container.querySelectorAll('.border-brand-500');
|
||||
expect(activeWidgets.length).toBe(defaultProps.activeWidgets.length);
|
||||
});
|
||||
|
||||
it('should show checkmark on active widgets', () => {
|
||||
render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
// Check icons should be present for active widgets
|
||||
const { container } = render(<WidgetConfigModal {...defaultProps} />);
|
||||
const svgs = container.querySelectorAll('svg');
|
||||
|
||||
// Should have check icons (hard to test exact count due to other icons)
|
||||
expect(svgs.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should not highlight inactive widgets', () => {
|
||||
const { container } = render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
// Inactive widgets should have gray border
|
||||
const inactiveWidgets = container.querySelectorAll('.border-gray-200');
|
||||
expect(inactiveWidgets.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should call onToggleWidget when widget is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
const widget = screen.getByText('Total Appointments').closest('button');
|
||||
expect(widget).toBeInTheDocument();
|
||||
|
||||
if (widget) {
|
||||
await user.click(widget);
|
||||
expect(mockOnToggleWidget).toHaveBeenCalledWith('appointments-metric');
|
||||
}
|
||||
});
|
||||
|
||||
it('should call onToggleWidget with correct widget ID', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
const revenueWidget = screen.getByText('Revenue').closest('button');
|
||||
if (revenueWidget) {
|
||||
await user.click(revenueWidget);
|
||||
expect(mockOnToggleWidget).toHaveBeenCalledWith('revenue-chart');
|
||||
}
|
||||
|
||||
const ticketsWidget = screen.getByText('Open Tickets').closest('button');
|
||||
if (ticketsWidget) {
|
||||
await user.click(ticketsWidget);
|
||||
expect(mockOnToggleWidget).toHaveBeenCalledWith('open-tickets');
|
||||
}
|
||||
});
|
||||
|
||||
it('should allow toggling multiple widgets', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
const widget1 = screen.getByText('Services').closest('button');
|
||||
const widget2 = screen.getByText('Resources').closest('button');
|
||||
|
||||
if (widget1) await user.click(widget1);
|
||||
if (widget2) await user.click(widget2);
|
||||
|
||||
expect(mockOnToggleWidget).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Active Widget Styling', () => {
|
||||
it('should apply active styling to selected widgets', () => {
|
||||
const { container } = render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
// Active widgets should have brand colors
|
||||
const brandBg = container.querySelectorAll('.bg-brand-50');
|
||||
expect(brandBg.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should apply inactive styling to unselected widgets', () => {
|
||||
const { container } = render(
|
||||
<WidgetConfigModal
|
||||
{...defaultProps}
|
||||
activeWidgets={['appointments-metric']} // Only one active
|
||||
/>
|
||||
);
|
||||
|
||||
// Many widgets should have gray styling
|
||||
const grayBorders = container.querySelectorAll('.border-gray-200');
|
||||
expect(grayBorders.length).toBeGreaterThan(5);
|
||||
});
|
||||
|
||||
it('should apply different icon colors for active vs inactive', () => {
|
||||
const { container } = render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
// Active widgets should have brand icon colors
|
||||
const brandIcons = container.querySelectorAll('.text-brand-600');
|
||||
expect(brandIcons.length).toBeGreaterThan(0);
|
||||
|
||||
// Inactive widgets should have gray icon colors
|
||||
const grayIcons = container.querySelectorAll('.text-gray-500');
|
||||
expect(grayIcons.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Modal Footer', () => {
|
||||
it('should render reset button', () => {
|
||||
render(<WidgetConfigModal {...defaultProps} />);
|
||||
expect(screen.getByText('Reset to Default')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render done button', () => {
|
||||
render(<WidgetConfigModal {...defaultProps} />);
|
||||
expect(screen.getByText('Done')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onResetLayout when reset button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
const resetButton = screen.getByText('Reset to Default');
|
||||
await user.click(resetButton);
|
||||
|
||||
expect(mockOnResetLayout).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call onClose when done button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
const doneButton = screen.getByText('Done');
|
||||
await user.click(doneButton);
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Backdrop Interaction', () => {
|
||||
it('should render backdrop', () => {
|
||||
const { container } = render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
// Backdrop div with bg-black/50
|
||||
const backdrop = container.querySelector('.bg-black\\/50');
|
||||
expect(backdrop).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onClose when backdrop is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const { container } = render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
const backdrop = container.querySelector('.bg-black\\/50') as HTMLElement;
|
||||
expect(backdrop).toBeInTheDocument();
|
||||
|
||||
if (backdrop) {
|
||||
await user.click(backdrop);
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not call onClose when modal content is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const { container } = render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
// Click on modal content, not backdrop
|
||||
const modalContent = container.querySelector('.bg-white') as HTMLElement;
|
||||
if (modalContent) {
|
||||
await user.click(modalContent);
|
||||
expect(mockOnClose).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Widget Grid Layout', () => {
|
||||
it('should display widgets in a grid', () => {
|
||||
const { container } = render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
// Grid container should exist
|
||||
const grid = container.querySelector('.grid');
|
||||
expect(grid).toBeInTheDocument();
|
||||
expect(grid).toHaveClass('grid-cols-1', 'sm:grid-cols-2');
|
||||
});
|
||||
|
||||
it('should render all 11 widgets', () => {
|
||||
render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
// Count widget buttons
|
||||
const widgetButtons = screen.getAllByRole('button');
|
||||
// Should have 11 widget buttons + 2 footer buttons + 1 close button = 14 total
|
||||
expect(widgetButtons.length).toBeGreaterThanOrEqual(11);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should apply modal container styles', () => {
|
||||
const { container } = render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
const modal = container.querySelector('.bg-white');
|
||||
expect(modal).toHaveClass(
|
||||
'bg-white',
|
||||
'rounded-xl',
|
||||
'shadow-xl',
|
||||
'max-w-2xl',
|
||||
'w-full'
|
||||
);
|
||||
});
|
||||
|
||||
it('should apply dark mode styles', () => {
|
||||
const { container } = render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
const modal = container.querySelector('.dark\\:bg-gray-800');
|
||||
expect(modal).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should make modal scrollable', () => {
|
||||
const { container } = render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
const scrollableContent = container.querySelector('.overflow-y-auto');
|
||||
expect(scrollableContent).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply max height to modal', () => {
|
||||
const { container } = render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
const modal = container.querySelector('.max-h-\\[80vh\\]');
|
||||
expect(modal).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have semantic HTML structure', () => {
|
||||
const { container } = render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
const headings = container.querySelectorAll('h2');
|
||||
expect(headings.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should have accessible buttons', () => {
|
||||
render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
const buttons = screen.getAllByRole('button');
|
||||
expect(buttons.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should have clear button text', () => {
|
||||
render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('Done')).toBeInTheDocument();
|
||||
expect(screen.getByText('Reset to Default')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have descriptive widget names', () => {
|
||||
render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('Total Appointments')).toBeInTheDocument();
|
||||
expect(screen.getByText('Recent Activity')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Widget Descriptions', () => {
|
||||
it('should show description for each widget', () => {
|
||||
render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
// Check a few widget descriptions
|
||||
expect(screen.getByText('Shows appointment count with weekly and monthly growth')).toBeInTheDocument();
|
||||
expect(screen.getByText('Timeline of recent business events')).toBeInTheDocument();
|
||||
expect(screen.getByText('Shows how booked your resources are this week')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display descriptions in smaller text', () => {
|
||||
const { container } = render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
const descriptions = container.querySelectorAll('.text-xs');
|
||||
expect(descriptions.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty activeWidgets array', () => {
|
||||
render(<WidgetConfigModal {...defaultProps} activeWidgets={[]} />);
|
||||
|
||||
// Should still render all widgets, just none selected
|
||||
expect(screen.getByText('Total Appointments')).toBeInTheDocument();
|
||||
|
||||
// No checkmarks should be visible
|
||||
const { container } = render(<WidgetConfigModal {...defaultProps} activeWidgets={[]} />);
|
||||
const activeWidgets = container.querySelectorAll('.border-brand-500');
|
||||
expect(activeWidgets.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle all widgets active', () => {
|
||||
const allWidgets = [
|
||||
'appointments-metric',
|
||||
'customers-metric',
|
||||
'services-metric',
|
||||
'resources-metric',
|
||||
'revenue-chart',
|
||||
'appointments-chart',
|
||||
'open-tickets',
|
||||
'recent-activity',
|
||||
'capacity-utilization',
|
||||
'no-show-rate',
|
||||
'customer-breakdown',
|
||||
];
|
||||
|
||||
const { container } = render(
|
||||
<WidgetConfigModal {...defaultProps} activeWidgets={allWidgets} />
|
||||
);
|
||||
|
||||
// All widgets should have active styling
|
||||
const activeWidgets = container.querySelectorAll('.border-brand-500');
|
||||
expect(activeWidgets.length).toBe(11);
|
||||
});
|
||||
|
||||
it('should handle rapid widget toggling', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<WidgetConfigModal {...defaultProps} />);
|
||||
|
||||
const widget = screen.getByText('Services').closest('button');
|
||||
|
||||
if (widget) {
|
||||
await user.click(widget);
|
||||
await user.click(widget);
|
||||
await user.click(widget);
|
||||
|
||||
expect(mockOnToggleWidget).toHaveBeenCalledTimes(3);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Internationalization', () => {
|
||||
it('should use translations for modal title', () => {
|
||||
render(<WidgetConfigModal {...defaultProps} />);
|
||||
expect(screen.getByText('Configure Widgets')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should use translations for widget titles', () => {
|
||||
render(<WidgetConfigModal {...defaultProps} />);
|
||||
expect(screen.getByText('Total Appointments')).toBeInTheDocument();
|
||||
expect(screen.getByText('Recent Activity')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should use translations for widget descriptions', () => {
|
||||
render(<WidgetConfigModal {...defaultProps} />);
|
||||
expect(screen.getByText('Timeline of recent business events')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should use translations for buttons', () => {
|
||||
render(<WidgetConfigModal {...defaultProps} />);
|
||||
expect(screen.getByText('Done')).toBeInTheDocument();
|
||||
expect(screen.getByText('Reset to Default')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration', () => {
|
||||
it('should render correctly with all props', () => {
|
||||
const handleClose = vi.fn();
|
||||
const handleToggle = vi.fn();
|
||||
const handleReset = vi.fn();
|
||||
|
||||
render(
|
||||
<WidgetConfigModal
|
||||
isOpen={true}
|
||||
onClose={handleClose}
|
||||
activeWidgets={['appointments-metric', 'revenue-chart']}
|
||||
onToggleWidget={handleToggle}
|
||||
onResetLayout={handleReset}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Configure Widgets')).toBeInTheDocument();
|
||||
expect(screen.getByText('Total Appointments')).toBeInTheDocument();
|
||||
expect(screen.getByText('Done')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should support complete user workflow', async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleClose = vi.fn();
|
||||
const handleToggle = vi.fn();
|
||||
const handleReset = vi.fn();
|
||||
|
||||
render(
|
||||
<WidgetConfigModal
|
||||
isOpen={true}
|
||||
onClose={handleClose}
|
||||
activeWidgets={['appointments-metric']}
|
||||
onToggleWidget={handleToggle}
|
||||
onResetLayout={handleReset}
|
||||
/>
|
||||
);
|
||||
|
||||
// User toggles a widget
|
||||
const widget = screen.getByText('Revenue').closest('button');
|
||||
if (widget) await user.click(widget);
|
||||
expect(handleToggle).toHaveBeenCalledWith('revenue-chart');
|
||||
|
||||
// User resets layout
|
||||
const resetButton = screen.getByText('Reset to Default');
|
||||
await user.click(resetButton);
|
||||
expect(handleReset).toHaveBeenCalledTimes(1);
|
||||
|
||||
// User closes modal
|
||||
const doneButton = screen.getByText('Done');
|
||||
await user.click(doneButton);
|
||||
expect(handleClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user