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:
|
profiles:
|
||||||
- frontend-tests
|
- 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:
|
networks:
|
||||||
readur_test_network:
|
readur_test_network:
|
||||||
name: 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"
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.53.0",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
|
|
@ -1591,6 +1592,22 @@
|
||||||
"node": ">=14"
|
"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": {
|
"node_modules/@popperjs/core": {
|
||||||
"version": "2.11.8",
|
"version": "2.11.8",
|
||||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||||
|
|
@ -4763,6 +4780,53 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/possible-typed-array-names": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "vitest",
|
"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"
|
"type-check": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -29,6 +33,7 @@
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.53.0",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@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_unit_tests
|
||||||
run_integration_tests
|
run_integration_tests
|
||||||
run_frontend_tests
|
run_frontend_tests
|
||||||
|
run_e2e_tests
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
print_error "Invalid test type: $TEST_TYPE"
|
print_error "Invalid test type: $TEST_TYPE"
|
||||||
|
|
@ -252,10 +253,74 @@ run_frontend_tests() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Function to run E2E tests (placeholder for future implementation)
|
# Function to run E2E tests
|
||||||
run_e2e_tests() {
|
run_e2e_tests() {
|
||||||
print_warning "E2E tests not yet implemented"
|
print_status "Running E2E tests..."
|
||||||
# TODO: Add E2E test implementation using Playwright or Cypress
|
|
||||||
|
# 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
|
# 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