Files
lcbp3/frontend/components/common/__tests__/file-preview-modal.test.tsx
T
admin c9bd403809
CI / CD Pipeline / build (push) Successful in 8m55s
CI / CD Pipeline / deploy (push) Failing after 5m38s
690503:1135 Update workflow #03
2026-05-03 11:35:27 +07:00

149 lines
5.1 KiB
TypeScript

// T043: Vitest component test for FilePreviewModal
// ตรวจสอบ: PDF → iframe, Image → img, unsupported → download link, onClose callback
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import apiClient from '@/lib/api/client';
import { FilePreviewModal } from '../file-preview-modal';
import type { WorkflowAttachmentSummary } from '@/types/workflow';
// Mock useTranslations — คืน stable function reference เพื่อป้องกัน useEffect loop
// ถ้า mock คืน inline arrow function ใหม่ทุก render, t จะเปลี่ยนทุกรอบ
// ทำให้ useEffect (ที่มี t ใน deps) ทำงานซ้ำแบบ infinite loop → isLoading ค้างเป็น true ตลอด
const stableT = (key: string): string => key;
vi.mock('@/hooks/use-translations', () => ({
useTranslations: () => stableT,
}));
// apiClient.get ถูก mock ใน vitest.setup.ts แล้ว
const mockApiGet = vi.mocked(apiClient.get);
// Mock URL.createObjectURL / revokeObjectURL
const mockObjectUrl = 'blob:http://localhost/mock-blob-url';
vi.stubGlobal('URL', {
createObjectURL: vi.fn().mockReturnValue(mockObjectUrl),
revokeObjectURL: vi.fn(),
});
const makeAttachment = (
overrides: Partial<WorkflowAttachmentSummary> = {}
): WorkflowAttachmentSummary => ({
publicId: 'att-preview-001',
originalFilename: 'test-file.pdf',
mimeType: 'application/pdf',
fileSize: 102400,
createdAt: '2026-01-01T00:00:00.000Z',
...overrides,
});
describe('FilePreviewModal', () => {
const onClose = vi.fn();
const onUnavailable = vi.fn();
const mockBlob = new Blob(['%PDF-1.4'], { type: 'application/pdf' });
beforeEach(() => {
vi.clearAllMocks();
mockApiGet.mockResolvedValue({ data: mockBlob });
});
it('renders iframe for PDF MIME type', async () => {
const attachment = makeAttachment({ mimeType: 'application/pdf' });
render(<FilePreviewModal attachment={attachment} onClose={onClose} />);
await waitFor(() => {
expect(screen.getByTitle('test-file.pdf')).toBeInTheDocument();
});
const iframe = screen.getByTitle('test-file.pdf') as HTMLIFrameElement;
expect(iframe.tagName).toBe('IFRAME');
expect(iframe.src).toContain('blob:');
});
it('renders img for image MIME type', async () => {
const imageBlob = new Blob(['fake-image'], { type: 'image/png' });
mockApiGet.mockResolvedValue({ data: imageBlob });
const attachment = makeAttachment({
mimeType: 'image/png',
originalFilename: 'photo.png',
});
render(<FilePreviewModal attachment={attachment} onClose={onClose} />);
await waitFor(() => {
expect(screen.getByAltText('photo.png')).toBeInTheDocument();
});
const img = screen.getByAltText('photo.png') as HTMLImageElement;
expect(img.tagName).toBe('IMG');
});
it('shows download link for unsupported MIME type (no iframe or img)', async () => {
const docxBlob = new Blob(['PK...'], {
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
});
mockApiGet.mockResolvedValue({ data: docxBlob });
const attachment = makeAttachment({
mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
originalFilename: 'report.docx',
});
render(<FilePreviewModal attachment={attachment} onClose={onClose} />);
await waitFor(() => {
// download link ต้องมี href = blobUrl
const link = screen.getByRole('link');
expect(link).toHaveAttribute('href', mockObjectUrl);
expect(link).toHaveAttribute('download', 'report.docx');
});
// ต้องไม่มี iframe หรือ img
expect(screen.queryByTitle('report.docx')).not.toBeInTheDocument();
});
it('calls onClose when close button is clicked', async () => {
const attachment = makeAttachment();
render(<FilePreviewModal attachment={attachment} onClose={onClose} />);
await waitFor(() => {
expect(screen.getByRole('button', { name: /filepreview.close/i })).toBeInTheDocument();
});
await userEvent.click(screen.getByRole('button', { name: /filepreview.close/i }));
expect(onClose).toHaveBeenCalledTimes(1);
});
it('calls onUnavailable when API returns 404', async () => {
const notFoundError = Object.assign(new Error('Not Found'), {
response: { status: 404 },
});
mockApiGet.mockRejectedValue(notFoundError);
const attachment = makeAttachment({ publicId: 'missing-att-001' });
render(
<FilePreviewModal
attachment={attachment}
onClose={onClose}
onUnavailable={onUnavailable}
/>
);
await waitFor(() => {
expect(onUnavailable).toHaveBeenCalledWith('missing-att-001');
});
});
it('does not render when attachment is null (dialog closed)', () => {
render(<FilePreviewModal attachment={null} onClose={onClose} />);
// Dialog should not be visible
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
});