Files
flyer-crawler.projectium.com/src/features/flyer/BulkImporter.test.tsx
Torben Sorensen 921c48fc57
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 10m14s
more unit test fixes now the UseProfileAddress OOM has been identified
2025-12-23 15:50:01 -08:00

217 lines
8.8 KiB
TypeScript

// src/features/flyer/BulkImporter.test.tsx
import React from 'react';
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { BulkImporter } from './BulkImporter';
describe('BulkImporter', () => {
const mockOnFilesChange = vi.fn();
const mockCreateObjectURL = vi.fn();
const mockRevokeObjectURL = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
// Mock the global URL object methods
global.URL.createObjectURL = mockCreateObjectURL;
global.URL.revokeObjectURL = mockRevokeObjectURL;
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should render the initial state correctly', () => {
render(<BulkImporter onFilesChange={mockOnFilesChange} isProcessing={false} />);
expect(screen.getByText(/click to upload/i)).toBeInTheDocument();
expect(screen.getByText(/drag and drop/i)).toBeInTheDocument();
expect(screen.getByText(/png, jpg, webp, or pdf/i)).toBeInTheDocument();
});
it('should render the processing state', () => {
render(<BulkImporter onFilesChange={mockOnFilesChange} isProcessing={true} />);
expect(screen.getByText(/processing, please wait.../i)).toBeInTheDocument();
const label = screen.getByText(/processing, please wait.../i).closest('label');
expect(label).toHaveClass('cursor-not-allowed');
expect(label).toHaveClass('opacity-60');
});
it('should call onFilesChange when files are selected via input', async () => {
render(<BulkImporter onFilesChange={mockOnFilesChange} isProcessing={false} />);
const file = new File(['content'], 'flyer.pdf', { type: 'application/pdf' });
const input = screen.getByLabelText(/click to upload/i);
await waitFor(() =>
fireEvent.change(input, {
target: { files: [file] },
}),
);
expect(mockOnFilesChange).toHaveBeenCalledTimes(1);
expect(mockOnFilesChange.mock.calls[0][0][0]).toBe(file);
});
it('should handle drag and drop events', () => {
render(<BulkImporter onFilesChange={mockOnFilesChange} isProcessing={false} />);
// `getByLabelText` finds the input associated with the label.
// We need to test the label itself, as it contains the drag-and-drop handlers and styles.
// We can find it by its text content.
const dropzone = screen.getByText(/click to upload/i).closest('label');
// Assert that the dropzone element was found before using it.
// This satisfies TypeScript's null-check and makes the test more robust.
expect(dropzone).not.toBeNull();
// Test drag enter
// We need to assert that dropzone is not null before passing it to fireEvent.
if (!dropzone) throw new Error('Dropzone not found');
fireEvent.dragEnter(dropzone, { dataTransfer: { files: [] } });
expect(dropzone).toHaveClass('border-brand-primary');
// Test drag leave
// We need to assert that dropzone is not null before passing it to fireEvent.
if (!dropzone) throw new Error('Dropzone not found');
fireEvent.dragLeave(dropzone);
expect(dropzone).not.toHaveClass('border-brand-primary');
});
it('should call onFilesChange when files are dropped', () => {
render(<BulkImporter onFilesChange={mockOnFilesChange} isProcessing={false} />);
// The drop event handlers are on the <label>, not the <input>
const dropzone = screen.getByText(/click to upload/i).closest('label')!;
const file = new File(['content'], 'flyer.pdf', { type: 'application/pdf' });
fireEvent.drop(dropzone, {
dataTransfer: {
files: [file],
items: [file], // Also mock the 'items' property for full DataTransfer simulation
},
});
expect(mockOnFilesChange).toHaveBeenCalledTimes(1);
expect(mockOnFilesChange.mock.calls[0][0][0]).toBe(file);
});
it('should not call onFilesChange if no files are dropped', () => {
render(<BulkImporter onFilesChange={mockOnFilesChange} isProcessing={false} />);
// The drop event handlers are on the <label>, not the <input>
const dropzone = screen.getByText(/click to upload/i).closest('label')!;
fireEvent.drop(dropzone, {
dataTransfer: {
files: [],
},
});
expect(mockOnFilesChange).not.toHaveBeenCalled();
});
it('should not respond to interactions when isProcessing is true', () => {
render(<BulkImporter onFilesChange={mockOnFilesChange} isProcessing={true} />);
const dropzone = screen.getByLabelText(/processing, please wait.../i);
fireEvent.dragEnter(dropzone, { dataTransfer: { files: [] } });
expect(dropzone).not.toHaveClass('border-brand-primary');
});
describe('when files are selected', () => {
const imageFile = new File(['image-content'], 'flyer.jpg', { type: 'image/jpeg' });
const pdfFile = new File(['pdf-content'], 'document.pdf', { type: 'application/pdf' });
it('should display file previews and distinguish between images and other file types', async () => {
mockCreateObjectURL
.mockReturnValueOnce('blob:http://localhost/image-guid') // For the image file
.mockReturnValueOnce(''); // For the PDF file
render(<BulkImporter onFilesChange={mockOnFilesChange} isProcessing={false} />);
const input = screen.getByLabelText(/click to upload/i);
await act(async () => {
fireEvent.change(input, { target: { files: [imageFile, pdfFile] } });
});
// Wait for previews to render
await waitFor(() => {
expect(screen.getByText('flyer.jpg')).toBeInTheDocument();
expect(screen.getByText('document.pdf')).toBeInTheDocument();
});
// Check that an image preview is rendered for the image file
const imagePreview = screen.getByAltText('flyer.jpg');
expect(imagePreview).toBeInTheDocument();
expect(imagePreview).toHaveAttribute('src', 'blob:http://localhost/image-guid');
// Check that a generic document icon is rendered for the PDF
const pdfPreviewContainer = screen
.getByText('document.pdf')
.closest('div.relative') as HTMLElement;
// The icon itself doesn't have a test-id, but we can find it by its role and class.
expect(pdfPreviewContainer.querySelector('svg.w-12.h-12')).toBeInTheDocument();
});
it('should allow removing a file from the preview list', async () => {
render(<BulkImporter onFilesChange={mockOnFilesChange} isProcessing={false} />);
const input = screen.getByLabelText(/click to upload/i);
await act(async () => {
fireEvent.change(input, { target: { files: [imageFile, pdfFile] } });
});
// Wait for previews to appear and check initial state
await screen.findByText('flyer.jpg');
expect(screen.getByText('document.pdf')).toBeInTheDocument();
expect(mockOnFilesChange).toHaveBeenLastCalledWith([imageFile, pdfFile]);
// Find and click the remove button for the first file ('flyer.jpg')
const removeButton = screen.getByLabelText(`Remove ${imageFile.name}`);
await act(async () => {
fireEvent.click(removeButton);
});
// Assert that the file was removed from the display and the parent was notified
expect(screen.queryByText('flyer.jpg')).not.toBeInTheDocument();
expect(screen.getByText('document.pdf')).toBeInTheDocument(); // The other file should remain
expect(mockOnFilesChange).toHaveBeenLastCalledWith([pdfFile]);
});
it('should prevent adding duplicate files', async () => {
render(<BulkImporter onFilesChange={mockOnFilesChange} isProcessing={false} />);
const input = screen.getByLabelText(/click to upload/i);
// Add the file for the first time
await act(async () => {
fireEvent.change(input, { target: { files: [imageFile] } });
});
expect(mockOnFilesChange).toHaveBeenCalledTimes(1);
expect(mockOnFilesChange).toHaveBeenLastCalledWith([imageFile]);
// Attempt to add the exact same file again
await act(async () => {
fireEvent.change(input, { target: { files: [imageFile] } });
});
// The number of calls should NOT increase because the file is a duplicate
expect(mockOnFilesChange).toHaveBeenCalledTimes(1);
});
it('should revoke object URLs on cleanup', async () => {
mockCreateObjectURL.mockReturnValue('blob:http://localhost/test-url');
const { unmount } = render(
<BulkImporter onFilesChange={mockOnFilesChange} isProcessing={false} />,
);
const input = screen.getByLabelText(/click to upload/i);
await act(async () => {
fireEvent.change(input, { target: { files: [imageFile] } });
});
await waitFor(() => {
expect(mockCreateObjectURL).toHaveBeenCalledWith(imageFile);
});
// Unmount the component to trigger the cleanup effect
unmount();
expect(mockRevokeObjectURL).toHaveBeenCalledTimes(1);
expect(mockRevokeObjectURL).toHaveBeenCalledWith('blob:http://localhost/test-url');
});
});
});