Files
roa2web-service-auto/e2e/bulk-upload.spec.js
Claude Agent b137e80b71 feat: multi-Oracle server support with runtime switching
Complete implementation of multi-server Oracle database support:

Backend:
- Multi-pool Oracle with lazy loading per server
- Email-to-server cache for automatic server discovery
- JWT tokens include server_id claim
- /auth/check-identity and /auth/check-email endpoints
- /auth/my-servers endpoint for listing user's accessible servers
- Server switch with password re-authentication

Frontend:
- New ServerSelector component for header dropdown
- Multi-step login flow (identity → server → password)
- Server switching from header with password modal
- Mobile drawer menu with server selection
- Dark mode support for all new components
- URL bookmark support with ?server= query param

Scripts:
- Unified start.sh replacing start-prod.sh/start-test.sh
- Unified ssh-tunnel.sh with multi-server support
- Updated status.sh for new architecture

Tests:
- E2E tests for multi-server and single-server login flows
- Backend unit tests for all new endpoints
- Oracle multi-pool integration tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 22:39:06 +00:00

325 lines
12 KiB
JavaScript

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);
});
});