test(frontend): raise overall statement coverage to 30.42% for Phase 1 MVP
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
// File: frontend/components/rfas/__tests__/detail.test.tsx
|
||||
// Change Log:
|
||||
// - 2026-06-13: Initial creation - test coverage for RFADetail component
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { RFADetail } from '../detail';
|
||||
import { RFA } from '@/types/rfa';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/hooks/use-rfa', () => ({
|
||||
useProcessRFA: () => ({
|
||||
mutate: vi.fn(),
|
||||
isPending: false,
|
||||
}),
|
||||
useSubmitRFA: () => ({
|
||||
mutate: vi.fn(),
|
||||
isPending: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/components/review-team/ReviewTeamSelector', () => ({
|
||||
ReviewTeamSelector: () => <div data-testid="review-team-selector">Review Team Selector</div>,
|
||||
}));
|
||||
|
||||
describe('RFADetail', () => {
|
||||
const mockRFA: RFA = {
|
||||
publicId: '019505a1-7c3e-7000-8000-abc123def456',
|
||||
correspondence: {
|
||||
publicId: '019505a1-7c3e-7000-8000-abc123def457',
|
||||
correspondenceNumber: 'RFA-001',
|
||||
project: {
|
||||
publicId: '019505a1-7c3e-7000-8000-abc123def458',
|
||||
projectName: 'Test Project',
|
||||
},
|
||||
discipline: {
|
||||
publicId: '019505a1-7c3e-7000-8000-abc123def459',
|
||||
codeNameEn: 'Structural',
|
||||
codeNameTh: 'โครงสร้าง',
|
||||
disciplineCode: 'STR',
|
||||
},
|
||||
createdAt: '2026-01-01T00:00:00Z',
|
||||
},
|
||||
discipline: {
|
||||
publicId: '019505a1-7c3e-7000-8000-abc123def460',
|
||||
name: 'Structural',
|
||||
},
|
||||
revisions: [
|
||||
{
|
||||
publicId: '019505a1-7c3e-7000-8000-abc123def461',
|
||||
isCurrent: true,
|
||||
subject: 'Test Subject',
|
||||
description: 'Test Description',
|
||||
statusCode: {
|
||||
publicId: '019505a1-7c3e-7000-8000-abc123def462',
|
||||
statusName: 'Draft',
|
||||
statusCode: 'DFT',
|
||||
},
|
||||
createdAt: '2026-01-01T00:00:00Z',
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
itemType: 'SHOP_DRAWING',
|
||||
shopDrawingRevision: {
|
||||
shopDrawing: {
|
||||
drawingNumber: 'SD-001',
|
||||
},
|
||||
revisionLabel: 'A',
|
||||
title: 'Test Drawing',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render RFA detail with data', () => {
|
||||
render(<RFADetail data={mockRFA} />);
|
||||
|
||||
expect(screen.getByText('RFA-001')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test Subject')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test Description')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test Project')).toBeInTheDocument();
|
||||
expect(screen.getByText('Structural')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display created date', () => {
|
||||
render(<RFADetail data={mockRFA} />);
|
||||
|
||||
expect(screen.getByText(/Created on/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display status badge', () => {
|
||||
render(<RFADetail data={mockRFA} />);
|
||||
|
||||
expect(screen.getByText('Draft')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show Edit and Submit buttons for DFT status', () => {
|
||||
render(<RFADetail data={mockRFA} />);
|
||||
|
||||
expect(screen.getByText('Edit')).toBeInTheDocument();
|
||||
expect(screen.getByText('Submit RFA')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show Approve and Reject buttons for FAP status', () => {
|
||||
const fapRFA: RFA = {
|
||||
...mockRFA,
|
||||
revisions: [
|
||||
{
|
||||
...mockRFA.revisions[0],
|
||||
statusCode: {
|
||||
...mockRFA.revisions[0].statusCode,
|
||||
statusName: 'For Approval',
|
||||
statusCode: 'FAP',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
render(<RFADetail data={fapRFA} />);
|
||||
|
||||
expect(screen.getByText('Reject')).toBeInTheDocument();
|
||||
expect(screen.getByText('Approve')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show Approve and Reject buttons for FRE status', () => {
|
||||
const freRFA: RFA = {
|
||||
...mockRFA,
|
||||
revisions: [
|
||||
{
|
||||
...mockRFA.revisions[0],
|
||||
statusCode: {
|
||||
...mockRFA.revisions[0].statusCode,
|
||||
statusName: 'For Review',
|
||||
statusCode: 'FRE',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
render(<RFADetail data={freRFA} />);
|
||||
|
||||
expect(screen.getByText('Reject')).toBeInTheDocument();
|
||||
expect(screen.getByText('Approve')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render RFA items table', () => {
|
||||
render(<RFADetail data={mockRFA} />);
|
||||
|
||||
expect(screen.getByText('Type')).toBeInTheDocument();
|
||||
expect(screen.getByText('Drawing No.')).toBeInTheDocument();
|
||||
expect(screen.getByText('Revision')).toBeInTheDocument();
|
||||
expect(screen.getByText('Title')).toBeInTheDocument();
|
||||
expect(screen.getByText('SHOP_DRAWING')).toBeInTheDocument();
|
||||
expect(screen.getByText('SD-001')).toBeInTheDocument();
|
||||
expect(screen.getByText('A')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test Drawing')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show empty state when no items', () => {
|
||||
const rfaWithoutItems: RFA = {
|
||||
...mockRFA,
|
||||
revisions: [
|
||||
{
|
||||
...mockRFA.revisions[0],
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
render(<RFADetail data={rfaWithoutItems} />);
|
||||
|
||||
expect(screen.getByText('No drawing items linked to this RFA.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle missing project name', () => {
|
||||
const rfaWithoutProject: RFA = {
|
||||
...mockRFA,
|
||||
correspondence: {
|
||||
...mockRFA.correspondence,
|
||||
project: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
render(<RFADetail data={rfaWithoutProject} />);
|
||||
|
||||
expect(screen.getByText('-')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle missing discipline', () => {
|
||||
const rfaWithoutDiscipline: RFA = {
|
||||
...mockRFA,
|
||||
correspondence: {
|
||||
...mockRFA.correspondence,
|
||||
discipline: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
render(<RFADetail data={rfaWithoutDiscipline} />);
|
||||
|
||||
expect(screen.getByText('-')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle missing subject', () => {
|
||||
const rfaWithoutSubject: RFA = {
|
||||
...mockRFA,
|
||||
revisions: [
|
||||
{
|
||||
...mockRFA.revisions[0],
|
||||
subject: undefined,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
render(<RFADetail data={rfaWithoutSubject} />);
|
||||
|
||||
expect(screen.getByText('Untitled RFA')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle missing description', () => {
|
||||
const rfaWithoutDescription: RFA = {
|
||||
...mockRFA,
|
||||
revisions: [
|
||||
{
|
||||
...mockRFA.revisions[0],
|
||||
description: undefined,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
render(<RFADetail data={rfaWithoutDescription} />);
|
||||
|
||||
expect(screen.getByText('No description provided.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should open submit dialog when Submit RFA clicked', () => {
|
||||
render(<RFADetail data={mockRFA} />);
|
||||
|
||||
const submitButton = screen.getByText('Submit RFA');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(screen.getByText('Submit RFA to Workflow')).toBeInTheDocument();
|
||||
expect(screen.getByText('Routing Template ID')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show review team selector when project has publicId', () => {
|
||||
render(<RFADetail data={mockRFA} />);
|
||||
|
||||
const submitButton = screen.getByText('Submit RFA');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(screen.getByTestId('review-team-selector')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should close submit dialog when Cancel clicked', () => {
|
||||
render(<RFADetail data={mockRFA} />);
|
||||
|
||||
const submitButton = screen.getByText('Submit RFA');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
const cancelButton = screen.getByText('Cancel');
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
expect(screen.queryByText('Submit RFA to Workflow')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should open approve dialog when Approve clicked', () => {
|
||||
const fapRFA: RFA = {
|
||||
...mockRFA,
|
||||
revisions: [
|
||||
{
|
||||
...mockRFA.revisions[0],
|
||||
statusCode: {
|
||||
...mockRFA.revisions[0].statusCode,
|
||||
statusCode: 'FAP',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
render(<RFADetail data={fapRFA} />);
|
||||
|
||||
const approveButton = screen.getByText('Approve');
|
||||
fireEvent.click(approveButton);
|
||||
|
||||
expect(screen.getByText('Confirm Approval')).toBeInTheDocument();
|
||||
expect(screen.getByText('Comments')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should open reject dialog when Reject clicked', () => {
|
||||
const fapRFA: RFA = {
|
||||
...mockRFA,
|
||||
revisions: [
|
||||
{
|
||||
...mockRFA.revisions[0],
|
||||
statusCode: {
|
||||
...mockRFA.revisions[0].statusCode,
|
||||
statusCode: 'FAP',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
render(<RFADetail data={fapRFA} />);
|
||||
|
||||
const rejectButton = screen.getByText('Reject');
|
||||
fireEvent.click(rejectButton);
|
||||
|
||||
expect(screen.getByText('Confirm Rejection')).toBeInTheDocument();
|
||||
expect(screen.getByText('Comments')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle missing correspondence number', () => {
|
||||
const rfaWithoutNumber: RFA = {
|
||||
...mockRFA,
|
||||
correspondence: {
|
||||
...mockRFA.correspondence,
|
||||
correspondenceNumber: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
render(<RFADetail data={rfaWithoutNumber} />);
|
||||
|
||||
expect(screen.getByText('RFA')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should use fallback discipline codes', () => {
|
||||
const rfaWithDisciplineCodes: RFA = {
|
||||
...mockRFA,
|
||||
correspondence: {
|
||||
...mockRFA.correspondence,
|
||||
discipline: {
|
||||
publicId: '019505a1-7c3e-7000-8000-abc123def460',
|
||||
codeNameEn: undefined,
|
||||
codeNameTh: undefined,
|
||||
disciplineCode: 'STR',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
render(<RFADetail data={rfaWithDisciplineCodes} />);
|
||||
|
||||
expect(screen.getByText('STR')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,392 @@
|
||||
// File: frontend/components/rfas/__tests__/form.test.tsx
|
||||
// Change Log:
|
||||
// - 2026-06-13: Initial creation - test coverage for RFAForm component
|
||||
// - 2026-06-13: Mock useCorrespondenceTypes and useRfaTypes to resolve preview tests failure
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { RFAForm, extractArrayData, dedupeByKey, getOptionValue, getMasterOptionValue } from '../form';
|
||||
import { useCreateRFA } from '@/hooks/use-rfa';
|
||||
import { correspondenceService } from '@/lib/services/correspondence.service';
|
||||
import { createTestQueryClient } from '@/lib/test-utils';
|
||||
|
||||
const renderWithClient = (ui: React.ReactElement) => {
|
||||
const { wrapper } = createTestQueryClient();
|
||||
return render(ui, { wrapper });
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/use-rfa', () => ({
|
||||
useCreateRFA: vi.fn(() => ({
|
||||
mutate: vi.fn(),
|
||||
isPending: false,
|
||||
})),
|
||||
}));
|
||||
|
||||
const mockUseDrawings = vi.fn(() => ({ data: [], isLoading: false }));
|
||||
const mockUseDisciplines = vi.fn(() => ({ data: [], isLoading: false }));
|
||||
const mockUseContracts = vi.fn(() => ({ data: [], isLoading: false }));
|
||||
const mockUseOrganizations = vi.fn(() => ({ data: [], isLoading: false }));
|
||||
const mockUseCorrespondenceTypes = vi.fn(() => ({ data: [] }));
|
||||
const mockUseRfaTypes = vi.fn(() => ({ data: [] }));
|
||||
const mockUseProjects = vi.fn(() => ({ data: [], isLoading: false }));
|
||||
const mockUseAiStatus = vi.fn(() => ({ data: null, isLoading: false }));
|
||||
|
||||
vi.mock('@/hooks/use-drawing', () => ({
|
||||
useDrawings: (...args: any[]) => mockUseDrawings(...args),
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/use-master-data', () => ({
|
||||
useDisciplines: (...args: any[]) => mockUseDisciplines(...args),
|
||||
useContracts: (...args: any[]) => mockUseContracts(...args),
|
||||
useOrganizations: (...args: any[]) => mockUseOrganizations(...args),
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/use-reference-data', () => ({
|
||||
useCorrespondenceTypes: (...args: any[]) => mockUseCorrespondenceTypes(...args),
|
||||
useRfaTypes: (...args: any[]) => mockUseRfaTypes(...args),
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/use-projects', () => ({
|
||||
useProjects: (...args: any[]) => mockUseProjects(...args),
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/use-ai-status', () => ({
|
||||
useAiStatus: (...args: any[]) => mockUseAiStatus(...args),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/services/correspondence.service', () => ({
|
||||
correspondenceService: {
|
||||
previewNumber: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ai/ai-suggestion-button', () => ({
|
||||
AiSuggestionButton: () => <div data-testid="ai-suggestion-button">AI Suggestion</div>,
|
||||
}));
|
||||
|
||||
describe('RFAForm', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockUseDrawings.mockReturnValue({ data: [], isLoading: false });
|
||||
mockUseDisciplines.mockReturnValue({ data: [], isLoading: false });
|
||||
mockUseContracts.mockReturnValue({ data: [], isLoading: false });
|
||||
mockUseOrganizations.mockReturnValue({ data: [], isLoading: false });
|
||||
mockUseCorrespondenceTypes.mockReturnValue({
|
||||
data: [{ id: 1, publicId: 'uuid-rfa-type-public', typeCode: 'RFA', typeName: 'Request for Approval' }]
|
||||
});
|
||||
mockUseRfaTypes.mockReturnValue({
|
||||
data: [{ publicId: 'uuid-type', typeCode: 'SDW', typeName: 'Shop Drawing RFA' }]
|
||||
});
|
||||
mockUseProjects.mockReturnValue({ data: [], isLoading: false });
|
||||
mockUseAiStatus.mockReturnValue({ data: null, isLoading: false });
|
||||
vi.mocked(useCreateRFA).mockReturnValue({
|
||||
mutate: vi.fn(),
|
||||
isPending: false,
|
||||
} as any);
|
||||
});
|
||||
|
||||
describe('Form Rendering', () => {
|
||||
it('should render form with all required fields', () => {
|
||||
renderWithClient(<RFAForm />);
|
||||
|
||||
expect(screen.getByText('Project *')).toBeInTheDocument();
|
||||
expect(screen.getByText('Contract *')).toBeInTheDocument();
|
||||
expect(screen.getByText('Discipline *')).toBeInTheDocument();
|
||||
expect(screen.getByText('RFA Type *')).toBeInTheDocument();
|
||||
expect(screen.getByText('Subject *')).toBeInTheDocument();
|
||||
expect(screen.getByText('To Organization *')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render optional fields', () => {
|
||||
renderWithClient(<RFAForm />);
|
||||
|
||||
expect(screen.getByText('Description')).toBeInTheDocument();
|
||||
expect(screen.getByText('Body (Content)')).toBeInTheDocument();
|
||||
expect(screen.getByText('Remarks')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render submit button', () => {
|
||||
renderWithClient(<RFAForm />);
|
||||
|
||||
expect(screen.getByText('Create RFA')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render AI suggestion button', () => {
|
||||
renderWithClient(<RFAForm />);
|
||||
|
||||
expect(screen.getByTestId('ai-suggestion-button')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Form Validation', () => {
|
||||
it('should show validation error for empty project', async () => {
|
||||
renderWithClient(<RFAForm />);
|
||||
|
||||
const submitButton = screen.getByText('Create RFA');
|
||||
fireEvent.submit(submitButton.closest('form')!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Project is required/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show validation error for empty contract', async () => {
|
||||
renderWithClient(<RFAForm />);
|
||||
|
||||
const submitButton = screen.getByText('Create RFA');
|
||||
fireEvent.submit(submitButton.closest('form')!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Contract is required/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show validation error for empty discipline', async () => {
|
||||
renderWithClient(<RFAForm />);
|
||||
|
||||
const submitButton = screen.getByText('Create RFA');
|
||||
fireEvent.submit(submitButton.closest('form')!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Discipline is required/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show validation error for empty type', async () => {
|
||||
renderWithClient(<RFAForm />);
|
||||
|
||||
const submitButton = screen.getByText('Create RFA');
|
||||
fireEvent.submit(submitButton.closest('form')!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Type is required/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show validation error for short subject', async () => {
|
||||
renderWithClient(<RFAForm />);
|
||||
|
||||
const subjectInput = screen.getByLabelText('Subject *');
|
||||
fireEvent.change(subjectInput, { target: { value: 'abc' } });
|
||||
|
||||
const submitButton = screen.getByText('Create RFA');
|
||||
fireEvent.submit(submitButton.closest('form')!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Subject must be at least 5 characters/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show validation error for empty to organization', async () => {
|
||||
renderWithClient(<RFAForm />);
|
||||
|
||||
const submitButton = screen.getByText('Create RFA');
|
||||
fireEvent.submit(submitButton.closest('form')!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Please select To Organization/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Field Interactions', () => {
|
||||
it('should allow subject input', () => {
|
||||
renderWithClient(<RFAForm />);
|
||||
|
||||
const subjectInput = screen.getByLabelText('Subject *');
|
||||
fireEvent.change(subjectInput, { target: { value: 'Test Subject' } });
|
||||
|
||||
expect(subjectInput).toHaveValue('Test Subject');
|
||||
});
|
||||
|
||||
it('should allow description input', () => {
|
||||
renderWithClient(<RFAForm />);
|
||||
|
||||
const descriptionInput = screen.getByLabelText('Description');
|
||||
fireEvent.change(descriptionInput, { target: { value: 'Test Description' } });
|
||||
|
||||
expect(descriptionInput).toHaveValue('Test Description');
|
||||
});
|
||||
|
||||
it('should allow body input', () => {
|
||||
renderWithClient(<RFAForm />);
|
||||
|
||||
const bodyInput = screen.getByLabelText('Body (Content)');
|
||||
fireEvent.change(bodyInput, { target: { value: 'Test Body' } });
|
||||
|
||||
expect(bodyInput).toHaveValue('Test Body');
|
||||
});
|
||||
|
||||
it('should allow remarks input', () => {
|
||||
renderWithClient(<RFAForm />);
|
||||
|
||||
const remarksInput = screen.getByLabelText('Remarks');
|
||||
fireEvent.change(remarksInput, { target: { value: 'Test Remarks' } });
|
||||
|
||||
expect(remarksInput).toHaveValue('Test Remarks');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Drawing Selection', () => {
|
||||
it('should render shop drawing section', () => {
|
||||
mockUseRfaTypes.mockReturnValue({
|
||||
data: [{ publicId: 'uuid-sdw', typeCode: 'SDW', typeName: 'Shop Drawing RFA' }]
|
||||
});
|
||||
|
||||
renderWithClient(<RFAForm defaultValues={{ rfaTypeId: 'uuid-sdw' }} />);
|
||||
expect(screen.getByText(/Shop Drawings/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render as-built drawing section', () => {
|
||||
mockUseRfaTypes.mockReturnValue({
|
||||
data: [{ publicId: 'uuid-adw', typeCode: 'ADW', typeName: 'As-Built Drawing RFA' }]
|
||||
});
|
||||
|
||||
renderWithClient(<RFAForm defaultValues={{ rfaTypeId: 'uuid-adw' }} />);
|
||||
expect(screen.getByText(/As-Built Drawings/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show search input for shop drawings', () => {
|
||||
mockUseRfaTypes.mockReturnValue({
|
||||
data: [{ publicId: 'uuid-sdw', typeCode: 'SDW', typeName: 'Shop Drawing RFA' }]
|
||||
});
|
||||
|
||||
renderWithClient(<RFAForm defaultValues={{ rfaTypeId: 'uuid-sdw' }} />);
|
||||
const searchInput = screen.getByPlaceholderText(/ค้นหาตาม Drawing Number/i);
|
||||
expect(searchInput).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show search input for as-built drawings', () => {
|
||||
mockUseRfaTypes.mockReturnValue({
|
||||
data: [{ publicId: 'uuid-adw', typeCode: 'ADW', typeName: 'As-Built RFA' }]
|
||||
});
|
||||
|
||||
renderWithClient(<RFAForm defaultValues={{ rfaTypeId: 'uuid-adw' }} />);
|
||||
const searchInput = screen.getByPlaceholderText(/ค้นหาตาม Drawing Number/i);
|
||||
expect(searchInput).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Preview Functionality', () => {
|
||||
it('should show preview section when form is valid', async () => {
|
||||
vi.mocked(correspondenceService.previewNumber).mockResolvedValue({
|
||||
number: 'RFA-001',
|
||||
isDefaultTemplate: false,
|
||||
});
|
||||
|
||||
renderWithClient(
|
||||
<RFAForm
|
||||
defaultValues={{
|
||||
projectId: 'uuid-project',
|
||||
rfaTypeId: 'uuid-type',
|
||||
disciplineId: '1',
|
||||
toOrganizationId: 'uuid-org',
|
||||
subject: 'Test Subject',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Document Number Preview/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display preview number', async () => {
|
||||
vi.mocked(correspondenceService.previewNumber).mockResolvedValue({
|
||||
number: 'RFA-001',
|
||||
isDefaultTemplate: false,
|
||||
});
|
||||
|
||||
renderWithClient(
|
||||
<RFAForm
|
||||
defaultValues={{
|
||||
projectId: 'uuid-project',
|
||||
rfaTypeId: 'uuid-type',
|
||||
disciplineId: '1',
|
||||
toOrganizationId: 'uuid-org',
|
||||
subject: 'Test Subject',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('RFA-001')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Submit Functionality', () => {
|
||||
it('should call create mutation on valid submit', async () => {
|
||||
const mockMutate = vi.fn();
|
||||
vi.mocked(useCreateRFA).mockReturnValue({
|
||||
mutate: mockMutate,
|
||||
isPending: false,
|
||||
} as any);
|
||||
|
||||
renderWithClient(<RFAForm />);
|
||||
|
||||
const submitButton = screen.getByText('Create RFA');
|
||||
expect(submitButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show loading state during submission', () => {
|
||||
vi.mocked(useCreateRFA).mockReturnValue({
|
||||
mutate: vi.fn(),
|
||||
isPending: true,
|
||||
} as any);
|
||||
|
||||
renderWithClient(<RFAForm />);
|
||||
|
||||
const submitButton = screen.getByText('Create RFA');
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Helper Functions', () => {
|
||||
it('should extract array data from nested structure', () => {
|
||||
const data = { data: { data: [1, 2, 3] } };
|
||||
const result = extractArrayData<number>(data);
|
||||
expect(result).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('should return empty array for non-array data', () => {
|
||||
const data = { data: 'not an array' };
|
||||
const result = extractArrayData<number>(data);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should dedupe items by key', () => {
|
||||
const items = [
|
||||
{ id: 1, name: 'A' },
|
||||
{ id: 2, name: 'B' },
|
||||
{ id: 1, name: 'C' },
|
||||
];
|
||||
const result = dedupeByKey(items, (item) => item.id);
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should get option value correctly', () => {
|
||||
expect(getOptionValue('123')).toBe('123');
|
||||
expect(getOptionValue(123)).toBe('123');
|
||||
expect(getOptionValue(undefined)).toBeUndefined();
|
||||
expect(getOptionValue(null)).toBeUndefined();
|
||||
expect(getOptionValue('')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should get master option value with fallback', () => {
|
||||
expect(getMasterOptionValue({ publicId: 'uuid-1' })).toBe('uuid-1');
|
||||
expect(getMasterOptionValue({ id: 1 })).toBe('1');
|
||||
expect(getMasterOptionValue({ publicId: 'uuid-1', id: 1 })).toBe('uuid-1');
|
||||
expect(getMasterOptionValue({})).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,225 @@
|
||||
// File: frontend/components/rfas/__tests__/list.test.tsx
|
||||
// Change Log:
|
||||
// - 2026-06-13: Initial creation - test coverage for RFAList component
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { RFAList } from '../list';
|
||||
import { RFA } from '@/types/rfa';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('sonner', () => ({
|
||||
toast: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('RFAList', () => {
|
||||
const mockRFAs: RFA[] = [
|
||||
{
|
||||
publicId: '019505a1-7c3e-7000-8000-abc123def456',
|
||||
correspondence: {
|
||||
publicId: '019505a1-7c3e-7000-8000-abc123def457',
|
||||
correspondenceNumber: 'RFA-001',
|
||||
project: {
|
||||
publicId: '019505a1-7c3e-7000-8000-abc123def458',
|
||||
projectName: 'Test Project',
|
||||
},
|
||||
createdAt: '2026-01-01T00:00:00Z',
|
||||
},
|
||||
discipline: {
|
||||
publicId: '019505a1-7c3e-7000-8000-abc123def459',
|
||||
name: 'Structural',
|
||||
},
|
||||
revisions: [
|
||||
{
|
||||
publicId: '019505a1-7c3e-7000-8000-abc123def460',
|
||||
subject: 'Test Subject 1',
|
||||
statusCode: {
|
||||
publicId: '019505a1-7c3e-7000-8000-abc123def461',
|
||||
statusName: 'Pending',
|
||||
statusCode: 'PENDING',
|
||||
},
|
||||
createdAt: '2026-01-01T00:00:00Z',
|
||||
items: [
|
||||
{
|
||||
shopDrawingRevision: {
|
||||
attachments: [
|
||||
{
|
||||
url: 'http://example.com/file.pdf',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
publicId: '019505a1-7c3e-7000-8000-abc123def462',
|
||||
correspondence: {
|
||||
publicId: '019505a1-7c3e-7000-8000-abc123def463',
|
||||
correspondenceNumber: 'RFA-002',
|
||||
project: {
|
||||
publicId: '019505a1-7c3e-7000-8000-abc123def464',
|
||||
projectName: 'Another Project',
|
||||
},
|
||||
createdAt: '2026-01-02T00:00:00Z',
|
||||
},
|
||||
discipline: {
|
||||
publicId: '019505a1-7c3e-7000-8000-abc123def465',
|
||||
name: 'Architectural',
|
||||
},
|
||||
revisions: [
|
||||
{
|
||||
publicId: '019505a1-7c3e-7000-8000-abc123def466',
|
||||
subject: 'Test Subject 2',
|
||||
statusCode: {
|
||||
publicId: '019505a1-7c3e-7000-8000-abc123def467',
|
||||
statusName: 'Approved',
|
||||
statusCode: 'APPROVED',
|
||||
},
|
||||
createdAt: '2026-01-02T00:00:00Z',
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render RFA list with data', () => {
|
||||
render(<RFAList data={mockRFAs} />);
|
||||
|
||||
expect(screen.getByText('RFA-001')).toBeInTheDocument();
|
||||
expect(screen.getByText('RFA-002')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test Subject 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test Subject 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test Project')).toBeInTheDocument();
|
||||
expect(screen.getByText('Another Project')).toBeInTheDocument();
|
||||
expect(screen.getByText('Structural')).toBeInTheDocument();
|
||||
expect(screen.getByText('Architectural')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render empty state when data is null', () => {
|
||||
const { container } = render(<RFAList data={null} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should render empty state when data is empty array', () => {
|
||||
render(<RFAList data={[]} />);
|
||||
|
||||
// DataTable should render with empty data
|
||||
expect(screen.queryByText('RFA-001')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display formatted dates', () => {
|
||||
render(<RFAList data={mockRFAs} />);
|
||||
|
||||
expect(screen.getByText('01 Jan 2026')).toBeInTheDocument();
|
||||
expect(screen.getByText('02 Jan 2026')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display status badges', () => {
|
||||
render(<RFAList data={mockRFAs} />);
|
||||
|
||||
expect(screen.getByText('Pending')).toBeInTheDocument();
|
||||
expect(screen.getByText('Approved')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render action buttons for each row', () => {
|
||||
render(<RFAList data={mockRFAs} />);
|
||||
|
||||
// Should have view, file, and edit buttons for each row
|
||||
const viewButtons = screen.getAllByTitle('View Details');
|
||||
const fileButtons = screen.getAllByTitle('View File');
|
||||
const editButtons = screen.getAllByTitle('Edit');
|
||||
|
||||
expect(viewButtons).toHaveLength(2);
|
||||
expect(fileButtons).toHaveLength(2);
|
||||
expect(editButtons).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should handle missing project name', () => {
|
||||
const rfaWithoutProject: RFA[] = [
|
||||
{
|
||||
...mockRFAs[0],
|
||||
correspondence: {
|
||||
...mockRFAs[0].correspondence,
|
||||
project: undefined,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
render(<RFAList data={rfaWithoutProject} />);
|
||||
|
||||
expect(screen.getByText('-')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle missing discipline name', () => {
|
||||
const rfaWithoutDiscipline: RFA[] = [
|
||||
{
|
||||
...mockRFAs[0],
|
||||
discipline: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
render(<RFAList data={rfaWithoutDiscipline} />);
|
||||
|
||||
expect(screen.getByText('-')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle missing correspondence number', () => {
|
||||
const rfaWithoutNumber: RFA[] = [
|
||||
{
|
||||
...mockRFAs[0],
|
||||
correspondence: {
|
||||
...mockRFAs[0].correspondence,
|
||||
correspondenceNumber: undefined,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
render(<RFAList data={rfaWithoutNumber} />);
|
||||
|
||||
expect(screen.getByText('-')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle missing subject', () => {
|
||||
const rfaWithoutSubject: RFA[] = [
|
||||
{
|
||||
...mockRFAs[0],
|
||||
revisions: [
|
||||
{
|
||||
...mockRFAs[0].revisions[0],
|
||||
subject: undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
render(<RFAList data={rfaWithoutSubject} />);
|
||||
|
||||
expect(screen.getByText('-')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle missing status', () => {
|
||||
const rfaWithoutStatus: RFA[] = [
|
||||
{
|
||||
...mockRFAs[0],
|
||||
revisions: [
|
||||
{
|
||||
...mockRFAs[0].revisions[0],
|
||||
statusCode: undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
render(<RFAList data={rfaWithoutStatus} />);
|
||||
|
||||
expect(screen.getByText('Unknown')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user