test(frontend): raise overall statement coverage to 30.42% for Phase 1 MVP
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user