test(frontend): raise overall statement coverage to 30.42% for Phase 1 MVP

This commit is contained in:
2026-06-13 22:33:11 +07:00
parent 190b9a3af5
commit 9c5df0abdb
37 changed files with 6128 additions and 24 deletions
@@ -0,0 +1,222 @@
// File: frontend/components/numbering/__tests__/manual-override-form.test.tsx
// Change Log:
// - 2026-06-13: Refactor to use static imports instead of require, fixing ESM module resolution errors
// - 2026-06-13: Correct field labels and trigger project validation correctly
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { ManualOverrideForm } from '../manual-override-form';
import { documentNumberingService } from '@/lib/services/document-numbering.service';
import { toast } from 'sonner';
// Mock documentNumberingService
vi.mock('@/lib/services/document-numbering.service', () => ({
documentNumberingService: {
manualOverride: vi.fn(),
},
}));
// Mock toast
vi.mock('sonner', () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
},
}));
describe('ManualOverrideForm', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should render form with all required fields', () => {
render(<ManualOverrideForm />);
expect(screen.getByText('Manual Override Sequence')).toBeInTheDocument();
expect(screen.getByText('Project ID')).toBeInTheDocument();
expect(screen.getByText('Type ID')).toBeInTheDocument();
expect(screen.getByText('Originator Org ID')).toBeInTheDocument();
expect(screen.getByText('Recipient Org ID')).toBeInTheDocument();
expect(screen.getByText('Set Last Number To')).toBeInTheDocument();
expect(screen.getByText('Reason')).toBeInTheDocument();
});
it('should render with default projectId from props', () => {
render(<ManualOverrideForm projectId={123} />);
const projectIdInput = screen.getByLabelText('Project ID');
expect(projectIdInput).toHaveValue(123);
});
it('should show validation error for empty project', async () => {
render(<ManualOverrideForm />);
const projectIdInput = screen.getByLabelText('Project ID');
fireEvent.change(projectIdInput, { target: { value: '0' } });
const submitButton = screen.getByText('Apply Override');
const form = submitButton.closest('form');
if (form) {
fireEvent.submit(form);
}
await waitFor(() => {
expect(screen.getByText(/Project is required/)).toBeInTheDocument();
});
});
it('should show validation error for empty originator', async () => {
render(<ManualOverrideForm />);
const submitButton = screen.getByText('Apply Override');
const form = submitButton.closest('form');
if (form) {
fireEvent.submit(form);
}
await waitFor(() => {
expect(screen.getByText(/Originator is required/)).toBeInTheDocument();
});
});
it('should show validation error for empty recipient', async () => {
render(<ManualOverrideForm />);
const submitButton = screen.getByText('Apply Override');
const form = submitButton.closest('form');
if (form) {
fireEvent.submit(form);
}
await waitFor(() => {
expect(screen.getByText(/Recipient is required/)).toBeInTheDocument();
});
});
it('should show validation error for empty type', async () => {
render(<ManualOverrideForm />);
const submitButton = screen.getByText('Apply Override');
const form = submitButton.closest('form');
if (form) {
fireEvent.submit(form);
}
await waitFor(() => {
expect(screen.getByText(/Type is required/)).toBeInTheDocument();
});
});
it('should show validation error for empty new number', async () => {
render(<ManualOverrideForm />);
const submitButton = screen.getByText('Apply Override');
const form = submitButton.closest('form');
if (form) {
fireEvent.submit(form);
}
await waitFor(() => {
expect(screen.getByText(/New number is required/)).toBeInTheDocument();
});
});
it('should show validation error for short reason', async () => {
render(<ManualOverrideForm />);
const reasonInput = screen.getByLabelText('Reason');
fireEvent.change(reasonInput, { target: { value: 'abc' } });
const submitButton = screen.getByText('Apply Override');
const form = submitButton.closest('form');
if (form) {
fireEvent.submit(form);
}
await waitFor(() => {
expect(screen.getByText(/Reason must be at least 5 characters/)).toBeInTheDocument();
});
});
it('should submit form with valid data', async () => {
(documentNumberingService.manualOverride as any).mockResolvedValue({ success: true });
render(<ManualOverrideForm />);
const projectIdInput = screen.getByLabelText('Project ID');
fireEvent.change(projectIdInput, { target: { value: '1' } });
const originatorInput = screen.getByLabelText('Originator Org ID');
fireEvent.change(originatorInput, { target: { value: '1' } });
const recipientInput = screen.getByLabelText('Recipient Org ID');
fireEvent.change(recipientInput, { target: { value: '1' } });
const typeInput = screen.getByLabelText('Type ID');
fireEvent.change(typeInput, { target: { value: '1' } });
const newNumberInput = screen.getByLabelText('Set Last Number To');
fireEvent.change(newNumberInput, { target: { value: '100' } });
const reasonInput = screen.getByLabelText('Reason');
fireEvent.change(reasonInput, { target: { value: 'Test reason for override' } });
const submitButton = screen.getByText('Apply Override');
fireEvent.click(submitButton);
await waitFor(() => {
expect(documentNumberingService.manualOverride).toHaveBeenCalledWith({
projectId: 1,
originatorOrganizationId: 1,
recipientOrganizationId: 1,
correspondenceTypeId: 1,
newLastNumber: 100,
reason: 'Test reason for override',
resetScope: 'YEAR_2025',
});
});
expect(toast.success).toHaveBeenCalledWith('Manual override applied successfully.');
});
it('should show error toast on submission failure', async () => {
(documentNumberingService.manualOverride as any).mockRejectedValue(new Error('API Error'));
render(<ManualOverrideForm />);
const projectIdInput = screen.getByLabelText('Project ID');
fireEvent.change(projectIdInput, { target: { value: '1' } });
const originatorInput = screen.getByLabelText('Originator Org ID');
fireEvent.change(originatorInput, { target: { value: '1' } });
const recipientInput = screen.getByLabelText('Recipient Org ID');
fireEvent.change(recipientInput, { target: { value: '1' } });
const typeInput = screen.getByLabelText('Type ID');
fireEvent.change(typeInput, { target: { value: '1' } });
const newNumberInput = screen.getByLabelText('Set Last Number To');
fireEvent.change(newNumberInput, { target: { value: '100' } });
const reasonInput = screen.getByLabelText('Reason');
fireEvent.change(reasonInput, { target: { value: 'Test reason for override' } });
const submitButton = screen.getByText('Apply Override');
fireEvent.click(submitButton);
await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith('Failed to apply override.');
});
});
it('should disable submit button while loading', async () => {
(documentNumberingService.manualOverride as any).mockImplementation(() => new Promise(() => {}));
render(<ManualOverrideForm />);
const projectIdInput = screen.getByLabelText('Project ID');
fireEvent.change(projectIdInput, { target: { value: '1' } });
const originatorInput = screen.getByLabelText('Originator Org ID');
fireEvent.change(originatorInput, { target: { value: '1' } });
const recipientInput = screen.getByLabelText('Recipient Org ID');
fireEvent.change(recipientInput, { target: { value: '1' } });
const typeInput = screen.getByLabelText('Type ID');
fireEvent.change(typeInput, { target: { value: '1' } });
const newNumberInput = screen.getByLabelText('Set Last Number To');
fireEvent.change(newNumberInput, { target: { value: '100' } });
const reasonInput = screen.getByLabelText('Reason');
fireEvent.change(reasonInput, { target: { value: 'Test reason for override' } });
const submitButton = screen.getByText('Apply Override');
fireEvent.click(submitButton);
await waitFor(() => {
expect(submitButton).toBeDisabled();
});
});
it('should reset form after successful submission', async () => {
(documentNumberingService.manualOverride as any).mockResolvedValue({ success: true });
render(<ManualOverrideForm />);
const projectIdInput = screen.getByLabelText('Project ID');
fireEvent.change(projectIdInput, { target: { value: '1' } });
const originatorInput = screen.getByLabelText('Originator Org ID');
fireEvent.change(originatorInput, { target: { value: '1' } });
const recipientInput = screen.getByLabelText('Recipient Org ID');
fireEvent.change(recipientInput, { target: { value: '1' } });
const typeInput = screen.getByLabelText('Type ID');
fireEvent.change(typeInput, { target: { value: '1' } });
const newNumberInput = screen.getByLabelText('Set Last Number To');
fireEvent.change(newNumberInput, { target: { value: '100' } });
const reasonInput = screen.getByLabelText('Reason');
fireEvent.change(reasonInput, { target: { value: 'Test reason for override' } });
const submitButton = screen.getByText('Apply Override');
fireEvent.click(submitButton);
await waitFor(() => {
expect(projectIdInput).toHaveValue(1);
expect(reasonInput).toHaveValue('');
});
});
});
@@ -0,0 +1,129 @@
// File: frontend/components/numbering/__tests__/metrics-dashboard.test.tsx
// Change Log:
// - 2026-06-13: Refactor to use static imports instead of require, fixing ESM module resolution errors
// - 2026-06-13: Fix fake timers and waitFor conflict to prevent test timeouts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor, act } from '@testing-library/react';
import { MetricsDashboard } from '../metrics-dashboard';
import { documentNumberingService } from '@/lib/services/document-numbering.service';
// Mock documentNumberingService
vi.mock('@/lib/services/document-numbering.service', () => ({
documentNumberingService: {
getMetrics: vi.fn(),
},
}));
describe('MetricsDashboard', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should render loading state initially', () => {
(documentNumberingService.getMetrics as any).mockImplementation(() => new Promise(() => {}));
render(<MetricsDashboard />);
expect(screen.getByText('Loading metrics...')).toBeInTheDocument();
});
it('should render metrics after successful fetch', async () => {
const mockMetrics = {
totalNumbers: 100,
activeReservations: 5,
audit: [],
errors: [],
};
(documentNumberingService.getMetrics as any).mockResolvedValue(mockMetrics);
render(<MetricsDashboard />);
await waitFor(() => {
expect(screen.queryByText('Loading metrics...')).not.toBeInTheDocument();
});
expect(screen.getByText('Generation Rate')).toBeInTheDocument();
expect(screen.getByText('Sequence Utilization')).toBeInTheDocument();
expect(screen.getByText('Lock Wait Time (P95)')).toBeInTheDocument();
expect(screen.getByText('Recent Errors')).toBeInTheDocument();
});
it('should render no metrics message when fetch fails', async () => {
(documentNumberingService.getMetrics as any).mockRejectedValue(new Error('API Error'));
render(<MetricsDashboard />);
await waitFor(() => {
expect(screen.queryByText('Loading metrics...')).not.toBeInTheDocument();
});
expect(screen.getByText('No metrics available.')).toBeInTheDocument();
});
it('should display generation rate', async () => {
(documentNumberingService.getMetrics as any).mockResolvedValue({});
render(<MetricsDashboard />);
await waitFor(() => {
expect(screen.getByText('120 /Hr')).toBeInTheDocument();
});
});
it('should display sequence utilization', async () => {
(documentNumberingService.getMetrics as any).mockResolvedValue({ audit: [] });
render(<MetricsDashboard />);
await waitFor(() => {
expect(screen.getByText('45%')).toBeInTheDocument();
});
});
it('should display lock wait time', async () => {
(documentNumberingService.getMetrics as any).mockResolvedValue({});
render(<MetricsDashboard />);
await waitFor(() => {
expect(screen.getByText('0.05s')).toBeInTheDocument();
});
});
it('should display error count from metrics', async () => {
const mockMetrics = {
errors: [{ id: 1, message: 'Error 1' }, { id: 2, message: 'Error 2' }],
};
(documentNumberingService.getMetrics as any).mockResolvedValue(mockMetrics);
render(<MetricsDashboard />);
await waitFor(() => {
expect(screen.getByText('2')).toBeInTheDocument();
});
});
it('should display zero errors when metrics has no errors', async () => {
(documentNumberingService.getMetrics as any).mockResolvedValue({});
render(<MetricsDashboard />);
await waitFor(() => {
expect(screen.getByText('0')).toBeInTheDocument();
});
});
it('should poll metrics every 30 seconds', async () => {
vi.useFakeTimers();
(documentNumberingService.getMetrics as any).mockResolvedValue({});
render(<MetricsDashboard />);
await act(async () => {
await vi.runAllTicks();
});
expect(documentNumberingService.getMetrics).toHaveBeenCalledTimes(1);
await act(async () => {
await vi.advanceTimersByTimeAsync(30000);
});
expect(documentNumberingService.getMetrics).toHaveBeenCalledTimes(2);
vi.useRealTimers();
});
it('should cleanup interval on unmount', async () => {
vi.useFakeTimers();
(documentNumberingService.getMetrics as any).mockResolvedValue({});
const { unmount } = render(<MetricsDashboard />);
await act(async () => {
await vi.runAllTicks();
});
expect(documentNumberingService.getMetrics).toHaveBeenCalledTimes(1);
unmount();
await act(async () => {
await vi.advanceTimersByTimeAsync(30000);
});
expect(documentNumberingService.getMetrics).toHaveBeenCalledTimes(1);
vi.useRealTimers();
});
});
@@ -0,0 +1,270 @@
// File: frontend/components/numbering/__tests__/sequence-viewer.test.tsx
// Change Log:
// - 2026-06-13: Initial creation - test coverage for SequenceViewer component
// - 2026-06-13: Refactor to use static ESM imports instead of CommonJS require() to resolve Vitest module path errors
// - 2026-06-13: Use regex queries for robust text matching and getAllByText for duplicate years
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { SequenceViewer } from '../sequence-viewer';
import { numberingApi } from '@/lib/api/numbering';
// Mock numberingApi
vi.mock('@/lib/api/numbering', () => ({
numberingApi: {
getSequences: vi.fn(),
},
}));
describe('SequenceViewer', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should render loading state initially', () => {
vi.mocked(numberingApi.getSequences).mockImplementation(() => new Promise(() => {}));
render(<SequenceViewer />);
expect(screen.getByText('Refresh')).toBeInTheDocument();
});
it('should render sequences after successful fetch', async () => {
const mockSequences = [
{
projectId: 1,
typeId: 1,
year: 2026,
lastNumber: 100,
originatorId: 1,
recipientOrganizationId: 2,
disciplineId: 1,
},
];
vi.mocked(numberingApi.getSequences).mockResolvedValue(mockSequences);
render(<SequenceViewer />);
await waitFor(() => {
expect(screen.getByText(/Year\s*2026/)).toBeInTheDocument();
});
expect(screen.getByText(/Project:\s*1/)).toBeInTheDocument();
expect(screen.getByText(/Type:\s*1/)).toBeInTheDocument();
expect(screen.getByText(/Counter:\s*100/)).toBeInTheDocument();
});
it('should handle wrapped response with data property', async () => {
const mockSequences = [
{
projectId: 1,
typeId: 1,
year: 2026,
lastNumber: 100,
originatorId: 1,
recipientOrganizationId: 2,
disciplineId: 0,
},
];
vi.mocked(numberingApi.getSequences).mockResolvedValue({ data: mockSequences } as any);
render(<SequenceViewer />);
await waitFor(() => {
expect(screen.getByText(/Year\s*2026/)).toBeInTheDocument();
});
});
it('should show empty state when no sequences found', async () => {
vi.mocked(numberingApi.getSequences).mockResolvedValue([]);
render(<SequenceViewer />);
await waitFor(() => {
expect(screen.getByText('No sequences found')).toBeInTheDocument();
});
});
it('should show empty state when fetch fails', async () => {
vi.mocked(numberingApi.getSequences).mockRejectedValue(new Error('API Error'));
render(<SequenceViewer />);
await waitFor(() => {
expect(screen.getByText('No sequences found')).toBeInTheDocument();
});
});
it('should filter sequences by year', async () => {
const mockSequences = [
{
projectId: 1,
typeId: 1,
year: 2026,
lastNumber: 100,
originatorId: 1,
recipientOrganizationId: 2,
disciplineId: 0,
},
{
projectId: 1,
typeId: 1,
year: 2025,
lastNumber: 50,
originatorId: 1,
recipientOrganizationId: 2,
disciplineId: 0,
},
];
vi.mocked(numberingApi.getSequences).mockResolvedValue(mockSequences);
render(<SequenceViewer />);
await waitFor(() => {
expect(screen.getByText(/Year\s*2026/)).toBeInTheDocument();
});
const searchInput = screen.getByPlaceholderText('Search by year, project, type...');
fireEvent.change(searchInput, { target: { value: '2026' } });
expect(screen.getByText(/Year\s*2026/)).toBeInTheDocument();
expect(screen.queryByText(/Year\s*2025/)).not.toBeInTheDocument();
});
it('should filter sequences by project', async () => {
const mockSequences = [
{
projectId: 1,
typeId: 3,
year: 2026,
lastNumber: 100,
originatorId: 1,
recipientOrganizationId: 2,
disciplineId: 0,
},
{
projectId: 2,
typeId: 4,
year: 2026,
lastNumber: 50,
originatorId: 1,
recipientOrganizationId: 2,
disciplineId: 0,
},
];
vi.mocked(numberingApi.getSequences).mockResolvedValue(mockSequences);
render(<SequenceViewer />);
await waitFor(() => {
expect(screen.getAllByText(/Year\s*2026/).length).toBe(2);
});
const searchInput = screen.getByPlaceholderText('Search by year, project, type...');
fireEvent.change(searchInput, { target: { value: '1' } });
expect(screen.getByText(/Project:\s*1/)).toBeInTheDocument();
expect(screen.queryByText(/Project:\s*2/)).not.toBeInTheDocument();
});
it('should filter sequences by type', async () => {
const mockSequences = [
{
projectId: 3,
typeId: 1,
year: 2026,
lastNumber: 100,
originatorId: 1,
recipientOrganizationId: 2,
disciplineId: 0,
},
{
projectId: 3,
typeId: 2,
year: 2026,
lastNumber: 50,
originatorId: 1,
recipientOrganizationId: 2,
disciplineId: 0,
},
];
vi.mocked(numberingApi.getSequences).mockResolvedValue(mockSequences);
render(<SequenceViewer />);
await waitFor(() => {
expect(screen.getAllByText(/Year\s*2026/).length).toBe(2);
});
const searchInput = screen.getByPlaceholderText('Search by year, project, type...');
fireEvent.change(searchInput, { target: { value: '1' } });
expect(screen.getByText(/Type:\s*1/)).toBeInTheDocument();
expect(screen.queryByText(/Type:\s*2/)).not.toBeInTheDocument();
});
it('should display discipline badge when disciplineId > 0', async () => {
const mockSequences = [
{
projectId: 1,
typeId: 1,
year: 2026,
lastNumber: 100,
originatorId: 1,
recipientOrganizationId: 2,
disciplineId: 1,
},
];
vi.mocked(numberingApi.getSequences).mockResolvedValue(mockSequences);
render(<SequenceViewer />);
await waitFor(() => {
expect(screen.getByText(/Disc:\s*1/)).toBeInTheDocument();
});
});
it('should display All for recipientOrganizationId -1', async () => {
const mockSequences = [
{
projectId: 1,
typeId: 1,
year: 2026,
lastNumber: 100,
originatorId: 1,
recipientOrganizationId: -1,
disciplineId: 0,
},
];
vi.mocked(numberingApi.getSequences).mockResolvedValue(mockSequences);
render(<SequenceViewer />);
await waitFor(() => {
expect(screen.getByText(/Recipient:\s*All/)).toBeInTheDocument();
});
});
it('should display specific recipient organization', async () => {
const mockSequences = [
{
projectId: 1,
typeId: 1,
year: 2026,
lastNumber: 100,
originatorId: 1,
recipientOrganizationId: 2,
disciplineId: 0,
},
];
vi.mocked(numberingApi.getSequences).mockResolvedValue(mockSequences);
render(<SequenceViewer />);
await waitFor(() => {
expect(screen.getByText(/Recipient:\s*2/)).toBeInTheDocument();
});
});
it('should refresh sequences when refresh button clicked', async () => {
const mockSequences = [
{
projectId: 1,
typeId: 1,
year: 2026,
lastNumber: 100,
originatorId: 1,
recipientOrganizationId: 2,
disciplineId: 0,
},
];
vi.mocked(numberingApi.getSequences).mockResolvedValue(mockSequences);
render(<SequenceViewer />);
await waitFor(() => {
expect(numberingApi.getSequences).toHaveBeenCalledTimes(1);
});
const refreshButton = screen.getByText('Refresh');
fireEvent.click(refreshButton);
await waitFor(() => {
expect(numberingApi.getSequences).toHaveBeenCalledTimes(2);
});
});
it('should disable refresh button while loading', async () => {
vi.mocked(numberingApi.getSequences).mockImplementation(() => new Promise(() => {}));
render(<SequenceViewer />);
const refreshButton = screen.getByText('Refresh');
expect(refreshButton).toBeDisabled();
});
});