Backend: - Add TenantCustomTier model for per-tenant feature overrides - Update EntitlementService to check custom tier before plan features - Add custom_tier action on TenantViewSet (GET/PUT/DELETE) - Add Celery task for grace period management (30-day expiry) Frontend: - Add DynamicFeaturesEditor component for dynamic feature management - Fix BusinessEditModal to load features from plan defaults when no custom tier - Update limits (max_users, max_resources, etc.) to use featureValues - Remove outdated canonical feature check from FeaturePicker (removes warning icons) - Add useBillingPlans hook for accessing billing system data - Add custom tier API functions to platform.ts Features now follow consistent rules: - Load from plan defaults when no custom tier exists - Load from custom tier when one exists - Reset to plan defaults when plan changes - Save to custom tier on edit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
158 lines
4.2 KiB
TypeScript
158 lines
4.2 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import { render, screen } from '@testing-library/react';
|
|
import { MemoryRouter } from 'react-router-dom';
|
|
import Scheduler from '../Scheduler';
|
|
|
|
// Mock the outlet context
|
|
const mockOutletContext = vi.fn();
|
|
vi.mock('react-router-dom', async () => {
|
|
const actual = await vi.importActual('react-router-dom');
|
|
return {
|
|
...actual,
|
|
useOutletContext: () => mockOutletContext(),
|
|
};
|
|
});
|
|
|
|
// Mock hooks
|
|
vi.mock('../../hooks/useAppointments', () => ({
|
|
useAppointments: vi.fn(() => ({ data: [], isLoading: false })),
|
|
useUpdateAppointment: vi.fn(),
|
|
useDeleteAppointment: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../../hooks/useResources', () => ({
|
|
useResources: vi.fn(() => ({ data: [], isLoading: false })),
|
|
}));
|
|
|
|
vi.mock('../../hooks/useServices', () => ({
|
|
useServices: vi.fn(() => ({ data: [], isLoading: false })),
|
|
}));
|
|
|
|
// Mock child components
|
|
vi.mock('../ResourceScheduler', () => ({
|
|
default: () => <div data-testid="resource-scheduler">Resource Scheduler</div>,
|
|
}));
|
|
|
|
vi.mock('../OwnerScheduler', () => ({
|
|
default: () => <div data-testid="owner-scheduler">Owner Scheduler</div>,
|
|
}));
|
|
|
|
import { useAppointments } from '../../hooks/useAppointments';
|
|
import { useResources } from '../../hooks/useResources';
|
|
import { useServices } from '../../hooks/useServices';
|
|
|
|
const mockUseAppointments = useAppointments as ReturnType<typeof vi.fn>;
|
|
const mockUseResources = useResources as ReturnType<typeof vi.fn>;
|
|
const mockUseServices = useServices as ReturnType<typeof vi.fn>;
|
|
|
|
describe('Scheduler', () => {
|
|
const defaultContext = {
|
|
user: { id: '1', role: 'owner', email: 'test@test.com' },
|
|
business: { id: '1', name: 'Test Business' },
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockOutletContext.mockReturnValue(defaultContext);
|
|
mockUseAppointments.mockReturnValue({ data: [], isLoading: false });
|
|
mockUseResources.mockReturnValue({ data: [], isLoading: false });
|
|
mockUseServices.mockReturnValue({ data: [], isLoading: false });
|
|
});
|
|
|
|
it('renders loading state when appointments are loading', () => {
|
|
mockUseAppointments.mockReturnValue({ data: [], isLoading: true });
|
|
|
|
render(
|
|
<MemoryRouter>
|
|
<Scheduler />
|
|
</MemoryRouter>
|
|
);
|
|
|
|
expect(screen.getByText('Loading scheduler...')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders loading state when resources are loading', () => {
|
|
mockUseResources.mockReturnValue({ data: [], isLoading: true });
|
|
|
|
render(
|
|
<MemoryRouter>
|
|
<Scheduler />
|
|
</MemoryRouter>
|
|
);
|
|
|
|
expect(screen.getByText('Loading scheduler...')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders loading state when services are loading', () => {
|
|
mockUseServices.mockReturnValue({ data: [], isLoading: true });
|
|
|
|
render(
|
|
<MemoryRouter>
|
|
<Scheduler />
|
|
</MemoryRouter>
|
|
);
|
|
|
|
expect(screen.getByText('Loading scheduler...')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders ResourceScheduler for resource role', () => {
|
|
mockOutletContext.mockReturnValue({
|
|
...defaultContext,
|
|
user: { ...defaultContext.user, role: 'resource' },
|
|
});
|
|
|
|
render(
|
|
<MemoryRouter>
|
|
<Scheduler />
|
|
</MemoryRouter>
|
|
);
|
|
|
|
expect(screen.getByTestId('resource-scheduler')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders OwnerScheduler for owner role', () => {
|
|
mockOutletContext.mockReturnValue({
|
|
...defaultContext,
|
|
user: { ...defaultContext.user, role: 'owner' },
|
|
});
|
|
|
|
render(
|
|
<MemoryRouter>
|
|
<Scheduler />
|
|
</MemoryRouter>
|
|
);
|
|
|
|
expect(screen.getByTestId('owner-scheduler')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders OwnerScheduler for manager role', () => {
|
|
mockOutletContext.mockReturnValue({
|
|
...defaultContext,
|
|
user: { ...defaultContext.user, role: 'manager' },
|
|
});
|
|
|
|
render(
|
|
<MemoryRouter>
|
|
<Scheduler />
|
|
</MemoryRouter>
|
|
);
|
|
|
|
expect(screen.getByTestId('owner-scheduler')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders OwnerScheduler for staff role', () => {
|
|
mockOutletContext.mockReturnValue({
|
|
...defaultContext,
|
|
user: { ...defaultContext.user, role: 'staff' },
|
|
});
|
|
|
|
render(
|
|
<MemoryRouter>
|
|
<Scheduler />
|
|
</MemoryRouter>
|
|
);
|
|
|
|
expect(screen.getByTestId('owner-scheduler')).toBeInTheDocument();
|
|
});
|
|
});
|