// File: frontend/components/migration/__tests__/review-queue-table.test.tsx import React from 'react'; import { render, screen, waitFor, fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { ReviewQueueTable } from '../review-queue-table'; import { MigrationReviewStatus } from '@/types/migration'; // Mock hooks const mockMutateAsyncCommit = vi.fn(); const mockMutateAsyncReject = vi.fn(); vi.mock('@/hooks/use-migration-review', () => ({ useCommitMigrationReview: () => ({ mutateAsync: mockMutateAsyncCommit, isPending: false }), useRejectMigrationReview: () => ({ mutateAsync: mockMutateAsyncReject, isPending: false }) })); vi.mock('@/hooks/use-master-data', () => ({ useProjects: () => ({ data: [ { publicId: 'proj-1', projectName: 'Project A', projectCode: 'PA' } ] }), useOrganizations: () => ({ data: [ { publicId: 'org-1', organizationName: 'Org A' } ] }) })); // Mock ResizeObserver for Radix UI class ResizeObserverMock { observe() {} unobserve() {} disconnect() {} } global.ResizeObserver = ResizeObserverMock; describe('ReviewQueueTable', () => { const mockItems: any[] = [ { id: 1, publicId: 'mig-1', documentNumber: 'DOC-001', subject: 'Test Migration Doc', aiSuggestedCategory: 'RFA', aiConfidence: 0.95, status: MigrationReviewStatus.PENDING, projectId: 'proj-1', senderOrganizationId: 'org-1', receiverOrganizationId: 'org-2', issuedDate: '2026-06-01T00:00:00.000Z', receivedDate: '2026-06-02T00:00:00.000Z', body: 'Migration test body', extractedTags: [{ name: 'Urgent', is_new: false }], aiIssues: [{ message: 'Confidence is slightly low on receiver' }] }, { id: 2, publicId: 'mig-2', documentNumber: 'DOC-002', subject: 'Test Migration Doc 2', aiSuggestedCategory: 'Correspondence', aiConfidence: 0.85, status: MigrationReviewStatus.IMPORTED, } ]; beforeEach(() => { vi.clearAllMocks(); // Mock window.confirm vi.spyOn(window, 'confirm').mockImplementation(() => true); // Mock scrollIntoView for Radix components window.HTMLElement.prototype.scrollIntoView = vi.fn(); }); it('renders loading state', () => { render(); expect(screen.getByText('กำลังโหลดรายการรอรีวิว...')).toBeInTheDocument(); }); it('renders empty state', () => { render(); expect(screen.getByText('ไม่พบรายการที่รอตรวจสอบในคิวขณะนี้')).toBeInTheDocument(); }); it('renders queue items', () => { render(); expect(screen.getByText('DOC-001')).toBeInTheDocument(); expect(screen.getByText('Test Migration Doc')).toBeInTheDocument(); expect(screen.getByText('95.0%')).toBeInTheDocument(); expect(screen.getByText('รอตรวจสอบ')).toBeInTheDocument(); expect(screen.getByText('DOC-002')).toBeInTheDocument(); expect(screen.getByText('Test Migration Doc 2')).toBeInTheDocument(); expect(screen.getByText('85.0%')).toBeInTheDocument(); expect(screen.getByText('นำเข้าแล้ว')).toBeInTheDocument(); }); it('opens sheet when review button is clicked', async () => { render(); const reviewButtons = screen.getAllByRole('button', { name: /รีวิว|ดูรายละเอียด/i }); // First button is for 'รอตรวจสอบ' (PENDING) fireEvent.click(reviewButtons[0]); await waitFor(() => { expect(screen.getByText('รีวิวการย้ายข้อมูลเอกสาร')).toBeInTheDocument(); // Should show the document number in a badge expect(screen.getAllByText('DOC-001').length).toBeGreaterThan(0); // Should show AI issues expect(screen.getByText('Confidence is slightly low on receiver')).toBeInTheDocument(); }); }); it('allows editing subject and other fields', async () => { render(); const reviewButtons = screen.getAllByRole('button', { name: /รีวิว|ดูรายละเอียด/i }); fireEvent.click(reviewButtons[0]); await waitFor(() => { expect(screen.getByRole('textbox', { name: /หัวข้อเรื่อง/i })).toHaveValue('Test Migration Doc'); }); const subjectInput = screen.getByRole('textbox', { name: /หัวข้อเรื่อง/i }); fireEvent.change(subjectInput, { target: { value: 'Updated Subject' } }); expect(subjectInput).toHaveValue('Updated Subject'); const bodyInput = screen.getByRole('textbox', { name: /เนื้อหาสรุปจดหมาย/i }); fireEvent.change(bodyInput, { target: { value: 'Updated Body' } }); expect(bodyInput).toHaveValue('Updated Body'); const issuedDateInput = screen.getByLabelText(/วันที่ออกเอกสาร/i); fireEvent.change(issuedDateInput, { target: { value: '2026-06-10' } }); expect(issuedDateInput).toHaveValue('2026-06-10'); const receivedDateInput = screen.getByLabelText(/วันที่ลงรับเอกสาร/i); fireEvent.change(receivedDateInput, { target: { value: '2026-06-11' } }); expect(receivedDateInput).toHaveValue('2026-06-11'); }); it('allows adding and removing tags', async () => { render(); const reviewButtons = screen.getAllByRole('button', { name: /รีวิว|ดูรายละเอียด/i }); fireEvent.click(reviewButtons[0]); await waitFor(() => { // Urgent is already there expect(screen.getByText('Urgent')).toBeInTheDocument(); }); // Add new tag with Enter key const addTagInput = screen.getByPlaceholderText('เพิ่มแท็กภาษาไทย...'); fireEvent.change(addTagInput, { target: { value: 'NewTag' } }); fireEvent.keyDown(addTagInput, { key: 'Enter', code: 'Enter' }); await waitFor(() => { expect(screen.getByText('NewTag')).toBeInTheDocument(); }); // Add another tag with button fireEvent.change(addTagInput, { target: { value: 'AnotherTag' } }); const addButton = screen.getByRole('button', { name: /เพิ่ม/i }); fireEvent.click(addButton); await waitFor(() => { expect(screen.getByText('AnotherTag')).toBeInTheDocument(); }); // Remove Urgent tag // The tag badge contains 'Urgent' and an 'X' button const removeButtons = screen.getAllByRole('button', { name: '' }); // The first X button inside a badge should be the one for 'Urgent' (assuming it's the only icon button without a distinct name there) // Actually, Lucide icon doesn't have a label by default, let's find the button by its parent const urgentTag = screen.getByText('Urgent'); const removeUrgentButton = urgentTag.nextElementSibling; if (removeUrgentButton) { fireEvent.click(removeUrgentButton); } await waitFor(() => { expect(screen.queryByText('Urgent')).not.toBeInTheDocument(); }); }); it('calls commit mutation on commit', async () => { render(); const reviewButtons = screen.getAllByRole('button', { name: /รีวิว|ดูรายละเอียด/i }); fireEvent.click(reviewButtons[0]); await waitFor(() => { expect(screen.getByRole('button', { name: /กดยอมรับการนำเข้า/i })).toBeInTheDocument(); }); const commitButton = screen.getByRole('button', { name: /กดยอมรับการนำเข้า/i }); fireEvent.click(commitButton); await waitFor(() => { expect(mockMutateAsyncCommit).toHaveBeenCalledWith(expect.objectContaining({ publicId: 'mig-1', subject: 'Test Migration Doc', category: 'RFA', projectId: 'proj-1', senderId: 'org-1', receiverId: 'org-2', issuedDate: '2026-06-01', receivedDate: '2026-06-02', body: 'Migration test body', tags: ['Urgent'], })); }); }); it('calls reject mutation on reject', async () => { render(); const reviewButtons = screen.getAllByRole('button', { name: /รีวิว|ดูรายละเอียด/i }); fireEvent.click(reviewButtons[0]); await waitFor(() => { expect(screen.getByRole('button', { name: /ปฏิเสธการนำเข้า/i })).toBeInTheDocument(); }); const rejectButton = screen.getByRole('button', { name: /ปฏิเสธการนำเข้า/i }); fireEvent.click(rejectButton); await waitFor(() => { expect(window.confirm).toHaveBeenCalled(); expect(mockMutateAsyncReject).toHaveBeenCalledWith(1); }); }); it('closes sheet when cancel is clicked', async () => { render(); const reviewButtons = screen.getAllByRole('button', { name: /รีวิว|ดูรายละเอียด/i }); fireEvent.click(reviewButtons[0]); await waitFor(() => { expect(screen.getByRole('button', { name: /ยกเลิก/i })).toBeInTheDocument(); }); const cancelButton = screen.getByRole('button', { name: /ยกเลิก/i }); fireEvent.click(cancelButton); // Wait for the sheet to be removed or hidden await waitFor(() => { expect(screen.queryByText('รีวิวการย้ายข้อมูลเอกสาร')).not.toBeInTheDocument(); }); }); });