import { test, expect } from '@playwright/test'; import path from 'path'; /** * E2E Tests for Bulk Receipt Upload (US-005) * * Prerequisites: * 1. Start the test environment: ./start.sh test * 2. Ensure backend is running on port 8000 * 3. Ensure frontend is running on port 3000 * * Run: * npm run test:e2e * npm run test:e2e:headed # With visible browser * * Test PDFs are located in: docs/data-entry/ */ // Test configuration const TEST_USER = { username: 'MARIUS M', password: '123', company: 'MARIUSM AUTO' }; // Sample test PDFs from docs/data-entry/ const TEST_PDFS = [ 'benzina 13 iulie.pdf', 'brick igiena 604.pdf', 'electrobering igiena iulie 604.pdf' ]; // Corrupted file for error handling test const CORRUPTED_FILE = 'test-corrupted.txt'; test.describe('Bulk Receipt Upload E2E Tests', () => { test.beforeEach(async ({ page }) => { // Navigate to login page await page.goto('/'); // Wait for login page to load await page.waitForSelector('input[placeholder*="Utilizator"]', { timeout: 10000 }); // Fill login credentials await page.fill('input[placeholder*="Utilizator"]', TEST_USER.username); await page.fill('input[type="password"]', TEST_USER.password); // Click login button await page.click('button:has-text("Autentificare")'); // Wait for redirect after login await page.waitForURL(/\/(reports|data-entry)/, { timeout: 15000 }); // Select company if modal appears const companyModal = page.locator('.company-select-modal'); if (await companyModal.isVisible({ timeout: 2000 }).catch(() => false)) { await page.click(`text="${TEST_USER.company}"`); } }); test('should navigate to bulk upload page', async ({ page }) => { // Navigate to data entry module await page.goto('/data-entry/bulk-upload'); // Verify page title await expect(page.locator('h1')).toContainText('Upload Bonuri în Lot'); // Verify upload zone is visible await expect(page.locator('.bulk-upload-zone')).toBeVisible(); }); test('should upload 3 real PDF receipts and process them', async ({ page }) => { // Navigate to bulk upload page await page.goto('/data-entry/bulk-upload'); await page.waitForSelector('.bulk-upload-zone', { timeout: 10000 }); // Get file paths const testFilesPath = path.join(process.cwd(), 'docs', 'data-entry'); const filePaths = TEST_PDFS.map(f => path.join(testFilesPath, f)); // Upload files via file input const fileInput = page.locator('input[type="file"]'); await fileInput.setInputFiles(filePaths); // Verify files appear in the list await expect(page.locator('.bulk-file-list')).toBeVisible(); await expect(page.locator('.file-item')).toHaveCount(3); // Verify file names are shown for (const pdfName of TEST_PDFS) { await expect(page.locator(`.file-item:has-text("${pdfName}")`)).toBeVisible(); } // Verify total count message await expect(page.locator('text=/3 fișier/')).toBeVisible(); // Click process button await page.click('button:has-text("Procesează")'); // Wait for upload to complete and processing to start await expect(page.locator('.bulk-progress-bar')).toBeVisible({ timeout: 10000 }); // Wait for all files to be processed (up to 3 minutes for OCR) await page.waitForFunction(() => { const progress = document.querySelector('.bulk-progress-bar'); if (!progress) return false; const text = progress.textContent || ''; // Check if all 3 are processed (3/3) return text.includes('3/3') || text.includes('100%'); }, { timeout: 180000 }); // Verify summary modal appears await expect(page.locator('.bulk-summary-modal, .p-dialog')).toBeVisible({ timeout: 10000 }); // Verify success count await expect(page.locator('.success-count, .stat-success .stat-value')).toBeVisible(); // Get the number of successful receipts const successCount = await page.locator('.success-count, .stat-success .stat-value').textContent(); console.log(`Successfully processed: ${successCount} receipts`); // Verify total amount is displayed await expect(page.locator('.total-amount')).toBeVisible(); }); test('should show receipts in list after processing', async ({ page }) => { // First upload and process files (abbreviated version) await page.goto('/data-entry/bulk-upload'); await page.waitForSelector('.bulk-upload-zone', { timeout: 10000 }); const testFilesPath = path.join(process.cwd(), 'docs', 'data-entry'); const filePaths = TEST_PDFS.slice(0, 1).map(f => path.join(testFilesPath, f)); const fileInput = page.locator('input[type="file"]'); await fileInput.setInputFiles(filePaths); await page.click('button:has-text("Procesează")'); // Wait for processing to complete await page.waitForFunction(() => { const modal = document.querySelector('.bulk-summary-modal, .p-dialog'); return modal && window.getComputedStyle(modal).display !== 'none'; }, { timeout: 120000 }); // Click "Vezi bonurile create" link await page.click('button:has-text("Vezi bonurile create")'); // Verify navigation to receipts list await expect(page).toHaveURL(/\/data-entry/); // Verify receipts are listed await expect(page.locator('.receipts-table, .p-datatable')).toBeVisible({ timeout: 10000 }); }); test('should handle file removal before upload', async ({ page }) => { await page.goto('/data-entry/bulk-upload'); await page.waitForSelector('.bulk-upload-zone', { timeout: 10000 }); const testFilesPath = path.join(process.cwd(), 'docs', 'data-entry'); const filePaths = TEST_PDFS.map(f => path.join(testFilesPath, f)); // Upload files const fileInput = page.locator('input[type="file"]'); await fileInput.setInputFiles(filePaths); // Verify 3 files are listed await expect(page.locator('.file-item')).toHaveCount(3); // Click remove button on first file await page.locator('.file-item').first().locator('button.btn-remove, .pi-times').click(); // Verify only 2 files remain await expect(page.locator('.file-item')).toHaveCount(2); }); test('should clear all files with "Golește lista" button', async ({ page }) => { await page.goto('/data-entry/bulk-upload'); await page.waitForSelector('.bulk-upload-zone', { timeout: 10000 }); const testFilesPath = path.join(process.cwd(), 'docs', 'data-entry'); const filePaths = TEST_PDFS.map(f => path.join(testFilesPath, f)); // Upload files const fileInput = page.locator('input[type="file"]'); await fileInput.setInputFiles(filePaths); // Verify files are listed await expect(page.locator('.file-item')).toHaveCount(3); // Click clear button await page.click('button:has-text("Golește lista")'); // Verify list is empty await expect(page.locator('.file-item')).toHaveCount(0); await expect(page.locator('.bulk-file-list')).not.toBeVisible(); }); test('should show progress during processing', async ({ page }) => { await page.goto('/data-entry/bulk-upload'); await page.waitForSelector('.bulk-upload-zone', { timeout: 10000 }); const testFilesPath = path.join(process.cwd(), 'docs', 'data-entry'); const filePaths = TEST_PDFS.slice(0, 2).map(f => path.join(testFilesPath, f)); const fileInput = page.locator('input[type="file"]'); await fileInput.setInputFiles(filePaths); await page.click('button:has-text("Procesează")'); // Verify progress bar appears await expect(page.locator('.bulk-progress-bar')).toBeVisible({ timeout: 10000 }); // Verify progress list shows items await expect(page.locator('.bulk-progress-list')).toBeVisible(); // Verify status badges are shown await expect(page.locator('.status-badge, .p-tag')).toBeVisible(); }); test('should disable upload zone during processing', async ({ page }) => { await page.goto('/data-entry/bulk-upload'); await page.waitForSelector('.bulk-upload-zone', { timeout: 10000 }); const testFilesPath = path.join(process.cwd(), 'docs', 'data-entry'); const filePaths = [path.join(testFilesPath, TEST_PDFS[0])]; const fileInput = page.locator('input[type="file"]'); await fileInput.setInputFiles(filePaths); await page.click('button:has-text("Procesează")'); // Wait for processing to start await expect(page.locator('.bulk-progress-bar')).toBeVisible({ timeout: 10000 }); // Verify upload zone is disabled const uploadZone = page.locator('.bulk-upload-zone'); await expect(uploadZone).toHaveClass(/disabled/); // Verify process button is disabled await expect(page.locator('button:has-text("Procesează")')).toBeDisabled(); }); test('should allow loading new batch after completion', async ({ page }) => { await page.goto('/data-entry/bulk-upload'); await page.waitForSelector('.bulk-upload-zone', { timeout: 10000 }); const testFilesPath = path.join(process.cwd(), 'docs', 'data-entry'); const filePaths = [path.join(testFilesPath, TEST_PDFS[0])]; const fileInput = page.locator('input[type="file"]'); await fileInput.setInputFiles(filePaths); await page.click('button:has-text("Procesează")'); // Wait for summary modal await page.waitForFunction(() => { const modal = document.querySelector('.bulk-summary-modal, .p-dialog'); return modal && window.getComputedStyle(modal).display !== 'none'; }, { timeout: 120000 }); // Click "Încarcă alt batch" button await page.click('button:has-text("Încarcă alt batch")'); // Verify modal closes and upload zone is enabled await expect(page.locator('.bulk-summary-modal, .p-dialog')).not.toBeVisible(); await expect(page.locator('.bulk-upload-zone')).toBeVisible(); await expect(page.locator('.bulk-upload-zone')).not.toHaveClass(/disabled/); }); }); test.describe('Bulk Upload Error Handling', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); await page.waitForSelector('input[placeholder*="Utilizator"]', { timeout: 10000 }); await page.fill('input[placeholder*="Utilizator"]', TEST_USER.username); await page.fill('input[type="password"]', TEST_USER.password); await page.click('button:has-text("Autentificare")'); await page.waitForURL(/\/(reports|data-entry)/, { timeout: 15000 }); }); test('should reject files with invalid MIME type', async ({ page }) => { await page.goto('/data-entry/bulk-upload'); await page.waitForSelector('.bulk-upload-zone', { timeout: 10000 }); // Create a text file (invalid type) const fileInput = page.locator('input[type="file"]'); // Try to set a text file - should be rejected by client-side validation // Note: This tests the accept attribute on the file input const acceptAttr = await fileInput.getAttribute('accept'); expect(acceptAttr).toContain('pdf'); expect(acceptAttr).toContain('image'); }); test('should show error message when upload fails', async ({ page }) => { // Navigate to bulk upload with backend not running on a specific endpoint await page.goto('/data-entry/bulk-upload'); await page.waitForSelector('.bulk-upload-zone', { timeout: 10000 }); const testFilesPath = path.join(process.cwd(), 'docs', 'data-entry'); const filePaths = [path.join(testFilesPath, TEST_PDFS[0])]; const fileInput = page.locator('input[type="file"]'); await fileInput.setInputFiles(filePaths); // Intercept the upload request and make it fail await page.route('**/api/data-entry/bulk/upload', route => { route.fulfill({ status: 500, body: JSON.stringify({ detail: 'Internal server error' }) }); }); await page.click('button:has-text("Procesează")'); // Verify error message appears await expect(page.locator('.error-message')).toBeVisible({ timeout: 10000 }); await expect(page.locator('.error-message')).toContainText(/eroare|error/i); }); });