690617:1443 237 #01.3
CI / CD Pipeline / build (push) Failing after 7m26s
CI / CD Pipeline / deploy (push) Has been skipped

This commit is contained in:
2026-06-17 14:43:30 +07:00
parent 82b41ad5d9
commit db16c95019
42 changed files with 3084 additions and 352 deletions
@@ -0,0 +1,79 @@
// File: frontend/components/numbering/__tests__/audit-logs-table.test.tsx
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { AuditLogsTable } from '../audit-logs-table';
import { documentNumberingService } from '@/lib/services/document-numbering.service';
vi.mock('@/lib/services/document-numbering.service', () => ({
documentNumberingService: {
getMetrics: vi.fn(),
},
}));
describe('AuditLogsTable', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('renders loading state initially', () => {
vi.mocked(documentNumberingService.getMetrics).mockImplementation(() => new Promise(() => {}));
render(<AuditLogsTable />);
expect(screen.getByText('Loading logs...')).toBeInTheDocument();
});
it('renders empty state when no logs returned', async () => {
vi.mocked(documentNumberingService.getMetrics).mockResolvedValue({ audit: [] } as any);
render(<AuditLogsTable />);
await waitFor(() => {
expect(screen.getByText('No logs found.')).toBeInTheDocument();
});
});
it('renders error state silently as empty state when API fails', async () => {
vi.mocked(documentNumberingService.getMetrics).mockRejectedValue(new Error('API failed'));
render(<AuditLogsTable />);
await waitFor(() => {
expect(screen.getByText('No logs found.')).toBeInTheDocument();
});
});
it('renders logs correctly', async () => {
const mockLogs = [
{
id: 1,
createdAt: '2023-10-27T10:00:00Z',
operation: 'GENERATE',
documentNumber: 'DOC-001',
createdBy: 'UserA',
status: 'SUCCESS',
},
{
id: 2,
createdAt: '2023-10-27T11:00:00Z',
operation: 'VOID',
documentNumber: 'DOC-002',
createdBy: null,
status: 'FAILED',
},
];
vi.mocked(documentNumberingService.getMetrics).mockResolvedValue({ audit: mockLogs } as any);
render(<AuditLogsTable />);
await waitFor(() => {
expect(screen.getByText('DOC-001')).toBeInTheDocument();
});
expect(screen.getByText('GENERATE')).toBeInTheDocument();
expect(screen.getByText('UserA')).toBeInTheDocument();
expect(screen.getByText('SUCCESS')).toBeInTheDocument();
expect(screen.getByText('DOC-002')).toBeInTheDocument();
expect(screen.getByText('VOID')).toBeInTheDocument();
expect(screen.getByText('System')).toBeInTheDocument(); // Falls back to System
expect(screen.getByText('FAILED')).toBeInTheDocument();
});
});
@@ -0,0 +1,83 @@
// File: frontend/components/numbering/__tests__/bulk-import-form.test.tsx
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { BulkImportForm } from '../bulk-import-form';
import { documentNumberingService } from '@/lib/services/document-numbering.service';
import { toast } from 'sonner';
vi.mock('@/lib/services/document-numbering.service', () => ({
documentNumberingService: {
bulkImport: vi.fn(),
},
}));
vi.mock('sonner', () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
},
}));
describe('BulkImportForm', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('renders correctly', () => {
render(<BulkImportForm projectId={1} />);
expect(screen.getByText('Bulk Import Numbers')).toBeInTheDocument();
expect(screen.getByLabelText('CSV File')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Upload & Import' })).toBeDisabled();
});
it('enables submit button when file is selected and handles successful upload', async () => {
const user = userEvent.setup();
render(<BulkImportForm projectId={1} />);
const file = new File(['test'], 'test.csv', { type: 'text/csv' });
const input = screen.getByLabelText('CSV File') as HTMLInputElement;
await user.upload(input, file);
const button = screen.getByRole('button', { name: 'Upload & Import' });
expect(button).not.toBeDisabled();
vi.mocked(documentNumberingService.bulkImport).mockResolvedValue({} as any);
await user.click(button);
expect(documentNumberingService.bulkImport).toHaveBeenCalledWith(expect.any(FormData));
const formDataArg = vi.mocked(documentNumberingService.bulkImport).mock.calls[0][0] as FormData;
expect(formDataArg.get('file')).toBe(file);
expect(formDataArg.get('projectId')).toBe('1');
await waitFor(() => {
expect(toast.success).toHaveBeenCalledWith('Bulk import initiated. Check audit logs for progress.');
});
// File input reset means button is disabled again
expect(button).toBeDisabled();
});
it('handles upload failure', async () => {
const user = userEvent.setup();
render(<BulkImportForm projectId={1} />);
const file = new File(['test'], 'test.csv', { type: 'text/csv' });
const input = screen.getByLabelText('CSV File') as HTMLInputElement;
await user.upload(input, file);
const button = screen.getByRole('button', { name: 'Upload & Import' });
vi.mocked(documentNumberingService.bulkImport).mockRejectedValue(new Error('Failed'));
await user.click(button);
await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith('Failed to import numbers.');
});
});
});
@@ -0,0 +1,113 @@
// File: frontend/components/numbering/__tests__/cancel-number-form.test.tsx
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { CancelNumberForm } from '../cancel-number-form';
import { documentNumberingService } from '@/lib/services/document-numbering.service';
import { toast } from 'sonner';
vi.mock('@/lib/services/document-numbering.service', () => ({
documentNumberingService: {
cancelNumber: vi.fn(),
},
}));
vi.mock('sonner', () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
},
}));
describe('CancelNumberForm', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('renders correctly', () => {
render(<CancelNumberForm />);
expect(screen.getByRole('heading', { name: 'Cancel Number' })).toBeInTheDocument();
expect(screen.getByLabelText('Document Number')).toBeInTheDocument();
expect(screen.getByLabelText('Reason')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Cancel Number' })).toBeInTheDocument();
});
it('shows validation error for empty fields', async () => {
const user = userEvent.setup();
render(<CancelNumberForm />);
const button = screen.getByRole('button', { name: 'Cancel Number' });
await user.click(button);
await waitFor(() => {
expect(screen.getByText('Document Number is required')).toBeInTheDocument();
expect(screen.getByText('Reason must be at least 5 characters')).toBeInTheDocument();
});
expect(documentNumberingService.cancelNumber).not.toHaveBeenCalled();
});
it('shows validation error for short reason', async () => {
const user = userEvent.setup();
render(<CancelNumberForm />);
await user.type(screen.getByLabelText('Document Number'), 'DOC-001');
await user.type(screen.getByLabelText('Reason'), 'abc'); // too short
const button = screen.getByRole('button', { name: 'Cancel Number' });
await user.click(button);
await waitFor(() => {
expect(screen.getByText('Reason must be at least 5 characters')).toBeInTheDocument();
});
expect(documentNumberingService.cancelNumber).not.toHaveBeenCalled();
});
it('handles successful cancellation', async () => {
const user = userEvent.setup();
render(<CancelNumberForm />);
await user.type(screen.getByLabelText('Document Number'), 'DOC-001');
await user.type(screen.getByLabelText('Reason'), 'Generated by mistake');
vi.mocked(documentNumberingService.cancelNumber).mockResolvedValue({} as any);
const button = screen.getByRole('button', { name: 'Cancel Number' });
await user.click(button);
expect(documentNumberingService.cancelNumber).toHaveBeenCalledWith({
documentNumber: 'DOC-001',
reason: 'Generated by mistake',
});
await waitFor(() => {
expect(toast.success).toHaveBeenCalledWith('Number cancelled successfully.');
});
// Check if form was reset
expect(screen.getByLabelText('Document Number')).toHaveValue('');
expect(screen.getByLabelText('Reason')).toHaveValue('');
});
it('handles cancellation failure', async () => {
const user = userEvent.setup();
render(<CancelNumberForm />);
await user.type(screen.getByLabelText('Document Number'), 'DOC-001');
await user.type(screen.getByLabelText('Reason'), 'Generated by mistake');
vi.mocked(documentNumberingService.cancelNumber).mockRejectedValue(new Error('Failed'));
const button = screen.getByRole('button', { name: 'Cancel Number' });
await user.click(button);
await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith('Failed to cancel number. It may not exist or is already cancelled.');
});
// Form is not reset on error
expect(screen.getByLabelText('Document Number')).toHaveValue('DOC-001');
});
});
@@ -0,0 +1,158 @@
// File: frontend/components/numbering/__tests__/template-editor.test.tsx
import React from 'react';
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { TemplateEditor } from '../template-editor';
import { CorrespondenceType, Discipline } from '@/types/master-data';
const mockTypes: CorrespondenceType[] = [
{ publicId: 'type1', typeCode: 'RFA', typeName: 'Request for Approval', isActive: true } as any,
{ publicId: 'type2', typeCode: 'TRN', typeName: 'Transmittal', isActive: true } as any,
];
const mockDisciplines: Discipline[] = [
{ publicId: 'disc1', disciplineCode: 'STR', codeNameEn: 'Structural', isActive: true } as any,
];
describe('TemplateEditor', () => {
const onSave = vi.fn();
const onCancel = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
});
it('renders correctly for new template', () => {
render(
<TemplateEditor
projectId={1}
projectName="Test Project"
correspondenceTypes={mockTypes}
disciplines={mockDisciplines}
onSave={onSave}
onCancel={onCancel}
/>
);
expect(screen.getByText('New Template')).toBeInTheDocument();
expect(screen.getByText('Project: Test Project')).toBeInTheDocument();
expect(screen.getByLabelText('Template Format *')).toHaveValue('');
expect(screen.getByRole('button', { name: 'Save Template' })).toBeDisabled();
});
it('renders correctly with existing template data', () => {
render(
<TemplateEditor
template={{
formatTemplate: '{ORG}-{TYPE}-{SEQ:4}',
correspondenceTypeId: 'type1' as any,
disciplineId: 'disc1' as any,
resetSequenceYearly: false,
} as any}
projectId={1}
projectName="Test Project"
correspondenceTypes={mockTypes}
disciplines={mockDisciplines}
onSave={onSave}
onCancel={onCancel}
/>
);
expect(screen.getByText('Edit Template')).toBeInTheDocument();
expect(screen.getByLabelText('Template Format *')).toHaveValue('{ORG}-{TYPE}-{SEQ:4}');
expect(screen.getByRole('button', { name: 'Save Template' })).not.toBeDisabled();
});
it('allows inserting variables into format', async () => {
const user = userEvent.setup();
render(
<TemplateEditor
projectId={1}
projectName="Test Project"
correspondenceTypes={mockTypes}
disciplines={mockDisciplines}
onSave={onSave}
onCancel={onCancel}
/>
);
const formatInput = screen.getByLabelText('Template Format *');
await user.type(formatInput, 'TEST-');
// Click a variable button
const orgButton = screen.getByRole('button', { name: '{ORG}' });
await user.click(orgButton);
expect(formatInput).toHaveValue('TEST-{ORG}');
});
it('updates preview when format changes', async () => {
const user = userEvent.setup();
render(
<TemplateEditor
projectId={1}
projectName="Test Project"
correspondenceTypes={mockTypes}
disciplines={mockDisciplines}
onSave={onSave}
onCancel={onCancel}
/>
);
const formatInput = screen.getByLabelText('Template Format *');
fireEvent.change(formatInput, { target: { value: '{PROJECT}-{SEQ:4}' } });
expect(screen.getByText('LCBP3-0001')).toBeInTheDocument();
});
it('calls onCancel when cancel button clicked', async () => {
const user = userEvent.setup();
render(
<TemplateEditor
projectId={1}
projectName="Test Project"
correspondenceTypes={mockTypes}
disciplines={mockDisciplines}
onSave={onSave}
onCancel={onCancel}
/>
);
const cancelButton = screen.getByRole('button', { name: 'Cancel' });
await user.click(cancelButton);
expect(onCancel).toHaveBeenCalled();
});
it('calls onSave with form data', async () => {
const user = userEvent.setup();
render(
<TemplateEditor
projectId={1}
projectName="Test Project"
correspondenceTypes={mockTypes}
disciplines={mockDisciplines}
onSave={onSave}
onCancel={onCancel}
/>
);
const formatInput = screen.getByLabelText('Template Format *');
fireEvent.change(formatInput, { target: { value: '{PROJECT}-{SEQ:4}' } });
// We cannot easily test Radix Select interactions in jsdom without massive pointer mocking,
// so we'll test the default values submission first.
const saveButton = screen.getByRole('button', { name: 'Save Template' });
await user.click(saveButton);
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({
projectId: 1,
formatTemplate: '{PROJECT}-{SEQ:4}',
resetSequenceYearly: true,
correspondenceTypeId: null,
disciplineId: 0,
}));
});
});
@@ -0,0 +1,87 @@
// File: frontend/components/numbering/__tests__/template-tester.test.tsx
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { TemplateTester } from '../template-tester';
import { numberingApi } from '@/lib/api/numbering';
vi.mock('@/lib/api/numbering', () => ({
numberingApi: {
previewNumber: vi.fn(),
},
}));
vi.mock('@/hooks/use-master-data', () => ({
useOrganizations: vi.fn(() => ({ data: [{ publicId: 'org1', organizationCode: 'ORG', organizationName: 'Org1' }] })),
useCorrespondenceTypes: vi.fn(() => ({ data: [{ id: 1, typeCode: 'TYPE', typeName: 'Type1' }] })),
useContracts: vi.fn(() => ({ data: [{ id: 1 }] })),
useDisciplines: vi.fn(() => ({ data: [{ id: 1, disciplineCode: 'DISC' }] })),
}));
describe('TemplateTester', () => {
const onOpenChange = vi.fn();
const mockTemplate = {
projectId: 1,
formatTemplate: '{ORG}-{TYPE}-{SEQ:4}',
} as any;
beforeEach(() => {
vi.clearAllMocks();
});
it('renders correctly when open', () => {
render(<TemplateTester open={true} onOpenChange={onOpenChange} template={mockTemplate} />);
expect(screen.getByText('Test Number Generation')).toBeInTheDocument();
expect(screen.getByText('{ORG}-{TYPE}-{SEQ:4}')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Generate Test Number' })).toBeInTheDocument();
});
it('does not render when closed', () => {
render(<TemplateTester open={false} onOpenChange={onOpenChange} template={mockTemplate} />);
expect(screen.queryByText('Test Number Generation')).not.toBeInTheDocument();
});
it('handles successful generation', async () => {
const user = userEvent.setup();
render(<TemplateTester open={true} onOpenChange={onOpenChange} template={mockTemplate} />);
vi.mocked(numberingApi.previewNumber).mockResolvedValue({
previewNumber: 'ORG-TYPE-0001',
isDefault: true,
} as any);
const generateBtn = screen.getByRole('button', { name: 'Generate Test Number' });
await user.click(generateBtn);
expect(numberingApi.previewNumber).toHaveBeenCalledWith({
projectId: 1,
originatorOrganizationId: '0',
recipientOrganizationId: '0',
correspondenceTypeId: 0,
disciplineId: 0,
year: new Date().getFullYear(),
});
await waitFor(() => {
expect(screen.getByText('ORG-TYPE-0001')).toBeInTheDocument();
expect(screen.getByText('Default Template')).toBeInTheDocument();
});
});
it('handles API error', async () => {
const user = userEvent.setup();
render(<TemplateTester open={true} onOpenChange={onOpenChange} template={mockTemplate} />);
vi.mocked(numberingApi.previewNumber).mockRejectedValue(new Error('Generation failed'));
const generateBtn = screen.getByRole('button', { name: 'Generate Test Number' });
await user.click(generateBtn);
await waitFor(() => {
expect(screen.getByText('Error: Generation failed')).toBeInTheDocument();
expect(screen.getByText('Generation Failed:')).toBeInTheDocument();
});
});
});
@@ -0,0 +1,136 @@
// File: frontend/components/numbering/__tests__/void-replace-form.test.tsx
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { VoidReplaceForm } from '../void-replace-form';
import { documentNumberingService } from '@/lib/services/document-numbering.service';
import { toast } from 'sonner';
vi.mock('@/lib/services/document-numbering.service', () => ({
documentNumberingService: {
voidAndReplace: vi.fn(),
},
}));
vi.mock('sonner', () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
},
}));
describe('VoidReplaceForm', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('renders correctly', () => {
render(<VoidReplaceForm projectId={1} />);
expect(screen.getByText('Void & Replace Number')).toBeInTheDocument();
expect(screen.getByLabelText('Document Number')).toBeInTheDocument();
expect(screen.getByLabelText('Reason')).toBeInTheDocument();
expect(screen.getByRole('checkbox', { name: 'Generate Replacement?' })).not.toBeChecked();
expect(screen.getByRole('button', { name: 'Void Number' })).toBeInTheDocument();
});
it('shows validation errors for empty fields', async () => {
const user = userEvent.setup();
render(<VoidReplaceForm projectId={1} />);
const button = screen.getByRole('button', { name: 'Void Number' });
await user.click(button);
await waitFor(() => {
expect(screen.getByText('Document Number is required')).toBeInTheDocument();
expect(screen.getByText('Reason must be at least 5 characters')).toBeInTheDocument();
});
expect(documentNumberingService.voidAndReplace).not.toHaveBeenCalled();
});
it('shows validation error for short reason', async () => {
const user = userEvent.setup();
render(<VoidReplaceForm projectId={1} />);
await user.type(screen.getByLabelText('Document Number'), 'DOC-001');
await user.type(screen.getByLabelText('Reason'), '123'); // Too short
const button = screen.getByRole('button', { name: 'Void Number' });
await user.click(button);
await waitFor(() => {
expect(screen.getByText('Reason must be at least 5 characters')).toBeInTheDocument();
});
expect(documentNumberingService.voidAndReplace).not.toHaveBeenCalled();
});
it('handles successful voiding without replacement', async () => {
const user = userEvent.setup();
render(<VoidReplaceForm projectId={1} />);
await user.type(screen.getByLabelText('Document Number'), 'DOC-001');
await user.type(screen.getByLabelText('Reason'), 'Voided because of typo');
vi.mocked(documentNumberingService.voidAndReplace).mockResolvedValue({} as any);
const button = screen.getByRole('button', { name: 'Void Number' });
await user.click(button);
expect(documentNumberingService.voidAndReplace).toHaveBeenCalledWith({
documentNumber: 'DOC-001',
reason: 'Voided because of typo',
replace: false,
projectId: 1,
});
await waitFor(() => {
expect(toast.success).toHaveBeenCalledWith('Number voided successfully. ');
});
});
it('handles successful voiding with replacement', async () => {
const user = userEvent.setup();
render(<VoidReplaceForm projectId={1} />);
await user.type(screen.getByLabelText('Document Number'), 'DOC-002');
await user.type(screen.getByLabelText('Reason'), 'Voided because of typo');
const checkbox = screen.getByRole('checkbox', { name: 'Generate Replacement?' });
await user.click(checkbox);
vi.mocked(documentNumberingService.voidAndReplace).mockResolvedValue({} as any);
const button = screen.getByRole('button', { name: 'Void Number' });
await user.click(button);
expect(documentNumberingService.voidAndReplace).toHaveBeenCalledWith({
documentNumber: 'DOC-002',
reason: 'Voided because of typo',
replace: true,
projectId: 1,
});
await waitFor(() => {
expect(toast.success).toHaveBeenCalledWith('Number voided successfully. Replacement generated.');
});
});
it('handles API error', async () => {
const user = userEvent.setup();
render(<VoidReplaceForm projectId={1} />);
await user.type(screen.getByLabelText('Document Number'), 'DOC-001');
await user.type(screen.getByLabelText('Reason'), 'Voided because of typo');
vi.mocked(documentNumberingService.voidAndReplace).mockRejectedValue(new Error('Failed'));
const button = screen.getByRole('button', { name: 'Void Number' });
await user.click(button);
await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith('Failed to void number. Check if it exists.');
});
});
});