feat(e2e): add playwright for e2e tests
This commit is contained in:
parent
ed9d467c9f
commit
efbd15774a
|
|
@ -0,0 +1,124 @@
|
|||
name: E2E Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, main ]
|
||||
pull_request:
|
||||
branches: [ master, main ]
|
||||
|
||||
jobs:
|
||||
test-e2e:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:13
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: readur_test
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: ./frontend
|
||||
run: npm ci
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
working-directory: ./frontend
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
- name: Build frontend
|
||||
working-directory: ./frontend
|
||||
run: npm run build
|
||||
|
||||
- name: Setup test database
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -U postgres -d readur_test -c "CREATE EXTENSION IF NOT EXISTS vector;"
|
||||
|
||||
- name: Run database migrations
|
||||
run: |
|
||||
# Set environment variables for test database
|
||||
export DATABASE_URL="postgresql://postgres:postgres@localhost:5432/readur_test"
|
||||
export TEST_MODE=true
|
||||
|
||||
# Run migrations
|
||||
cargo run --bin migrate
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/readur_test
|
||||
TEST_MODE: true
|
||||
|
||||
- name: Start backend server
|
||||
run: |
|
||||
# Build and start the backend in test mode
|
||||
cargo build --release
|
||||
|
||||
# Start server in background
|
||||
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/readur_test" \
|
||||
TEST_MODE=true \
|
||||
ROCKET_PORT=8000 \
|
||||
./target/release/readur &
|
||||
|
||||
# Wait for server to be ready
|
||||
timeout 60 bash -c 'until curl -f http://localhost:8000/health; do sleep 2; done'
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/readur_test
|
||||
TEST_MODE: true
|
||||
ROCKET_PORT: 8000
|
||||
|
||||
- name: Start frontend dev server
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
# Start Vite dev server in background
|
||||
npm run dev &
|
||||
|
||||
# Wait for frontend to be ready
|
||||
timeout 60 bash -c 'until curl -f http://localhost:5173; do sleep 2; done'
|
||||
env:
|
||||
VITE_API_BASE_URL: http://localhost:8000
|
||||
|
||||
- name: Run Playwright tests
|
||||
working-directory: ./frontend
|
||||
run: npm run test:e2e
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: frontend/test-results/
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload test artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-artifacts
|
||||
path: |
|
||||
frontend/test-results/e2e-artifacts/
|
||||
frontend/test-results/screenshots/
|
||||
retention-days: 30
|
||||
|
|
@ -0,0 +1,279 @@
|
|||
# Readur E2E Testing Guide
|
||||
|
||||
This guide covers the end-to-end (E2E) testing setup for Readur using Playwright.
|
||||
|
||||
## Overview
|
||||
|
||||
The E2E test suite covers:
|
||||
- User authentication flows
|
||||
- Document upload and processing
|
||||
- Search functionality
|
||||
- Document management
|
||||
- Complete user workflows
|
||||
|
||||
## Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18+ and npm
|
||||
- Rust and Cargo
|
||||
- PostgreSQL
|
||||
- Git
|
||||
|
||||
### Installation
|
||||
|
||||
1. **Install Playwright dependencies**:
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npx playwright install
|
||||
```
|
||||
|
||||
2. **Set up test database**:
|
||||
```bash
|
||||
# Create test database
|
||||
createdb readur_e2e_test
|
||||
|
||||
# Add vector extension (if available)
|
||||
psql -d readur_e2e_test -c "CREATE EXTENSION IF NOT EXISTS vector;"
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Local Development
|
||||
|
||||
#### Quick Start
|
||||
Use the provided script for automated setup:
|
||||
```bash
|
||||
./scripts/run-e2e-local.sh
|
||||
```
|
||||
|
||||
#### Manual Setup
|
||||
If you prefer manual control:
|
||||
|
||||
1. **Start backend server**:
|
||||
```bash
|
||||
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/readur_e2e_test" \
|
||||
TEST_MODE=true \
|
||||
ROCKET_PORT=8001 \
|
||||
cargo run --release
|
||||
```
|
||||
|
||||
2. **Start frontend dev server**:
|
||||
```bash
|
||||
cd frontend
|
||||
VITE_API_BASE_URL="http://localhost:8001" \
|
||||
npm run dev -- --port 5174
|
||||
```
|
||||
|
||||
3. **Run tests**:
|
||||
```bash
|
||||
cd frontend
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
### Test Options
|
||||
|
||||
- **Headless mode** (default): `npm run test:e2e`
|
||||
- **Headed mode** (show browser): `npm run test:e2e:headed`
|
||||
- **Debug mode**: `npm run test:e2e:debug`
|
||||
- **UI mode**: `npm run test:e2e:ui`
|
||||
|
||||
### Using the Local Script
|
||||
|
||||
The `run-e2e-local.sh` script provides additional options:
|
||||
|
||||
```bash
|
||||
# Run tests normally
|
||||
./scripts/run-e2e-local.sh
|
||||
|
||||
# Run in headed mode
|
||||
./scripts/run-e2e-local.sh --headed
|
||||
|
||||
# Run in debug mode
|
||||
./scripts/run-e2e-local.sh --debug
|
||||
|
||||
# Run with Playwright UI
|
||||
./scripts/run-e2e-local.sh --ui
|
||||
|
||||
# Show help
|
||||
./scripts/run-e2e-local.sh --help
|
||||
```
|
||||
|
||||
## GitHub Actions
|
||||
|
||||
The E2E tests automatically run in GitHub Actions on:
|
||||
- Push to `master`/`main` branch
|
||||
- Pull requests to `master`/`main` branch
|
||||
|
||||
The workflow:
|
||||
1. Sets up PostgreSQL database
|
||||
2. Builds and starts the backend server
|
||||
3. Starts the frontend dev server
|
||||
4. Runs all E2E tests
|
||||
5. Uploads test reports and artifacts
|
||||
|
||||
## Test Structure
|
||||
|
||||
### Test Files
|
||||
|
||||
- `e2e/auth.spec.ts` - Authentication flows
|
||||
- `e2e/upload.spec.ts` - Document upload functionality
|
||||
- `e2e/search.spec.ts` - Search workflows
|
||||
- `e2e/document-management.spec.ts` - Document management
|
||||
|
||||
### Utilities
|
||||
|
||||
- `e2e/fixtures/auth.ts` - Authentication fixture for logged-in tests
|
||||
- `e2e/utils/test-helpers.ts` - Common helper functions
|
||||
- `e2e/utils/test-data.ts` - Test data and configuration
|
||||
|
||||
### Configuration
|
||||
|
||||
- `playwright.config.ts` - Playwright configuration
|
||||
- `.github/workflows/e2e-tests.yml` - GitHub Actions workflow
|
||||
|
||||
## Test Data
|
||||
|
||||
Tests use sample files from:
|
||||
- `frontend/test_data/hello_ocr.png` - Sample image for OCR
|
||||
- `frontend/test_data/multiline.png` - Multi-line text image
|
||||
- `frontend/test_data/numbers.png` - Numbers image
|
||||
|
||||
Add additional test files to `frontend/test_data/` as needed.
|
||||
|
||||
## Writing Tests
|
||||
|
||||
### Basic Test Structure
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { TestHelpers } from './utils/test-helpers';
|
||||
|
||||
test.describe('Feature Name', () => {
|
||||
let helpers: TestHelpers;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
helpers = new TestHelpers(page);
|
||||
await helpers.navigateToPage('/your-page');
|
||||
});
|
||||
|
||||
test('should do something', async ({ page }) => {
|
||||
// Your test logic here
|
||||
await expect(page.locator('[data-testid="element"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Using Authentication Fixture
|
||||
|
||||
For tests requiring authentication:
|
||||
|
||||
```typescript
|
||||
import { test, expect } from './fixtures/auth';
|
||||
|
||||
test.describe('Authenticated Feature', () => {
|
||||
test('should work when logged in', async ({ authenticatedPage }) => {
|
||||
// Page is already authenticated
|
||||
await authenticatedPage.goto('/protected-page');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Use data-testid attributes** for reliable element selection
|
||||
2. **Wait for API calls** using `helpers.waitForApiCall()`
|
||||
3. **Handle loading states** with `helpers.waitForLoadingToComplete()`
|
||||
4. **Use meaningful test descriptions** that describe user actions
|
||||
5. **Clean up test data** when necessary
|
||||
6. **Use timeouts appropriately** from `TIMEOUTS` constants
|
||||
|
||||
## Debugging
|
||||
|
||||
### Local Debugging
|
||||
|
||||
1. **Run with --debug flag**:
|
||||
```bash
|
||||
npm run test:e2e:debug
|
||||
```
|
||||
|
||||
2. **Use Playwright UI**:
|
||||
```bash
|
||||
npm run test:e2e:ui
|
||||
```
|
||||
|
||||
3. **Add debugging code**:
|
||||
```typescript
|
||||
await page.pause(); // Pauses execution
|
||||
await page.screenshot({ path: 'debug.png' }); // Take screenshot
|
||||
```
|
||||
|
||||
### CI Debugging
|
||||
|
||||
- Check uploaded test artifacts in GitHub Actions
|
||||
- Review test reports in the workflow summary
|
||||
- Examine screenshots and videos from failed tests
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
- `PLAYWRIGHT_BASE_URL` - Base URL for tests (default: http://localhost:5173)
|
||||
- `CI` - Set to true in CI environment
|
||||
- `TEST_MODE` - Set to true for backend test mode
|
||||
|
||||
### Timeouts
|
||||
|
||||
Configure timeouts in `utils/test-data.ts`:
|
||||
- `TIMEOUTS.short` (5s) - Quick operations
|
||||
- `TIMEOUTS.medium` (10s) - Normal operations
|
||||
- `TIMEOUTS.long` (30s) - Slow operations
|
||||
- `TIMEOUTS.upload` (60s) - File uploads
|
||||
- `TIMEOUTS.ocr` (120s) - OCR processing
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Tests timing out**:
|
||||
- Increase timeouts in configuration
|
||||
- Check if services are running properly
|
||||
- Verify database connectivity
|
||||
|
||||
2. **Authentication failures**:
|
||||
- Ensure test user exists in database
|
||||
- Check authentication fixture implementation
|
||||
- Verify API endpoints are correct
|
||||
|
||||
3. **File upload failures**:
|
||||
- Ensure test files exist in `test_data/`
|
||||
- Check file permissions
|
||||
- Verify upload API is working
|
||||
|
||||
4. **Database issues**:
|
||||
- Ensure PostgreSQL is running
|
||||
- Check database migrations
|
||||
- Verify test database exists
|
||||
|
||||
### Getting Help
|
||||
|
||||
1. Check logs in `backend.log` and `frontend.log`
|
||||
2. Review Playwright documentation
|
||||
3. Examine existing test implementations
|
||||
4. Use browser dev tools in headed mode
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new features:
|
||||
|
||||
1. **Add E2E tests** for new user workflows
|
||||
2. **Update test data** if needed
|
||||
3. **Add data-testid attributes** to new UI elements
|
||||
4. **Update this documentation** if test setup changes
|
||||
|
||||
Ensure tests:
|
||||
- Are reliable and not flaky
|
||||
- Test realistic user scenarios
|
||||
- Have good error messages
|
||||
- Clean up after themselves
|
||||
|
|
@ -104,6 +104,30 @@ services:
|
|||
profiles:
|
||||
- frontend-tests
|
||||
|
||||
# Frontend development server for E2E tests
|
||||
frontend_dev:
|
||||
image: node:18-alpine
|
||||
container_name: readur_frontend_dev
|
||||
working_dir: /app
|
||||
environment:
|
||||
NODE_ENV: development
|
||||
VITE_API_BASE_URL: http://readur_test:8001
|
||||
VITE_HOST: 0.0.0.0
|
||||
VITE_PORT: 5174
|
||||
volumes:
|
||||
- ./frontend:/app
|
||||
- /app/node_modules
|
||||
command: ["sh", "-c", "npm ci && npm run dev -- --host 0.0.0.0 --port 5174"]
|
||||
ports:
|
||||
- "5174:5174"
|
||||
networks:
|
||||
- readur_test_network
|
||||
depends_on:
|
||||
readur_test:
|
||||
condition: service_healthy
|
||||
profiles:
|
||||
- e2e-tests
|
||||
|
||||
networks:
|
||||
readur_test_network:
|
||||
name: readur_test_network
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { TEST_USERS, TIMEOUTS } from './utils/test-data';
|
||||
import { TestHelpers } from './utils/test-helpers';
|
||||
|
||||
test.describe('Authentication', () => {
|
||||
let helpers: TestHelpers;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
helpers = new TestHelpers(page);
|
||||
});
|
||||
|
||||
test('should display login form on initial visit', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Check for login form elements using Material-UI structure
|
||||
await expect(page.locator('input[name="username"]')).toBeVisible();
|
||||
await expect(page.locator('input[name="password"]')).toBeVisible();
|
||||
await expect(page.locator('button[type="submit"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should login with valid credentials', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Fill login form with demo credentials
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
await page.fill('input[name="password"]', 'readur2024');
|
||||
|
||||
// Wait for login API call
|
||||
const loginResponse = helpers.waitForApiCall('/auth/login');
|
||||
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// Verify login was successful
|
||||
await loginResponse;
|
||||
|
||||
// Should redirect to dashboard or main page
|
||||
await page.waitForURL(/\/dashboard|\//, { timeout: TIMEOUTS.medium });
|
||||
|
||||
// Verify we're no longer on login page
|
||||
await expect(page.locator('input[name="username"]')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('should show error with invalid credentials', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
await page.fill('input[name="username"]', 'invaliduser');
|
||||
await page.fill('input[name="password"]', 'wrongpassword');
|
||||
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// Should show error message (Material-UI Alert)
|
||||
await expect(page.locator('.MuiAlert-root, [role="alert"]')).toBeVisible({ timeout: TIMEOUTS.short });
|
||||
|
||||
// Should remain on login page
|
||||
await expect(page.locator('input[name="username"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should logout successfully', async ({ page }) => {
|
||||
// First login
|
||||
await page.goto('/');
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
await page.fill('input[name="password"]', 'readur2024');
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
await page.waitForURL(/\/dashboard|\//, { timeout: TIMEOUTS.medium });
|
||||
|
||||
// Find and click logout button
|
||||
const logoutButton = page.locator('button:has-text("Logout"), [data-testid="logout"]');
|
||||
if (await logoutButton.isVisible()) {
|
||||
await logoutButton.click();
|
||||
} else {
|
||||
// Try menu-based logout
|
||||
await page.click('[data-testid="user-menu"], .user-menu, button:has([data-testid="user-avatar"])');
|
||||
await page.click('button:has-text("Logout"), [data-testid="logout"]');
|
||||
}
|
||||
|
||||
// Should redirect back to login
|
||||
await page.waitForURL(/\/login|\//, { timeout: TIMEOUTS.medium });
|
||||
await expect(page.locator('input[name="username"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should persist session on page reload', async ({ page }) => {
|
||||
// Login first
|
||||
await page.goto('/');
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
await page.fill('input[name="password"]', 'readur2024');
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
await page.waitForURL(/\/dashboard|\//, { timeout: TIMEOUTS.medium });
|
||||
|
||||
// Reload the page
|
||||
await page.reload();
|
||||
|
||||
// Should still be logged in
|
||||
await expect(page.locator('input[name="username"]')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('should validate required fields', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Try to submit without filling fields
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// Should show validation errors or prevent submission
|
||||
const usernameInput = page.locator('input[name="username"]');
|
||||
const passwordInput = page.locator('input[name="password"]');
|
||||
|
||||
// Check for HTML5 validation or custom validation messages
|
||||
await expect(usernameInput).toBeVisible();
|
||||
await expect(passwordInput).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
import { test, expect } from './fixtures/auth';
|
||||
import { TIMEOUTS } from './utils/test-data';
|
||||
import { TestHelpers } from './utils/test-helpers';
|
||||
|
||||
test.describe('Document Management', () => {
|
||||
let helpers: TestHelpers;
|
||||
|
||||
test.beforeEach(async ({ authenticatedPage }) => {
|
||||
helpers = new TestHelpers(authenticatedPage);
|
||||
await helpers.navigateToPage('/documents');
|
||||
});
|
||||
|
||||
test('should display document list', async ({ authenticatedPage: page }) => {
|
||||
// Check for document list components
|
||||
await expect(page.locator('[data-testid="document-list"], .document-list, .documents-grid')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should navigate to document details', async ({ authenticatedPage: page }) => {
|
||||
// Click on first document if available
|
||||
const firstDocument = page.locator('[data-testid="document-item"], .document-item, .document-card').first();
|
||||
|
||||
if (await firstDocument.isVisible()) {
|
||||
await firstDocument.click();
|
||||
|
||||
// Should navigate to document details page
|
||||
await page.waitForURL(/\/documents\/[^\/]+/, { timeout: TIMEOUTS.medium });
|
||||
|
||||
// Should show document details
|
||||
await expect(page.locator('[data-testid="document-details"], .document-details')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should display document metadata', async ({ authenticatedPage: page }) => {
|
||||
const firstDocument = page.locator('[data-testid="document-item"], .document-item, .document-card').first();
|
||||
|
||||
if (await firstDocument.isVisible()) {
|
||||
await firstDocument.click();
|
||||
await page.waitForURL(/\/documents\/[^\/]+/, { timeout: TIMEOUTS.medium });
|
||||
|
||||
// Should show various metadata fields
|
||||
await expect(page.locator(':has-text("File size"), :has-text("Upload date"), :has-text("Modified")')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should allow document download', async ({ authenticatedPage: page }) => {
|
||||
const firstDocument = page.locator('[data-testid="document-item"], .document-item, .document-card').first();
|
||||
|
||||
if (await firstDocument.isVisible()) {
|
||||
await firstDocument.click();
|
||||
await page.waitForURL(/\/documents\/[^\/]+/, { timeout: TIMEOUTS.medium });
|
||||
|
||||
// Look for download button
|
||||
const downloadButton = page.locator('[data-testid="download"], button:has-text("Download"), .download-button');
|
||||
if (await downloadButton.isVisible()) {
|
||||
// Set up download listener
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
await downloadButton.click();
|
||||
|
||||
// Verify download started
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBeTruthy();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should allow document deletion', async ({ authenticatedPage: page }) => {
|
||||
const firstDocument = page.locator('[data-testid="document-item"], .document-item, .document-card').first();
|
||||
|
||||
if (await firstDocument.isVisible()) {
|
||||
await firstDocument.click();
|
||||
await page.waitForURL(/\/documents\/[^\/]+/, { timeout: TIMEOUTS.medium });
|
||||
|
||||
// Look for delete button
|
||||
const deleteButton = page.locator('[data-testid="delete"], button:has-text("Delete"), .delete-button');
|
||||
if (await deleteButton.isVisible()) {
|
||||
await deleteButton.click();
|
||||
|
||||
// Should show confirmation dialog
|
||||
const confirmButton = page.locator('button:has-text("Confirm"), button:has-text("Yes"), [data-testid="confirm-delete"]');
|
||||
if (await confirmButton.isVisible()) {
|
||||
await confirmButton.click();
|
||||
|
||||
// Should redirect back to documents list
|
||||
await page.waitForURL(/\/documents$/, { timeout: TIMEOUTS.medium });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should filter documents by type', async ({ authenticatedPage: page }) => {
|
||||
// Look for filter controls
|
||||
const filterDropdown = page.locator('[data-testid="type-filter"], select[name="type"], .type-filter');
|
||||
if (await filterDropdown.isVisible()) {
|
||||
await filterDropdown.selectOption('pdf');
|
||||
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
// Should show only PDF documents
|
||||
const documentItems = page.locator('[data-testid="document-item"], .document-item');
|
||||
if (await documentItems.count() > 0) {
|
||||
// Check that visible documents are PDFs
|
||||
await expect(documentItems.first().locator(':has-text(".pdf"), .pdf-icon')).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should sort documents', async ({ authenticatedPage: page }) => {
|
||||
const sortDropdown = page.locator('[data-testid="sort"], select[name="sort"], .sort-dropdown');
|
||||
if (await sortDropdown.isVisible()) {
|
||||
await sortDropdown.selectOption('date-desc');
|
||||
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
// Documents should be reordered
|
||||
await expect(page.locator('[data-testid="document-list"], .document-list')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should display OCR status', async ({ authenticatedPage: page }) => {
|
||||
const firstDocument = page.locator('[data-testid="document-item"], .document-item, .document-card').first();
|
||||
|
||||
if (await firstDocument.isVisible()) {
|
||||
await firstDocument.click();
|
||||
await page.waitForURL(/\/documents\/[^\/]+/, { timeout: TIMEOUTS.medium });
|
||||
|
||||
// Should show OCR status information
|
||||
await expect(page.locator(':has-text("OCR"), [data-testid="ocr-status"], .ocr-status')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should search within document content', async ({ authenticatedPage: page }) => {
|
||||
const firstDocument = page.locator('[data-testid="document-item"], .document-item, .document-card').first();
|
||||
|
||||
if (await firstDocument.isVisible()) {
|
||||
await firstDocument.click();
|
||||
await page.waitForURL(/\/documents\/[^\/]+/, { timeout: TIMEOUTS.medium });
|
||||
|
||||
// Look for in-document search
|
||||
const searchInput = page.locator('[data-testid="document-search"], input[placeholder*="search" i]');
|
||||
if (await searchInput.isVisible()) {
|
||||
await searchInput.fill('test');
|
||||
|
||||
// Should highlight matches in document content
|
||||
await expect(page.locator('.highlight, mark, .search-highlight')).toBeVisible({
|
||||
timeout: TIMEOUTS.short
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should paginate document list', async ({ authenticatedPage: page }) => {
|
||||
// Look for pagination controls
|
||||
const nextPageButton = page.locator('[data-testid="next-page"], button:has-text("Next"), .pagination-next');
|
||||
if (await nextPageButton.isVisible()) {
|
||||
const initialDocuments = await page.locator('[data-testid="document-item"], .document-item').count();
|
||||
|
||||
await nextPageButton.click();
|
||||
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
// Should load different documents
|
||||
const newDocuments = await page.locator('[data-testid="document-item"], .document-item').count();
|
||||
expect(newDocuments).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
test('should show document thumbnails', async ({ authenticatedPage: page }) => {
|
||||
// Check for document thumbnails in list view
|
||||
const documentThumbnails = page.locator('[data-testid="document-thumbnail"], .thumbnail, .document-preview');
|
||||
if (await documentThumbnails.first().isVisible()) {
|
||||
await expect(documentThumbnails.first()).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { test as base, expect } from '@playwright/test';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
export interface AuthFixture {
|
||||
authenticatedPage: Page;
|
||||
}
|
||||
|
||||
export const test = base.extend<AuthFixture>({
|
||||
authenticatedPage: async ({ page }, use) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Check if already logged in by looking for username input (login page)
|
||||
const usernameInput = await page.locator('input[name="username"]').isVisible().catch(() => false);
|
||||
|
||||
if (usernameInput) {
|
||||
// Fill login form with demo credentials
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
await page.fill('input[name="password"]', 'readur2024');
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// Wait for navigation away from login page
|
||||
await page.waitForURL(/\/dashboard|\//, { timeout: 10000 });
|
||||
}
|
||||
|
||||
await use(page);
|
||||
},
|
||||
});
|
||||
|
||||
export { expect };
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
import { test, expect } from './fixtures/auth';
|
||||
import { SEARCH_QUERIES, TIMEOUTS, API_ENDPOINTS } from './utils/test-data';
|
||||
import { TestHelpers } from './utils/test-helpers';
|
||||
|
||||
test.describe('Search Functionality', () => {
|
||||
let helpers: TestHelpers;
|
||||
|
||||
test.beforeEach(async ({ authenticatedPage }) => {
|
||||
helpers = new TestHelpers(authenticatedPage);
|
||||
await helpers.navigateToPage('/search');
|
||||
});
|
||||
|
||||
test('should display search interface', async ({ authenticatedPage: page }) => {
|
||||
// Check for search components
|
||||
await expect(page.locator('input[type="search"], input[placeholder*="search" i], [data-testid="search-input"]')).toBeVisible();
|
||||
await expect(page.locator('button:has-text("Search"), [data-testid="search-button"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should perform basic search', async ({ authenticatedPage: page }) => {
|
||||
const searchInput = page.locator('input[type="search"], input[placeholder*="search" i], [data-testid="search-input"]').first();
|
||||
|
||||
// Enter search query
|
||||
await searchInput.fill(SEARCH_QUERIES.simple);
|
||||
|
||||
// Wait for search API call
|
||||
const searchResponse = helpers.waitForApiCall(API_ENDPOINTS.search);
|
||||
|
||||
// Press Enter or click search button
|
||||
await searchInput.press('Enter');
|
||||
|
||||
// Verify search was performed
|
||||
await searchResponse;
|
||||
|
||||
// Should show search results
|
||||
await expect(page.locator('[data-testid="search-results"], .search-results')).toBeVisible({
|
||||
timeout: TIMEOUTS.medium
|
||||
});
|
||||
});
|
||||
|
||||
test('should show search suggestions', async ({ authenticatedPage: page }) => {
|
||||
const searchInput = page.locator('input[type="search"], input[placeholder*="search" i], [data-testid="search-input"]').first();
|
||||
|
||||
// Start typing to trigger suggestions
|
||||
await searchInput.type('test', { delay: 100 });
|
||||
|
||||
// Should show suggestion dropdown
|
||||
await expect(page.locator('[data-testid="search-suggestions"], .suggestions, .autocomplete')).toBeVisible({
|
||||
timeout: TIMEOUTS.short
|
||||
});
|
||||
});
|
||||
|
||||
test('should filter search results', async ({ authenticatedPage: page }) => {
|
||||
const searchInput = page.locator('input[type="search"], input[placeholder*="search" i], [data-testid="search-input"]').first();
|
||||
|
||||
// Perform initial search
|
||||
await searchInput.fill(SEARCH_QUERIES.simple);
|
||||
await searchInput.press('Enter');
|
||||
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
// Apply filters
|
||||
const filterButton = page.locator('[data-testid="filters"], button:has-text("Filter"), .filter-toggle');
|
||||
if (await filterButton.isVisible()) {
|
||||
await filterButton.click();
|
||||
|
||||
// Select document type filter
|
||||
const pdfFilter = page.locator('input[type="checkbox"][value="pdf"], label:has-text("PDF")');
|
||||
if (await pdfFilter.isVisible()) {
|
||||
await pdfFilter.check();
|
||||
|
||||
// Should update search results
|
||||
await helpers.waitForApiCall(API_ENDPOINTS.search);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should perform advanced search', async ({ authenticatedPage: page }) => {
|
||||
// Look for advanced search toggle
|
||||
const advancedToggle = page.locator('[data-testid="advanced-search"], button:has-text("Advanced"), .advanced-toggle');
|
||||
|
||||
if (await advancedToggle.isVisible()) {
|
||||
await advancedToggle.click();
|
||||
|
||||
// Fill advanced search fields
|
||||
await page.fill('[data-testid="title-search"], input[name="title"]', SEARCH_QUERIES.advanced.title);
|
||||
await page.fill('[data-testid="content-search"], input[name="content"]', SEARCH_QUERIES.advanced.content);
|
||||
|
||||
// Set date filters if available
|
||||
const dateFromInput = page.locator('[data-testid="date-from"], input[name="dateFrom"], input[type="date"]').first();
|
||||
if (await dateFromInput.isVisible()) {
|
||||
await dateFromInput.fill(SEARCH_QUERIES.advanced.dateFrom);
|
||||
}
|
||||
|
||||
// Perform advanced search
|
||||
await page.click('button:has-text("Search"), [data-testid="search-button"]');
|
||||
|
||||
// Verify search results
|
||||
await expect(page.locator('[data-testid="search-results"], .search-results')).toBeVisible({
|
||||
timeout: TIMEOUTS.medium
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle empty search results', async ({ authenticatedPage: page }) => {
|
||||
const searchInput = page.locator('input[type="search"], input[placeholder*="search" i], [data-testid="search-input"]').first();
|
||||
|
||||
// Search for something that doesn't exist
|
||||
await searchInput.fill(SEARCH_QUERIES.noResults);
|
||||
await searchInput.press('Enter');
|
||||
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
// Should show no results message
|
||||
await expect(page.locator(':has-text("No results"), :has-text("not found"), [data-testid="no-results"]')).toBeVisible({
|
||||
timeout: TIMEOUTS.medium
|
||||
});
|
||||
});
|
||||
|
||||
test('should navigate to document from search results', async ({ authenticatedPage: page }) => {
|
||||
const searchInput = page.locator('input[type="search"], input[placeholder*="search" i], [data-testid="search-input"]').first();
|
||||
|
||||
// Perform search
|
||||
await searchInput.fill(SEARCH_QUERIES.simple);
|
||||
await searchInput.press('Enter');
|
||||
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
// Click on first search result
|
||||
const firstResult = page.locator('[data-testid="search-results"] > *, .search-result').first();
|
||||
if (await firstResult.isVisible()) {
|
||||
await firstResult.click();
|
||||
|
||||
// Should navigate to document details
|
||||
await page.waitForURL(/\/documents\/[^\/]+/, { timeout: TIMEOUTS.medium });
|
||||
}
|
||||
});
|
||||
|
||||
test('should preserve search state on page reload', async ({ authenticatedPage: page }) => {
|
||||
const searchInput = page.locator('input[type="search"], input[placeholder*="search" i], [data-testid="search-input"]').first();
|
||||
|
||||
// Perform search
|
||||
await searchInput.fill(SEARCH_QUERIES.simple);
|
||||
await searchInput.press('Enter');
|
||||
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
// Reload page
|
||||
await page.reload();
|
||||
|
||||
// Should preserve search query and results
|
||||
await expect(searchInput).toHaveValue(SEARCH_QUERIES.simple);
|
||||
await expect(page.locator('[data-testid="search-results"], .search-results')).toBeVisible({
|
||||
timeout: TIMEOUTS.medium
|
||||
});
|
||||
});
|
||||
|
||||
test('should sort search results', async ({ authenticatedPage: page }) => {
|
||||
const searchInput = page.locator('input[type="search"], input[placeholder*="search" i], [data-testid="search-input"]').first();
|
||||
|
||||
// Perform search
|
||||
await searchInput.fill(SEARCH_QUERIES.simple);
|
||||
await searchInput.press('Enter');
|
||||
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
// Look for sort options
|
||||
const sortDropdown = page.locator('[data-testid="sort"], select[name="sort"], .sort-selector');
|
||||
if (await sortDropdown.isVisible()) {
|
||||
await sortDropdown.selectOption('date-desc');
|
||||
|
||||
// Should update search results order
|
||||
await helpers.waitForApiCall(API_ENDPOINTS.search);
|
||||
}
|
||||
});
|
||||
|
||||
test('should paginate search results', async ({ authenticatedPage: page }) => {
|
||||
const searchInput = page.locator('input[type="search"], input[placeholder*="search" i], [data-testid="search-input"]').first();
|
||||
|
||||
// Perform search
|
||||
await searchInput.fill(SEARCH_QUERIES.simple);
|
||||
await searchInput.press('Enter');
|
||||
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
// Look for pagination
|
||||
const nextPageButton = page.locator('[data-testid="next-page"], button:has-text("Next"), .pagination button:last-child');
|
||||
if (await nextPageButton.isVisible()) {
|
||||
await nextPageButton.click();
|
||||
|
||||
// Should load next page of results
|
||||
await helpers.waitForApiCall(API_ENDPOINTS.search);
|
||||
await expect(page.locator('[data-testid="search-results"], .search-results')).toBeVisible({
|
||||
timeout: TIMEOUTS.medium
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('should highlight search terms in results', async ({ authenticatedPage: page }) => {
|
||||
const searchInput = page.locator('input[type="search"], input[placeholder*="search" i], [data-testid="search-input"]').first();
|
||||
|
||||
// Perform search with specific term
|
||||
await searchInput.fill('test');
|
||||
await searchInput.press('Enter');
|
||||
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
// Should highlight search terms in results
|
||||
await expect(page.locator('.highlight, mark, .search-highlight')).toBeVisible({
|
||||
timeout: TIMEOUTS.medium
|
||||
});
|
||||
});
|
||||
|
||||
test('should clear search results', async ({ authenticatedPage: page }) => {
|
||||
const searchInput = page.locator('input[type="search"], input[placeholder*="search" i], [data-testid="search-input"]').first();
|
||||
|
||||
// Perform search
|
||||
await searchInput.fill(SEARCH_QUERIES.simple);
|
||||
await searchInput.press('Enter');
|
||||
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
// Clear search
|
||||
const clearButton = page.locator('[data-testid="clear-search"], button:has-text("Clear"), .clear-button');
|
||||
if (await clearButton.isVisible()) {
|
||||
await clearButton.click();
|
||||
} else {
|
||||
// Clear by emptying input
|
||||
await searchInput.clear();
|
||||
await searchInput.press('Enter');
|
||||
}
|
||||
|
||||
// Should clear results
|
||||
await expect(page.locator('[data-testid="search-results"], .search-results')).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,283 @@
|
|||
import { test, expect } from './fixtures/auth';
|
||||
import { TIMEOUTS } from './utils/test-data';
|
||||
import { TestHelpers } from './utils/test-helpers';
|
||||
|
||||
test.describe('Settings Management', () => {
|
||||
let helpers: TestHelpers;
|
||||
|
||||
test.beforeEach(async ({ authenticatedPage }) => {
|
||||
helpers = new TestHelpers(authenticatedPage);
|
||||
await helpers.navigateToPage('/settings');
|
||||
});
|
||||
|
||||
test('should display settings interface', async ({ authenticatedPage: page }) => {
|
||||
// Check for settings page components
|
||||
await expect(page.locator('[data-testid="settings-container"], .settings-page, .settings-form')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should update OCR settings', async ({ authenticatedPage: page }) => {
|
||||
// Look for OCR settings section
|
||||
const ocrSection = page.locator('[data-testid="ocr-settings"], .ocr-section, .settings-section:has-text("OCR")');
|
||||
if (await ocrSection.isVisible()) {
|
||||
// Change OCR language
|
||||
const languageSelect = page.locator('select[name="ocrLanguage"], [data-testid="ocr-language"]');
|
||||
if (await languageSelect.isVisible()) {
|
||||
await languageSelect.selectOption('spa'); // Spanish
|
||||
|
||||
const saveResponse = helpers.waitForApiCall('/api/settings');
|
||||
|
||||
// Save settings
|
||||
await page.click('button[type="submit"], button:has-text("Save"), [data-testid="save-settings"]');
|
||||
|
||||
await saveResponse;
|
||||
await helpers.waitForToast();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should update watch folder settings', async ({ authenticatedPage: page }) => {
|
||||
// Navigate to watch folder section if it's a separate page
|
||||
const watchFolderNav = page.locator('a[href="/watch-folder"], [data-testid="watch-folder-nav"]');
|
||||
if (await watchFolderNav.isVisible()) {
|
||||
await watchFolderNav.click();
|
||||
await helpers.waitForLoadingToComplete();
|
||||
}
|
||||
|
||||
// Look for watch folder settings
|
||||
const watchSection = page.locator('[data-testid="watch-settings"], .watch-folder-section, .settings-section:has-text("Watch")');
|
||||
if (await watchSection.isVisible()) {
|
||||
// Enable watch folder
|
||||
const enableWatch = page.locator('input[type="checkbox"][name="enableWatch"], [data-testid="enable-watch"]');
|
||||
if (await enableWatch.isVisible()) {
|
||||
await enableWatch.check();
|
||||
|
||||
// Set watch folder path
|
||||
const pathInput = page.locator('input[name="watchPath"], [data-testid="watch-path"]');
|
||||
if (await pathInput.isVisible()) {
|
||||
await pathInput.fill('/tmp/watch-folder');
|
||||
}
|
||||
|
||||
const saveResponse = helpers.waitForApiCall('/api/settings');
|
||||
|
||||
await page.click('button[type="submit"], button:has-text("Save")');
|
||||
|
||||
await saveResponse;
|
||||
await helpers.waitForToast();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should update notification settings', async ({ authenticatedPage: page }) => {
|
||||
const notificationSection = page.locator('[data-testid="notification-settings"], .notification-section, .settings-section:has-text("Notification")');
|
||||
if (await notificationSection.isVisible()) {
|
||||
// Enable notifications
|
||||
const enableNotifications = page.locator('input[type="checkbox"][name="enableNotifications"], [data-testid="enable-notifications"]');
|
||||
if (await enableNotifications.isVisible()) {
|
||||
await enableNotifications.check();
|
||||
|
||||
// Configure notification types
|
||||
const ocrNotifications = page.locator('input[type="checkbox"][name="ocrNotifications"], [data-testid="ocr-notifications"]');
|
||||
if (await ocrNotifications.isVisible()) {
|
||||
await ocrNotifications.check();
|
||||
}
|
||||
|
||||
const syncNotifications = page.locator('input[type="checkbox"][name="syncNotifications"], [data-testid="sync-notifications"]');
|
||||
if (await syncNotifications.isVisible()) {
|
||||
await syncNotifications.check();
|
||||
}
|
||||
|
||||
const saveResponse = helpers.waitForApiCall('/api/settings');
|
||||
|
||||
await page.click('button[type="submit"], button:has-text("Save")');
|
||||
|
||||
await saveResponse;
|
||||
await helpers.waitForToast();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should update search settings', async ({ authenticatedPage: page }) => {
|
||||
const searchSection = page.locator('[data-testid="search-settings"], .search-section, .settings-section:has-text("Search")');
|
||||
if (await searchSection.isVisible()) {
|
||||
// Configure search results per page
|
||||
const resultsPerPage = page.locator('select[name="resultsPerPage"], [data-testid="results-per-page"]');
|
||||
if (await resultsPerPage.isVisible()) {
|
||||
await resultsPerPage.selectOption('25');
|
||||
}
|
||||
|
||||
// Enable/disable features
|
||||
const enhancedSearch = page.locator('input[type="checkbox"][name="enhancedSearch"], [data-testid="enhanced-search"]');
|
||||
if (await enhancedSearch.isVisible()) {
|
||||
await enhancedSearch.check();
|
||||
}
|
||||
|
||||
const saveResponse = helpers.waitForApiCall('/api/settings');
|
||||
|
||||
await page.click('button[type="submit"], button:has-text("Save")');
|
||||
|
||||
await saveResponse;
|
||||
await helpers.waitForToast();
|
||||
}
|
||||
});
|
||||
|
||||
test('should reset settings to defaults', async ({ authenticatedPage: page }) => {
|
||||
// Look for reset button
|
||||
const resetButton = page.locator('button:has-text("Reset"), button:has-text("Default"), [data-testid="reset-settings"]');
|
||||
if (await resetButton.isVisible()) {
|
||||
await resetButton.click();
|
||||
|
||||
// Should show confirmation
|
||||
const confirmButton = page.locator('button:has-text("Confirm"), button:has-text("Yes"), [data-testid="confirm-reset"]');
|
||||
if (await confirmButton.isVisible()) {
|
||||
const resetResponse = helpers.waitForApiCall('/api/settings/reset');
|
||||
|
||||
await confirmButton.click();
|
||||
|
||||
await resetResponse;
|
||||
await helpers.waitForToast();
|
||||
|
||||
// Page should reload with default values
|
||||
await helpers.waitForLoadingToComplete();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should validate settings before saving', async ({ authenticatedPage: page }) => {
|
||||
// Try to set invalid values
|
||||
const pathInput = page.locator('input[name="watchPath"], [data-testid="watch-path"]');
|
||||
if (await pathInput.isVisible()) {
|
||||
// Enter invalid path
|
||||
await pathInput.fill('invalid/path/with/spaces and special chars!');
|
||||
|
||||
await page.click('button[type="submit"], button:has-text("Save")');
|
||||
|
||||
// Should show validation error
|
||||
await helpers.waitForToast();
|
||||
|
||||
// Should not save invalid settings
|
||||
expect(await pathInput.inputValue()).toBe('invalid/path/with/spaces and special chars!');
|
||||
}
|
||||
});
|
||||
|
||||
test('should export settings', async ({ authenticatedPage: page }) => {
|
||||
const exportButton = page.locator('button:has-text("Export"), [data-testid="export-settings"]');
|
||||
if (await exportButton.isVisible()) {
|
||||
// Set up download listener
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
await exportButton.click();
|
||||
|
||||
// Verify download started
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toContain('settings');
|
||||
}
|
||||
});
|
||||
|
||||
test('should import settings', async ({ authenticatedPage: page }) => {
|
||||
const importButton = page.locator('button:has-text("Import"), [data-testid="import-settings"]');
|
||||
if (await importButton.isVisible()) {
|
||||
// Look for file input
|
||||
const fileInput = page.locator('input[type="file"], [data-testid="settings-file"]');
|
||||
if (await fileInput.isVisible()) {
|
||||
// Create a mock settings file
|
||||
const settingsContent = JSON.stringify({
|
||||
ocrLanguage: 'eng',
|
||||
enableNotifications: true,
|
||||
resultsPerPage: 20
|
||||
});
|
||||
|
||||
await fileInput.setInputFiles({
|
||||
name: 'settings.json',
|
||||
mimeType: 'application/json',
|
||||
buffer: Buffer.from(settingsContent)
|
||||
});
|
||||
|
||||
const importResponse = helpers.waitForApiCall('/api/settings/import');
|
||||
|
||||
await importButton.click();
|
||||
|
||||
await importResponse;
|
||||
await helpers.waitForToast();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should display current system status', async ({ authenticatedPage: page }) => {
|
||||
// Look for system status section
|
||||
const statusSection = page.locator('[data-testid="system-status"], .status-section, .settings-section:has-text("Status")');
|
||||
if (await statusSection.isVisible()) {
|
||||
// Should show various system metrics
|
||||
await expect(statusSection.locator(':has-text("Database"), :has-text("Storage"), :has-text("OCR")')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should test OCR functionality', async ({ authenticatedPage: page }) => {
|
||||
const ocrSection = page.locator('[data-testid="ocr-settings"], .ocr-section');
|
||||
if (await ocrSection.isVisible()) {
|
||||
const testButton = page.locator('button:has-text("Test OCR"), [data-testid="test-ocr"]');
|
||||
if (await testButton.isVisible()) {
|
||||
const testResponse = helpers.waitForApiCall('/api/ocr/test');
|
||||
|
||||
await testButton.click();
|
||||
|
||||
await testResponse;
|
||||
|
||||
// Should show test result
|
||||
await helpers.waitForToast();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should clear cache', async ({ authenticatedPage: page }) => {
|
||||
const clearCacheButton = page.locator('button:has-text("Clear Cache"), [data-testid="clear-cache"]');
|
||||
if (await clearCacheButton.isVisible()) {
|
||||
const clearResponse = helpers.waitForApiCall('/api/cache/clear');
|
||||
|
||||
await clearCacheButton.click();
|
||||
|
||||
await clearResponse;
|
||||
await helpers.waitForToast();
|
||||
}
|
||||
});
|
||||
|
||||
test('should update user profile', async ({ authenticatedPage: page }) => {
|
||||
// Look for user profile section
|
||||
const profileSection = page.locator('[data-testid="profile-settings"], .profile-section, .settings-section:has-text("Profile")');
|
||||
if (await profileSection.isVisible()) {
|
||||
// Update email
|
||||
const emailInput = page.locator('input[name="email"], [data-testid="user-email"]');
|
||||
if (await emailInput.isVisible()) {
|
||||
await emailInput.fill('newemail@example.com');
|
||||
}
|
||||
|
||||
// Update name
|
||||
const nameInput = page.locator('input[name="name"], [data-testid="user-name"]');
|
||||
if (await nameInput.isVisible()) {
|
||||
await nameInput.fill('Updated Name');
|
||||
}
|
||||
|
||||
const saveResponse = helpers.waitForApiCall('/api/users/profile');
|
||||
|
||||
await page.click('button[type="submit"], button:has-text("Save")');
|
||||
|
||||
await saveResponse;
|
||||
await helpers.waitForToast();
|
||||
}
|
||||
});
|
||||
|
||||
test('should change password', async ({ authenticatedPage: page }) => {
|
||||
const passwordSection = page.locator('[data-testid="password-settings"], .password-section, .settings-section:has-text("Password")');
|
||||
if (await passwordSection.isVisible()) {
|
||||
await page.fill('input[name="currentPassword"], [data-testid="current-password"]', 'currentpass');
|
||||
await page.fill('input[name="newPassword"], [data-testid="new-password"]', 'newpassword123');
|
||||
await page.fill('input[name="confirmPassword"], [data-testid="confirm-password"]', 'newpassword123');
|
||||
|
||||
const changeResponse = helpers.waitForApiCall('/api/users/password');
|
||||
|
||||
await page.click('button[type="submit"], button:has-text("Change Password")');
|
||||
|
||||
await changeResponse;
|
||||
await helpers.waitForToast();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,353 @@
|
|||
import { test, expect } from './fixtures/auth';
|
||||
import { TIMEOUTS, API_ENDPOINTS } from './utils/test-data';
|
||||
import { TestHelpers } from './utils/test-helpers';
|
||||
|
||||
test.describe('Source Management', () => {
|
||||
let helpers: TestHelpers;
|
||||
|
||||
test.beforeEach(async ({ authenticatedPage }) => {
|
||||
helpers = new TestHelpers(authenticatedPage);
|
||||
await helpers.navigateToPage('/sources');
|
||||
});
|
||||
|
||||
test('should display sources interface', async ({ authenticatedPage: page }) => {
|
||||
// Check for sources page components
|
||||
await expect(page.locator('[data-testid="sources-list"], .sources-list, .sources-container')).toBeVisible();
|
||||
await expect(page.locator('button:has-text("Add Source"), [data-testid="add-source"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should create a new local folder source', async ({ authenticatedPage: page }) => {
|
||||
// Click add source button
|
||||
await page.click('button:has-text("Add Source"), [data-testid="add-source"]');
|
||||
|
||||
// Should show add source form/modal
|
||||
await expect(page.locator('[data-testid="add-source-form"], .add-source-modal, .source-form')).toBeVisible();
|
||||
|
||||
// Fill in source details
|
||||
await page.fill('input[name="name"], [data-testid="source-name"]', 'Test Local Folder');
|
||||
|
||||
// Select source type
|
||||
const typeSelector = page.locator('select[name="type"], [data-testid="source-type"]');
|
||||
if (await typeSelector.isVisible()) {
|
||||
await typeSelector.selectOption('local_folder');
|
||||
}
|
||||
|
||||
// Fill in folder path
|
||||
await page.fill('input[name="path"], [data-testid="folder-path"]', '/tmp/test-folder');
|
||||
|
||||
// Wait for source creation API call
|
||||
const createResponse = helpers.waitForApiCall('/api/sources', TIMEOUTS.medium);
|
||||
|
||||
// Submit form
|
||||
await page.click('button[type="submit"], button:has-text("Create"), [data-testid="create-source"]');
|
||||
|
||||
// Verify source was created
|
||||
await createResponse;
|
||||
|
||||
// Should show success message
|
||||
await helpers.waitForToast();
|
||||
|
||||
// Should appear in sources list
|
||||
await expect(page.locator(':has-text("Test Local Folder")')).toBeVisible({ timeout: TIMEOUTS.medium });
|
||||
});
|
||||
|
||||
test('should create a new WebDAV source', async ({ authenticatedPage: page }) => {
|
||||
await page.click('button:has-text("Add Source"), [data-testid="add-source"]');
|
||||
|
||||
await expect(page.locator('[data-testid="add-source-form"], .add-source-modal, .source-form')).toBeVisible();
|
||||
|
||||
// Fill in WebDAV source details
|
||||
await page.fill('input[name="name"], [data-testid="source-name"]', 'Test WebDAV');
|
||||
|
||||
const typeSelector = page.locator('select[name="type"], [data-testid="source-type"]');
|
||||
if (await typeSelector.isVisible()) {
|
||||
await typeSelector.selectOption('webdav');
|
||||
}
|
||||
|
||||
// Fill WebDAV specific fields
|
||||
await page.fill('input[name="url"], [data-testid="webdav-url"]', 'https://example.com/webdav');
|
||||
await page.fill('input[name="username"], [data-testid="webdav-username"]', 'testuser');
|
||||
await page.fill('input[name="password"], [data-testid="webdav-password"]', 'testpass');
|
||||
|
||||
const createResponse = helpers.waitForApiCall('/api/sources');
|
||||
|
||||
await page.click('button[type="submit"], button:has-text("Create"), [data-testid="create-source"]');
|
||||
|
||||
await createResponse;
|
||||
await helpers.waitForToast();
|
||||
|
||||
await expect(page.locator(':has-text("Test WebDAV")')).toBeVisible({ timeout: TIMEOUTS.medium });
|
||||
});
|
||||
|
||||
test('should create a new S3 source', async ({ authenticatedPage: page }) => {
|
||||
await page.click('button:has-text("Add Source"), [data-testid="add-source"]');
|
||||
|
||||
await expect(page.locator('[data-testid="add-source-form"], .add-source-modal, .source-form')).toBeVisible();
|
||||
|
||||
// Fill in S3 source details
|
||||
await page.fill('input[name="name"], [data-testid="source-name"]', 'Test S3 Bucket');
|
||||
|
||||
const typeSelector = page.locator('select[name="type"], [data-testid="source-type"]');
|
||||
if (await typeSelector.isVisible()) {
|
||||
await typeSelector.selectOption('s3');
|
||||
}
|
||||
|
||||
// Fill S3 specific fields
|
||||
await page.fill('input[name="bucket"], [data-testid="s3-bucket"]', 'test-bucket');
|
||||
await page.fill('input[name="region"], [data-testid="s3-region"]', 'us-east-1');
|
||||
await page.fill('input[name="accessKey"], [data-testid="s3-access-key"]', 'AKIATEST');
|
||||
await page.fill('input[name="secretKey"], [data-testid="s3-secret-key"]', 'secretkey123');
|
||||
|
||||
const createResponse = helpers.waitForApiCall('/api/sources');
|
||||
|
||||
await page.click('button[type="submit"], button:has-text("Create"), [data-testid="create-source"]');
|
||||
|
||||
await createResponse;
|
||||
await helpers.waitForToast();
|
||||
|
||||
await expect(page.locator(':has-text("Test S3 Bucket")')).toBeVisible({ timeout: TIMEOUTS.medium });
|
||||
});
|
||||
|
||||
test('should edit existing source', async ({ authenticatedPage: page }) => {
|
||||
// Look for existing source to edit
|
||||
const firstSource = page.locator('[data-testid="source-item"], .source-item, .source-card').first();
|
||||
|
||||
if (await firstSource.isVisible()) {
|
||||
// Click edit button
|
||||
const editButton = firstSource.locator('button:has-text("Edit"), [data-testid="edit-source"], .edit-button');
|
||||
if (await editButton.isVisible()) {
|
||||
await editButton.click();
|
||||
|
||||
// Should show edit form
|
||||
await expect(page.locator('[data-testid="edit-source-form"], .edit-source-modal, .source-form')).toBeVisible();
|
||||
|
||||
// Modify source name
|
||||
const nameInput = page.locator('input[name="name"], [data-testid="source-name"]');
|
||||
await nameInput.fill('Updated Source Name');
|
||||
|
||||
const updateResponse = helpers.waitForApiCall('/api/sources');
|
||||
|
||||
await page.click('button[type="submit"], button:has-text("Save"), [data-testid="save-source"]');
|
||||
|
||||
await updateResponse;
|
||||
await helpers.waitForToast();
|
||||
|
||||
// Should show updated name
|
||||
await expect(page.locator(':has-text("Updated Source Name")')).toBeVisible({ timeout: TIMEOUTS.medium });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should delete source', async ({ authenticatedPage: page }) => {
|
||||
const firstSource = page.locator('[data-testid="source-item"], .source-item, .source-card').first();
|
||||
|
||||
if (await firstSource.isVisible()) {
|
||||
const sourceName = await firstSource.locator('[data-testid="source-name"], .source-name, h3, h4').textContent();
|
||||
|
||||
// Click delete button
|
||||
const deleteButton = firstSource.locator('button:has-text("Delete"), [data-testid="delete-source"], .delete-button');
|
||||
if (await deleteButton.isVisible()) {
|
||||
await deleteButton.click();
|
||||
|
||||
// Should show confirmation dialog
|
||||
const confirmButton = page.locator('button:has-text("Confirm"), button:has-text("Yes"), [data-testid="confirm-delete"]');
|
||||
if (await confirmButton.isVisible()) {
|
||||
const deleteResponse = helpers.waitForApiCall('/api/sources');
|
||||
|
||||
await confirmButton.click();
|
||||
|
||||
await deleteResponse;
|
||||
await helpers.waitForToast();
|
||||
|
||||
// Source should be removed from list
|
||||
if (sourceName) {
|
||||
await expect(page.locator(`:has-text("${sourceName}")`)).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should start source sync', async ({ authenticatedPage: page }) => {
|
||||
const firstSource = page.locator('[data-testid="source-item"], .source-item, .source-card').first();
|
||||
|
||||
if (await firstSource.isVisible()) {
|
||||
// Look for sync button
|
||||
const syncButton = firstSource.locator('button:has-text("Sync"), [data-testid="sync-source"], .sync-button');
|
||||
if (await syncButton.isVisible()) {
|
||||
const syncResponse = helpers.waitForApiCall('/api/sources/*/sync');
|
||||
|
||||
await syncButton.click();
|
||||
|
||||
await syncResponse;
|
||||
|
||||
// Should show sync status
|
||||
await expect(firstSource.locator(':has-text("Syncing"), [data-testid="sync-status"], .sync-status')).toBeVisible({
|
||||
timeout: TIMEOUTS.medium
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should stop source sync', async ({ authenticatedPage: page }) => {
|
||||
const firstSource = page.locator('[data-testid="source-item"], .source-item, .source-card').first();
|
||||
|
||||
if (await firstSource.isVisible()) {
|
||||
// First start sync if not running
|
||||
const syncButton = firstSource.locator('button:has-text("Sync"), [data-testid="sync-source"]');
|
||||
if (await syncButton.isVisible()) {
|
||||
await syncButton.click();
|
||||
await helpers.waitForLoadingToComplete();
|
||||
}
|
||||
|
||||
// Look for stop button
|
||||
const stopButton = firstSource.locator('button:has-text("Stop"), [data-testid="stop-sync"], .stop-button');
|
||||
if (await stopButton.isVisible()) {
|
||||
const stopResponse = helpers.waitForApiCall('/api/sources/*/stop');
|
||||
|
||||
await stopButton.click();
|
||||
|
||||
await stopResponse;
|
||||
|
||||
// Should show stopped status
|
||||
await expect(firstSource.locator(':has-text("Stopped"), :has-text("Idle")')).toBeVisible({
|
||||
timeout: TIMEOUTS.medium
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should display source status and statistics', async ({ authenticatedPage: page }) => {
|
||||
const firstSource = page.locator('[data-testid="source-item"], .source-item, .source-card').first();
|
||||
|
||||
if (await firstSource.isVisible()) {
|
||||
// Should show source status information
|
||||
await expect(firstSource.locator('[data-testid="source-status"], .source-status, .status')).toBeVisible();
|
||||
|
||||
// Click to view details
|
||||
await firstSource.click();
|
||||
|
||||
// Should show detailed statistics if available
|
||||
const statsSection = page.locator('[data-testid="source-stats"], .source-statistics, .stats-section');
|
||||
if (await statsSection.isVisible()) {
|
||||
await expect(statsSection.locator(':has-text("Documents"), :has-text("Files"), :has-text("Size")')).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should test source connection', async ({ authenticatedPage: page }) => {
|
||||
await page.click('button:has-text("Add Source"), [data-testid="add-source"]');
|
||||
|
||||
await expect(page.locator('[data-testid="add-source-form"], .add-source-modal')).toBeVisible();
|
||||
|
||||
// Fill in source details
|
||||
await page.fill('input[name="name"], [data-testid="source-name"]', 'Test Connection');
|
||||
|
||||
const typeSelector = page.locator('select[name="type"], [data-testid="source-type"]');
|
||||
if (await typeSelector.isVisible()) {
|
||||
await typeSelector.selectOption('webdav');
|
||||
}
|
||||
|
||||
await page.fill('input[name="url"], [data-testid="webdav-url"]', 'https://example.com/webdav');
|
||||
await page.fill('input[name="username"], [data-testid="webdav-username"]', 'testuser');
|
||||
await page.fill('input[name="password"], [data-testid="webdav-password"]', 'testpass');
|
||||
|
||||
// Look for test connection button
|
||||
const testButton = page.locator('button:has-text("Test"), [data-testid="test-connection"], .test-button');
|
||||
if (await testButton.isVisible()) {
|
||||
const testResponse = helpers.waitForApiCall('/api/sources/test');
|
||||
|
||||
await testButton.click();
|
||||
|
||||
await testResponse;
|
||||
|
||||
// Should show test result
|
||||
await helpers.waitForToast();
|
||||
}
|
||||
});
|
||||
|
||||
test('should filter sources by type', async ({ authenticatedPage: page }) => {
|
||||
// Look for filter dropdown
|
||||
const filterDropdown = page.locator('[data-testid="source-filter"], select[name="filter"], .source-filter');
|
||||
if (await filterDropdown.isVisible()) {
|
||||
await filterDropdown.selectOption('webdav');
|
||||
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
// Should show only WebDAV sources
|
||||
const sourceItems = page.locator('[data-testid="source-item"], .source-item');
|
||||
if (await sourceItems.count() > 0) {
|
||||
await expect(sourceItems.first().locator(':has-text("WebDAV"), .webdav-icon')).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should display sync history', async ({ authenticatedPage: page }) => {
|
||||
const firstSource = page.locator('[data-testid="source-item"], .source-item, .source-card').first();
|
||||
|
||||
if (await firstSource.isVisible()) {
|
||||
await firstSource.click();
|
||||
|
||||
// Look for sync history section
|
||||
const historySection = page.locator('[data-testid="sync-history"], .sync-history, .history-section');
|
||||
if (await historySection.isVisible()) {
|
||||
// Should show sync runs
|
||||
await expect(historySection.locator('[data-testid="sync-run"], .sync-run, .history-item')).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should validate required fields in source creation', async ({ authenticatedPage: page }) => {
|
||||
await page.click('button:has-text("Add Source"), [data-testid="add-source"]');
|
||||
|
||||
await expect(page.locator('[data-testid="add-source-form"], .add-source-modal')).toBeVisible();
|
||||
|
||||
// Try to submit without filling required fields
|
||||
await page.click('button[type="submit"], button:has-text("Create"), [data-testid="create-source"]');
|
||||
|
||||
// Should show validation errors
|
||||
const nameInput = page.locator('input[name="name"], [data-testid="source-name"]');
|
||||
await expect(nameInput).toBeVisible();
|
||||
|
||||
// Check for validation messages
|
||||
const validationMessages = page.locator('.error, .validation-error, [data-testid="validation-error"]');
|
||||
if (await validationMessages.count() > 0) {
|
||||
await expect(validationMessages.first()).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should schedule automatic sync', async ({ authenticatedPage: page }) => {
|
||||
const firstSource = page.locator('[data-testid="source-item"], .source-item, .source-card').first();
|
||||
|
||||
if (await firstSource.isVisible()) {
|
||||
// Click settings or edit button
|
||||
const settingsButton = firstSource.locator('button:has-text("Settings"), button:has-text("Edit"), [data-testid="source-settings"]');
|
||||
if (await settingsButton.isVisible()) {
|
||||
await settingsButton.click();
|
||||
|
||||
// Look for scheduling options
|
||||
const scheduleSection = page.locator('[data-testid="schedule-section"], .schedule-options');
|
||||
if (await scheduleSection.isVisible()) {
|
||||
// Enable automatic sync
|
||||
const enableSchedule = page.locator('input[type="checkbox"][name="enableSchedule"], [data-testid="enable-schedule"]');
|
||||
if (await enableSchedule.isVisible()) {
|
||||
await enableSchedule.check();
|
||||
|
||||
// Set sync interval
|
||||
const intervalSelect = page.locator('select[name="interval"], [data-testid="sync-interval"]');
|
||||
if (await intervalSelect.isVisible()) {
|
||||
await intervalSelect.selectOption('daily');
|
||||
}
|
||||
|
||||
const saveResponse = helpers.waitForApiCall('/api/sources');
|
||||
|
||||
await page.click('button[type="submit"], button:has-text("Save")');
|
||||
|
||||
await saveResponse;
|
||||
await helpers.waitForToast();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
import { test, expect } from './fixtures/auth';
|
||||
import { TEST_FILES, TIMEOUTS, API_ENDPOINTS } from './utils/test-data';
|
||||
import { TestHelpers } from './utils/test-helpers';
|
||||
|
||||
test.describe('Document Upload', () => {
|
||||
let helpers: TestHelpers;
|
||||
|
||||
test.beforeEach(async ({ authenticatedPage }) => {
|
||||
helpers = new TestHelpers(authenticatedPage);
|
||||
await helpers.navigateToPage('/upload');
|
||||
});
|
||||
|
||||
test('should display upload interface', async ({ authenticatedPage: page }) => {
|
||||
// Check for upload components
|
||||
await expect(page.locator('input[type="file"], [data-testid="file-upload"]')).toBeVisible();
|
||||
await expect(page.locator('button:has-text("Upload"), [data-testid="upload-button"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should upload single document successfully', async ({ authenticatedPage: page }) => {
|
||||
// Find file input - try multiple selectors
|
||||
const fileInput = page.locator('input[type="file"]').first();
|
||||
|
||||
// Upload a test file
|
||||
await fileInput.setInputFiles(TEST_FILES.image);
|
||||
|
||||
// Wait for upload API call
|
||||
const uploadResponse = helpers.waitForApiCall(API_ENDPOINTS.upload, TIMEOUTS.upload);
|
||||
|
||||
// Click upload button if present
|
||||
const uploadButton = page.locator('button:has-text("Upload"), [data-testid="upload-button"]');
|
||||
if (await uploadButton.isVisible()) {
|
||||
await uploadButton.click();
|
||||
}
|
||||
|
||||
// Verify upload was successful
|
||||
await uploadResponse;
|
||||
|
||||
// Check for success message
|
||||
await helpers.waitForToast();
|
||||
|
||||
// Should show uploaded document in list
|
||||
await expect(page.locator('[data-testid="uploaded-files"], .uploaded-file')).toBeVisible({ timeout: TIMEOUTS.medium });
|
||||
});
|
||||
|
||||
test('should upload multiple documents', async ({ authenticatedPage: page }) => {
|
||||
const fileInput = page.locator('input[type="file"]').first();
|
||||
|
||||
// Upload multiple files
|
||||
await fileInput.setInputFiles([TEST_FILES.image, TEST_FILES.multiline]);
|
||||
|
||||
const uploadButton = page.locator('button:has-text("Upload"), [data-testid="upload-button"]');
|
||||
if (await uploadButton.isVisible()) {
|
||||
await uploadButton.click();
|
||||
}
|
||||
|
||||
// Wait for both uploads to complete
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
// Should show multiple uploaded documents
|
||||
const uploadedFiles = page.locator('[data-testid="uploaded-files"] > *, .uploaded-file');
|
||||
await expect(uploadedFiles).toHaveCount(2, { timeout: TIMEOUTS.medium });
|
||||
});
|
||||
|
||||
test('should show upload progress', async ({ authenticatedPage: page }) => {
|
||||
const fileInput = page.locator('input[type="file"]').first();
|
||||
await fileInput.setInputFiles(TEST_FILES.image);
|
||||
|
||||
const uploadButton = page.locator('button:has-text("Upload"), [data-testid="upload-button"]');
|
||||
if (await uploadButton.isVisible()) {
|
||||
await uploadButton.click();
|
||||
}
|
||||
|
||||
// Should show progress indicator
|
||||
await expect(page.locator('[data-testid="upload-progress"], .progress, [role="progressbar"]')).toBeVisible({ timeout: TIMEOUTS.short });
|
||||
});
|
||||
|
||||
test('should handle upload errors gracefully', async ({ authenticatedPage: page }) => {
|
||||
// Mock a failed upload by using a non-existent file type or intercepting the request
|
||||
await page.route('**/api/documents/upload', route => {
|
||||
route.fulfill({
|
||||
status: 500,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ error: 'Upload failed' })
|
||||
});
|
||||
});
|
||||
|
||||
const fileInput = page.locator('input[type="file"]').first();
|
||||
await fileInput.setInputFiles(TEST_FILES.image);
|
||||
|
||||
const uploadButton = page.locator('button:has-text("Upload"), [data-testid="upload-button"]');
|
||||
if (await uploadButton.isVisible()) {
|
||||
await uploadButton.click();
|
||||
}
|
||||
|
||||
// Should show error message
|
||||
await helpers.waitForToast();
|
||||
});
|
||||
|
||||
test('should validate file types', async ({ authenticatedPage: page }) => {
|
||||
// Try to upload an unsupported file type
|
||||
const fileInput = page.locator('input[type="file"]').first();
|
||||
|
||||
// Create a mock file with unsupported extension
|
||||
const buffer = Buffer.from('fake content');
|
||||
await fileInput.setInputFiles({
|
||||
name: 'test.xyz',
|
||||
mimeType: 'application/octet-stream',
|
||||
buffer
|
||||
});
|
||||
|
||||
const uploadButton = page.locator('button:has-text("Upload"), [data-testid="upload-button"]');
|
||||
if (await uploadButton.isVisible()) {
|
||||
await uploadButton.click();
|
||||
}
|
||||
|
||||
// Should show validation error
|
||||
await helpers.waitForToast();
|
||||
});
|
||||
|
||||
test('should navigate to uploaded document after successful upload', async ({ authenticatedPage: page }) => {
|
||||
const fileInput = page.locator('input[type="file"]').first();
|
||||
await fileInput.setInputFiles(TEST_FILES.image);
|
||||
|
||||
const uploadButton = page.locator('button:has-text("Upload"), [data-testid="upload-button"]');
|
||||
if (await uploadButton.isVisible()) {
|
||||
await uploadButton.click();
|
||||
}
|
||||
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
// Click on uploaded document to view details
|
||||
const uploadedDocument = page.locator('[data-testid="uploaded-files"] > *, .uploaded-file').first();
|
||||
if (await uploadedDocument.isVisible()) {
|
||||
await uploadedDocument.click();
|
||||
|
||||
// Should navigate to document details page
|
||||
await page.waitForURL(/\/documents\/[^\/]+/, { timeout: TIMEOUTS.medium });
|
||||
}
|
||||
});
|
||||
|
||||
test('should show OCR processing status', async ({ authenticatedPage: page }) => {
|
||||
const fileInput = page.locator('input[type="file"]').first();
|
||||
await fileInput.setInputFiles(TEST_FILES.image);
|
||||
|
||||
const uploadButton = page.locator('button:has-text("Upload"), [data-testid="upload-button"]');
|
||||
if (await uploadButton.isVisible()) {
|
||||
await uploadButton.click();
|
||||
}
|
||||
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
// Should show OCR processing status
|
||||
await expect(page.locator(':has-text("OCR"), :has-text("Processing"), [data-testid="ocr-status"]')).toBeVisible({
|
||||
timeout: TIMEOUTS.medium
|
||||
});
|
||||
});
|
||||
|
||||
test('should allow drag and drop upload', async ({ authenticatedPage: page }) => {
|
||||
// Look for dropzone
|
||||
const dropzone = page.locator('[data-testid="dropzone"], .dropzone, .upload-area');
|
||||
|
||||
if (await dropzone.isVisible()) {
|
||||
// Simulate drag and drop
|
||||
await dropzone.dispatchEvent('dragover', { dataTransfer: { files: [] } });
|
||||
await dropzone.dispatchEvent('drop', {
|
||||
dataTransfer: {
|
||||
files: [{ name: TEST_FILES.image, type: 'image/png' }]
|
||||
}
|
||||
});
|
||||
|
||||
// Should show uploaded file
|
||||
await expect(page.locator('[data-testid="uploaded-files"], .uploaded-file')).toBeVisible({
|
||||
timeout: TIMEOUTS.medium
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
export const TEST_USERS = {
|
||||
valid: {
|
||||
username: 'admin',
|
||||
password: 'readur2024'
|
||||
},
|
||||
invalid: {
|
||||
username: 'invaliduser',
|
||||
password: 'wrongpassword'
|
||||
}
|
||||
};
|
||||
|
||||
export const TEST_FILES = {
|
||||
pdf: 'test_data/sample.pdf',
|
||||
image: 'test_data/hello_ocr.png',
|
||||
text: 'test_data/sample.txt',
|
||||
multiline: 'test_data/multiline.png',
|
||||
numbers: 'test_data/numbers.png'
|
||||
};
|
||||
|
||||
export const SEARCH_QUERIES = {
|
||||
simple: 'test document',
|
||||
advanced: {
|
||||
title: 'important',
|
||||
content: 'contract',
|
||||
dateFrom: '2024-01-01',
|
||||
dateTo: '2024-12-31'
|
||||
},
|
||||
empty: '',
|
||||
noResults: 'xyzabc123nonexistent'
|
||||
};
|
||||
|
||||
export const API_ENDPOINTS = {
|
||||
login: '/api/auth/login',
|
||||
upload: '/api/documents/upload',
|
||||
search: '/api/search',
|
||||
documents: '/api/documents',
|
||||
settings: '/api/settings'
|
||||
};
|
||||
|
||||
export const TIMEOUTS = {
|
||||
short: 5000,
|
||||
medium: 10000,
|
||||
long: 30000,
|
||||
upload: 60000,
|
||||
ocr: 120000
|
||||
};
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
export class TestHelpers {
|
||||
constructor(private page: Page) {}
|
||||
|
||||
async waitForApiCall(urlPattern: string | RegExp, timeout = 10000) {
|
||||
return this.page.waitForResponse(resp =>
|
||||
typeof urlPattern === 'string'
|
||||
? resp.url().includes(urlPattern)
|
||||
: urlPattern.test(resp.url()),
|
||||
{ timeout }
|
||||
);
|
||||
}
|
||||
|
||||
async uploadFile(inputSelector: string, filePath: string) {
|
||||
const fileInput = this.page.locator(inputSelector);
|
||||
await fileInput.setInputFiles(filePath);
|
||||
}
|
||||
|
||||
async clearAndType(selector: string, text: string) {
|
||||
await this.page.fill(selector, '');
|
||||
await this.page.type(selector, text);
|
||||
}
|
||||
|
||||
async waitForToast(message?: string) {
|
||||
const toast = this.page.locator('[data-testid="toast"], .toast, [role="alert"]');
|
||||
await expect(toast).toBeVisible({ timeout: 5000 });
|
||||
|
||||
if (message) {
|
||||
await expect(toast).toContainText(message);
|
||||
}
|
||||
|
||||
return toast;
|
||||
}
|
||||
|
||||
async waitForLoadingToComplete() {
|
||||
// Wait for any loading spinners to disappear
|
||||
await this.page.waitForFunction(() =>
|
||||
!document.querySelector('[data-testid="loading"], .loading, [aria-label*="loading" i]')
|
||||
);
|
||||
}
|
||||
|
||||
async navigateToPage(path: string) {
|
||||
await this.page.goto(path);
|
||||
await this.waitForLoadingToComplete();
|
||||
}
|
||||
|
||||
async takeScreenshotOnFailure(testName: string) {
|
||||
await this.page.screenshot({
|
||||
path: `test-results/screenshots/${testName}-${Date.now()}.png`,
|
||||
fullPage: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@
|
|||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.53.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
|
|
@ -1591,6 +1592,22 @@
|
|||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.53.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.0.tgz",
|
||||
"integrity": "sha512-15hjKreZDcp7t6TL/7jkAo6Df5STZN09jGiv5dbP9A6vMVncXRqE7/B2SncsyOwrkZRBH2i6/TPOL8BVmm3c7w==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.53.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
|
|
@ -4763,6 +4780,53 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.53.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.0.tgz",
|
||||
"integrity": "sha512-ghGNnIEYZC4E+YtclRn4/p6oYbdPiASELBIYkBXfaTVKreQUYbMUYQDwS12a8F0/HtIjr/CkGjtwABeFPGcS4Q==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.53.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.53.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.0.tgz",
|
||||
"integrity": "sha512-mGLg8m0pm4+mmtB7M89Xw/GSqoNC+twivl8ITteqvAndachozYe2ZA7srU6uleV1vEdAHYqjq+SV8SNxRRFYBw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/possible-typed-array-names": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@
|
|||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"test:e2e:headed": "playwright test --headed",
|
||||
"test:e2e:debug": "playwright test --debug",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
@ -29,6 +33,7 @@
|
|||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.53.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
timeout: 30 * 1000,
|
||||
expect: {
|
||||
timeout: 5000,
|
||||
},
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: [
|
||||
['html', { outputFolder: 'test-results/e2e-report' }],
|
||||
['json', { outputFile: 'test-results/e2e-results.json' }],
|
||||
['list']
|
||||
],
|
||||
outputDir: 'test-results/e2e-artifacts',
|
||||
use: {
|
||||
baseURL: 'http://localhost:5173',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
url: 'http://localhost:5173',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120 * 1000,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_WATCH_FOLDER: string
|
||||
// Add more env variables as needed
|
||||
readonly VITE_API_URL?: string
|
||||
readonly VITE_APP_TITLE?: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
This is a sample text document for testing.
|
||||
It contains multiple lines of text.
|
||||
|
||||
This document is used for E2E testing of the upload functionality.
|
||||
The OCR system should be able to process this text correctly.
|
||||
|
||||
Key words for search testing:
|
||||
- contract
|
||||
- important
|
||||
- test document
|
||||
- readur
|
||||
71
run-tests.sh
71
run-tests.sh
|
|
@ -154,6 +154,7 @@ main() {
|
|||
run_unit_tests
|
||||
run_integration_tests
|
||||
run_frontend_tests
|
||||
run_e2e_tests
|
||||
;;
|
||||
*)
|
||||
print_error "Invalid test type: $TEST_TYPE"
|
||||
|
|
@ -252,10 +253,74 @@ run_frontend_tests() {
|
|||
fi
|
||||
}
|
||||
|
||||
# Function to run E2E tests (placeholder for future implementation)
|
||||
# Function to run E2E tests
|
||||
run_e2e_tests() {
|
||||
print_warning "E2E tests not yet implemented"
|
||||
# TODO: Add E2E test implementation using Playwright or Cypress
|
||||
print_status "Running E2E tests..."
|
||||
|
||||
# Start frontend container for E2E tests
|
||||
print_status "Starting frontend development server for E2E tests..."
|
||||
docker compose -f $COMPOSE_FILE -p $COMPOSE_PROJECT_NAME --profile e2e-tests up -d frontend_dev
|
||||
|
||||
# Wait for frontend to be ready
|
||||
local max_attempts=30
|
||||
local attempt=0
|
||||
|
||||
print_status "Waiting for frontend to be ready..."
|
||||
while [ $attempt -lt $max_attempts ]; do
|
||||
if curl -f http://localhost:5174 >/dev/null 2>&1; then
|
||||
print_success "Frontend is ready"
|
||||
break
|
||||
fi
|
||||
|
||||
attempt=$((attempt + 1))
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [ $attempt -eq $max_attempts ]; then
|
||||
print_error "Frontend failed to start within expected time"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run E2E tests
|
||||
local output
|
||||
local exit_code
|
||||
|
||||
# Run tests in frontend directory with proper environment variables
|
||||
cd frontend
|
||||
|
||||
# Install dependencies if not already installed
|
||||
if [ ! -d "node_modules" ]; then
|
||||
print_status "Installing frontend dependencies..."
|
||||
npm ci
|
||||
fi
|
||||
|
||||
# Install Playwright browsers if not already installed
|
||||
if [ ! -d "node_modules/@playwright" ]; then
|
||||
print_status "Installing Playwright browsers..."
|
||||
npx playwright install --with-deps
|
||||
fi
|
||||
|
||||
# Set environment variables for E2E tests
|
||||
export PLAYWRIGHT_BASE_URL="http://localhost:5174"
|
||||
export API_BASE_URL="http://localhost:8001"
|
||||
|
||||
output=$(npm run test:e2e 2>&1)
|
||||
exit_code=$?
|
||||
|
||||
# Return to root directory
|
||||
cd ..
|
||||
|
||||
# Display output in terminal
|
||||
echo "$output"
|
||||
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
print_success "E2E tests passed"
|
||||
save_output "e2e" "$output" "passed"
|
||||
else
|
||||
print_error "E2E tests failed"
|
||||
save_output "e2e" "$output" "failed"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to generate detailed test report
|
||||
|
|
|
|||
|
|
@ -0,0 +1,217 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Local E2E Test Runner for Readur
|
||||
# This script sets up and runs E2E tests locally
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
DB_NAME="readur_e2e_test"
|
||||
DB_USER="postgres"
|
||||
DB_PASSWORD="postgres"
|
||||
DB_HOST="localhost"
|
||||
DB_PORT="5432"
|
||||
BACKEND_PORT="8001"
|
||||
FRONTEND_PORT="5174"
|
||||
|
||||
# Function to print colored output
|
||||
print_status() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Function to check if command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Function to check if port is in use
|
||||
port_in_use() {
|
||||
lsof -i :"$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Function to wait for service
|
||||
wait_for_service() {
|
||||
local url="$1"
|
||||
local timeout="${2:-60}"
|
||||
local counter=0
|
||||
|
||||
print_status "Waiting for service at $url..."
|
||||
|
||||
while [ $counter -lt $timeout ]; do
|
||||
if curl -f "$url" >/dev/null 2>&1; then
|
||||
print_status "Service is ready!"
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
((counter += 2))
|
||||
done
|
||||
|
||||
print_error "Service at $url did not become ready within $timeout seconds"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Function to cleanup on exit
|
||||
cleanup() {
|
||||
print_status "Cleaning up..."
|
||||
|
||||
# Kill background processes
|
||||
if [ ! -z "$BACKEND_PID" ]; then
|
||||
kill $BACKEND_PID 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [ ! -z "$FRONTEND_PID" ]; then
|
||||
kill $FRONTEND_PID 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Drop test database
|
||||
PGPASSWORD=$DB_PASSWORD dropdb -h $DB_HOST -U $DB_USER $DB_NAME 2>/dev/null || true
|
||||
|
||||
print_status "Cleanup complete"
|
||||
}
|
||||
|
||||
# Set up trap to cleanup on exit
|
||||
trap cleanup EXIT
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
print_status "Starting Readur E2E Test Setup"
|
||||
|
||||
# Check prerequisites
|
||||
print_status "Checking prerequisites..."
|
||||
|
||||
if ! command_exists cargo; then
|
||||
print_error "Rust/Cargo not found. Please install Rust."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command_exists npm; then
|
||||
print_error "npm not found. Please install Node.js and npm."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command_exists psql; then
|
||||
print_error "PostgreSQL client not found. Please install PostgreSQL."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if ports are available
|
||||
if port_in_use $BACKEND_PORT; then
|
||||
print_error "Port $BACKEND_PORT is already in use. Please free it or change BACKEND_PORT in this script."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if port_in_use $FRONTEND_PORT; then
|
||||
print_error "Port $FRONTEND_PORT is already in use. Please free it or change FRONTEND_PORT in this script."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set up test database
|
||||
print_status "Setting up test database..."
|
||||
|
||||
# Drop existing test database if it exists
|
||||
PGPASSWORD=$DB_PASSWORD dropdb -h $DB_HOST -U $DB_USER $DB_NAME 2>/dev/null || true
|
||||
|
||||
# Create test database
|
||||
PGPASSWORD=$DB_PASSWORD createdb -h $DB_HOST -U $DB_USER $DB_NAME
|
||||
|
||||
# Add vector extension if available
|
||||
PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -U $DB_USER -d $DB_NAME -c "CREATE EXTENSION IF NOT EXISTS vector;" 2>/dev/null || print_warning "Vector extension not available"
|
||||
|
||||
# Run migrations
|
||||
print_status "Running database migrations..."
|
||||
export DATABASE_URL="postgresql://$DB_USER:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_NAME"
|
||||
export TEST_MODE=true
|
||||
|
||||
cargo run --bin migrate || {
|
||||
print_error "Failed to run migrations"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Build backend
|
||||
print_status "Building backend..."
|
||||
cargo build --release
|
||||
|
||||
# Start backend server
|
||||
print_status "Starting backend server on port $BACKEND_PORT..."
|
||||
DATABASE_URL="postgresql://$DB_USER:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_NAME" \
|
||||
TEST_MODE=true \
|
||||
ROCKET_PORT=$BACKEND_PORT \
|
||||
./target/release/readur > backend.log 2>&1 &
|
||||
BACKEND_PID=$!
|
||||
|
||||
# Wait for backend to be ready
|
||||
wait_for_service "http://localhost:$BACKEND_PORT/health" || {
|
||||
print_error "Backend failed to start. Check backend.log for details."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Install frontend dependencies
|
||||
print_status "Installing frontend dependencies..."
|
||||
cd frontend
|
||||
npm ci
|
||||
|
||||
# Install Playwright browsers
|
||||
print_status "Installing Playwright browsers..."
|
||||
npx playwright install
|
||||
|
||||
# Start frontend dev server
|
||||
print_status "Starting frontend dev server on port $FRONTEND_PORT..."
|
||||
VITE_API_BASE_URL="http://localhost:$BACKEND_PORT" \
|
||||
npm run dev -- --port $FRONTEND_PORT > ../frontend.log 2>&1 &
|
||||
FRONTEND_PID=$!
|
||||
|
||||
# Wait for frontend to be ready
|
||||
wait_for_service "http://localhost:$FRONTEND_PORT" || {
|
||||
print_error "Frontend failed to start. Check frontend.log for details."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Run E2E tests
|
||||
print_status "Running E2E tests..."
|
||||
|
||||
# Update Playwright config for local testing
|
||||
export PLAYWRIGHT_BASE_URL="http://localhost:$FRONTEND_PORT"
|
||||
|
||||
if [ "$1" = "--headed" ]; then
|
||||
npm run test:e2e:headed
|
||||
elif [ "$1" = "--debug" ]; then
|
||||
npm run test:e2e:debug
|
||||
elif [ "$1" = "--ui" ]; then
|
||||
npm run test:e2e:ui
|
||||
else
|
||||
npm run test:e2e
|
||||
fi
|
||||
|
||||
print_status "E2E tests completed!"
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
case "$1" in
|
||||
--help|-h)
|
||||
echo "Usage: $0 [--headed|--debug|--ui|--help]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --headed Run tests in headed mode (show browser)"
|
||||
echo " --debug Run tests in debug mode"
|
||||
echo " --ui Run tests with Playwright UI"
|
||||
echo " --help Show this help message"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
Loading…
Reference in New Issue