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,214 @@
// File: frontend/components/auth/__tests__/auth-sync.test.tsx
// Change Log:
// - 2026-06-13: Initial creation - test coverage for AuthSync component
// - 2026-06-13: Refactor to use static ESM imports instead of CommonJS require() to resolve Vitest module path errors
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, renderHook } from '@testing-library/react';
import { AuthSync } from '../auth-sync';
import { useSession, signOut } from 'next-auth/react';
import { useAuthStore } from '@/lib/stores/auth-store';
import { clearAuthTokenCache } from '@/lib/api/client';
// Mock next-auth
vi.mock('next-auth/react', () => ({
useSession: vi.fn(),
signOut: vi.fn(),
}));
// Mock auth-store
vi.mock('@/lib/stores/auth-store', () => ({
useAuthStore: vi.fn(),
}));
// Mock clearAuthTokenCache
vi.mock('@/lib/api/client', () => ({
clearAuthTokenCache: vi.fn(),
}));
describe('AuthSync', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should render null (component renders nothing)', () => {
vi.mocked(useSession).mockReturnValue({
data: null,
status: 'unauthenticated',
} as any);
vi.mocked(useAuthStore).mockReturnValue({
setAuth: vi.fn(),
logout: vi.fn(),
} as any);
const { container } = render(<AuthSync />);
expect(container.firstChild).toBeNull();
});
it('should sync user data when authenticated', () => {
const mockSetAuth = vi.fn();
const mockLogout = vi.fn();
vi.mocked(useSession).mockReturnValue({
data: {
user: {
id: 'user-id-1',
publicId: '019505a1-7c3e-7000-8000-abc123def456',
username: 'testuser',
email: 'test@example.com',
firstName: 'Test',
lastName: 'User',
role: 'Admin',
permissions: ['read', 'write'],
},
accessToken: 'test-token',
},
status: 'authenticated',
} as any);
vi.mocked(useAuthStore).mockReturnValue({
setAuth: mockSetAuth,
logout: mockLogout,
} as any);
render(<AuthSync />);
expect(mockSetAuth).toHaveBeenCalledWith(
{
id: 'user-id-1',
publicId: '019505a1-7c3e-7000-8000-abc123def456',
username: 'testuser',
email: 'test@example.com',
firstName: 'Test',
lastName: 'User',
role: 'Admin',
permissions: ['read', 'write'],
},
'test-token'
);
});
it('should handle user_id fallback when id is missing', () => {
const mockSetAuth = vi.fn();
const mockLogout = vi.fn();
vi.mocked(useSession).mockReturnValue({
data: {
user: {
user_id: 'user-id-2',
publicId: '019505a1-7c3e-7000-8000-abc123def457',
username: 'testuser2',
email: 'test2@example.com',
firstName: 'Test2',
lastName: 'User2',
role: 'User',
},
accessToken: 'test-token-2',
},
status: 'authenticated',
} as any);
vi.mocked(useAuthStore).mockReturnValue({
setAuth: mockSetAuth,
logout: mockLogout,
} as any);
render(<AuthSync />);
expect(mockSetAuth).toHaveBeenCalledWith(
expect.objectContaining({
id: 'user-id-2',
}),
'test-token-2'
);
});
it('should clear auth cache and logout on unauthenticated', () => {
const mockSetAuth = vi.fn();
const mockLogout = vi.fn();
vi.mocked(useSession).mockReturnValue({
data: null,
status: 'unauthenticated',
} as any);
vi.mocked(useAuthStore).mockReturnValue({
setAuth: mockSetAuth,
logout: mockLogout,
} as any);
render(<AuthSync />);
expect(clearAuthTokenCache).toHaveBeenCalled();
expect(mockLogout).toHaveBeenCalled();
});
it('should clear auth cache and sign out on RefreshAccessTokenError', () => {
const mockSetAuth = vi.fn();
const mockLogout = vi.fn();
vi.mocked(useSession).mockReturnValue({
data: {
error: 'RefreshAccessTokenError',
},
status: 'authenticated',
} as any);
vi.mocked(useAuthStore).mockReturnValue({
setAuth: mockSetAuth,
logout: mockLogout,
} as any);
render(<AuthSync />);
expect(clearAuthTokenCache).toHaveBeenCalled();
expect(signOut).toHaveBeenCalledWith({ callbackUrl: '/login' });
});
it('should use default values when user fields are missing', () => {
const mockSetAuth = vi.fn();
const mockLogout = vi.fn();
vi.mocked(useSession).mockReturnValue({
data: {
user: {
publicId: '019505a1-7c3e-7000-8000-abc123def458',
},
accessToken: 'test-token-3',
},
status: 'authenticated',
} as any);
vi.mocked(useAuthStore).mockReturnValue({
setAuth: mockSetAuth,
logout: mockLogout,
} as any);
render(<AuthSync />);
expect(mockSetAuth).toHaveBeenCalledWith(
expect.objectContaining({
id: '',
username: '',
email: '',
firstName: '',
lastName: '',
role: 'User',
}),
'test-token-3'
);
});
it('should use session.user fields when typed user fields are missing', () => {
const mockSetAuth = vi.fn();
const mockLogout = vi.fn();
vi.mocked(useSession).mockReturnValue({
data: {
user: {
publicId: '019505a1-7c3e-7000-8000-abc123def459',
username: 'session-username',
email: 'session-email@example.com',
firstName: 'SessionFirst',
lastName: 'SessionLast',
role: 'SessionRole',
},
accessToken: 'test-token-4',
},
status: 'authenticated',
} as any);
vi.mocked(useAuthStore).mockReturnValue({
setAuth: mockSetAuth,
logout: mockLogout,
} as any);
render(<AuthSync />);
expect(mockSetAuth).toHaveBeenCalledWith(
expect.objectContaining({
username: 'session-username',
email: 'session-email@example.com',
firstName: 'SessionFirst',
lastName: 'SessionLast',
role: 'SessionRole',
}),
'test-token-4'
);
});
});
@@ -0,0 +1,119 @@
// File: frontend/components/correspondences/circulation-status-card.test.tsx
// Change Log:
// - 2026-06-13: Initial creation - test coverage for circulation-status-card component
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen } from '@testing-library/react';
import { CirculationStatusCard } from './circulation-status-card';
import { useCirculationsByCorrespondence } from '@/hooks/use-circulation';
// Mock hook สำหรับ useCirculationsByCorrespondence
vi.mock('@/hooks/use-circulation', () => ({
useCirculationsByCorrespondence: vi.fn(),
}));
describe('CirculationStatusCard Component', () => {
const correspondencePublicId = '019505a1-7c3e-7000-8000-abc123def456';
beforeEach(() => {
vi.clearAllMocks();
});
it('ควรแสดง loading state เมื่อกำลังโหลดข้อมูล', () => {
vi.mocked(useCirculationsByCorrespondence).mockReturnValue({
data: undefined,
isLoading: true,
} as any);
render(<CirculationStatusCard correspondencePublicId={correspondencePublicId} />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
it('ควรแสดง empty state เมื่อไม่มีข้อมูล circulation', () => {
vi.mocked(useCirculationsByCorrespondence).mockReturnValue({
data: [],
isLoading: false,
} as any);
render(<CirculationStatusCard correspondencePublicId={correspondencePublicId} />);
expect(screen.getByText('No circulations yet')).toBeInTheDocument();
expect(screen.getByText('New Circulation')).toBeInTheDocument();
});
it('ควรแสดงรายการ circulation อย่างถูกต้องเมื่อโหลดสำเร็จ', () => {
const mockData = [
{
publicId: '019505a1-7c3e-7000-8000-abc123defaaa',
circulationNo: 'CIRC-2026-001',
subject: 'Circulation Subject A',
statusCode: 'OPEN',
routings: [
{
id: 1,
status: 'COMPLETED',
completedAt: '2026-06-13T00:00:00.000Z',
assignee: {
userId: 101,
username: 'john_doe',
firstName: 'John',
lastName: 'Doe',
},
},
{
id: 2,
status: 'PENDING',
assignee: {
userId: 102,
username: 'jane_smith',
firstName: 'Jane',
lastName: 'Smith',
},
},
],
},
];
vi.mocked(useCirculationsByCorrespondence).mockReturnValue({
data: mockData,
isLoading: false,
} as any);
render(<CirculationStatusCard correspondencePublicId={correspondencePublicId} />);
expect(screen.getByText('CIRC-2026-001')).toBeInTheDocument();
expect(screen.getByText('Circulation Subject A')).toBeInTheDocument();
expect(screen.getByText('OPEN')).toBeInTheDocument();
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('Jane Smith')).toBeInTheDocument();
expect(screen.getByText('13 Jun')).toBeInTheDocument();
});
it('ควรแสดงข้อความ +X more assignees เมื่อมีผู้รับมากกว่า 3 คน', () => {
const mockData = [
{
publicId: '019505a1-7c3e-7000-8000-abc123defbbb',
circulationNo: 'CIRC-2026-002',
subject: 'Circulation Subject B',
statusCode: 'COMPLETED',
routings: [
{ id: 1, status: 'COMPLETED', assignee: { username: 'u1' } },
{ id: 2, status: 'COMPLETED', assignee: { username: 'u2' } },
{ id: 3, status: 'COMPLETED', assignee: { username: 'u3' } },
{ id: 4, status: 'COMPLETED', assignee: { username: 'u4' } },
{ id: 5, status: 'PENDING', assignee: { username: 'u5' } },
],
},
];
vi.mocked(useCirculationsByCorrespondence).mockReturnValue({
data: mockData,
isLoading: false,
} as any);
render(<CirculationStatusCard correspondencePublicId={correspondencePublicId} />);
expect(screen.getByText('+2 more assignees')).toBeInTheDocument();
});
});
@@ -0,0 +1,157 @@
// File: frontend/components/correspondences/tag-manager.test.tsx
// Change Log:
// - 2026-06-13: Initial creation - test coverage for tag-manager component
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { TagManager } from './tag-manager';
import { useCorrespondenceTags, useAddTag, useRemoveTag } from '@/hooks/use-correspondence';
import { useQuery } from '@tanstack/react-query';
import { masterDataService } from '@/lib/services/master-data.service';
// Mock React Query and hook implementations
vi.mock('@/hooks/use-correspondence', () => ({
useCorrespondenceTags: vi.fn(),
useAddTag: vi.fn(),
useRemoveTag: vi.fn(),
}));
vi.mock('@tanstack/react-query', () => ({
useQuery: vi.fn(),
}));
vi.mock('@/lib/services/master-data.service', () => ({
masterDataService: {
getTags: vi.fn(),
},
}));
describe('TagManager Component', () => {
const correspondenceUuid = '019505a1-7c3e-7000-8000-abc123def456';
const mockAddMutate = vi.fn();
const mockRemoveMutate = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(useQuery).mockReturnValue({
data: undefined,
isLoading: false,
} as any);
vi.mocked(useAddTag).mockReturnValue({
mutate: mockAddMutate,
isPending: false,
} as any);
vi.mocked(useRemoveTag).mockReturnValue({
mutate: mockRemoveMutate,
isPending: false,
} as any);
});
it('ควรแสดง loading state เมื่อกำลังโหลดข้อมูล tag', () => {
vi.mocked(useCorrespondenceTags).mockReturnValue({
data: undefined,
isLoading: true,
} as any);
render(<TagManager uuid={correspondenceUuid} canEdit={false} />);
expect(screen.getByText('Loading tags...')).toBeInTheDocument();
});
it('ควรแสดง empty state เมื่อไม่มี tag ถูกมอบหมาย', () => {
vi.mocked(useCorrespondenceTags).mockReturnValue({
data: [],
isLoading: false,
} as any);
render(<TagManager uuid={correspondenceUuid} canEdit={false} />);
expect(screen.getByText('No tags assigned')).toBeInTheDocument();
});
it('ควรแสดงรายการ tags ของเอกสารอย่างถูกต้อง', () => {
const mockTags = [
{ publicId: '019505a1-7c3e-7000-8000-tag111111111', tagName: 'Critical', colorCode: '#ff0000' },
{ publicId: '019505a1-7c3e-7000-8000-tag222222222', tagName: 'Draft', colorCode: 'default' },
];
vi.mocked(useCorrespondenceTags).mockReturnValue({
data: mockTags,
isLoading: false,
} as any);
render(<TagManager uuid={correspondenceUuid} canEdit={false} />);
expect(screen.getByText('Critical')).toBeInTheDocument();
expect(screen.getByText('Draft')).toBeInTheDocument();
});
it('ควรเรียก remove mutation เมื่อคลิกปุ่มลบ tag และมีสิทธิ์แก้ไข', () => {
const mockTags = [
{ publicId: '019505a1-7c3e-7000-8000-tag111111111', tagName: 'Critical', colorCode: '#ff0000' },
];
vi.mocked(useCorrespondenceTags).mockReturnValue({
data: mockTags,
isLoading: false,
} as any);
render(<TagManager uuid={correspondenceUuid} canEdit={true} />);
const removeBtn = screen.getAllByRole('button')[0];
fireEvent.click(removeBtn);
expect(mockRemoveMutate).toHaveBeenCalledWith({
uuid: correspondenceUuid,
tagId: '019505a1-7c3e-7000-8000-tag111111111',
});
});
it('ควรเปิดส่วนเลือก tag และแสดง tag ที่พร้อมให้เพิ่มเมื่อคลิก Add Tag', async () => {
const mockAssigned = [
{ publicId: '019505a1-7c3e-7000-8000-tag111111111', tagName: 'Critical', colorCode: '#ff0000' },
];
const mockAllTags = [
{ publicId: '019505a1-7c3e-7000-8000-tag111111111', tagName: 'Critical', colorCode: '#ff0000' },
{ publicId: '019505a1-7c3e-7000-8000-tag222222222', tagName: 'Draft', colorCode: '#00ff00' },
{ publicId: '019505a1-7c3e-7000-8000-tag333333333', tagName: 'Pending Review', colorCode: '#0000ff' },
];
vi.mocked(useCorrespondenceTags).mockReturnValue({
data: mockAssigned,
isLoading: false,
} as any);
vi.mocked(useQuery).mockReturnValue({
data: mockAllTags,
isLoading: false,
} as any);
render(<TagManager uuid={correspondenceUuid} canEdit={true} />);
const addTagBtn = screen.getByText('Add Tag');
fireEvent.click(addTagBtn);
await waitFor(() => {
expect(screen.getByText('Draft')).toBeInTheDocument();
expect(screen.getByText('Pending Review')).toBeInTheDocument();
});
// Tag 'Critical' มีอยู่แล้ว จึงไม่ควรปรากฏในส่วน list ที่พร้อมเพิ่ม
const listButtons = screen.getAllByRole('button');
const hasCriticalInList = listButtons.some(btn => btn.textContent === 'Critical');
expect(hasCriticalInList).toBe(false);
// คลิกเพื่อเพิ่ม tag 'Draft'
const draftBtn = screen.getByText('Draft');
fireEvent.click(draftBtn);
expect(mockAddMutate).toHaveBeenCalledWith({
uuid: correspondenceUuid,
tagId: '019505a1-7c3e-7000-8000-tag222222222',
});
});
});
@@ -0,0 +1,165 @@
// File: frontend/components/drawings/__tests__/card.test.tsx
// Change Log:
// - 2026-06-13: Initial creation - test coverage for DrawingCard component
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen } from '@testing-library/react';
import { DrawingCard } from '../card';
describe('DrawingCard', () => {
const mockDrawing = {
publicId: '019505a1-7c3e-7000-8000-abc123def456',
drawingNumber: 'SD-001',
title: 'Test Drawing',
sheetNumber: 'A1',
revision: 'A',
scale: '1:100',
issueDate: '2026-01-01',
discipline: {
publicId: '019505a1-7c3e-7000-8000-abc123def457',
disciplineCode: 'STR',
},
revisionCount: 1,
};
beforeEach(() => {
vi.clearAllMocks();
});
it('should render drawing card with data', () => {
render(<DrawingCard drawing={mockDrawing} />);
expect(screen.getByText('SD-001')).toBeInTheDocument();
expect(screen.getByText('Test Drawing')).toBeInTheDocument();
expect(screen.getByText('STR')).toBeInTheDocument();
});
it('should render placeholder when drawing number is missing', () => {
const drawingWithoutNumber = { ...mockDrawing, drawingNumber: undefined };
render(<DrawingCard drawing={drawingWithoutNumber} />);
expect(screen.getByText('No Number')).toBeInTheDocument();
});
it('should render placeholder when title is missing', () => {
const drawingWithoutTitle = { ...mockDrawing, title: undefined };
render(<DrawingCard drawing={drawingWithoutTitle} />);
expect(screen.getByText('No Title')).toBeInTheDocument();
});
it('should display sheet number', () => {
render(<DrawingCard drawing={mockDrawing} />);
expect(screen.getByText('Sheet:')).toBeInTheDocument();
expect(screen.getByText('A1')).toBeInTheDocument();
});
it('should display revision', () => {
render(<DrawingCard drawing={mockDrawing} />);
expect(screen.getByText('Rev:')).toBeInTheDocument();
expect(screen.getByText('A')).toBeInTheDocument();
});
it('should display scale', () => {
render(<DrawingCard drawing={mockDrawing} />);
expect(screen.getByText('Scale:')).toBeInTheDocument();
expect(screen.getByText('1:100')).toBeInTheDocument();
});
it('should display formatted issue date', () => {
render(<DrawingCard drawing={mockDrawing} />);
expect(screen.getByText('Date:')).toBeInTheDocument();
expect(screen.getByText('01/01/2026')).toBeInTheDocument();
});
it('should display legacy drawing number when present', () => {
const drawingWithLegacy = { ...mockDrawing, legacyDrawingNumber: 'LEG-001' };
render(<DrawingCard drawing={drawingWithLegacy} />);
expect(screen.getByText('Legacy:')).toBeInTheDocument();
expect(screen.getByText('LEG-001')).toBeInTheDocument();
});
it('should display volume page when present', () => {
const drawingWithPage = { ...mockDrawing, volumePage: 5 };
render(<DrawingCard drawing={drawingWithPage} />);
expect(screen.getByText('Page:')).toBeInTheDocument();
expect(screen.getByText('5')).toBeInTheDocument();
});
it('should display discipline code from object', () => {
render(<DrawingCard drawing={mockDrawing} />);
expect(screen.getByText('STR')).toBeInTheDocument();
});
it('should display discipline code from string', () => {
const drawingWithStringDiscipline = { ...mockDrawing, discipline: 'MECH' };
render(<DrawingCard drawing={drawingWithStringDiscipline} />);
expect(screen.getByText('MECH')).toBeInTheDocument();
});
it('should show View button with link', () => {
render(<DrawingCard drawing={mockDrawing} />);
const viewButton = screen.getByText('View');
expect(viewButton).toBeInTheDocument();
expect(viewButton.closest('a')).toHaveAttribute('href', '/drawings/019505a1-7c3e-7000-8000-abc123def456');
});
it('should show Download button', () => {
render(<DrawingCard drawing={mockDrawing} />);
expect(screen.getByText('Download')).toBeInTheDocument();
});
it('should show Compare button when revisionCount > 1', () => {
const drawingWithRevisions = { ...mockDrawing, revisionCount: 2 };
render(<DrawingCard drawing={drawingWithRevisions} />);
expect(screen.getByText('Compare')).toBeInTheDocument();
});
it('should not show Compare button when revisionCount <= 1', () => {
render(<DrawingCard drawing={mockDrawing} />);
expect(screen.queryByText('Compare')).not.toBeInTheDocument();
});
it('should display dash for missing sheet number', () => {
const drawingWithoutSheet = { ...mockDrawing, sheetNumber: undefined };
render(<DrawingCard drawing={drawingWithoutSheet} />);
expect(screen.getByText('Sheet:')).toBeInTheDocument();
expect(screen.getByText('-')).toBeInTheDocument();
});
it('should display 0 for missing revision', () => {
const drawingWithoutRevision = { ...mockDrawing, revision: undefined };
render(<DrawingCard drawing={drawingWithoutRevision} />);
expect(screen.getByText('Rev:')).toBeInTheDocument();
expect(screen.getByText('0')).toBeInTheDocument();
});
it('should display N/A for missing scale', () => {
const drawingWithoutScale = { ...mockDrawing, scale: undefined };
render(<DrawingCard drawing={drawingWithoutScale} />);
expect(screen.getByText('Scale:')).toBeInTheDocument();
expect(screen.getByText('N/A')).toBeInTheDocument();
});
it('should not display date when issueDate is missing', () => {
const drawingWithoutDate = { ...mockDrawing, issueDate: undefined };
render(<DrawingCard drawing={drawingWithoutDate} />);
expect(screen.getByText('Date:')).toBeInTheDocument();
});
});
@@ -0,0 +1,161 @@
// File: frontend/components/drawings/__tests__/list.test.tsx
// Change Log:
// - 2026-06-13: Refactor to use static imports instead of require, fixing ESM module resolution errors
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen } from '@testing-library/react';
import { DrawingList } from '../list';
import { useDrawings } from '@/hooks/use-drawing';
// Mock useDrawings hook
vi.mock('@/hooks/use-drawing', () => ({
useDrawings: vi.fn(),
}));
// Mock ServerDataTable
vi.mock('@/components/documents/common/server-data-table', () => ({
ServerDataTable: ({ isLoading, data }: { isLoading: boolean; data: unknown[] }) => (
<div>
{isLoading ? <div>Loading...</div> : <div>{data.length} items</div>}
</div>
),
}));
describe('DrawingList', () => {
const mockProjectUuid = '019505a1-7c3e-7000-8000-abc123def456';
beforeEach(() => {
vi.clearAllMocks();
});
it('should render loading state', () => {
(useDrawings as any).mockReturnValue({
data: undefined,
isLoading: true,
isError: false,
error: null,
});
render(<DrawingList type="SHOP" projectUuid={mockProjectUuid} />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
it('should render drawings data', () => {
(useDrawings as any).mockReturnValue({
data: {
data: [{ publicId: 'uuid-1', drawingNumber: 'SD-001' }],
meta: { total: 1, page: 1, limit: 20, totalPages: 1 },
},
isLoading: false,
isError: false,
error: null,
});
render(<DrawingList type="SHOP" projectUuid={mockProjectUuid} />);
expect(screen.getByText('1 items')).toBeInTheDocument();
});
it('should render error state', () => {
(useDrawings as any).mockReturnValue({
data: undefined,
isLoading: false,
isError: true,
error: {
response: {
status: 500,
data: { message: 'Server error' },
},
},
});
render(<DrawingList type="SHOP" projectUuid={mockProjectUuid} />);
expect(screen.getByText('Failed to load shop drawings')).toBeInTheDocument();
expect(screen.getByText('HTTP 500: Server error')).toBeInTheDocument();
});
it('should render error with array message', () => {
(useDrawings as any).mockReturnValue({
data: undefined,
isLoading: false,
isError: true,
error: {
response: {
status: 400,
data: { message: ['Error 1', 'Error 2'] },
},
},
});
render(<DrawingList type="CONTRACT" projectUuid={mockProjectUuid} />);
expect(screen.getByText('Failed to load contract drawings')).toBeInTheDocument();
expect(screen.getByText(/Error 1, Error 2/)).toBeInTheDocument();
});
it('should render error with generic message', () => {
(useDrawings as any).mockReturnValue({
data: undefined,
isLoading: false,
isError: true,
error: {
message: 'Network error',
},
});
render(<DrawingList type="AS_BUILT" projectUuid={mockProjectUuid} />);
expect(screen.getByText('Failed to load as_built drawings')).toBeInTheDocument();
expect(screen.getByText('Network error')).toBeInTheDocument();
});
it('should handle empty data', () => {
(useDrawings as any).mockReturnValue({
data: {
data: [],
meta: { total: 0, page: 1, limit: 20, totalPages: 0 },
},
isLoading: false,
isError: false,
error: null,
});
render(<DrawingList type="SHOP" projectUuid={mockProjectUuid} />);
expect(screen.getByText('0 items')).toBeInTheDocument();
});
it('should pass filters to useDrawings', () => {
(useDrawings as any).mockReturnValue({
data: { data: [], meta: { total: 0, page: 1, limit: 20, totalPages: 0 } },
isLoading: false,
isError: false,
error: null,
});
render(
<DrawingList
type="SHOP"
projectUuid={mockProjectUuid}
filters={{ search: 'test' }}
/>
);
expect(useDrawings).toHaveBeenCalledWith('SHOP', {
projectUuid: mockProjectUuid,
search: 'test',
page: 1,
limit: 20,
});
});
it('should handle CONTRACT type', () => {
(useDrawings as any).mockReturnValue({
data: { data: [], meta: { total: 0, page: 1, limit: 20, totalPages: 0 } },
isLoading: false,
isError: false,
error: null,
});
render(<DrawingList type="CONTRACT" projectUuid={mockProjectUuid} />);
expect(useDrawings).toHaveBeenCalledWith('CONTRACT', expect.any(Object));
});
it('should handle AS_BUILT type', () => {
(useDrawings as any).mockReturnValue({
data: { data: [], meta: { total: 0, page: 1, limit: 20, totalPages: 0 } },
isLoading: false,
isError: false,
error: null,
});
render(<DrawingList type="AS_BUILT" projectUuid={mockProjectUuid} />);
expect(useDrawings).toHaveBeenCalledWith('AS_BUILT', expect.any(Object));
});
});
@@ -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();
});
});
@@ -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();
});
});