Spaces:
Running
Running
tests
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .github/workflows/e2e.yml +78 -0
- docker-compose.e2e.yml +86 -0
- e2e/README.md +219 -0
- e2e/conftest.py +126 -0
- e2e/fixtures/test_image.jpg +3 -0
- e2e/pages/admin_page.py +74 -0
- e2e/pages/base_page.py +46 -0
- e2e/pages/explore_page.py +80 -0
- e2e/pages/upload_page.py +59 -0
- e2e/pytest.ini +17 -0
- e2e/requirements.txt +6 -0
- e2e/run_e2e_tests.sh +117 -0
- e2e/specs/admin_settings_spec.py +136 -0
- e2e/specs/export_spec.py +128 -0
- e2e/specs/upload_flow_spec.py +90 -0
- frontend/TESTING.md +87 -0
- frontend/package-lock.json +1204 -7
- frontend/package.json +15 -2
- frontend/src/pages/UploadPage/UploadPage.module.css +1 -1
- frontend/src/test/integration/AppWorkflow.test.tsx +287 -0
- frontend/src/test/integration/FilterBarWithFilterContext.test.tsx +109 -0
- frontend/src/test/integration/FilterBarWithSelectInput.test.tsx +227 -0
- frontend/src/test/integration/HeaderNavWithRouting.test.tsx +139 -0
- frontend/src/test/integration/HelpPageWithRouting.test.tsx +95 -0
- frontend/src/test/integration/README.md +170 -0
- frontend/src/test/integration/run-integration-tests.ts +42 -0
- frontend/src/test/setup.ts +17 -0
- frontend/src/test/test-utils.tsx +29 -0
- frontend/src/test/unit_tests/ExportModal.test.tsx +119 -0
- frontend/src/test/unit_tests/FilterBar.test.tsx +56 -0
- frontend/src/test/unit_tests/FilterContext.test.tsx +186 -0
- frontend/src/test/unit_tests/HeaderNav.test.tsx +38 -0
- frontend/src/test/unit_tests/HelpPage.test.tsx +91 -0
- frontend/src/test/unit_tests/README.md +104 -0
- frontend/tsconfig.app.json +1 -1
- frontend/vite.config.ts +10 -0
- py_backend/tests/README.md +42 -43
- py_backend/tests/e2e_tests/README.md +46 -0
- py_backend/tests/e2e_tests/__init__.py +1 -0
- py_backend/tests/integration_tests/README.md +39 -0
- py_backend/tests/integration_tests/__init__.py +1 -0
- py_backend/tests/integration_tests/run_integration_tests.py +65 -0
- py_backend/tests/{test_admin_endpoints.py → integration_tests/test_admin_endpoints.py} +0 -0
- py_backend/tests/integration_tests/test_admin_management_workflow.py +107 -0
- py_backend/tests/{test_config.py → integration_tests/test_config.py} +0 -0
- py_backend/tests/{test_core.py → integration_tests/test_core.py} +0 -0
- py_backend/tests/integration_tests/test_crisis_analysis_workflow.py +114 -0
- py_backend/tests/integration_tests/test_data_export_workflow.py +124 -0
- py_backend/tests/{test_explore_page.py → integration_tests/test_explore_page.py} +0 -0
- py_backend/tests/{test_openai_integration.py → integration_tests/test_openai_integration.py} +0 -0
.github/workflows/e2e.yml
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: E2E Tests
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [main, develop]
|
| 6 |
+
pull_request:
|
| 7 |
+
branches: [main, develop]
|
| 8 |
+
workflow_dispatch: # Allow manual triggering
|
| 9 |
+
|
| 10 |
+
jobs:
|
| 11 |
+
e2e-tests:
|
| 12 |
+
runs-on: ubuntu-latest
|
| 13 |
+
|
| 14 |
+
steps:
|
| 15 |
+
- name: Check out code
|
| 16 |
+
uses: actions/checkout@v4
|
| 17 |
+
|
| 18 |
+
- name: Setup Python
|
| 19 |
+
uses: actions/setup-python@v4
|
| 20 |
+
with:
|
| 21 |
+
python-version: "3.11"
|
| 22 |
+
|
| 23 |
+
- name: Install Python dependencies
|
| 24 |
+
run: |
|
| 25 |
+
cd e2e
|
| 26 |
+
pip install -r requirements.txt
|
| 27 |
+
|
| 28 |
+
- name: Install Playwright browsers
|
| 29 |
+
run: |
|
| 30 |
+
cd e2e
|
| 31 |
+
playwright install
|
| 32 |
+
|
| 33 |
+
- name: Start E2E environment
|
| 34 |
+
run: |
|
| 35 |
+
docker-compose -f e2e/docker-compose.e2e.yml up -d --build
|
| 36 |
+
|
| 37 |
+
- name: Wait for services to be ready
|
| 38 |
+
run: |
|
| 39 |
+
echo "Waiting for services to be ready..."
|
| 40 |
+
sleep 30
|
| 41 |
+
|
| 42 |
+
# Wait for backend health
|
| 43 |
+
for i in {1..30}; do
|
| 44 |
+
if curl -f http://localhost:7860/health > /dev/null 2>&1; then
|
| 45 |
+
echo "Backend is ready"
|
| 46 |
+
break
|
| 47 |
+
fi
|
| 48 |
+
echo "Waiting for backend..."
|
| 49 |
+
sleep 2
|
| 50 |
+
done
|
| 51 |
+
|
| 52 |
+
# Wait for frontend
|
| 53 |
+
for i in {1..30}; do
|
| 54 |
+
if curl -f http://localhost:3000 > /dev/null 2>&1; then
|
| 55 |
+
echo "Frontend is ready"
|
| 56 |
+
break
|
| 57 |
+
fi
|
| 58 |
+
echo "Waiting for frontend..."
|
| 59 |
+
sleep 2
|
| 60 |
+
done
|
| 61 |
+
|
| 62 |
+
- name: Run E2E tests
|
| 63 |
+
run: |
|
| 64 |
+
cd e2e
|
| 65 |
+
pytest -m e2e -v --tb=short
|
| 66 |
+
|
| 67 |
+
- name: Upload E2E test results
|
| 68 |
+
uses: actions/upload-artifact@v4
|
| 69 |
+
if: always()
|
| 70 |
+
with:
|
| 71 |
+
name: e2e-test-results
|
| 72 |
+
path: e2e/test-results/
|
| 73 |
+
retention-days: 7
|
| 74 |
+
|
| 75 |
+
- name: Cleanup E2E environment
|
| 76 |
+
if: always()
|
| 77 |
+
run: |
|
| 78 |
+
docker-compose -f e2e/docker-compose.e2e.yml down -v
|
docker-compose.e2e.yml
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
# PostgreSQL Database
|
| 5 |
+
db:
|
| 6 |
+
image: postgres:16-alpine
|
| 7 |
+
environment:
|
| 8 |
+
POSTGRES_DB: promptaid_e2e
|
| 9 |
+
POSTGRES_USER: promptaid
|
| 10 |
+
POSTGRES_PASSWORD: promptaid_e2e_password
|
| 11 |
+
ports:
|
| 12 |
+
- "5433:5432"
|
| 13 |
+
volumes:
|
| 14 |
+
- postgres_e2e_data:/var/lib/postgresql/data
|
| 15 |
+
healthcheck:
|
| 16 |
+
test: ["CMD-SHELL", "pg_isready -U promptaid -d promptaid_e2e"]
|
| 17 |
+
interval: 5s
|
| 18 |
+
timeout: 5s
|
| 19 |
+
retries: 5
|
| 20 |
+
|
| 21 |
+
# MinIO S3 Storage
|
| 22 |
+
minio:
|
| 23 |
+
image: minio/minio:latest
|
| 24 |
+
environment:
|
| 25 |
+
MINIO_ROOT_USER: minioadmin
|
| 26 |
+
MINIO_ROOT_PASSWORD: minioadmin123
|
| 27 |
+
ports:
|
| 28 |
+
- "9000:9000"
|
| 29 |
+
- "9001:9001"
|
| 30 |
+
volumes:
|
| 31 |
+
- minio_e2e_data:/data
|
| 32 |
+
command: server /data --console-address ":9001"
|
| 33 |
+
healthcheck:
|
| 34 |
+
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
| 35 |
+
interval: 10s
|
| 36 |
+
timeout: 5s
|
| 37 |
+
retries: 5
|
| 38 |
+
|
| 39 |
+
# Backend API
|
| 40 |
+
backend:
|
| 41 |
+
build:
|
| 42 |
+
context: ./py_backend
|
| 43 |
+
dockerfile: Dockerfile
|
| 44 |
+
environment:
|
| 45 |
+
ENV: e2e
|
| 46 |
+
DATABASE_URL: postgresql://promptaid:promptaid_e2e_password@db:5432/promptaid_e2e
|
| 47 |
+
S3_ENDPOINT: http://minio:9000
|
| 48 |
+
S3_ACCESS_KEY: minioadmin
|
| 49 |
+
S3_SECRET_KEY: minioadmin123
|
| 50 |
+
S3_BUCKET: promptaid-e2e
|
| 51 |
+
VISION_PROVIDER: mock
|
| 52 |
+
ADMIN_PASSWORD: admin_e2e_password
|
| 53 |
+
ports:
|
| 54 |
+
- "7860:7860"
|
| 55 |
+
depends_on:
|
| 56 |
+
db:
|
| 57 |
+
condition: service_healthy
|
| 58 |
+
minio:
|
| 59 |
+
condition: service_healthy
|
| 60 |
+
healthcheck:
|
| 61 |
+
test: ["CMD", "curl", "-f", "http://localhost:7860/health"]
|
| 62 |
+
interval: 10s
|
| 63 |
+
timeout: 5s
|
| 64 |
+
retries: 5
|
| 65 |
+
|
| 66 |
+
# Frontend
|
| 67 |
+
frontend:
|
| 68 |
+
build:
|
| 69 |
+
context: ./frontend
|
| 70 |
+
dockerfile: Dockerfile
|
| 71 |
+
environment:
|
| 72 |
+
VITE_API_BASE_URL: http://localhost:7860
|
| 73 |
+
ports:
|
| 74 |
+
- "3000:3000"
|
| 75 |
+
depends_on:
|
| 76 |
+
backend:
|
| 77 |
+
condition: service_healthy
|
| 78 |
+
healthcheck:
|
| 79 |
+
test: ["CMD", "curl", "-f", "http://localhost:3000"]
|
| 80 |
+
interval: 10s
|
| 81 |
+
timeout: 5s
|
| 82 |
+
retries: 5
|
| 83 |
+
|
| 84 |
+
volumes:
|
| 85 |
+
postgres_e2e_data:
|
| 86 |
+
minio_e2e_data:
|
e2e/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# End-to-End Test Suite for PromptAid Vision
|
| 2 |
+
|
| 3 |
+
This directory contains comprehensive end-to-end tests that validate the complete user experience through the entire application stack.
|
| 4 |
+
|
| 5 |
+
## 🎯 Overview
|
| 6 |
+
|
| 7 |
+
These are **true E2E tests** that:
|
| 8 |
+
- ✅ Hit the running app over HTTP via real browsers
|
| 9 |
+
- ✅ Test complete user workflows from start to finish
|
| 10 |
+
- ✅ Validate frontend, backend, and database integration
|
| 11 |
+
- ✅ Use real browser automation with Playwright
|
| 12 |
+
- ✅ Run against containerized services
|
| 13 |
+
|
| 14 |
+
## 🏗️ Architecture
|
| 15 |
+
|
| 16 |
+
```
|
| 17 |
+
e2e/
|
| 18 |
+
├── docker-compose.e2e.yml # E2E environment setup
|
| 19 |
+
├── requirements.txt # Python dependencies
|
| 20 |
+
├── pytest.ini # Pytest configuration
|
| 21 |
+
├── conftest.py # Test fixtures and setup
|
| 22 |
+
├── run_e2e_tests.sh # Test runner script
|
| 23 |
+
├── pages/ # Page Object Models
|
| 24 |
+
│ ├── base_page.py
|
| 25 |
+
│ ├── upload_page.py
|
| 26 |
+
│ ├── explore_page.py
|
| 27 |
+
│ └── admin_page.py
|
| 28 |
+
├── specs/ # Test specifications
|
| 29 |
+
│ ├── upload_flow_spec.py
|
| 30 |
+
│ ├── admin_settings_spec.py
|
| 31 |
+
│ └── export_spec.py
|
| 32 |
+
└── fixtures/ # Test data
|
| 33 |
+
└── test_image.jpg
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
## 🚀 Quick Start
|
| 37 |
+
|
| 38 |
+
### Prerequisites
|
| 39 |
+
- Docker and Docker Compose
|
| 40 |
+
- Python 3.8+
|
| 41 |
+
- Git
|
| 42 |
+
|
| 43 |
+
### Run E2E Tests
|
| 44 |
+
|
| 45 |
+
```bash
|
| 46 |
+
# Option 1: Use the automated script
|
| 47 |
+
chmod +x run_e2e_tests.sh
|
| 48 |
+
./run_e2e_tests.sh
|
| 49 |
+
|
| 50 |
+
# Option 2: Manual steps
|
| 51 |
+
docker-compose -f docker-compose.e2e.yml up -d --build
|
| 52 |
+
pip install -r requirements.txt
|
| 53 |
+
playwright install
|
| 54 |
+
pytest -m e2e -v
|
| 55 |
+
docker-compose -f docker-compose.e2e.yml down -v
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
## 🧪 Test Categories
|
| 59 |
+
|
| 60 |
+
### 1. Upload Flow Tests (`upload_flow_spec.py`)
|
| 61 |
+
- **Complete upload workflow**: File selection → Analysis → Success
|
| 62 |
+
- **Invalid file handling**: Error messages for wrong file types
|
| 63 |
+
- **Large file handling**: Performance with large images
|
| 64 |
+
|
| 65 |
+
### 2. Admin Settings Tests (`admin_settings_spec.py`)
|
| 66 |
+
- **Authentication flow**: Login/logout with correct/incorrect credentials
|
| 67 |
+
- **Schema management**: Admin interface for schema configuration
|
| 68 |
+
- **Model configuration**: VLM service configuration
|
| 69 |
+
- **System monitoring**: Health checks and monitoring
|
| 70 |
+
|
| 71 |
+
### 3. Export Tests (`export_spec.py`)
|
| 72 |
+
- **Filtered data export**: Export with applied filters
|
| 73 |
+
- **Bulk export workflow**: Export multiple selected items
|
| 74 |
+
- **Export format validation**: Different export formats
|
| 75 |
+
- **Performance testing**: Export with large datasets
|
| 76 |
+
|
| 77 |
+
## 🔧 Environment Setup
|
| 78 |
+
|
| 79 |
+
### Docker Services
|
| 80 |
+
- **PostgreSQL 16**: Test database with health checks
|
| 81 |
+
- **MinIO**: S3-compatible storage for file uploads
|
| 82 |
+
- **Backend**: FastAPI with mock VLM provider
|
| 83 |
+
- **Frontend**: React application with Vite
|
| 84 |
+
|
| 85 |
+
### Health Checks
|
| 86 |
+
- Backend: `http://localhost:7860/health`
|
| 87 |
+
- Frontend: `http://localhost:3000`
|
| 88 |
+
- Database: PostgreSQL connection check
|
| 89 |
+
- MinIO: S3 health endpoint
|
| 90 |
+
|
| 91 |
+
## 📊 Test Metrics
|
| 92 |
+
|
| 93 |
+
### What We Measure
|
| 94 |
+
- **Flakiness rate**: Test stability and reliability
|
| 95 |
+
- **Test duration**: Median and 95th percentile times
|
| 96 |
+
- **Critical path coverage**: Key user workflows
|
| 97 |
+
- **Failure triage speed**: Debug information availability
|
| 98 |
+
|
| 99 |
+
### What We Don't Measure
|
| 100 |
+
- ❌ Code coverage (not relevant for E2E)
|
| 101 |
+
- ❌ Individual test duration targets
|
| 102 |
+
- ❌ UI element coverage percentages
|
| 103 |
+
|
| 104 |
+
## 🎭 Playwright Configuration
|
| 105 |
+
|
| 106 |
+
### Browser Settings
|
| 107 |
+
- **Viewport**: 1920x1080
|
| 108 |
+
- **Video recording**: Enabled for all tests
|
| 109 |
+
- **Screenshots**: On failure
|
| 110 |
+
- **Traces**: Available for debugging
|
| 111 |
+
|
| 112 |
+
### Auto-wait Strategy
|
| 113 |
+
- No explicit `sleep()` calls
|
| 114 |
+
- Uses Playwright's built-in auto-wait
|
| 115 |
+
- Relies on `expect().toBeVisible()` assertions
|
| 116 |
+
- URL-based navigation verification
|
| 117 |
+
|
| 118 |
+
## 📁 Test Data Management
|
| 119 |
+
|
| 120 |
+
### Data Isolation
|
| 121 |
+
- **Per-test reset**: `/test/reset` endpoint (E2E mode only)
|
| 122 |
+
- **Volume cleanup**: `docker-compose down -v` after suite
|
| 123 |
+
- **Namespaced data**: Unique filenames per test
|
| 124 |
+
|
| 125 |
+
### Test Fixtures
|
| 126 |
+
- Sample images for upload testing
|
| 127 |
+
- Test schemas for validation
|
| 128 |
+
- Mock data for various scenarios
|
| 129 |
+
|
| 130 |
+
## 🚨 Troubleshooting
|
| 131 |
+
|
| 132 |
+
### Common Issues
|
| 133 |
+
|
| 134 |
+
1. **Services not ready**
|
| 135 |
+
```bash
|
| 136 |
+
# Check service health
|
| 137 |
+
curl http://localhost:7860/health
|
| 138 |
+
curl http://localhost:3000
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
2. **Browser installation issues**
|
| 142 |
+
```bash
|
| 143 |
+
# Reinstall Playwright browsers
|
| 144 |
+
playwright install
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
3. **Test failures with traces**
|
| 148 |
+
```bash
|
| 149 |
+
# View test traces
|
| 150 |
+
playwright show-trace test-results/trace.zip
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
+
### Debug Mode
|
| 154 |
+
```bash
|
| 155 |
+
# Run tests with headed browser
|
| 156 |
+
pytest -m e2e --headed --slowmo=1000
|
| 157 |
+
|
| 158 |
+
# Run specific test with debugging
|
| 159 |
+
pytest specs/upload_flow_spec.py::TestUploadFlow::test_complete_upload_flow -v --headed
|
| 160 |
+
```
|
| 161 |
+
|
| 162 |
+
## 📈 CI/CD Integration
|
| 163 |
+
|
| 164 |
+
### GitHub Actions Example
|
| 165 |
+
```yaml
|
| 166 |
+
name: E2E Tests
|
| 167 |
+
on: [push, pull_request]
|
| 168 |
+
jobs:
|
| 169 |
+
e2e:
|
| 170 |
+
runs-on: ubuntu-latest
|
| 171 |
+
steps:
|
| 172 |
+
- uses: actions/checkout@v3
|
| 173 |
+
- name: Start E2E environment
|
| 174 |
+
run: docker-compose -f e2e/docker-compose.e2e.yml up -d --build
|
| 175 |
+
- name: Wait for services
|
| 176 |
+
run: sleep 30
|
| 177 |
+
- name: Install dependencies
|
| 178 |
+
run: |
|
| 179 |
+
cd e2e
|
| 180 |
+
pip install -r requirements.txt
|
| 181 |
+
playwright install
|
| 182 |
+
- name: Run E2E tests
|
| 183 |
+
run: |
|
| 184 |
+
cd e2e
|
| 185 |
+
pytest -m e2e -v
|
| 186 |
+
- name: Upload test results
|
| 187 |
+
uses: actions/upload-artifact@v3
|
| 188 |
+
with:
|
| 189 |
+
name: e2e-test-results
|
| 190 |
+
path: e2e/test-results/
|
| 191 |
+
```
|
| 192 |
+
|
| 193 |
+
## 📋 Test Results
|
| 194 |
+
|
| 195 |
+
### Output Locations
|
| 196 |
+
- **Videos**: `test-results/videos/`
|
| 197 |
+
- **Screenshots**: `test-results/screenshots/`
|
| 198 |
+
- **Traces**: `test-results/har/`
|
| 199 |
+
- **Reports**: Playwright HTML report
|
| 200 |
+
|
| 201 |
+
### Success Criteria
|
| 202 |
+
- All critical user paths covered
|
| 203 |
+
- <5% flakiness rate
|
| 204 |
+
- <5 minutes total suite duration
|
| 205 |
+
- Clear failure debugging information
|
| 206 |
+
|
| 207 |
+
## 🔄 Maintenance
|
| 208 |
+
|
| 209 |
+
### Regular Tasks
|
| 210 |
+
- Update test selectors when UI changes
|
| 211 |
+
- Refresh test data periodically
|
| 212 |
+
- Monitor flakiness trends
|
| 213 |
+
- Update dependencies
|
| 214 |
+
|
| 215 |
+
### Best Practices
|
| 216 |
+
- Use stable `data-testid` selectors
|
| 217 |
+
- Keep page objects thin and focused
|
| 218 |
+
- Write descriptive test names
|
| 219 |
+
- Maintain test data isolation
|
e2e/conftest.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
import requests
|
| 3 |
+
import time
|
| 4 |
+
import os
|
| 5 |
+
from playwright.sync_api import sync_playwright
|
| 6 |
+
|
| 7 |
+
def pytest_configure(config):
|
| 8 |
+
"""Configure pytest for E2E tests"""
|
| 9 |
+
config.addinivalue_line(
|
| 10 |
+
"markers", "e2e: marks tests as end-to-end tests"
|
| 11 |
+
)
|
| 12 |
+
config.addinivalue_line(
|
| 13 |
+
"markers", "upload: marks tests related to upload functionality"
|
| 14 |
+
)
|
| 15 |
+
config.addinivalue_line(
|
| 16 |
+
"markers", "admin: marks tests related to admin functionality"
|
| 17 |
+
)
|
| 18 |
+
config.addinivalue_line(
|
| 19 |
+
"markers", "export: marks tests related to export functionality"
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
@pytest.fixture(scope="session")
|
| 23 |
+
def browser_context_args(browser_context_args):
|
| 24 |
+
"""Configure browser context for E2E tests"""
|
| 25 |
+
return {
|
| 26 |
+
**browser_context_args,
|
| 27 |
+
"viewport": {
|
| 28 |
+
"width": 1920,
|
| 29 |
+
"height": 1080,
|
| 30 |
+
},
|
| 31 |
+
"ignore_https_errors": True,
|
| 32 |
+
"record_video_dir": "./test-results/videos/",
|
| 33 |
+
"record_har_path": "./test-results/har/",
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
@pytest.fixture(scope="session")
|
| 37 |
+
def browser_type_launch_args(browser_type_launch_args):
|
| 38 |
+
"""Configure browser launch arguments"""
|
| 39 |
+
return {
|
| 40 |
+
**browser_type_launch_args,
|
| 41 |
+
"args": [
|
| 42 |
+
"--disable-web-security",
|
| 43 |
+
"--disable-features=VizDisplayCompositor",
|
| 44 |
+
"--no-sandbox",
|
| 45 |
+
"--disable-setuid-sandbox",
|
| 46 |
+
]
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
@pytest.fixture(scope="session")
|
| 50 |
+
def wait_for_services():
|
| 51 |
+
"""Wait for all services to be ready before running tests"""
|
| 52 |
+
print("Waiting for services to be ready...")
|
| 53 |
+
|
| 54 |
+
# Wait for backend
|
| 55 |
+
backend_ready = False
|
| 56 |
+
for i in range(30): # Wait up to 30 seconds
|
| 57 |
+
try:
|
| 58 |
+
response = requests.get("http://localhost:7860/health", timeout=5)
|
| 59 |
+
if response.status_code == 200:
|
| 60 |
+
backend_ready = True
|
| 61 |
+
print("Backend is ready")
|
| 62 |
+
break
|
| 63 |
+
except requests.exceptions.RequestException:
|
| 64 |
+
pass
|
| 65 |
+
time.sleep(1)
|
| 66 |
+
|
| 67 |
+
if not backend_ready:
|
| 68 |
+
pytest.fail("Backend service is not ready")
|
| 69 |
+
|
| 70 |
+
# Wait for frontend
|
| 71 |
+
frontend_ready = False
|
| 72 |
+
for i in range(30): # Wait up to 30 seconds
|
| 73 |
+
try:
|
| 74 |
+
response = requests.get("http://localhost:3000", timeout=5)
|
| 75 |
+
if response.status_code == 200:
|
| 76 |
+
frontend_ready = True
|
| 77 |
+
print("Frontend is ready")
|
| 78 |
+
break
|
| 79 |
+
except requests.exceptions.RequestException:
|
| 80 |
+
pass
|
| 81 |
+
time.sleep(1)
|
| 82 |
+
|
| 83 |
+
if not frontend_ready:
|
| 84 |
+
pytest.fail("Frontend service is not ready")
|
| 85 |
+
|
| 86 |
+
print("All services are ready!")
|
| 87 |
+
|
| 88 |
+
@pytest.fixture(scope="function")
|
| 89 |
+
def reset_test_data():
|
| 90 |
+
"""Reset test data between tests"""
|
| 91 |
+
try:
|
| 92 |
+
# Call the test reset endpoint if available
|
| 93 |
+
response = requests.post("http://localhost:7860/test/reset", timeout=10)
|
| 94 |
+
if response.status_code == 200:
|
| 95 |
+
print("Test data reset successful")
|
| 96 |
+
else:
|
| 97 |
+
print("Test data reset failed, continuing anyway")
|
| 98 |
+
except requests.exceptions.RequestException:
|
| 99 |
+
print("Test reset endpoint not available, continuing anyway")
|
| 100 |
+
|
| 101 |
+
yield
|
| 102 |
+
|
| 103 |
+
@pytest.fixture(scope="function")
|
| 104 |
+
def page(page, wait_for_services, reset_test_data):
|
| 105 |
+
"""Configure page for E2E tests"""
|
| 106 |
+
# Set up page with proper viewport and other settings
|
| 107 |
+
page.set_viewport_size({"width": 1920, "height": 1080})
|
| 108 |
+
|
| 109 |
+
# Enable video recording
|
| 110 |
+
page.video.start()
|
| 111 |
+
|
| 112 |
+
yield page
|
| 113 |
+
|
| 114 |
+
# Clean up after test
|
| 115 |
+
if page.video:
|
| 116 |
+
page.video.save_as(f"./test-results/videos/{page.url.replace('/', '_')}.webm")
|
| 117 |
+
|
| 118 |
+
def pytest_runtest_setup(item):
|
| 119 |
+
"""Setup before each test"""
|
| 120 |
+
# Ensure we're in the e2e directory
|
| 121 |
+
os.chdir(os.path.dirname(__file__))
|
| 122 |
+
|
| 123 |
+
def pytest_runtest_teardown(item, nextitem):
|
| 124 |
+
"""Teardown after each test"""
|
| 125 |
+
# Any cleanup needed after tests
|
| 126 |
+
pass
|
e2e/fixtures/test_image.jpg
ADDED
|
Git LFS Details
|
e2e/pages/admin_page.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .base_page import BasePage
|
| 2 |
+
from playwright.sync_api import expect
|
| 3 |
+
|
| 4 |
+
class AdminPage(BasePage):
|
| 5 |
+
"""Page object for the admin page"""
|
| 6 |
+
|
| 7 |
+
# Selectors using data-testid for stability
|
| 8 |
+
LOGIN_FORM = "[data-testid='login-form']"
|
| 9 |
+
PASSWORD_INPUT = "[data-testid='password-input']"
|
| 10 |
+
LOGIN_BUTTON = "[data-testid='login-button']"
|
| 11 |
+
ADMIN_DASHBOARD = "[data-testid='admin-dashboard']"
|
| 12 |
+
SCHEMA_MANAGEMENT = "[data-testid='schema-management']"
|
| 13 |
+
MODEL_CONFIG = "[data-testid='model-config']"
|
| 14 |
+
SYSTEM_MONITORING = "[data-testid='system-monitoring']"
|
| 15 |
+
LOGOUT_BUTTON = "[data-testid='logout-button']"
|
| 16 |
+
SAVE_BUTTON = "[data-testid='save-button']"
|
| 17 |
+
SUCCESS_MESSAGE = "[data-testid='success-message']"
|
| 18 |
+
ERROR_MESSAGE = "[data-testid='error-message']"
|
| 19 |
+
|
| 20 |
+
def __init__(self, page):
|
| 21 |
+
super().__init__(page)
|
| 22 |
+
self.page_url = "/admin"
|
| 23 |
+
|
| 24 |
+
def navigate(self):
|
| 25 |
+
"""Navigate to admin page"""
|
| 26 |
+
self.navigate_to(self.page_url)
|
| 27 |
+
self.expect_element_visible(self.LOGIN_FORM)
|
| 28 |
+
|
| 29 |
+
def login(self, password: str):
|
| 30 |
+
"""Login to admin panel"""
|
| 31 |
+
self.fill_input(self.PASSWORD_INPUT, password)
|
| 32 |
+
self.click_element(self.LOGIN_BUTTON)
|
| 33 |
+
self.expect_element_visible(self.ADMIN_DASHBOARD)
|
| 34 |
+
|
| 35 |
+
def logout(self):
|
| 36 |
+
"""Logout from admin panel"""
|
| 37 |
+
self.click_element(self.LOGOUT_BUTTON)
|
| 38 |
+
self.expect_element_visible(self.LOGIN_FORM)
|
| 39 |
+
|
| 40 |
+
def navigate_to_schema_management(self):
|
| 41 |
+
"""Navigate to schema management section"""
|
| 42 |
+
self.click_element(self.SCHEMA_MANAGEMENT)
|
| 43 |
+
self.page.wait_for_load_state("networkidle")
|
| 44 |
+
|
| 45 |
+
def navigate_to_model_config(self):
|
| 46 |
+
"""Navigate to model configuration section"""
|
| 47 |
+
self.click_element(self.MODEL_CONFIG)
|
| 48 |
+
self.page.wait_for_load_state("networkidle")
|
| 49 |
+
|
| 50 |
+
def navigate_to_system_monitoring(self):
|
| 51 |
+
"""Navigate to system monitoring section"""
|
| 52 |
+
self.click_element(self.SYSTEM_MONITORING)
|
| 53 |
+
self.page.wait_for_load_state("networkidle")
|
| 54 |
+
|
| 55 |
+
def save_configuration(self):
|
| 56 |
+
"""Save configuration changes"""
|
| 57 |
+
self.click_element(self.SAVE_BUTTON)
|
| 58 |
+
self.expect_element_visible(self.SUCCESS_MESSAGE)
|
| 59 |
+
|
| 60 |
+
def expect_admin_access(self):
|
| 61 |
+
"""Expect admin dashboard to be visible"""
|
| 62 |
+
self.expect_element_visible(self.ADMIN_DASHBOARD)
|
| 63 |
+
|
| 64 |
+
def expect_login_required(self):
|
| 65 |
+
"""Expect login form to be visible"""
|
| 66 |
+
self.expect_element_visible(self.LOGIN_FORM)
|
| 67 |
+
|
| 68 |
+
def expect_success_message(self):
|
| 69 |
+
"""Expect success message to be visible"""
|
| 70 |
+
self.expect_element_visible(self.SUCCESS_MESSAGE)
|
| 71 |
+
|
| 72 |
+
def expect_error_message(self):
|
| 73 |
+
"""Expect error message to be visible"""
|
| 74 |
+
self.expect_element_visible(self.ERROR_MESSAGE)
|
e2e/pages/base_page.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from playwright.sync_api import Page, expect
|
| 2 |
+
import time
|
| 3 |
+
|
| 4 |
+
class BasePage:
|
| 5 |
+
"""Base page object with common functionality"""
|
| 6 |
+
|
| 7 |
+
def __init__(self, page: Page):
|
| 8 |
+
self.page = page
|
| 9 |
+
self.base_url = "http://localhost:3000"
|
| 10 |
+
|
| 11 |
+
def navigate_to(self, path: str = ""):
|
| 12 |
+
"""Navigate to the page"""
|
| 13 |
+
self.page.goto(f"{self.base_url}{path}")
|
| 14 |
+
self.page.wait_for_load_state("networkidle")
|
| 15 |
+
|
| 16 |
+
def wait_for_element(self, selector: str, timeout: int = 10000):
|
| 17 |
+
"""Wait for element to be visible"""
|
| 18 |
+
self.page.wait_for_selector(selector, timeout=timeout)
|
| 19 |
+
|
| 20 |
+
def click_element(self, selector: str):
|
| 21 |
+
"""Click element with auto-wait"""
|
| 22 |
+
self.page.click(selector)
|
| 23 |
+
|
| 24 |
+
def fill_input(self, selector: str, value: str):
|
| 25 |
+
"""Fill input field with auto-wait"""
|
| 26 |
+
self.page.fill(selector, value)
|
| 27 |
+
|
| 28 |
+
def expect_element_visible(self, selector: str):
|
| 29 |
+
"""Expect element to be visible"""
|
| 30 |
+
expect(self.page.locator(selector)).toBeVisible()
|
| 31 |
+
|
| 32 |
+
def expect_element_not_visible(self, selector: str):
|
| 33 |
+
"""Expect element to not be visible"""
|
| 34 |
+
expect(self.page.locator(selector)).not_to_be_visible()
|
| 35 |
+
|
| 36 |
+
def expect_url_contains(self, url_part: str):
|
| 37 |
+
"""Expect URL to contain specific part"""
|
| 38 |
+
expect(self.page).to_have_url(f".*{url_part}.*")
|
| 39 |
+
|
| 40 |
+
def get_text(self, selector: str) -> str:
|
| 41 |
+
"""Get text content of element"""
|
| 42 |
+
return self.page.locator(selector).text_content()
|
| 43 |
+
|
| 44 |
+
def upload_file(self, file_input_selector: str, file_path: str):
|
| 45 |
+
"""Upload file using file input"""
|
| 46 |
+
self.page.set_input_files(file_input_selector, file_path)
|
e2e/pages/explore_page.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .base_page import BasePage
|
| 2 |
+
from playwright.sync_api import expect
|
| 3 |
+
|
| 4 |
+
class ExplorePage(BasePage):
|
| 5 |
+
"""Page object for the explore page"""
|
| 6 |
+
|
| 7 |
+
# Selectors using data-testid for stability
|
| 8 |
+
SEARCH_INPUT = "[data-testid='search-input']"
|
| 9 |
+
FILTER_SOURCE = "[data-testid='filter-source']"
|
| 10 |
+
FILTER_CATEGORY = "[data-testid='filter-category']"
|
| 11 |
+
FILTER_REGION = "[data-testid='filter-region']"
|
| 12 |
+
FILTER_COUNTRY = "[data-testid='filter-country']"
|
| 13 |
+
FILTER_IMAGE_TYPE = "[data-testid='filter-image-type']"
|
| 14 |
+
CLEAR_FILTERS_BUTTON = "[data-testid='clear-filters-button']"
|
| 15 |
+
IMAGE_GRID = "[data-testid='image-grid']"
|
| 16 |
+
IMAGE_CARD = "[data-testid='image-card']"
|
| 17 |
+
EXPORT_BUTTON = "[data-testid='export-button']"
|
| 18 |
+
LOADING_SPINNER = "[data-testid='loading-spinner']"
|
| 19 |
+
|
| 20 |
+
def __init__(self, page):
|
| 21 |
+
super().__init__(page)
|
| 22 |
+
self.page_url = "/explore"
|
| 23 |
+
|
| 24 |
+
def navigate(self):
|
| 25 |
+
"""Navigate to explore page"""
|
| 26 |
+
self.navigate_to(self.page_url)
|
| 27 |
+
self.expect_element_visible(self.IMAGE_GRID)
|
| 28 |
+
|
| 29 |
+
def search_images(self, search_term: str):
|
| 30 |
+
"""Search for images"""
|
| 31 |
+
self.fill_input(self.SEARCH_INPUT, search_term)
|
| 32 |
+
self.page.keyboard.press("Enter")
|
| 33 |
+
self.page.wait_for_load_state("networkidle")
|
| 34 |
+
|
| 35 |
+
def filter_by_source(self, source: str):
|
| 36 |
+
"""Filter by source"""
|
| 37 |
+
self.click_element(self.FILTER_SOURCE)
|
| 38 |
+
self.page.click(f"text={source}")
|
| 39 |
+
self.page.wait_for_load_state("networkidle")
|
| 40 |
+
|
| 41 |
+
def filter_by_category(self, category: str):
|
| 42 |
+
"""Filter by category"""
|
| 43 |
+
self.click_element(self.FILTER_CATEGORY)
|
| 44 |
+
self.page.click(f"text={category}")
|
| 45 |
+
self.page.wait_for_load_state("networkidle")
|
| 46 |
+
|
| 47 |
+
def filter_by_region(self, region: str):
|
| 48 |
+
"""Filter by region"""
|
| 49 |
+
self.click_element(self.FILTER_REGION)
|
| 50 |
+
self.page.click(f"text={region}")
|
| 51 |
+
self.page.wait_for_load_state("networkidle")
|
| 52 |
+
|
| 53 |
+
def clear_filters(self):
|
| 54 |
+
"""Clear all filters"""
|
| 55 |
+
self.click_element(self.CLEAR_FILTERS_BUTTON)
|
| 56 |
+
self.page.wait_for_load_state("networkidle")
|
| 57 |
+
|
| 58 |
+
def get_image_count(self) -> int:
|
| 59 |
+
"""Get the number of images displayed"""
|
| 60 |
+
return len(self.page.locator(self.IMAGE_CARD).all())
|
| 61 |
+
|
| 62 |
+
def click_image(self, index: int = 0):
|
| 63 |
+
"""Click on an image to view details"""
|
| 64 |
+
images = self.page.locator(self.IMAGE_CARD).all()
|
| 65 |
+
if len(images) > index:
|
| 66 |
+
images[index].click()
|
| 67 |
+
self.page.wait_for_load_state("networkidle")
|
| 68 |
+
|
| 69 |
+
def click_export(self):
|
| 70 |
+
"""Click the export button"""
|
| 71 |
+
self.click_element(self.EXPORT_BUTTON)
|
| 72 |
+
|
| 73 |
+
def expect_images_loaded(self):
|
| 74 |
+
"""Expect images to be loaded"""
|
| 75 |
+
self.expect_element_not_visible(self.LOADING_SPINNER)
|
| 76 |
+
self.expect_element_visible(self.IMAGE_GRID)
|
| 77 |
+
|
| 78 |
+
def expect_no_images_found(self):
|
| 79 |
+
"""Expect no images message"""
|
| 80 |
+
self.page.locator("text=No images found").toBeVisible()
|
e2e/pages/upload_page.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .base_page import BasePage
|
| 2 |
+
from playwright.sync_api import expect
|
| 3 |
+
|
| 4 |
+
class UploadPage(BasePage):
|
| 5 |
+
"""Page object for the upload page"""
|
| 6 |
+
|
| 7 |
+
# Selectors using data-testid for stability
|
| 8 |
+
DROP_ZONE = "[data-testid='drop-zone']"
|
| 9 |
+
FILE_INPUT = "[data-testid='file-input']"
|
| 10 |
+
UPLOAD_BUTTON = "[data-testid='upload-button']"
|
| 11 |
+
GENERATE_BUTTON = "[data-testid='generate-button']"
|
| 12 |
+
FILE_PREVIEW = "[data-testid='file-preview']"
|
| 13 |
+
LOADING_SPINNER = "[data-testid='loading-spinner']"
|
| 14 |
+
SUCCESS_MESSAGE = "[data-testid='success-message']"
|
| 15 |
+
ERROR_MESSAGE = "[data-testid='error-message']"
|
| 16 |
+
|
| 17 |
+
def __init__(self, page):
|
| 18 |
+
super().__init__(page)
|
| 19 |
+
self.page_url = "/"
|
| 20 |
+
|
| 21 |
+
def navigate(self):
|
| 22 |
+
"""Navigate to upload page"""
|
| 23 |
+
self.navigate_to(self.page_url)
|
| 24 |
+
self.expect_element_visible(self.DROP_ZONE)
|
| 25 |
+
|
| 26 |
+
def upload_file(self, file_path: str):
|
| 27 |
+
"""Upload a file using drag and drop or file input"""
|
| 28 |
+
# Try drag and drop first, fallback to file input
|
| 29 |
+
try:
|
| 30 |
+
self.page.drag_and_drop(f"input[type='file']", self.DROP_ZONE)
|
| 31 |
+
self.page.set_input_files(self.FILE_INPUT, file_path)
|
| 32 |
+
except:
|
| 33 |
+
# Fallback to direct file input
|
| 34 |
+
self.page.set_input_files(self.FILE_INPUT, file_path)
|
| 35 |
+
|
| 36 |
+
# Wait for file preview
|
| 37 |
+
self.expect_element_visible(self.FILE_PREVIEW)
|
| 38 |
+
|
| 39 |
+
def click_generate(self):
|
| 40 |
+
"""Click the generate button to start analysis"""
|
| 41 |
+
self.click_element(self.GENERATE_BUTTON)
|
| 42 |
+
self.expect_element_visible(self.LOADING_SPINNER)
|
| 43 |
+
|
| 44 |
+
def wait_for_generation_complete(self):
|
| 45 |
+
"""Wait for generation to complete"""
|
| 46 |
+
self.expect_element_not_visible(self.LOADING_SPINNER)
|
| 47 |
+
self.expect_element_visible(self.SUCCESS_MESSAGE)
|
| 48 |
+
|
| 49 |
+
def expect_success_message(self):
|
| 50 |
+
"""Expect success message to be visible"""
|
| 51 |
+
self.expect_element_visible(self.SUCCESS_MESSAGE)
|
| 52 |
+
|
| 53 |
+
def expect_error_message(self):
|
| 54 |
+
"""Expect error message to be visible"""
|
| 55 |
+
self.expect_element_visible(self.ERROR_MESSAGE)
|
| 56 |
+
|
| 57 |
+
def get_uploaded_file_name(self) -> str:
|
| 58 |
+
"""Get the name of the uploaded file"""
|
| 59 |
+
return self.get_text(self.FILE_PREVIEW)
|
e2e/pytest.ini
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[tool:pytest]
|
| 2 |
+
testpaths = specs
|
| 3 |
+
python_files = *_spec.py
|
| 4 |
+
python_classes = Test*
|
| 5 |
+
python_functions = test_*
|
| 6 |
+
addopts =
|
| 7 |
+
--strict-markers
|
| 8 |
+
--strict-config
|
| 9 |
+
--verbose
|
| 10 |
+
--tb=short
|
| 11 |
+
--capture=no
|
| 12 |
+
markers =
|
| 13 |
+
e2e: marks tests as end-to-end tests
|
| 14 |
+
slow: marks tests as slow running
|
| 15 |
+
upload: marks tests related to upload functionality
|
| 16 |
+
admin: marks tests related to admin functionality
|
| 17 |
+
export: marks tests related to export functionality
|
e2e/requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
playwright==1.40.0
|
| 2 |
+
pytest==7.4.3
|
| 3 |
+
pytest-asyncio==0.21.1
|
| 4 |
+
pytest-playwright==0.4.2
|
| 5 |
+
requests==2.31.0
|
| 6 |
+
python-dotenv==1.0.0
|
e2e/run_e2e_tests.sh
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# E2E Test Runner Script
|
| 4 |
+
set -e
|
| 5 |
+
|
| 6 |
+
echo "🚀 Starting E2E Test Suite for PromptAid Vision"
|
| 7 |
+
|
| 8 |
+
# Colors for output
|
| 9 |
+
RED='\033[0;31m'
|
| 10 |
+
GREEN='\033[0;32m'
|
| 11 |
+
YELLOW='\033[1;33m'
|
| 12 |
+
NC='\033[0m' # No Color
|
| 13 |
+
|
| 14 |
+
# Function to print colored output
|
| 15 |
+
print_status() {
|
| 16 |
+
echo -e "${GREEN}[INFO]${NC} $1"
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
print_warning() {
|
| 20 |
+
echo -e "${YELLOW}[WARNING]${NC} $1"
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
print_error() {
|
| 24 |
+
echo -e "${RED}[ERROR]${NC} $1"
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
# Check if Docker is running
|
| 28 |
+
if ! docker info > /dev/null 2>&1; then
|
| 29 |
+
print_error "Docker is not running. Please start Docker and try again."
|
| 30 |
+
exit 1
|
| 31 |
+
fi
|
| 32 |
+
|
| 33 |
+
# Check if docker-compose is available
|
| 34 |
+
if ! command -v docker-compose &> /dev/null; then
|
| 35 |
+
print_error "docker-compose is not installed. Please install it and try again."
|
| 36 |
+
exit 1
|
| 37 |
+
fi
|
| 38 |
+
|
| 39 |
+
# Create test results directory
|
| 40 |
+
mkdir -p test-results/videos
|
| 41 |
+
mkdir -p test-results/screenshots
|
| 42 |
+
mkdir -p test-results/har
|
| 43 |
+
|
| 44 |
+
print_status "Setting up E2E test environment..."
|
| 45 |
+
|
| 46 |
+
# Start the E2E environment
|
| 47 |
+
print_status "Starting services with docker-compose..."
|
| 48 |
+
docker-compose -f docker-compose.e2e.yml up -d --build
|
| 49 |
+
|
| 50 |
+
# Wait for services to be ready
|
| 51 |
+
print_status "Waiting for services to be ready..."
|
| 52 |
+
sleep 30
|
| 53 |
+
|
| 54 |
+
# Check if services are healthy
|
| 55 |
+
print_status "Checking service health..."
|
| 56 |
+
|
| 57 |
+
# Check backend health
|
| 58 |
+
for i in {1..30}; do
|
| 59 |
+
if curl -f http://localhost:7860/health > /dev/null 2>&1; then
|
| 60 |
+
print_status "Backend is healthy"
|
| 61 |
+
break
|
| 62 |
+
fi
|
| 63 |
+
if [ $i -eq 30 ]; then
|
| 64 |
+
print_error "Backend health check failed"
|
| 65 |
+
exit 1
|
| 66 |
+
fi
|
| 67 |
+
sleep 2
|
| 68 |
+
done
|
| 69 |
+
|
| 70 |
+
# Check frontend health
|
| 71 |
+
for i in {1..30}; do
|
| 72 |
+
if curl -f http://localhost:3000 > /dev/null 2>&1; then
|
| 73 |
+
print_status "Frontend is healthy"
|
| 74 |
+
break
|
| 75 |
+
fi
|
| 76 |
+
if [ $i -eq 30 ]; then
|
| 77 |
+
print_error "Frontend health check failed"
|
| 78 |
+
exit 1
|
| 79 |
+
fi
|
| 80 |
+
sleep 2
|
| 81 |
+
done
|
| 82 |
+
|
| 83 |
+
print_status "All services are ready!"
|
| 84 |
+
|
| 85 |
+
# Install Python dependencies
|
| 86 |
+
print_status "Installing Python dependencies..."
|
| 87 |
+
pip install -r requirements.txt
|
| 88 |
+
|
| 89 |
+
# Install Playwright browsers
|
| 90 |
+
print_status "Installing Playwright browsers..."
|
| 91 |
+
playwright install
|
| 92 |
+
|
| 93 |
+
# Run the tests
|
| 94 |
+
print_status "Running E2E tests..."
|
| 95 |
+
pytest -m e2e -v --tb=short
|
| 96 |
+
|
| 97 |
+
# Capture test results
|
| 98 |
+
TEST_EXIT_CODE=$?
|
| 99 |
+
|
| 100 |
+
# Generate test report
|
| 101 |
+
print_status "Generating test report..."
|
| 102 |
+
if [ -d "test-results" ]; then
|
| 103 |
+
print_status "Test results available in test-results/ directory"
|
| 104 |
+
fi
|
| 105 |
+
|
| 106 |
+
# Cleanup
|
| 107 |
+
print_status "Cleaning up..."
|
| 108 |
+
docker-compose -f docker-compose.e2e.yml down -v
|
| 109 |
+
|
| 110 |
+
# Exit with test result
|
| 111 |
+
if [ $TEST_EXIT_CODE -eq 0 ]; then
|
| 112 |
+
print_status "✅ E2E tests completed successfully!"
|
| 113 |
+
else
|
| 114 |
+
print_error "❌ E2E tests failed!"
|
| 115 |
+
fi
|
| 116 |
+
|
| 117 |
+
exit $TEST_EXIT_CODE
|
e2e/specs/admin_settings_spec.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
import requests
|
| 3 |
+
from playwright.sync_api import Page, expect
|
| 4 |
+
from pages.admin_page import AdminPage
|
| 5 |
+
|
| 6 |
+
class TestAdminSettings:
|
| 7 |
+
"""E2E tests for admin configuration save/health"""
|
| 8 |
+
|
| 9 |
+
@pytest.fixture(autouse=True)
|
| 10 |
+
def setup(self, page: Page):
|
| 11 |
+
"""Setup for each test"""
|
| 12 |
+
self.admin_page = AdminPage(page)
|
| 13 |
+
self.admin_password = "admin_e2e_password"
|
| 14 |
+
|
| 15 |
+
@pytest.mark.e2e
|
| 16 |
+
@pytest.mark.admin
|
| 17 |
+
def test_admin_login_and_authentication(self, page: Page):
|
| 18 |
+
"""Test admin login and authentication flow"""
|
| 19 |
+
# Step 1: Navigate to admin page
|
| 20 |
+
self.admin_page.navigate()
|
| 21 |
+
|
| 22 |
+
# Step 2: Verify login form is shown
|
| 23 |
+
self.admin_page.expect_login_required()
|
| 24 |
+
|
| 25 |
+
# Step 3: Login with correct password
|
| 26 |
+
self.admin_page.login(self.admin_password)
|
| 27 |
+
|
| 28 |
+
# Step 4: Verify admin dashboard is accessible
|
| 29 |
+
self.admin_page.expect_admin_access()
|
| 30 |
+
|
| 31 |
+
# Step 5: Logout
|
| 32 |
+
self.admin_page.logout()
|
| 33 |
+
|
| 34 |
+
# Step 6: Verify back to login form
|
| 35 |
+
self.admin_page.expect_login_required()
|
| 36 |
+
|
| 37 |
+
@pytest.mark.e2e
|
| 38 |
+
@pytest.mark.admin
|
| 39 |
+
def test_admin_login_invalid_password(self, page: Page):
|
| 40 |
+
"""Test admin login with invalid password"""
|
| 41 |
+
# Step 1: Navigate to admin page
|
| 42 |
+
self.admin_page.navigate()
|
| 43 |
+
|
| 44 |
+
# Step 2: Try to login with wrong password
|
| 45 |
+
self.admin_page.login("wrong_password")
|
| 46 |
+
|
| 47 |
+
# Step 3: Verify error message is shown
|
| 48 |
+
self.admin_page.expect_error_message()
|
| 49 |
+
|
| 50 |
+
# Step 4: Verify still on login form
|
| 51 |
+
self.admin_page.expect_login_required()
|
| 52 |
+
|
| 53 |
+
@pytest.mark.e2e
|
| 54 |
+
@pytest.mark.admin
|
| 55 |
+
def test_schema_management_flow(self, page: Page):
|
| 56 |
+
"""Test schema management functionality"""
|
| 57 |
+
# Step 1: Login to admin
|
| 58 |
+
self.admin_page.navigate()
|
| 59 |
+
self.admin_page.login(self.admin_password)
|
| 60 |
+
|
| 61 |
+
# Step 2: Navigate to schema management
|
| 62 |
+
self.admin_page.navigate_to_schema_management()
|
| 63 |
+
|
| 64 |
+
# Step 3: Verify schema management interface is loaded
|
| 65 |
+
self.admin_page.expect_element_visible("[data-testid='schema-list']")
|
| 66 |
+
|
| 67 |
+
# Step 4: Test schema operations (if available)
|
| 68 |
+
# This would depend on the actual schema management interface
|
| 69 |
+
pass
|
| 70 |
+
|
| 71 |
+
@pytest.mark.e2e
|
| 72 |
+
@pytest.mark.admin
|
| 73 |
+
def test_model_configuration_flow(self, page: Page):
|
| 74 |
+
"""Test model configuration functionality"""
|
| 75 |
+
# Step 1: Login to admin
|
| 76 |
+
self.admin_page.navigate()
|
| 77 |
+
self.admin_page.login(self.admin_password)
|
| 78 |
+
|
| 79 |
+
# Step 2: Navigate to model configuration
|
| 80 |
+
self.admin_page.navigate_to_model_config()
|
| 81 |
+
|
| 82 |
+
# Step 3: Verify model configuration interface is loaded
|
| 83 |
+
self.admin_page.expect_element_visible("[data-testid='model-config-form']")
|
| 84 |
+
|
| 85 |
+
# Step 4: Test model configuration operations
|
| 86 |
+
# This would depend on the actual model configuration interface
|
| 87 |
+
pass
|
| 88 |
+
|
| 89 |
+
@pytest.mark.e2e
|
| 90 |
+
@pytest.mark.admin
|
| 91 |
+
def test_system_monitoring_flow(self, page: Page):
|
| 92 |
+
"""Test system monitoring functionality"""
|
| 93 |
+
# Step 1: Login to admin
|
| 94 |
+
self.admin_page.navigate()
|
| 95 |
+
self.admin_page.login(self.admin_password)
|
| 96 |
+
|
| 97 |
+
# Step 2: Navigate to system monitoring
|
| 98 |
+
self.admin_page.navigate_to_system_monitoring()
|
| 99 |
+
|
| 100 |
+
# Step 3: Verify system monitoring interface is loaded
|
| 101 |
+
self.admin_page.expect_element_visible("[data-testid='system-stats']")
|
| 102 |
+
|
| 103 |
+
# Step 4: Test monitoring data display
|
| 104 |
+
# This would depend on the actual monitoring interface
|
| 105 |
+
pass
|
| 106 |
+
|
| 107 |
+
@pytest.mark.e2e
|
| 108 |
+
@pytest.mark.admin
|
| 109 |
+
def test_backend_health_endpoint(self):
|
| 110 |
+
"""Test backend health endpoint"""
|
| 111 |
+
# Step 1: Check backend health endpoint
|
| 112 |
+
response = requests.get("http://localhost:7860/health")
|
| 113 |
+
|
| 114 |
+
# Step 2: Verify health endpoint responds
|
| 115 |
+
assert response.status_code == 200
|
| 116 |
+
|
| 117 |
+
# Step 3: Verify health data
|
| 118 |
+
health_data = response.json()
|
| 119 |
+
assert "status" in health_data
|
| 120 |
+
assert health_data["status"] == "healthy"
|
| 121 |
+
|
| 122 |
+
@pytest.mark.e2e
|
| 123 |
+
@pytest.mark.admin
|
| 124 |
+
def test_frontend_health_endpoint(self):
|
| 125 |
+
"""Test frontend health endpoint (if available)"""
|
| 126 |
+
try:
|
| 127 |
+
# Step 1: Check frontend health endpoint
|
| 128 |
+
response = requests.get("http://localhost:3000/healthz")
|
| 129 |
+
|
| 130 |
+
# Step 2: Verify health endpoint responds
|
| 131 |
+
assert response.status_code == 200
|
| 132 |
+
|
| 133 |
+
except requests.exceptions.RequestException:
|
| 134 |
+
# Frontend health endpoint might not be implemented
|
| 135 |
+
# This is acceptable for now
|
| 136 |
+
pass
|
e2e/specs/export_spec.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
import os
|
| 3 |
+
import time
|
| 4 |
+
from playwright.sync_api import Page, expect
|
| 5 |
+
from pages.explore_page import ExplorePage
|
| 6 |
+
|
| 7 |
+
class TestExportFunctionality:
|
| 8 |
+
"""E2E tests for export functionality - export produces file"""
|
| 9 |
+
|
| 10 |
+
@pytest.fixture(autouse=True)
|
| 11 |
+
def setup(self, page: Page):
|
| 12 |
+
"""Setup for each test"""
|
| 13 |
+
self.explore_page = ExplorePage(page)
|
| 14 |
+
self.download_path = os.path.join(os.path.dirname(__file__), "../downloads")
|
| 15 |
+
|
| 16 |
+
# Create downloads directory if it doesn't exist
|
| 17 |
+
os.makedirs(self.download_path, exist_ok=True)
|
| 18 |
+
|
| 19 |
+
@pytest.mark.e2e
|
| 20 |
+
@pytest.mark.export
|
| 21 |
+
def test_filtered_data_export(self, page: Page):
|
| 22 |
+
"""Test export of filtered data"""
|
| 23 |
+
# Step 1: Navigate to explore page
|
| 24 |
+
self.explore_page.navigate()
|
| 25 |
+
|
| 26 |
+
# Step 2: Wait for images to load
|
| 27 |
+
self.explore_page.expect_images_loaded()
|
| 28 |
+
|
| 29 |
+
# Step 3: Apply a filter
|
| 30 |
+
self.explore_page.filter_by_source("WFP")
|
| 31 |
+
|
| 32 |
+
# Step 4: Verify filtered results
|
| 33 |
+
image_count = self.explore_page.get_image_count()
|
| 34 |
+
assert image_count >= 0 # Could be 0 if no WFP images
|
| 35 |
+
|
| 36 |
+
# Step 5: Click export button
|
| 37 |
+
self.explore_page.click_export()
|
| 38 |
+
|
| 39 |
+
# Step 6: Wait for export to complete
|
| 40 |
+
# This would depend on the actual export implementation
|
| 41 |
+
page.wait_for_timeout(5000) # Wait 5 seconds for export
|
| 42 |
+
|
| 43 |
+
# Step 7: Verify export modal or download
|
| 44 |
+
# This would depend on the actual export UI
|
| 45 |
+
pass
|
| 46 |
+
|
| 47 |
+
@pytest.mark.e2e
|
| 48 |
+
@pytest.mark.export
|
| 49 |
+
def test_bulk_export_workflow(self, page: Page):
|
| 50 |
+
"""Test bulk export workflow"""
|
| 51 |
+
# Step 1: Navigate to explore page
|
| 52 |
+
self.explore_page.navigate()
|
| 53 |
+
|
| 54 |
+
# Step 2: Wait for images to load
|
| 55 |
+
self.explore_page.expect_images_loaded()
|
| 56 |
+
|
| 57 |
+
# Step 3: Select multiple images (if selection is available)
|
| 58 |
+
# This would depend on the actual UI implementation
|
| 59 |
+
pass
|
| 60 |
+
|
| 61 |
+
# Step 4: Click bulk export
|
| 62 |
+
# This would depend on the actual UI implementation
|
| 63 |
+
pass
|
| 64 |
+
|
| 65 |
+
# Step 5: Verify bulk export completion
|
| 66 |
+
# This would depend on the actual export implementation
|
| 67 |
+
pass
|
| 68 |
+
|
| 69 |
+
@pytest.mark.e2e
|
| 70 |
+
@pytest.mark.export
|
| 71 |
+
def test_export_format_validation(self, page: Page):
|
| 72 |
+
"""Test export format validation"""
|
| 73 |
+
# Step 1: Navigate to explore page
|
| 74 |
+
self.explore_page.navigate()
|
| 75 |
+
|
| 76 |
+
# Step 2: Wait for images to load
|
| 77 |
+
self.explore_page.expect_images_loaded()
|
| 78 |
+
|
| 79 |
+
# Step 3: Test different export formats
|
| 80 |
+
# This would depend on the actual export UI implementation
|
| 81 |
+
pass
|
| 82 |
+
|
| 83 |
+
@pytest.mark.e2e
|
| 84 |
+
@pytest.mark.export
|
| 85 |
+
def test_export_with_no_data(self, page: Page):
|
| 86 |
+
"""Test export when no data is available"""
|
| 87 |
+
# Step 1: Navigate to explore page
|
| 88 |
+
self.explore_page.navigate()
|
| 89 |
+
|
| 90 |
+
# Step 2: Apply filter that returns no results
|
| 91 |
+
self.explore_page.search_images("nonexistent_search_term")
|
| 92 |
+
|
| 93 |
+
# Step 3: Verify no images found
|
| 94 |
+
self.explore_page.expect_no_images_found()
|
| 95 |
+
|
| 96 |
+
# Step 4: Try to export (should show appropriate message)
|
| 97 |
+
# This would depend on the actual export UI implementation
|
| 98 |
+
pass
|
| 99 |
+
|
| 100 |
+
@pytest.mark.e2e
|
| 101 |
+
@pytest.mark.export
|
| 102 |
+
def test_export_performance(self, page: Page):
|
| 103 |
+
"""Test export performance with large datasets"""
|
| 104 |
+
# Step 1: Navigate to explore page
|
| 105 |
+
self.explore_page.navigate()
|
| 106 |
+
|
| 107 |
+
# Step 2: Wait for images to load
|
| 108 |
+
self.explore_page.expect_images_loaded()
|
| 109 |
+
|
| 110 |
+
# Step 3: Record start time
|
| 111 |
+
start_time = time.time()
|
| 112 |
+
|
| 113 |
+
# Step 4: Click export
|
| 114 |
+
self.explore_page.click_export()
|
| 115 |
+
|
| 116 |
+
# Step 5: Wait for export to complete
|
| 117 |
+
page.wait_for_timeout(10000) # Wait up to 10 seconds
|
| 118 |
+
|
| 119 |
+
# Step 6: Record end time
|
| 120 |
+
end_time = time.time()
|
| 121 |
+
|
| 122 |
+
# Step 7: Verify export completed within reasonable time
|
| 123 |
+
export_duration = end_time - start_time
|
| 124 |
+
assert export_duration < 30 # Should complete within 30 seconds
|
| 125 |
+
|
| 126 |
+
# Step 8: Verify export was successful
|
| 127 |
+
# This would depend on the actual export implementation
|
| 128 |
+
pass
|
e2e/specs/upload_flow_spec.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
import os
|
| 3 |
+
from playwright.sync_api import Page, expect
|
| 4 |
+
from pages.upload_page import UploadPage
|
| 5 |
+
from pages.explore_page import ExplorePage
|
| 6 |
+
|
| 7 |
+
class TestUploadFlow:
|
| 8 |
+
"""E2E tests for the upload flow - user-facing happy path"""
|
| 9 |
+
|
| 10 |
+
@pytest.fixture(autouse=True)
|
| 11 |
+
def setup(self, page: Page):
|
| 12 |
+
"""Setup for each test"""
|
| 13 |
+
self.upload_page = UploadPage(page)
|
| 14 |
+
self.explore_page = ExplorePage(page)
|
| 15 |
+
self.test_image_path = os.path.join(os.path.dirname(__file__), "../fixtures/test_image.jpg")
|
| 16 |
+
|
| 17 |
+
@pytest.mark.e2e
|
| 18 |
+
@pytest.mark.upload
|
| 19 |
+
def test_complete_upload_flow(self, page: Page):
|
| 20 |
+
"""Test complete upload workflow from file selection to analysis completion"""
|
| 21 |
+
# Step 1: Navigate to upload page
|
| 22 |
+
self.upload_page.navigate()
|
| 23 |
+
|
| 24 |
+
# Step 2: Upload a file
|
| 25 |
+
self.upload_page.upload_file(self.test_image_path)
|
| 26 |
+
|
| 27 |
+
# Step 3: Verify file preview is shown
|
| 28 |
+
file_name = self.upload_page.get_uploaded_file_name()
|
| 29 |
+
assert "test_image" in file_name.lower()
|
| 30 |
+
|
| 31 |
+
# Step 4: Click generate button
|
| 32 |
+
self.upload_page.click_generate()
|
| 33 |
+
|
| 34 |
+
# Step 5: Wait for generation to complete
|
| 35 |
+
self.upload_page.wait_for_generation_complete()
|
| 36 |
+
|
| 37 |
+
# Step 6: Verify success message
|
| 38 |
+
self.upload_page.expect_success_message()
|
| 39 |
+
|
| 40 |
+
# Step 7: Navigate to explore page to verify image appears
|
| 41 |
+
self.explore_page.navigate()
|
| 42 |
+
self.explore_page.expect_images_loaded()
|
| 43 |
+
|
| 44 |
+
# Step 8: Verify uploaded image is in the list
|
| 45 |
+
image_count = self.explore_page.get_image_count()
|
| 46 |
+
assert image_count > 0
|
| 47 |
+
|
| 48 |
+
@pytest.mark.e2e
|
| 49 |
+
@pytest.mark.upload
|
| 50 |
+
def test_upload_invalid_file(self, page: Page):
|
| 51 |
+
"""Test upload with invalid file type"""
|
| 52 |
+
# Step 1: Navigate to upload page
|
| 53 |
+
self.upload_page.navigate()
|
| 54 |
+
|
| 55 |
+
# Step 2: Try to upload an invalid file (text file)
|
| 56 |
+
invalid_file_path = os.path.join(os.path.dirname(__file__), "../fixtures/invalid.txt")
|
| 57 |
+
with open(invalid_file_path, "w") as f:
|
| 58 |
+
f.write("This is not an image file")
|
| 59 |
+
|
| 60 |
+
self.upload_page.upload_file(invalid_file_path)
|
| 61 |
+
|
| 62 |
+
# Step 3: Verify error message is shown
|
| 63 |
+
self.upload_page.expect_error_message()
|
| 64 |
+
|
| 65 |
+
# Cleanup
|
| 66 |
+
os.remove(invalid_file_path)
|
| 67 |
+
|
| 68 |
+
@pytest.mark.e2e
|
| 69 |
+
@pytest.mark.upload
|
| 70 |
+
def test_upload_large_file(self, page: Page):
|
| 71 |
+
"""Test upload with large file handling"""
|
| 72 |
+
# Step 1: Navigate to upload page
|
| 73 |
+
self.upload_page.navigate()
|
| 74 |
+
|
| 75 |
+
# Step 2: Create a large file (simulate large image)
|
| 76 |
+
large_file_path = os.path.join(os.path.dirname(__file__), "../fixtures/large_image.jpg")
|
| 77 |
+
with open(large_file_path, "wb") as f:
|
| 78 |
+
f.write(b"0" * 10 * 1024 * 1024) # 10MB file
|
| 79 |
+
|
| 80 |
+
# Step 3: Upload the large file
|
| 81 |
+
self.upload_page.upload_file(large_file_path)
|
| 82 |
+
|
| 83 |
+
# Step 4: Verify file is accepted or appropriate error shown
|
| 84 |
+
try:
|
| 85 |
+
self.upload_page.expect_element_visible(self.upload_page.FILE_PREVIEW)
|
| 86 |
+
except:
|
| 87 |
+
self.upload_page.expect_error_message()
|
| 88 |
+
|
| 89 |
+
# Cleanup
|
| 90 |
+
os.remove(large_file_path)
|
frontend/TESTING.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Testing Guide
|
| 2 |
+
|
| 3 |
+
This project uses Vitest for unit testing with React Testing Library for component testing.
|
| 4 |
+
|
| 5 |
+
## Available Scripts
|
| 6 |
+
|
| 7 |
+
- `npm test` - Run tests in watch mode
|
| 8 |
+
- `npm run test:run` - Run tests once
|
| 9 |
+
- `npm run test:ui` - Run tests with UI (requires @vitest/ui)
|
| 10 |
+
- `npm run test:coverage` - Run tests with coverage report
|
| 11 |
+
|
| 12 |
+
## Test File Structure
|
| 13 |
+
|
| 14 |
+
- Test files should be named `ComponentName.test.tsx` or `ComponentName.spec.tsx`
|
| 15 |
+
- Place test files alongside the components they test
|
| 16 |
+
- Use the `src/test/` directory for test utilities and setup
|
| 17 |
+
|
| 18 |
+
## Writing Tests
|
| 19 |
+
|
| 20 |
+
### Basic Component Test
|
| 21 |
+
```tsx
|
| 22 |
+
import { render, screen } from '../test/test-utils'
|
| 23 |
+
import { describe, it, expect } from 'vitest'
|
| 24 |
+
import MyComponent from './MyComponent'
|
| 25 |
+
|
| 26 |
+
describe('MyComponent', () => {
|
| 27 |
+
it('renders without crashing', () => {
|
| 28 |
+
render(<MyComponent />)
|
| 29 |
+
expect(screen.getByText('Hello')).toBeInTheDocument()
|
| 30 |
+
})
|
| 31 |
+
})
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
### Testing User Interactions
|
| 35 |
+
```tsx
|
| 36 |
+
import { fireEvent } from '../test/test-utils'
|
| 37 |
+
|
| 38 |
+
it('responds to button clicks', () => {
|
| 39 |
+
render(<MyComponent />)
|
| 40 |
+
const button = screen.getByRole('button')
|
| 41 |
+
fireEvent.click(button)
|
| 42 |
+
expect(screen.getByText('Clicked!')).toBeInTheDocument()
|
| 43 |
+
})
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
### Testing with Context
|
| 47 |
+
Use the custom `render` function from `test-utils.tsx` to automatically wrap components with necessary providers.
|
| 48 |
+
|
| 49 |
+
## Mocking
|
| 50 |
+
|
| 51 |
+
### Mocking External Dependencies
|
| 52 |
+
```tsx
|
| 53 |
+
vi.mock('react-router-dom', () => ({
|
| 54 |
+
useNavigate: () => vi.fn(),
|
| 55 |
+
Link: ({ children, to }: { children: React.ReactNode; to: string }) => (
|
| 56 |
+
<a href={to}>{children}</a>
|
| 57 |
+
),
|
| 58 |
+
}))
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
### Mocking API Calls
|
| 62 |
+
```tsx
|
| 63 |
+
vi.mock('../services/api', () => ({
|
| 64 |
+
fetchData: vi.fn(() => Promise.resolve({ data: 'test' }))
|
| 65 |
+
}))
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
## Best Practices
|
| 69 |
+
|
| 70 |
+
1. Test behavior, not implementation
|
| 71 |
+
2. Use semantic queries (getByRole, getByLabelText) over getByTestId
|
| 72 |
+
3. Write tests that resemble how users interact with your app
|
| 73 |
+
4. Keep tests focused and isolated
|
| 74 |
+
5. Use descriptive test names that explain the expected behavior
|
| 75 |
+
|
| 76 |
+
## Running Specific Tests
|
| 77 |
+
|
| 78 |
+
```bash
|
| 79 |
+
# Run tests matching a pattern
|
| 80 |
+
npm test -- --grep "FilterBar"
|
| 81 |
+
|
| 82 |
+
# Run tests in a specific file
|
| 83 |
+
npm test HeaderNav.test.tsx
|
| 84 |
+
|
| 85 |
+
# Run tests with verbose output
|
| 86 |
+
npm test -- --reporter=verbose
|
| 87 |
+
```
|
frontend/package-lock.json
CHANGED
|
@@ -21,6 +21,9 @@
|
|
| 21 |
"devDependencies": {
|
| 22 |
"@eslint/js": "^9.30.1",
|
| 23 |
"@tailwindcss/postcss": "^4.1.11",
|
|
|
|
|
|
|
|
|
|
| 24 |
"@types/node": "^24.1.0",
|
| 25 |
"@types/react": "^19.1.8",
|
| 26 |
"@types/react-dom": "^19.1.6",
|
|
@@ -31,12 +34,21 @@
|
|
| 31 |
"eslint-plugin-react-hooks": "^5.2.0",
|
| 32 |
"eslint-plugin-react-refresh": "^0.4.20",
|
| 33 |
"globals": "^16.3.0",
|
|
|
|
| 34 |
"postcss": "^8.5.6",
|
| 35 |
"tailwindcss": "^4.1.11",
|
| 36 |
"typescript": "~5.8.3",
|
| 37 |
-
"typescript-eslint": "^8.35.1"
|
|
|
|
| 38 |
}
|
| 39 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
"node_modules/@alloc/quick-lru": {
|
| 41 |
"version": "5.2.0",
|
| 42 |
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
|
@@ -64,6 +76,27 @@
|
|
| 64 |
"node": ">=6.0.0"
|
| 65 |
}
|
| 66 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
"node_modules/@babel/code-frame": {
|
| 68 |
"version": "7.27.1",
|
| 69 |
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
|
@@ -387,6 +420,121 @@
|
|
| 387 |
"node": ">=6.9.0"
|
| 388 |
}
|
| 389 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
"node_modules/@esbuild/aix-ppc64": {
|
| 391 |
"version": "0.25.6",
|
| 392 |
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz",
|
|
@@ -1930,6 +2078,96 @@
|
|
| 1930 |
"tailwindcss": "4.1.11"
|
| 1931 |
}
|
| 1932 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1933 |
"node_modules/@togglecorp/fujs": {
|
| 1934 |
"version": "2.2.0",
|
| 1935 |
"resolved": "https://registry.npmjs.org/@togglecorp/fujs/-/fujs-2.2.0.tgz",
|
|
@@ -1939,6 +2177,14 @@
|
|
| 1939 |
"@babel/runtime-corejs3": "^7.22.6"
|
| 1940 |
}
|
| 1941 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1942 |
"node_modules/@types/babel__core": {
|
| 1943 |
"version": "7.20.5",
|
| 1944 |
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
|
@@ -1984,6 +2230,23 @@
|
|
| 1984 |
"@babel/types": "^7.20.7"
|
| 1985 |
}
|
| 1986 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1987 |
"node_modules/@types/estree": {
|
| 1988 |
"version": "1.0.8",
|
| 1989 |
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
|
@@ -2335,6 +2598,121 @@
|
|
| 2335 |
"dev": true,
|
| 2336 |
"license": "MIT"
|
| 2337 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2338 |
"node_modules/acorn": {
|
| 2339 |
"version": "8.15.0",
|
| 2340 |
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
|
@@ -2358,6 +2736,16 @@
|
|
| 2358 |
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
| 2359 |
}
|
| 2360 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2361 |
"node_modules/ajv": {
|
| 2362 |
"version": "6.12.6",
|
| 2363 |
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
|
@@ -2375,6 +2763,17 @@
|
|
| 2375 |
"url": "https://github.com/sponsors/epoberezkin"
|
| 2376 |
}
|
| 2377 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2378 |
"node_modules/ansi-styles": {
|
| 2379 |
"version": "4.3.0",
|
| 2380 |
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
|
@@ -2410,6 +2809,26 @@
|
|
| 2410 |
"node": ">=10"
|
| 2411 |
}
|
| 2412 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2413 |
"node_modules/autoprefixer": {
|
| 2414 |
"version": "10.4.21",
|
| 2415 |
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
|
|
@@ -2512,6 +2931,16 @@
|
|
| 2512 |
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
| 2513 |
}
|
| 2514 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2515 |
"node_modules/callsites": {
|
| 2516 |
"version": "3.1.0",
|
| 2517 |
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
|
@@ -2543,6 +2972,23 @@
|
|
| 2543 |
],
|
| 2544 |
"license": "CC-BY-4.0"
|
| 2545 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2546 |
"node_modules/chalk": {
|
| 2547 |
"version": "4.1.2",
|
| 2548 |
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
|
@@ -2560,6 +3006,16 @@
|
|
| 2560 |
"url": "https://github.com/chalk/chalk?sponsor=1"
|
| 2561 |
}
|
| 2562 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2563 |
"node_modules/chownr": {
|
| 2564 |
"version": "3.0.0",
|
| 2565 |
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
|
@@ -2636,14 +3092,49 @@
|
|
| 2636 |
"node": ">= 8"
|
| 2637 |
}
|
| 2638 |
},
|
| 2639 |
-
"node_modules/
|
| 2640 |
-
"version": "
|
| 2641 |
-
"resolved": "https://registry.npmjs.org/
|
| 2642 |
-
"integrity": "sha512-
|
| 2643 |
-
"
|
| 2644 |
"license": "MIT"
|
| 2645 |
},
|
| 2646 |
-
"node_modules/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2647 |
"version": "4.4.1",
|
| 2648 |
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
| 2649 |
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
|
@@ -2661,6 +3152,23 @@
|
|
| 2661 |
}
|
| 2662 |
}
|
| 2663 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2664 |
"node_modules/deep-is": {
|
| 2665 |
"version": "0.1.4",
|
| 2666 |
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
|
@@ -2677,6 +3185,16 @@
|
|
| 2677 |
"node": ">=0.10.0"
|
| 2678 |
}
|
| 2679 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2680 |
"node_modules/detect-libc": {
|
| 2681 |
"version": "2.0.4",
|
| 2682 |
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
|
@@ -2693,6 +3211,14 @@
|
|
| 2693 |
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
| 2694 |
"license": "MIT"
|
| 2695 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2696 |
"node_modules/dom-serializer": {
|
| 2697 |
"version": "2.0.0",
|
| 2698 |
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
|
@@ -2781,6 +3307,13 @@
|
|
| 2781 |
"url": "https://github.com/fb55/entities?sponsor=1"
|
| 2782 |
}
|
| 2783 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2784 |
"node_modules/esbuild": {
|
| 2785 |
"version": "0.25.6",
|
| 2786 |
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz",
|
|
@@ -3012,6 +3545,16 @@
|
|
| 3012 |
"node": ">=4.0"
|
| 3013 |
}
|
| 3014 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3015 |
"node_modules/esutils": {
|
| 3016 |
"version": "2.0.3",
|
| 3017 |
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
|
@@ -3022,6 +3565,16 @@
|
|
| 3022 |
"node": ">=0.10.0"
|
| 3023 |
}
|
| 3024 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3025 |
"node_modules/fast-deep-equal": {
|
| 3026 |
"version": "3.1.3",
|
| 3027 |
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
|
@@ -3256,6 +3809,19 @@
|
|
| 3256 |
"node": ">=8"
|
| 3257 |
}
|
| 3258 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3259 |
"node_modules/htmlparser2": {
|
| 3260 |
"version": "8.0.2",
|
| 3261 |
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
|
@@ -3275,6 +3841,47 @@
|
|
| 3275 |
"entities": "^4.4.0"
|
| 3276 |
}
|
| 3277 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3278 |
"node_modules/ignore": {
|
| 3279 |
"version": "5.3.2",
|
| 3280 |
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
|
@@ -3318,6 +3925,16 @@
|
|
| 3318 |
"node": ">=0.8.19"
|
| 3319 |
}
|
| 3320 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3321 |
"node_modules/inherits": {
|
| 3322 |
"version": "2.0.4",
|
| 3323 |
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
|
@@ -3366,6 +3983,13 @@
|
|
| 3366 |
"node": ">=0.10.0"
|
| 3367 |
}
|
| 3368 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3369 |
"node_modules/isarray": {
|
| 3370 |
"version": "1.0.0",
|
| 3371 |
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
|
@@ -3408,6 +4032,46 @@
|
|
| 3408 |
"js-yaml": "bin/js-yaml.js"
|
| 3409 |
}
|
| 3410 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3411 |
"node_modules/jsesc": {
|
| 3412 |
"version": "3.1.0",
|
| 3413 |
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
|
@@ -3764,6 +4428,13 @@
|
|
| 3764 |
"loose-envify": "cli.js"
|
| 3765 |
}
|
| 3766 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3767 |
"node_modules/lru-cache": {
|
| 3768 |
"version": "5.1.1",
|
| 3769 |
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
|
@@ -3790,6 +4461,17 @@
|
|
| 3790 |
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
| 3791 |
}
|
| 3792 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3793 |
"node_modules/magic-string": {
|
| 3794 |
"version": "0.30.17",
|
| 3795 |
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
|
@@ -3824,6 +4506,16 @@
|
|
| 3824 |
"node": ">=8.6"
|
| 3825 |
}
|
| 3826 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3827 |
"node_modules/minimatch": {
|
| 3828 |
"version": "3.1.2",
|
| 3829 |
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
|
@@ -3925,6 +4617,13 @@
|
|
| 3925 |
"node": ">=0.10.0"
|
| 3926 |
}
|
| 3927 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3928 |
"node_modules/object-assign": {
|
| 3929 |
"version": "4.1.1",
|
| 3930 |
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
|
@@ -4009,6 +4708,32 @@
|
|
| 4009 |
"integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==",
|
| 4010 |
"license": "MIT"
|
| 4011 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4012 |
"node_modules/path-exists": {
|
| 4013 |
"version": "4.0.0",
|
| 4014 |
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
|
@@ -4029,6 +4754,23 @@
|
|
| 4029 |
"node": ">=8"
|
| 4030 |
}
|
| 4031 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4032 |
"node_modules/picocolors": {
|
| 4033 |
"version": "1.1.1",
|
| 4034 |
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
|
@@ -4093,6 +4835,44 @@
|
|
| 4093 |
"node": ">= 0.8.0"
|
| 4094 |
}
|
| 4095 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4096 |
"node_modules/process-nextick-args": {
|
| 4097 |
"version": "2.0.1",
|
| 4098 |
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
|
@@ -4359,6 +5139,20 @@
|
|
| 4359 |
"util-deprecate": "~1.0.1"
|
| 4360 |
}
|
| 4361 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4362 |
"node_modules/resolve-from": {
|
| 4363 |
"version": "4.0.0",
|
| 4364 |
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
|
@@ -4419,6 +5213,13 @@
|
|
| 4419 |
"fsevents": "~2.3.2"
|
| 4420 |
}
|
| 4421 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4422 |
"node_modules/run-parallel": {
|
| 4423 |
"version": "1.2.0",
|
| 4424 |
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
|
@@ -4449,6 +5250,13 @@
|
|
| 4449 |
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
| 4450 |
"license": "MIT"
|
| 4451 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4452 |
"node_modules/sanitize-html": {
|
| 4453 |
"version": "2.17.0",
|
| 4454 |
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.0.tgz",
|
|
@@ -4463,6 +5271,19 @@
|
|
| 4463 |
"postcss": "^8.3.11"
|
| 4464 |
}
|
| 4465 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4466 |
"node_modules/scheduler": {
|
| 4467 |
"version": "0.23.2",
|
| 4468 |
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
|
@@ -4514,6 +5335,13 @@
|
|
| 4514 |
"node": ">=8"
|
| 4515 |
}
|
| 4516 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4517 |
"node_modules/source-map-js": {
|
| 4518 |
"version": "1.2.1",
|
| 4519 |
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
|
@@ -4523,6 +5351,20 @@
|
|
| 4523 |
"node": ">=0.10.0"
|
| 4524 |
}
|
| 4525 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4526 |
"node_modules/string_decoder": {
|
| 4527 |
"version": "1.1.1",
|
| 4528 |
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
|
@@ -4532,6 +5374,19 @@
|
|
| 4532 |
"safe-buffer": "~5.1.0"
|
| 4533 |
}
|
| 4534 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4535 |
"node_modules/strip-json-comments": {
|
| 4536 |
"version": "3.1.1",
|
| 4537 |
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
|
@@ -4545,6 +5400,26 @@
|
|
| 4545 |
"url": "https://github.com/sponsors/sindresorhus"
|
| 4546 |
}
|
| 4547 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4548 |
"node_modules/supports-color": {
|
| 4549 |
"version": "7.2.0",
|
| 4550 |
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
|
@@ -4558,6 +5433,13 @@
|
|
| 4558 |
"node": ">=8"
|
| 4559 |
}
|
| 4560 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4561 |
"node_modules/tailwindcss": {
|
| 4562 |
"version": "4.1.11",
|
| 4563 |
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
|
|
@@ -4593,6 +5475,20 @@
|
|
| 4593 |
"node": ">=18"
|
| 4594 |
}
|
| 4595 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4596 |
"node_modules/tinyglobby": {
|
| 4597 |
"version": "0.2.14",
|
| 4598 |
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
|
@@ -4635,6 +5531,56 @@
|
|
| 4635 |
"url": "https://github.com/sponsors/jonschlinkert"
|
| 4636 |
}
|
| 4637 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4638 |
"node_modules/to-regex-range": {
|
| 4639 |
"version": "5.0.1",
|
| 4640 |
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
|
@@ -4648,6 +5594,32 @@
|
|
| 4648 |
"node": ">=8.0"
|
| 4649 |
}
|
| 4650 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4651 |
"node_modules/ts-api-utils": {
|
| 4652 |
"version": "2.1.0",
|
| 4653 |
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
|
@@ -4888,6 +5860,29 @@
|
|
| 4888 |
}
|
| 4889 |
}
|
| 4890 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4891 |
"node_modules/vite/node_modules/fdir": {
|
| 4892 |
"version": "6.5.0",
|
| 4893 |
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
|
@@ -4917,6 +5912,152 @@
|
|
| 4917 |
"url": "https://github.com/sponsors/jonschlinkert"
|
| 4918 |
}
|
| 4919 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4920 |
"node_modules/which": {
|
| 4921 |
"version": "2.0.2",
|
| 4922 |
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
|
@@ -4933,6 +6074,23 @@
|
|
| 4933 |
"node": ">= 8"
|
| 4934 |
}
|
| 4935 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4936 |
"node_modules/word-wrap": {
|
| 4937 |
"version": "1.2.5",
|
| 4938 |
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
|
@@ -4943,6 +6101,45 @@
|
|
| 4943 |
"node": ">=0.10.0"
|
| 4944 |
}
|
| 4945 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4946 |
"node_modules/yallist": {
|
| 4947 |
"version": "5.0.0",
|
| 4948 |
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
|
|
|
| 21 |
"devDependencies": {
|
| 22 |
"@eslint/js": "^9.30.1",
|
| 23 |
"@tailwindcss/postcss": "^4.1.11",
|
| 24 |
+
"@testing-library/jest-dom": "^6.8.0",
|
| 25 |
+
"@testing-library/react": "^16.3.0",
|
| 26 |
+
"@testing-library/user-event": "^14.6.1",
|
| 27 |
"@types/node": "^24.1.0",
|
| 28 |
"@types/react": "^19.1.8",
|
| 29 |
"@types/react-dom": "^19.1.6",
|
|
|
|
| 34 |
"eslint-plugin-react-hooks": "^5.2.0",
|
| 35 |
"eslint-plugin-react-refresh": "^0.4.20",
|
| 36 |
"globals": "^16.3.0",
|
| 37 |
+
"jsdom": "^26.1.0",
|
| 38 |
"postcss": "^8.5.6",
|
| 39 |
"tailwindcss": "^4.1.11",
|
| 40 |
"typescript": "~5.8.3",
|
| 41 |
+
"typescript-eslint": "^8.35.1",
|
| 42 |
+
"vitest": "^3.2.4"
|
| 43 |
}
|
| 44 |
},
|
| 45 |
+
"node_modules/@adobe/css-tools": {
|
| 46 |
+
"version": "4.4.4",
|
| 47 |
+
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz",
|
| 48 |
+
"integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==",
|
| 49 |
+
"dev": true,
|
| 50 |
+
"license": "MIT"
|
| 51 |
+
},
|
| 52 |
"node_modules/@alloc/quick-lru": {
|
| 53 |
"version": "5.2.0",
|
| 54 |
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
|
|
|
| 76 |
"node": ">=6.0.0"
|
| 77 |
}
|
| 78 |
},
|
| 79 |
+
"node_modules/@asamuzakjp/css-color": {
|
| 80 |
+
"version": "3.2.0",
|
| 81 |
+
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
|
| 82 |
+
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
|
| 83 |
+
"dev": true,
|
| 84 |
+
"license": "MIT",
|
| 85 |
+
"dependencies": {
|
| 86 |
+
"@csstools/css-calc": "^2.1.3",
|
| 87 |
+
"@csstools/css-color-parser": "^3.0.9",
|
| 88 |
+
"@csstools/css-parser-algorithms": "^3.0.4",
|
| 89 |
+
"@csstools/css-tokenizer": "^3.0.3",
|
| 90 |
+
"lru-cache": "^10.4.3"
|
| 91 |
+
}
|
| 92 |
+
},
|
| 93 |
+
"node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
|
| 94 |
+
"version": "10.4.3",
|
| 95 |
+
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
| 96 |
+
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
| 97 |
+
"dev": true,
|
| 98 |
+
"license": "ISC"
|
| 99 |
+
},
|
| 100 |
"node_modules/@babel/code-frame": {
|
| 101 |
"version": "7.27.1",
|
| 102 |
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
|
|
|
| 420 |
"node": ">=6.9.0"
|
| 421 |
}
|
| 422 |
},
|
| 423 |
+
"node_modules/@csstools/color-helpers": {
|
| 424 |
+
"version": "5.1.0",
|
| 425 |
+
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
|
| 426 |
+
"integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
|
| 427 |
+
"dev": true,
|
| 428 |
+
"funding": [
|
| 429 |
+
{
|
| 430 |
+
"type": "github",
|
| 431 |
+
"url": "https://github.com/sponsors/csstools"
|
| 432 |
+
},
|
| 433 |
+
{
|
| 434 |
+
"type": "opencollective",
|
| 435 |
+
"url": "https://opencollective.com/csstools"
|
| 436 |
+
}
|
| 437 |
+
],
|
| 438 |
+
"license": "MIT-0",
|
| 439 |
+
"engines": {
|
| 440 |
+
"node": ">=18"
|
| 441 |
+
}
|
| 442 |
+
},
|
| 443 |
+
"node_modules/@csstools/css-calc": {
|
| 444 |
+
"version": "2.1.4",
|
| 445 |
+
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
|
| 446 |
+
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
|
| 447 |
+
"dev": true,
|
| 448 |
+
"funding": [
|
| 449 |
+
{
|
| 450 |
+
"type": "github",
|
| 451 |
+
"url": "https://github.com/sponsors/csstools"
|
| 452 |
+
},
|
| 453 |
+
{
|
| 454 |
+
"type": "opencollective",
|
| 455 |
+
"url": "https://opencollective.com/csstools"
|
| 456 |
+
}
|
| 457 |
+
],
|
| 458 |
+
"license": "MIT",
|
| 459 |
+
"engines": {
|
| 460 |
+
"node": ">=18"
|
| 461 |
+
},
|
| 462 |
+
"peerDependencies": {
|
| 463 |
+
"@csstools/css-parser-algorithms": "^3.0.5",
|
| 464 |
+
"@csstools/css-tokenizer": "^3.0.4"
|
| 465 |
+
}
|
| 466 |
+
},
|
| 467 |
+
"node_modules/@csstools/css-color-parser": {
|
| 468 |
+
"version": "3.1.0",
|
| 469 |
+
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
|
| 470 |
+
"integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
|
| 471 |
+
"dev": true,
|
| 472 |
+
"funding": [
|
| 473 |
+
{
|
| 474 |
+
"type": "github",
|
| 475 |
+
"url": "https://github.com/sponsors/csstools"
|
| 476 |
+
},
|
| 477 |
+
{
|
| 478 |
+
"type": "opencollective",
|
| 479 |
+
"url": "https://opencollective.com/csstools"
|
| 480 |
+
}
|
| 481 |
+
],
|
| 482 |
+
"license": "MIT",
|
| 483 |
+
"dependencies": {
|
| 484 |
+
"@csstools/color-helpers": "^5.1.0",
|
| 485 |
+
"@csstools/css-calc": "^2.1.4"
|
| 486 |
+
},
|
| 487 |
+
"engines": {
|
| 488 |
+
"node": ">=18"
|
| 489 |
+
},
|
| 490 |
+
"peerDependencies": {
|
| 491 |
+
"@csstools/css-parser-algorithms": "^3.0.5",
|
| 492 |
+
"@csstools/css-tokenizer": "^3.0.4"
|
| 493 |
+
}
|
| 494 |
+
},
|
| 495 |
+
"node_modules/@csstools/css-parser-algorithms": {
|
| 496 |
+
"version": "3.0.5",
|
| 497 |
+
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
|
| 498 |
+
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
|
| 499 |
+
"dev": true,
|
| 500 |
+
"funding": [
|
| 501 |
+
{
|
| 502 |
+
"type": "github",
|
| 503 |
+
"url": "https://github.com/sponsors/csstools"
|
| 504 |
+
},
|
| 505 |
+
{
|
| 506 |
+
"type": "opencollective",
|
| 507 |
+
"url": "https://opencollective.com/csstools"
|
| 508 |
+
}
|
| 509 |
+
],
|
| 510 |
+
"license": "MIT",
|
| 511 |
+
"engines": {
|
| 512 |
+
"node": ">=18"
|
| 513 |
+
},
|
| 514 |
+
"peerDependencies": {
|
| 515 |
+
"@csstools/css-tokenizer": "^3.0.4"
|
| 516 |
+
}
|
| 517 |
+
},
|
| 518 |
+
"node_modules/@csstools/css-tokenizer": {
|
| 519 |
+
"version": "3.0.4",
|
| 520 |
+
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
|
| 521 |
+
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
|
| 522 |
+
"dev": true,
|
| 523 |
+
"funding": [
|
| 524 |
+
{
|
| 525 |
+
"type": "github",
|
| 526 |
+
"url": "https://github.com/sponsors/csstools"
|
| 527 |
+
},
|
| 528 |
+
{
|
| 529 |
+
"type": "opencollective",
|
| 530 |
+
"url": "https://opencollective.com/csstools"
|
| 531 |
+
}
|
| 532 |
+
],
|
| 533 |
+
"license": "MIT",
|
| 534 |
+
"engines": {
|
| 535 |
+
"node": ">=18"
|
| 536 |
+
}
|
| 537 |
+
},
|
| 538 |
"node_modules/@esbuild/aix-ppc64": {
|
| 539 |
"version": "0.25.6",
|
| 540 |
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz",
|
|
|
|
| 2078 |
"tailwindcss": "4.1.11"
|
| 2079 |
}
|
| 2080 |
},
|
| 2081 |
+
"node_modules/@testing-library/dom": {
|
| 2082 |
+
"version": "10.4.1",
|
| 2083 |
+
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
|
| 2084 |
+
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
| 2085 |
+
"dev": true,
|
| 2086 |
+
"license": "MIT",
|
| 2087 |
+
"peer": true,
|
| 2088 |
+
"dependencies": {
|
| 2089 |
+
"@babel/code-frame": "^7.10.4",
|
| 2090 |
+
"@babel/runtime": "^7.12.5",
|
| 2091 |
+
"@types/aria-query": "^5.0.1",
|
| 2092 |
+
"aria-query": "5.3.0",
|
| 2093 |
+
"dom-accessibility-api": "^0.5.9",
|
| 2094 |
+
"lz-string": "^1.5.0",
|
| 2095 |
+
"picocolors": "1.1.1",
|
| 2096 |
+
"pretty-format": "^27.0.2"
|
| 2097 |
+
},
|
| 2098 |
+
"engines": {
|
| 2099 |
+
"node": ">=18"
|
| 2100 |
+
}
|
| 2101 |
+
},
|
| 2102 |
+
"node_modules/@testing-library/jest-dom": {
|
| 2103 |
+
"version": "6.8.0",
|
| 2104 |
+
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.8.0.tgz",
|
| 2105 |
+
"integrity": "sha512-WgXcWzVM6idy5JaftTVC8Vs83NKRmGJz4Hqs4oyOuO2J4r/y79vvKZsb+CaGyCSEbUPI6OsewfPd0G1A0/TUZQ==",
|
| 2106 |
+
"dev": true,
|
| 2107 |
+
"license": "MIT",
|
| 2108 |
+
"dependencies": {
|
| 2109 |
+
"@adobe/css-tools": "^4.4.0",
|
| 2110 |
+
"aria-query": "^5.0.0",
|
| 2111 |
+
"css.escape": "^1.5.1",
|
| 2112 |
+
"dom-accessibility-api": "^0.6.3",
|
| 2113 |
+
"picocolors": "^1.1.1",
|
| 2114 |
+
"redent": "^3.0.0"
|
| 2115 |
+
},
|
| 2116 |
+
"engines": {
|
| 2117 |
+
"node": ">=14",
|
| 2118 |
+
"npm": ">=6",
|
| 2119 |
+
"yarn": ">=1"
|
| 2120 |
+
}
|
| 2121 |
+
},
|
| 2122 |
+
"node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
|
| 2123 |
+
"version": "0.6.3",
|
| 2124 |
+
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
|
| 2125 |
+
"integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
|
| 2126 |
+
"dev": true,
|
| 2127 |
+
"license": "MIT"
|
| 2128 |
+
},
|
| 2129 |
+
"node_modules/@testing-library/react": {
|
| 2130 |
+
"version": "16.3.0",
|
| 2131 |
+
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz",
|
| 2132 |
+
"integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==",
|
| 2133 |
+
"dev": true,
|
| 2134 |
+
"license": "MIT",
|
| 2135 |
+
"dependencies": {
|
| 2136 |
+
"@babel/runtime": "^7.12.5"
|
| 2137 |
+
},
|
| 2138 |
+
"engines": {
|
| 2139 |
+
"node": ">=18"
|
| 2140 |
+
},
|
| 2141 |
+
"peerDependencies": {
|
| 2142 |
+
"@testing-library/dom": "^10.0.0",
|
| 2143 |
+
"@types/react": "^18.0.0 || ^19.0.0",
|
| 2144 |
+
"@types/react-dom": "^18.0.0 || ^19.0.0",
|
| 2145 |
+
"react": "^18.0.0 || ^19.0.0",
|
| 2146 |
+
"react-dom": "^18.0.0 || ^19.0.0"
|
| 2147 |
+
},
|
| 2148 |
+
"peerDependenciesMeta": {
|
| 2149 |
+
"@types/react": {
|
| 2150 |
+
"optional": true
|
| 2151 |
+
},
|
| 2152 |
+
"@types/react-dom": {
|
| 2153 |
+
"optional": true
|
| 2154 |
+
}
|
| 2155 |
+
}
|
| 2156 |
+
},
|
| 2157 |
+
"node_modules/@testing-library/user-event": {
|
| 2158 |
+
"version": "14.6.1",
|
| 2159 |
+
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
|
| 2160 |
+
"integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
|
| 2161 |
+
"dev": true,
|
| 2162 |
+
"license": "MIT",
|
| 2163 |
+
"engines": {
|
| 2164 |
+
"node": ">=12",
|
| 2165 |
+
"npm": ">=6"
|
| 2166 |
+
},
|
| 2167 |
+
"peerDependencies": {
|
| 2168 |
+
"@testing-library/dom": ">=7.21.4"
|
| 2169 |
+
}
|
| 2170 |
+
},
|
| 2171 |
"node_modules/@togglecorp/fujs": {
|
| 2172 |
"version": "2.2.0",
|
| 2173 |
"resolved": "https://registry.npmjs.org/@togglecorp/fujs/-/fujs-2.2.0.tgz",
|
|
|
|
| 2177 |
"@babel/runtime-corejs3": "^7.22.6"
|
| 2178 |
}
|
| 2179 |
},
|
| 2180 |
+
"node_modules/@types/aria-query": {
|
| 2181 |
+
"version": "5.0.4",
|
| 2182 |
+
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
| 2183 |
+
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
| 2184 |
+
"dev": true,
|
| 2185 |
+
"license": "MIT",
|
| 2186 |
+
"peer": true
|
| 2187 |
+
},
|
| 2188 |
"node_modules/@types/babel__core": {
|
| 2189 |
"version": "7.20.5",
|
| 2190 |
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
|
|
|
| 2230 |
"@babel/types": "^7.20.7"
|
| 2231 |
}
|
| 2232 |
},
|
| 2233 |
+
"node_modules/@types/chai": {
|
| 2234 |
+
"version": "5.2.2",
|
| 2235 |
+
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz",
|
| 2236 |
+
"integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==",
|
| 2237 |
+
"dev": true,
|
| 2238 |
+
"license": "MIT",
|
| 2239 |
+
"dependencies": {
|
| 2240 |
+
"@types/deep-eql": "*"
|
| 2241 |
+
}
|
| 2242 |
+
},
|
| 2243 |
+
"node_modules/@types/deep-eql": {
|
| 2244 |
+
"version": "4.0.2",
|
| 2245 |
+
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
| 2246 |
+
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
| 2247 |
+
"dev": true,
|
| 2248 |
+
"license": "MIT"
|
| 2249 |
+
},
|
| 2250 |
"node_modules/@types/estree": {
|
| 2251 |
"version": "1.0.8",
|
| 2252 |
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
|
|
|
| 2598 |
"dev": true,
|
| 2599 |
"license": "MIT"
|
| 2600 |
},
|
| 2601 |
+
"node_modules/@vitest/expect": {
|
| 2602 |
+
"version": "3.2.4",
|
| 2603 |
+
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
|
| 2604 |
+
"integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
|
| 2605 |
+
"dev": true,
|
| 2606 |
+
"license": "MIT",
|
| 2607 |
+
"dependencies": {
|
| 2608 |
+
"@types/chai": "^5.2.2",
|
| 2609 |
+
"@vitest/spy": "3.2.4",
|
| 2610 |
+
"@vitest/utils": "3.2.4",
|
| 2611 |
+
"chai": "^5.2.0",
|
| 2612 |
+
"tinyrainbow": "^2.0.0"
|
| 2613 |
+
},
|
| 2614 |
+
"funding": {
|
| 2615 |
+
"url": "https://opencollective.com/vitest"
|
| 2616 |
+
}
|
| 2617 |
+
},
|
| 2618 |
+
"node_modules/@vitest/mocker": {
|
| 2619 |
+
"version": "3.2.4",
|
| 2620 |
+
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
|
| 2621 |
+
"integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
|
| 2622 |
+
"dev": true,
|
| 2623 |
+
"license": "MIT",
|
| 2624 |
+
"dependencies": {
|
| 2625 |
+
"@vitest/spy": "3.2.4",
|
| 2626 |
+
"estree-walker": "^3.0.3",
|
| 2627 |
+
"magic-string": "^0.30.17"
|
| 2628 |
+
},
|
| 2629 |
+
"funding": {
|
| 2630 |
+
"url": "https://opencollective.com/vitest"
|
| 2631 |
+
},
|
| 2632 |
+
"peerDependencies": {
|
| 2633 |
+
"msw": "^2.4.9",
|
| 2634 |
+
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
|
| 2635 |
+
},
|
| 2636 |
+
"peerDependenciesMeta": {
|
| 2637 |
+
"msw": {
|
| 2638 |
+
"optional": true
|
| 2639 |
+
},
|
| 2640 |
+
"vite": {
|
| 2641 |
+
"optional": true
|
| 2642 |
+
}
|
| 2643 |
+
}
|
| 2644 |
+
},
|
| 2645 |
+
"node_modules/@vitest/pretty-format": {
|
| 2646 |
+
"version": "3.2.4",
|
| 2647 |
+
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
|
| 2648 |
+
"integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
|
| 2649 |
+
"dev": true,
|
| 2650 |
+
"license": "MIT",
|
| 2651 |
+
"dependencies": {
|
| 2652 |
+
"tinyrainbow": "^2.0.0"
|
| 2653 |
+
},
|
| 2654 |
+
"funding": {
|
| 2655 |
+
"url": "https://opencollective.com/vitest"
|
| 2656 |
+
}
|
| 2657 |
+
},
|
| 2658 |
+
"node_modules/@vitest/runner": {
|
| 2659 |
+
"version": "3.2.4",
|
| 2660 |
+
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
|
| 2661 |
+
"integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
|
| 2662 |
+
"dev": true,
|
| 2663 |
+
"license": "MIT",
|
| 2664 |
+
"dependencies": {
|
| 2665 |
+
"@vitest/utils": "3.2.4",
|
| 2666 |
+
"pathe": "^2.0.3",
|
| 2667 |
+
"strip-literal": "^3.0.0"
|
| 2668 |
+
},
|
| 2669 |
+
"funding": {
|
| 2670 |
+
"url": "https://opencollective.com/vitest"
|
| 2671 |
+
}
|
| 2672 |
+
},
|
| 2673 |
+
"node_modules/@vitest/snapshot": {
|
| 2674 |
+
"version": "3.2.4",
|
| 2675 |
+
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
|
| 2676 |
+
"integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
|
| 2677 |
+
"dev": true,
|
| 2678 |
+
"license": "MIT",
|
| 2679 |
+
"dependencies": {
|
| 2680 |
+
"@vitest/pretty-format": "3.2.4",
|
| 2681 |
+
"magic-string": "^0.30.17",
|
| 2682 |
+
"pathe": "^2.0.3"
|
| 2683 |
+
},
|
| 2684 |
+
"funding": {
|
| 2685 |
+
"url": "https://opencollective.com/vitest"
|
| 2686 |
+
}
|
| 2687 |
+
},
|
| 2688 |
+
"node_modules/@vitest/spy": {
|
| 2689 |
+
"version": "3.2.4",
|
| 2690 |
+
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
|
| 2691 |
+
"integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
|
| 2692 |
+
"dev": true,
|
| 2693 |
+
"license": "MIT",
|
| 2694 |
+
"dependencies": {
|
| 2695 |
+
"tinyspy": "^4.0.3"
|
| 2696 |
+
},
|
| 2697 |
+
"funding": {
|
| 2698 |
+
"url": "https://opencollective.com/vitest"
|
| 2699 |
+
}
|
| 2700 |
+
},
|
| 2701 |
+
"node_modules/@vitest/utils": {
|
| 2702 |
+
"version": "3.2.4",
|
| 2703 |
+
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
|
| 2704 |
+
"integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
|
| 2705 |
+
"dev": true,
|
| 2706 |
+
"license": "MIT",
|
| 2707 |
+
"dependencies": {
|
| 2708 |
+
"@vitest/pretty-format": "3.2.4",
|
| 2709 |
+
"loupe": "^3.1.4",
|
| 2710 |
+
"tinyrainbow": "^2.0.0"
|
| 2711 |
+
},
|
| 2712 |
+
"funding": {
|
| 2713 |
+
"url": "https://opencollective.com/vitest"
|
| 2714 |
+
}
|
| 2715 |
+
},
|
| 2716 |
"node_modules/acorn": {
|
| 2717 |
"version": "8.15.0",
|
| 2718 |
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
|
|
|
| 2736 |
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
| 2737 |
}
|
| 2738 |
},
|
| 2739 |
+
"node_modules/agent-base": {
|
| 2740 |
+
"version": "7.1.4",
|
| 2741 |
+
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
| 2742 |
+
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
| 2743 |
+
"dev": true,
|
| 2744 |
+
"license": "MIT",
|
| 2745 |
+
"engines": {
|
| 2746 |
+
"node": ">= 14"
|
| 2747 |
+
}
|
| 2748 |
+
},
|
| 2749 |
"node_modules/ajv": {
|
| 2750 |
"version": "6.12.6",
|
| 2751 |
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
|
|
|
| 2763 |
"url": "https://github.com/sponsors/epoberezkin"
|
| 2764 |
}
|
| 2765 |
},
|
| 2766 |
+
"node_modules/ansi-regex": {
|
| 2767 |
+
"version": "5.0.1",
|
| 2768 |
+
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
| 2769 |
+
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
| 2770 |
+
"dev": true,
|
| 2771 |
+
"license": "MIT",
|
| 2772 |
+
"peer": true,
|
| 2773 |
+
"engines": {
|
| 2774 |
+
"node": ">=8"
|
| 2775 |
+
}
|
| 2776 |
+
},
|
| 2777 |
"node_modules/ansi-styles": {
|
| 2778 |
"version": "4.3.0",
|
| 2779 |
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
|
|
|
| 2809 |
"node": ">=10"
|
| 2810 |
}
|
| 2811 |
},
|
| 2812 |
+
"node_modules/aria-query": {
|
| 2813 |
+
"version": "5.3.0",
|
| 2814 |
+
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
|
| 2815 |
+
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
|
| 2816 |
+
"dev": true,
|
| 2817 |
+
"license": "Apache-2.0",
|
| 2818 |
+
"dependencies": {
|
| 2819 |
+
"dequal": "^2.0.3"
|
| 2820 |
+
}
|
| 2821 |
+
},
|
| 2822 |
+
"node_modules/assertion-error": {
|
| 2823 |
+
"version": "2.0.1",
|
| 2824 |
+
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
| 2825 |
+
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
| 2826 |
+
"dev": true,
|
| 2827 |
+
"license": "MIT",
|
| 2828 |
+
"engines": {
|
| 2829 |
+
"node": ">=12"
|
| 2830 |
+
}
|
| 2831 |
+
},
|
| 2832 |
"node_modules/autoprefixer": {
|
| 2833 |
"version": "10.4.21",
|
| 2834 |
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
|
|
|
|
| 2931 |
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
| 2932 |
}
|
| 2933 |
},
|
| 2934 |
+
"node_modules/cac": {
|
| 2935 |
+
"version": "6.7.14",
|
| 2936 |
+
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
| 2937 |
+
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
|
| 2938 |
+
"dev": true,
|
| 2939 |
+
"license": "MIT",
|
| 2940 |
+
"engines": {
|
| 2941 |
+
"node": ">=8"
|
| 2942 |
+
}
|
| 2943 |
+
},
|
| 2944 |
"node_modules/callsites": {
|
| 2945 |
"version": "3.1.0",
|
| 2946 |
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
|
|
|
| 2972 |
],
|
| 2973 |
"license": "CC-BY-4.0"
|
| 2974 |
},
|
| 2975 |
+
"node_modules/chai": {
|
| 2976 |
+
"version": "5.3.3",
|
| 2977 |
+
"resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
|
| 2978 |
+
"integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
|
| 2979 |
+
"dev": true,
|
| 2980 |
+
"license": "MIT",
|
| 2981 |
+
"dependencies": {
|
| 2982 |
+
"assertion-error": "^2.0.1",
|
| 2983 |
+
"check-error": "^2.1.1",
|
| 2984 |
+
"deep-eql": "^5.0.1",
|
| 2985 |
+
"loupe": "^3.1.0",
|
| 2986 |
+
"pathval": "^2.0.0"
|
| 2987 |
+
},
|
| 2988 |
+
"engines": {
|
| 2989 |
+
"node": ">=18"
|
| 2990 |
+
}
|
| 2991 |
+
},
|
| 2992 |
"node_modules/chalk": {
|
| 2993 |
"version": "4.1.2",
|
| 2994 |
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
|
|
|
| 3006 |
"url": "https://github.com/chalk/chalk?sponsor=1"
|
| 3007 |
}
|
| 3008 |
},
|
| 3009 |
+
"node_modules/check-error": {
|
| 3010 |
+
"version": "2.1.1",
|
| 3011 |
+
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
|
| 3012 |
+
"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
|
| 3013 |
+
"dev": true,
|
| 3014 |
+
"license": "MIT",
|
| 3015 |
+
"engines": {
|
| 3016 |
+
"node": ">= 16"
|
| 3017 |
+
}
|
| 3018 |
+
},
|
| 3019 |
"node_modules/chownr": {
|
| 3020 |
"version": "3.0.0",
|
| 3021 |
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
|
|
|
| 3092 |
"node": ">= 8"
|
| 3093 |
}
|
| 3094 |
},
|
| 3095 |
+
"node_modules/css.escape": {
|
| 3096 |
+
"version": "1.5.1",
|
| 3097 |
+
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
|
| 3098 |
+
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
|
| 3099 |
+
"dev": true,
|
| 3100 |
"license": "MIT"
|
| 3101 |
},
|
| 3102 |
+
"node_modules/cssstyle": {
|
| 3103 |
+
"version": "4.6.0",
|
| 3104 |
+
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
|
| 3105 |
+
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
|
| 3106 |
+
"dev": true,
|
| 3107 |
+
"license": "MIT",
|
| 3108 |
+
"dependencies": {
|
| 3109 |
+
"@asamuzakjp/css-color": "^3.2.0",
|
| 3110 |
+
"rrweb-cssom": "^0.8.0"
|
| 3111 |
+
},
|
| 3112 |
+
"engines": {
|
| 3113 |
+
"node": ">=18"
|
| 3114 |
+
}
|
| 3115 |
+
},
|
| 3116 |
+
"node_modules/csstype": {
|
| 3117 |
+
"version": "3.1.3",
|
| 3118 |
+
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
| 3119 |
+
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
| 3120 |
+
"devOptional": true,
|
| 3121 |
+
"license": "MIT"
|
| 3122 |
+
},
|
| 3123 |
+
"node_modules/data-urls": {
|
| 3124 |
+
"version": "5.0.0",
|
| 3125 |
+
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
|
| 3126 |
+
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
|
| 3127 |
+
"dev": true,
|
| 3128 |
+
"license": "MIT",
|
| 3129 |
+
"dependencies": {
|
| 3130 |
+
"whatwg-mimetype": "^4.0.0",
|
| 3131 |
+
"whatwg-url": "^14.0.0"
|
| 3132 |
+
},
|
| 3133 |
+
"engines": {
|
| 3134 |
+
"node": ">=18"
|
| 3135 |
+
}
|
| 3136 |
+
},
|
| 3137 |
+
"node_modules/debug": {
|
| 3138 |
"version": "4.4.1",
|
| 3139 |
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
| 3140 |
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
|
|
|
| 3152 |
}
|
| 3153 |
}
|
| 3154 |
},
|
| 3155 |
+
"node_modules/decimal.js": {
|
| 3156 |
+
"version": "10.6.0",
|
| 3157 |
+
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
| 3158 |
+
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
| 3159 |
+
"dev": true,
|
| 3160 |
+
"license": "MIT"
|
| 3161 |
+
},
|
| 3162 |
+
"node_modules/deep-eql": {
|
| 3163 |
+
"version": "5.0.2",
|
| 3164 |
+
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
|
| 3165 |
+
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
|
| 3166 |
+
"dev": true,
|
| 3167 |
+
"license": "MIT",
|
| 3168 |
+
"engines": {
|
| 3169 |
+
"node": ">=6"
|
| 3170 |
+
}
|
| 3171 |
+
},
|
| 3172 |
"node_modules/deep-is": {
|
| 3173 |
"version": "0.1.4",
|
| 3174 |
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
|
|
|
| 3185 |
"node": ">=0.10.0"
|
| 3186 |
}
|
| 3187 |
},
|
| 3188 |
+
"node_modules/dequal": {
|
| 3189 |
+
"version": "2.0.3",
|
| 3190 |
+
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
| 3191 |
+
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
| 3192 |
+
"dev": true,
|
| 3193 |
+
"license": "MIT",
|
| 3194 |
+
"engines": {
|
| 3195 |
+
"node": ">=6"
|
| 3196 |
+
}
|
| 3197 |
+
},
|
| 3198 |
"node_modules/detect-libc": {
|
| 3199 |
"version": "2.0.4",
|
| 3200 |
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
|
|
|
| 3211 |
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
| 3212 |
"license": "MIT"
|
| 3213 |
},
|
| 3214 |
+
"node_modules/dom-accessibility-api": {
|
| 3215 |
+
"version": "0.5.16",
|
| 3216 |
+
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
| 3217 |
+
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
| 3218 |
+
"dev": true,
|
| 3219 |
+
"license": "MIT",
|
| 3220 |
+
"peer": true
|
| 3221 |
+
},
|
| 3222 |
"node_modules/dom-serializer": {
|
| 3223 |
"version": "2.0.0",
|
| 3224 |
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
|
|
|
| 3307 |
"url": "https://github.com/fb55/entities?sponsor=1"
|
| 3308 |
}
|
| 3309 |
},
|
| 3310 |
+
"node_modules/es-module-lexer": {
|
| 3311 |
+
"version": "1.7.0",
|
| 3312 |
+
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
| 3313 |
+
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
|
| 3314 |
+
"dev": true,
|
| 3315 |
+
"license": "MIT"
|
| 3316 |
+
},
|
| 3317 |
"node_modules/esbuild": {
|
| 3318 |
"version": "0.25.6",
|
| 3319 |
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz",
|
|
|
|
| 3545 |
"node": ">=4.0"
|
| 3546 |
}
|
| 3547 |
},
|
| 3548 |
+
"node_modules/estree-walker": {
|
| 3549 |
+
"version": "3.0.3",
|
| 3550 |
+
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
| 3551 |
+
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
| 3552 |
+
"dev": true,
|
| 3553 |
+
"license": "MIT",
|
| 3554 |
+
"dependencies": {
|
| 3555 |
+
"@types/estree": "^1.0.0"
|
| 3556 |
+
}
|
| 3557 |
+
},
|
| 3558 |
"node_modules/esutils": {
|
| 3559 |
"version": "2.0.3",
|
| 3560 |
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
|
|
|
| 3565 |
"node": ">=0.10.0"
|
| 3566 |
}
|
| 3567 |
},
|
| 3568 |
+
"node_modules/expect-type": {
|
| 3569 |
+
"version": "1.2.2",
|
| 3570 |
+
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
|
| 3571 |
+
"integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==",
|
| 3572 |
+
"dev": true,
|
| 3573 |
+
"license": "Apache-2.0",
|
| 3574 |
+
"engines": {
|
| 3575 |
+
"node": ">=12.0.0"
|
| 3576 |
+
}
|
| 3577 |
+
},
|
| 3578 |
"node_modules/fast-deep-equal": {
|
| 3579 |
"version": "3.1.3",
|
| 3580 |
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
|
|
|
| 3809 |
"node": ">=8"
|
| 3810 |
}
|
| 3811 |
},
|
| 3812 |
+
"node_modules/html-encoding-sniffer": {
|
| 3813 |
+
"version": "4.0.0",
|
| 3814 |
+
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
|
| 3815 |
+
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
|
| 3816 |
+
"dev": true,
|
| 3817 |
+
"license": "MIT",
|
| 3818 |
+
"dependencies": {
|
| 3819 |
+
"whatwg-encoding": "^3.1.1"
|
| 3820 |
+
},
|
| 3821 |
+
"engines": {
|
| 3822 |
+
"node": ">=18"
|
| 3823 |
+
}
|
| 3824 |
+
},
|
| 3825 |
"node_modules/htmlparser2": {
|
| 3826 |
"version": "8.0.2",
|
| 3827 |
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
|
|
|
| 3841 |
"entities": "^4.4.0"
|
| 3842 |
}
|
| 3843 |
},
|
| 3844 |
+
"node_modules/http-proxy-agent": {
|
| 3845 |
+
"version": "7.0.2",
|
| 3846 |
+
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
| 3847 |
+
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
| 3848 |
+
"dev": true,
|
| 3849 |
+
"license": "MIT",
|
| 3850 |
+
"dependencies": {
|
| 3851 |
+
"agent-base": "^7.1.0",
|
| 3852 |
+
"debug": "^4.3.4"
|
| 3853 |
+
},
|
| 3854 |
+
"engines": {
|
| 3855 |
+
"node": ">= 14"
|
| 3856 |
+
}
|
| 3857 |
+
},
|
| 3858 |
+
"node_modules/https-proxy-agent": {
|
| 3859 |
+
"version": "7.0.6",
|
| 3860 |
+
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
| 3861 |
+
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
| 3862 |
+
"dev": true,
|
| 3863 |
+
"license": "MIT",
|
| 3864 |
+
"dependencies": {
|
| 3865 |
+
"agent-base": "^7.1.2",
|
| 3866 |
+
"debug": "4"
|
| 3867 |
+
},
|
| 3868 |
+
"engines": {
|
| 3869 |
+
"node": ">= 14"
|
| 3870 |
+
}
|
| 3871 |
+
},
|
| 3872 |
+
"node_modules/iconv-lite": {
|
| 3873 |
+
"version": "0.6.3",
|
| 3874 |
+
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
| 3875 |
+
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
| 3876 |
+
"dev": true,
|
| 3877 |
+
"license": "MIT",
|
| 3878 |
+
"dependencies": {
|
| 3879 |
+
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
| 3880 |
+
},
|
| 3881 |
+
"engines": {
|
| 3882 |
+
"node": ">=0.10.0"
|
| 3883 |
+
}
|
| 3884 |
+
},
|
| 3885 |
"node_modules/ignore": {
|
| 3886 |
"version": "5.3.2",
|
| 3887 |
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
|
|
|
| 3925 |
"node": ">=0.8.19"
|
| 3926 |
}
|
| 3927 |
},
|
| 3928 |
+
"node_modules/indent-string": {
|
| 3929 |
+
"version": "4.0.0",
|
| 3930 |
+
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
| 3931 |
+
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
| 3932 |
+
"dev": true,
|
| 3933 |
+
"license": "MIT",
|
| 3934 |
+
"engines": {
|
| 3935 |
+
"node": ">=8"
|
| 3936 |
+
}
|
| 3937 |
+
},
|
| 3938 |
"node_modules/inherits": {
|
| 3939 |
"version": "2.0.4",
|
| 3940 |
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
|
|
|
| 3983 |
"node": ">=0.10.0"
|
| 3984 |
}
|
| 3985 |
},
|
| 3986 |
+
"node_modules/is-potential-custom-element-name": {
|
| 3987 |
+
"version": "1.0.1",
|
| 3988 |
+
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
| 3989 |
+
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
| 3990 |
+
"dev": true,
|
| 3991 |
+
"license": "MIT"
|
| 3992 |
+
},
|
| 3993 |
"node_modules/isarray": {
|
| 3994 |
"version": "1.0.0",
|
| 3995 |
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
|
|
|
| 4032 |
"js-yaml": "bin/js-yaml.js"
|
| 4033 |
}
|
| 4034 |
},
|
| 4035 |
+
"node_modules/jsdom": {
|
| 4036 |
+
"version": "26.1.0",
|
| 4037 |
+
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
|
| 4038 |
+
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
| 4039 |
+
"dev": true,
|
| 4040 |
+
"license": "MIT",
|
| 4041 |
+
"dependencies": {
|
| 4042 |
+
"cssstyle": "^4.2.1",
|
| 4043 |
+
"data-urls": "^5.0.0",
|
| 4044 |
+
"decimal.js": "^10.5.0",
|
| 4045 |
+
"html-encoding-sniffer": "^4.0.0",
|
| 4046 |
+
"http-proxy-agent": "^7.0.2",
|
| 4047 |
+
"https-proxy-agent": "^7.0.6",
|
| 4048 |
+
"is-potential-custom-element-name": "^1.0.1",
|
| 4049 |
+
"nwsapi": "^2.2.16",
|
| 4050 |
+
"parse5": "^7.2.1",
|
| 4051 |
+
"rrweb-cssom": "^0.8.0",
|
| 4052 |
+
"saxes": "^6.0.0",
|
| 4053 |
+
"symbol-tree": "^3.2.4",
|
| 4054 |
+
"tough-cookie": "^5.1.1",
|
| 4055 |
+
"w3c-xmlserializer": "^5.0.0",
|
| 4056 |
+
"webidl-conversions": "^7.0.0",
|
| 4057 |
+
"whatwg-encoding": "^3.1.1",
|
| 4058 |
+
"whatwg-mimetype": "^4.0.0",
|
| 4059 |
+
"whatwg-url": "^14.1.1",
|
| 4060 |
+
"ws": "^8.18.0",
|
| 4061 |
+
"xml-name-validator": "^5.0.0"
|
| 4062 |
+
},
|
| 4063 |
+
"engines": {
|
| 4064 |
+
"node": ">=18"
|
| 4065 |
+
},
|
| 4066 |
+
"peerDependencies": {
|
| 4067 |
+
"canvas": "^3.0.0"
|
| 4068 |
+
},
|
| 4069 |
+
"peerDependenciesMeta": {
|
| 4070 |
+
"canvas": {
|
| 4071 |
+
"optional": true
|
| 4072 |
+
}
|
| 4073 |
+
}
|
| 4074 |
+
},
|
| 4075 |
"node_modules/jsesc": {
|
| 4076 |
"version": "3.1.0",
|
| 4077 |
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
|
|
|
| 4428 |
"loose-envify": "cli.js"
|
| 4429 |
}
|
| 4430 |
},
|
| 4431 |
+
"node_modules/loupe": {
|
| 4432 |
+
"version": "3.2.1",
|
| 4433 |
+
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
|
| 4434 |
+
"integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
|
| 4435 |
+
"dev": true,
|
| 4436 |
+
"license": "MIT"
|
| 4437 |
+
},
|
| 4438 |
"node_modules/lru-cache": {
|
| 4439 |
"version": "5.1.1",
|
| 4440 |
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
|
|
|
| 4461 |
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
| 4462 |
}
|
| 4463 |
},
|
| 4464 |
+
"node_modules/lz-string": {
|
| 4465 |
+
"version": "1.5.0",
|
| 4466 |
+
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
| 4467 |
+
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
| 4468 |
+
"dev": true,
|
| 4469 |
+
"license": "MIT",
|
| 4470 |
+
"peer": true,
|
| 4471 |
+
"bin": {
|
| 4472 |
+
"lz-string": "bin/bin.js"
|
| 4473 |
+
}
|
| 4474 |
+
},
|
| 4475 |
"node_modules/magic-string": {
|
| 4476 |
"version": "0.30.17",
|
| 4477 |
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
|
|
|
| 4506 |
"node": ">=8.6"
|
| 4507 |
}
|
| 4508 |
},
|
| 4509 |
+
"node_modules/min-indent": {
|
| 4510 |
+
"version": "1.0.1",
|
| 4511 |
+
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
| 4512 |
+
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
|
| 4513 |
+
"dev": true,
|
| 4514 |
+
"license": "MIT",
|
| 4515 |
+
"engines": {
|
| 4516 |
+
"node": ">=4"
|
| 4517 |
+
}
|
| 4518 |
+
},
|
| 4519 |
"node_modules/minimatch": {
|
| 4520 |
"version": "3.1.2",
|
| 4521 |
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
|
|
|
| 4617 |
"node": ">=0.10.0"
|
| 4618 |
}
|
| 4619 |
},
|
| 4620 |
+
"node_modules/nwsapi": {
|
| 4621 |
+
"version": "2.2.21",
|
| 4622 |
+
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz",
|
| 4623 |
+
"integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==",
|
| 4624 |
+
"dev": true,
|
| 4625 |
+
"license": "MIT"
|
| 4626 |
+
},
|
| 4627 |
"node_modules/object-assign": {
|
| 4628 |
"version": "4.1.1",
|
| 4629 |
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
|
|
|
| 4708 |
"integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==",
|
| 4709 |
"license": "MIT"
|
| 4710 |
},
|
| 4711 |
+
"node_modules/parse5": {
|
| 4712 |
+
"version": "7.3.0",
|
| 4713 |
+
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
| 4714 |
+
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
| 4715 |
+
"dev": true,
|
| 4716 |
+
"license": "MIT",
|
| 4717 |
+
"dependencies": {
|
| 4718 |
+
"entities": "^6.0.0"
|
| 4719 |
+
},
|
| 4720 |
+
"funding": {
|
| 4721 |
+
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
| 4722 |
+
}
|
| 4723 |
+
},
|
| 4724 |
+
"node_modules/parse5/node_modules/entities": {
|
| 4725 |
+
"version": "6.0.1",
|
| 4726 |
+
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
| 4727 |
+
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
| 4728 |
+
"dev": true,
|
| 4729 |
+
"license": "BSD-2-Clause",
|
| 4730 |
+
"engines": {
|
| 4731 |
+
"node": ">=0.12"
|
| 4732 |
+
},
|
| 4733 |
+
"funding": {
|
| 4734 |
+
"url": "https://github.com/fb55/entities?sponsor=1"
|
| 4735 |
+
}
|
| 4736 |
+
},
|
| 4737 |
"node_modules/path-exists": {
|
| 4738 |
"version": "4.0.0",
|
| 4739 |
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
|
|
|
| 4754 |
"node": ">=8"
|
| 4755 |
}
|
| 4756 |
},
|
| 4757 |
+
"node_modules/pathe": {
|
| 4758 |
+
"version": "2.0.3",
|
| 4759 |
+
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
| 4760 |
+
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
| 4761 |
+
"dev": true,
|
| 4762 |
+
"license": "MIT"
|
| 4763 |
+
},
|
| 4764 |
+
"node_modules/pathval": {
|
| 4765 |
+
"version": "2.0.1",
|
| 4766 |
+
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
|
| 4767 |
+
"integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
|
| 4768 |
+
"dev": true,
|
| 4769 |
+
"license": "MIT",
|
| 4770 |
+
"engines": {
|
| 4771 |
+
"node": ">= 14.16"
|
| 4772 |
+
}
|
| 4773 |
+
},
|
| 4774 |
"node_modules/picocolors": {
|
| 4775 |
"version": "1.1.1",
|
| 4776 |
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
|
|
|
| 4835 |
"node": ">= 0.8.0"
|
| 4836 |
}
|
| 4837 |
},
|
| 4838 |
+
"node_modules/pretty-format": {
|
| 4839 |
+
"version": "27.5.1",
|
| 4840 |
+
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
| 4841 |
+
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
| 4842 |
+
"dev": true,
|
| 4843 |
+
"license": "MIT",
|
| 4844 |
+
"peer": true,
|
| 4845 |
+
"dependencies": {
|
| 4846 |
+
"ansi-regex": "^5.0.1",
|
| 4847 |
+
"ansi-styles": "^5.0.0",
|
| 4848 |
+
"react-is": "^17.0.1"
|
| 4849 |
+
},
|
| 4850 |
+
"engines": {
|
| 4851 |
+
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
| 4852 |
+
}
|
| 4853 |
+
},
|
| 4854 |
+
"node_modules/pretty-format/node_modules/ansi-styles": {
|
| 4855 |
+
"version": "5.2.0",
|
| 4856 |
+
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
| 4857 |
+
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
| 4858 |
+
"dev": true,
|
| 4859 |
+
"license": "MIT",
|
| 4860 |
+
"peer": true,
|
| 4861 |
+
"engines": {
|
| 4862 |
+
"node": ">=10"
|
| 4863 |
+
},
|
| 4864 |
+
"funding": {
|
| 4865 |
+
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
| 4866 |
+
}
|
| 4867 |
+
},
|
| 4868 |
+
"node_modules/pretty-format/node_modules/react-is": {
|
| 4869 |
+
"version": "17.0.2",
|
| 4870 |
+
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
| 4871 |
+
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
| 4872 |
+
"dev": true,
|
| 4873 |
+
"license": "MIT",
|
| 4874 |
+
"peer": true
|
| 4875 |
+
},
|
| 4876 |
"node_modules/process-nextick-args": {
|
| 4877 |
"version": "2.0.1",
|
| 4878 |
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
|
|
|
| 5139 |
"util-deprecate": "~1.0.1"
|
| 5140 |
}
|
| 5141 |
},
|
| 5142 |
+
"node_modules/redent": {
|
| 5143 |
+
"version": "3.0.0",
|
| 5144 |
+
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
| 5145 |
+
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
|
| 5146 |
+
"dev": true,
|
| 5147 |
+
"license": "MIT",
|
| 5148 |
+
"dependencies": {
|
| 5149 |
+
"indent-string": "^4.0.0",
|
| 5150 |
+
"strip-indent": "^3.0.0"
|
| 5151 |
+
},
|
| 5152 |
+
"engines": {
|
| 5153 |
+
"node": ">=8"
|
| 5154 |
+
}
|
| 5155 |
+
},
|
| 5156 |
"node_modules/resolve-from": {
|
| 5157 |
"version": "4.0.0",
|
| 5158 |
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
|
|
|
| 5213 |
"fsevents": "~2.3.2"
|
| 5214 |
}
|
| 5215 |
},
|
| 5216 |
+
"node_modules/rrweb-cssom": {
|
| 5217 |
+
"version": "0.8.0",
|
| 5218 |
+
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
|
| 5219 |
+
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
|
| 5220 |
+
"dev": true,
|
| 5221 |
+
"license": "MIT"
|
| 5222 |
+
},
|
| 5223 |
"node_modules/run-parallel": {
|
| 5224 |
"version": "1.2.0",
|
| 5225 |
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
|
|
|
| 5250 |
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
| 5251 |
"license": "MIT"
|
| 5252 |
},
|
| 5253 |
+
"node_modules/safer-buffer": {
|
| 5254 |
+
"version": "2.1.2",
|
| 5255 |
+
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
| 5256 |
+
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
| 5257 |
+
"dev": true,
|
| 5258 |
+
"license": "MIT"
|
| 5259 |
+
},
|
| 5260 |
"node_modules/sanitize-html": {
|
| 5261 |
"version": "2.17.0",
|
| 5262 |
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.0.tgz",
|
|
|
|
| 5271 |
"postcss": "^8.3.11"
|
| 5272 |
}
|
| 5273 |
},
|
| 5274 |
+
"node_modules/saxes": {
|
| 5275 |
+
"version": "6.0.0",
|
| 5276 |
+
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
| 5277 |
+
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
| 5278 |
+
"dev": true,
|
| 5279 |
+
"license": "ISC",
|
| 5280 |
+
"dependencies": {
|
| 5281 |
+
"xmlchars": "^2.2.0"
|
| 5282 |
+
},
|
| 5283 |
+
"engines": {
|
| 5284 |
+
"node": ">=v12.22.7"
|
| 5285 |
+
}
|
| 5286 |
+
},
|
| 5287 |
"node_modules/scheduler": {
|
| 5288 |
"version": "0.23.2",
|
| 5289 |
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
|
|
|
| 5335 |
"node": ">=8"
|
| 5336 |
}
|
| 5337 |
},
|
| 5338 |
+
"node_modules/siginfo": {
|
| 5339 |
+
"version": "2.0.0",
|
| 5340 |
+
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
| 5341 |
+
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
| 5342 |
+
"dev": true,
|
| 5343 |
+
"license": "ISC"
|
| 5344 |
+
},
|
| 5345 |
"node_modules/source-map-js": {
|
| 5346 |
"version": "1.2.1",
|
| 5347 |
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
|
|
|
| 5351 |
"node": ">=0.10.0"
|
| 5352 |
}
|
| 5353 |
},
|
| 5354 |
+
"node_modules/stackback": {
|
| 5355 |
+
"version": "0.0.2",
|
| 5356 |
+
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
| 5357 |
+
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
| 5358 |
+
"dev": true,
|
| 5359 |
+
"license": "MIT"
|
| 5360 |
+
},
|
| 5361 |
+
"node_modules/std-env": {
|
| 5362 |
+
"version": "3.9.0",
|
| 5363 |
+
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz",
|
| 5364 |
+
"integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==",
|
| 5365 |
+
"dev": true,
|
| 5366 |
+
"license": "MIT"
|
| 5367 |
+
},
|
| 5368 |
"node_modules/string_decoder": {
|
| 5369 |
"version": "1.1.1",
|
| 5370 |
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
|
|
|
| 5374 |
"safe-buffer": "~5.1.0"
|
| 5375 |
}
|
| 5376 |
},
|
| 5377 |
+
"node_modules/strip-indent": {
|
| 5378 |
+
"version": "3.0.0",
|
| 5379 |
+
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
|
| 5380 |
+
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
|
| 5381 |
+
"dev": true,
|
| 5382 |
+
"license": "MIT",
|
| 5383 |
+
"dependencies": {
|
| 5384 |
+
"min-indent": "^1.0.0"
|
| 5385 |
+
},
|
| 5386 |
+
"engines": {
|
| 5387 |
+
"node": ">=8"
|
| 5388 |
+
}
|
| 5389 |
+
},
|
| 5390 |
"node_modules/strip-json-comments": {
|
| 5391 |
"version": "3.1.1",
|
| 5392 |
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
|
|
|
| 5400 |
"url": "https://github.com/sponsors/sindresorhus"
|
| 5401 |
}
|
| 5402 |
},
|
| 5403 |
+
"node_modules/strip-literal": {
|
| 5404 |
+
"version": "3.0.0",
|
| 5405 |
+
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz",
|
| 5406 |
+
"integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==",
|
| 5407 |
+
"dev": true,
|
| 5408 |
+
"license": "MIT",
|
| 5409 |
+
"dependencies": {
|
| 5410 |
+
"js-tokens": "^9.0.1"
|
| 5411 |
+
},
|
| 5412 |
+
"funding": {
|
| 5413 |
+
"url": "https://github.com/sponsors/antfu"
|
| 5414 |
+
}
|
| 5415 |
+
},
|
| 5416 |
+
"node_modules/strip-literal/node_modules/js-tokens": {
|
| 5417 |
+
"version": "9.0.1",
|
| 5418 |
+
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
|
| 5419 |
+
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
|
| 5420 |
+
"dev": true,
|
| 5421 |
+
"license": "MIT"
|
| 5422 |
+
},
|
| 5423 |
"node_modules/supports-color": {
|
| 5424 |
"version": "7.2.0",
|
| 5425 |
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
|
|
|
| 5433 |
"node": ">=8"
|
| 5434 |
}
|
| 5435 |
},
|
| 5436 |
+
"node_modules/symbol-tree": {
|
| 5437 |
+
"version": "3.2.4",
|
| 5438 |
+
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
| 5439 |
+
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
| 5440 |
+
"dev": true,
|
| 5441 |
+
"license": "MIT"
|
| 5442 |
+
},
|
| 5443 |
"node_modules/tailwindcss": {
|
| 5444 |
"version": "4.1.11",
|
| 5445 |
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
|
|
|
|
| 5475 |
"node": ">=18"
|
| 5476 |
}
|
| 5477 |
},
|
| 5478 |
+
"node_modules/tinybench": {
|
| 5479 |
+
"version": "2.9.0",
|
| 5480 |
+
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
| 5481 |
+
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
| 5482 |
+
"dev": true,
|
| 5483 |
+
"license": "MIT"
|
| 5484 |
+
},
|
| 5485 |
+
"node_modules/tinyexec": {
|
| 5486 |
+
"version": "0.3.2",
|
| 5487 |
+
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
|
| 5488 |
+
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
|
| 5489 |
+
"dev": true,
|
| 5490 |
+
"license": "MIT"
|
| 5491 |
+
},
|
| 5492 |
"node_modules/tinyglobby": {
|
| 5493 |
"version": "0.2.14",
|
| 5494 |
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
|
|
|
| 5531 |
"url": "https://github.com/sponsors/jonschlinkert"
|
| 5532 |
}
|
| 5533 |
},
|
| 5534 |
+
"node_modules/tinypool": {
|
| 5535 |
+
"version": "1.1.1",
|
| 5536 |
+
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
|
| 5537 |
+
"integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
|
| 5538 |
+
"dev": true,
|
| 5539 |
+
"license": "MIT",
|
| 5540 |
+
"engines": {
|
| 5541 |
+
"node": "^18.0.0 || >=20.0.0"
|
| 5542 |
+
}
|
| 5543 |
+
},
|
| 5544 |
+
"node_modules/tinyrainbow": {
|
| 5545 |
+
"version": "2.0.0",
|
| 5546 |
+
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
|
| 5547 |
+
"integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
|
| 5548 |
+
"dev": true,
|
| 5549 |
+
"license": "MIT",
|
| 5550 |
+
"engines": {
|
| 5551 |
+
"node": ">=14.0.0"
|
| 5552 |
+
}
|
| 5553 |
+
},
|
| 5554 |
+
"node_modules/tinyspy": {
|
| 5555 |
+
"version": "4.0.3",
|
| 5556 |
+
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz",
|
| 5557 |
+
"integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==",
|
| 5558 |
+
"dev": true,
|
| 5559 |
+
"license": "MIT",
|
| 5560 |
+
"engines": {
|
| 5561 |
+
"node": ">=14.0.0"
|
| 5562 |
+
}
|
| 5563 |
+
},
|
| 5564 |
+
"node_modules/tldts": {
|
| 5565 |
+
"version": "6.1.86",
|
| 5566 |
+
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
|
| 5567 |
+
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
|
| 5568 |
+
"dev": true,
|
| 5569 |
+
"license": "MIT",
|
| 5570 |
+
"dependencies": {
|
| 5571 |
+
"tldts-core": "^6.1.86"
|
| 5572 |
+
},
|
| 5573 |
+
"bin": {
|
| 5574 |
+
"tldts": "bin/cli.js"
|
| 5575 |
+
}
|
| 5576 |
+
},
|
| 5577 |
+
"node_modules/tldts-core": {
|
| 5578 |
+
"version": "6.1.86",
|
| 5579 |
+
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
|
| 5580 |
+
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
|
| 5581 |
+
"dev": true,
|
| 5582 |
+
"license": "MIT"
|
| 5583 |
+
},
|
| 5584 |
"node_modules/to-regex-range": {
|
| 5585 |
"version": "5.0.1",
|
| 5586 |
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
|
|
|
| 5594 |
"node": ">=8.0"
|
| 5595 |
}
|
| 5596 |
},
|
| 5597 |
+
"node_modules/tough-cookie": {
|
| 5598 |
+
"version": "5.1.2",
|
| 5599 |
+
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
|
| 5600 |
+
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
|
| 5601 |
+
"dev": true,
|
| 5602 |
+
"license": "BSD-3-Clause",
|
| 5603 |
+
"dependencies": {
|
| 5604 |
+
"tldts": "^6.1.32"
|
| 5605 |
+
},
|
| 5606 |
+
"engines": {
|
| 5607 |
+
"node": ">=16"
|
| 5608 |
+
}
|
| 5609 |
+
},
|
| 5610 |
+
"node_modules/tr46": {
|
| 5611 |
+
"version": "5.1.1",
|
| 5612 |
+
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
| 5613 |
+
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
| 5614 |
+
"dev": true,
|
| 5615 |
+
"license": "MIT",
|
| 5616 |
+
"dependencies": {
|
| 5617 |
+
"punycode": "^2.3.1"
|
| 5618 |
+
},
|
| 5619 |
+
"engines": {
|
| 5620 |
+
"node": ">=18"
|
| 5621 |
+
}
|
| 5622 |
+
},
|
| 5623 |
"node_modules/ts-api-utils": {
|
| 5624 |
"version": "2.1.0",
|
| 5625 |
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
|
|
|
| 5860 |
}
|
| 5861 |
}
|
| 5862 |
},
|
| 5863 |
+
"node_modules/vite-node": {
|
| 5864 |
+
"version": "3.2.4",
|
| 5865 |
+
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
|
| 5866 |
+
"integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
|
| 5867 |
+
"dev": true,
|
| 5868 |
+
"license": "MIT",
|
| 5869 |
+
"dependencies": {
|
| 5870 |
+
"cac": "^6.7.14",
|
| 5871 |
+
"debug": "^4.4.1",
|
| 5872 |
+
"es-module-lexer": "^1.7.0",
|
| 5873 |
+
"pathe": "^2.0.3",
|
| 5874 |
+
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
|
| 5875 |
+
},
|
| 5876 |
+
"bin": {
|
| 5877 |
+
"vite-node": "vite-node.mjs"
|
| 5878 |
+
},
|
| 5879 |
+
"engines": {
|
| 5880 |
+
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
| 5881 |
+
},
|
| 5882 |
+
"funding": {
|
| 5883 |
+
"url": "https://opencollective.com/vitest"
|
| 5884 |
+
}
|
| 5885 |
+
},
|
| 5886 |
"node_modules/vite/node_modules/fdir": {
|
| 5887 |
"version": "6.5.0",
|
| 5888 |
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
|
|
|
| 5912 |
"url": "https://github.com/sponsors/jonschlinkert"
|
| 5913 |
}
|
| 5914 |
},
|
| 5915 |
+
"node_modules/vitest": {
|
| 5916 |
+
"version": "3.2.4",
|
| 5917 |
+
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
| 5918 |
+
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
| 5919 |
+
"dev": true,
|
| 5920 |
+
"license": "MIT",
|
| 5921 |
+
"dependencies": {
|
| 5922 |
+
"@types/chai": "^5.2.2",
|
| 5923 |
+
"@vitest/expect": "3.2.4",
|
| 5924 |
+
"@vitest/mocker": "3.2.4",
|
| 5925 |
+
"@vitest/pretty-format": "^3.2.4",
|
| 5926 |
+
"@vitest/runner": "3.2.4",
|
| 5927 |
+
"@vitest/snapshot": "3.2.4",
|
| 5928 |
+
"@vitest/spy": "3.2.4",
|
| 5929 |
+
"@vitest/utils": "3.2.4",
|
| 5930 |
+
"chai": "^5.2.0",
|
| 5931 |
+
"debug": "^4.4.1",
|
| 5932 |
+
"expect-type": "^1.2.1",
|
| 5933 |
+
"magic-string": "^0.30.17",
|
| 5934 |
+
"pathe": "^2.0.3",
|
| 5935 |
+
"picomatch": "^4.0.2",
|
| 5936 |
+
"std-env": "^3.9.0",
|
| 5937 |
+
"tinybench": "^2.9.0",
|
| 5938 |
+
"tinyexec": "^0.3.2",
|
| 5939 |
+
"tinyglobby": "^0.2.14",
|
| 5940 |
+
"tinypool": "^1.1.1",
|
| 5941 |
+
"tinyrainbow": "^2.0.0",
|
| 5942 |
+
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
|
| 5943 |
+
"vite-node": "3.2.4",
|
| 5944 |
+
"why-is-node-running": "^2.3.0"
|
| 5945 |
+
},
|
| 5946 |
+
"bin": {
|
| 5947 |
+
"vitest": "vitest.mjs"
|
| 5948 |
+
},
|
| 5949 |
+
"engines": {
|
| 5950 |
+
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
| 5951 |
+
},
|
| 5952 |
+
"funding": {
|
| 5953 |
+
"url": "https://opencollective.com/vitest"
|
| 5954 |
+
},
|
| 5955 |
+
"peerDependencies": {
|
| 5956 |
+
"@edge-runtime/vm": "*",
|
| 5957 |
+
"@types/debug": "^4.1.12",
|
| 5958 |
+
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
| 5959 |
+
"@vitest/browser": "3.2.4",
|
| 5960 |
+
"@vitest/ui": "3.2.4",
|
| 5961 |
+
"happy-dom": "*",
|
| 5962 |
+
"jsdom": "*"
|
| 5963 |
+
},
|
| 5964 |
+
"peerDependenciesMeta": {
|
| 5965 |
+
"@edge-runtime/vm": {
|
| 5966 |
+
"optional": true
|
| 5967 |
+
},
|
| 5968 |
+
"@types/debug": {
|
| 5969 |
+
"optional": true
|
| 5970 |
+
},
|
| 5971 |
+
"@types/node": {
|
| 5972 |
+
"optional": true
|
| 5973 |
+
},
|
| 5974 |
+
"@vitest/browser": {
|
| 5975 |
+
"optional": true
|
| 5976 |
+
},
|
| 5977 |
+
"@vitest/ui": {
|
| 5978 |
+
"optional": true
|
| 5979 |
+
},
|
| 5980 |
+
"happy-dom": {
|
| 5981 |
+
"optional": true
|
| 5982 |
+
},
|
| 5983 |
+
"jsdom": {
|
| 5984 |
+
"optional": true
|
| 5985 |
+
}
|
| 5986 |
+
}
|
| 5987 |
+
},
|
| 5988 |
+
"node_modules/vitest/node_modules/picomatch": {
|
| 5989 |
+
"version": "4.0.3",
|
| 5990 |
+
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
| 5991 |
+
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
| 5992 |
+
"dev": true,
|
| 5993 |
+
"license": "MIT",
|
| 5994 |
+
"engines": {
|
| 5995 |
+
"node": ">=12"
|
| 5996 |
+
},
|
| 5997 |
+
"funding": {
|
| 5998 |
+
"url": "https://github.com/sponsors/jonschlinkert"
|
| 5999 |
+
}
|
| 6000 |
+
},
|
| 6001 |
+
"node_modules/w3c-xmlserializer": {
|
| 6002 |
+
"version": "5.0.0",
|
| 6003 |
+
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
| 6004 |
+
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
| 6005 |
+
"dev": true,
|
| 6006 |
+
"license": "MIT",
|
| 6007 |
+
"dependencies": {
|
| 6008 |
+
"xml-name-validator": "^5.0.0"
|
| 6009 |
+
},
|
| 6010 |
+
"engines": {
|
| 6011 |
+
"node": ">=18"
|
| 6012 |
+
}
|
| 6013 |
+
},
|
| 6014 |
+
"node_modules/webidl-conversions": {
|
| 6015 |
+
"version": "7.0.0",
|
| 6016 |
+
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
| 6017 |
+
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
| 6018 |
+
"dev": true,
|
| 6019 |
+
"license": "BSD-2-Clause",
|
| 6020 |
+
"engines": {
|
| 6021 |
+
"node": ">=12"
|
| 6022 |
+
}
|
| 6023 |
+
},
|
| 6024 |
+
"node_modules/whatwg-encoding": {
|
| 6025 |
+
"version": "3.1.1",
|
| 6026 |
+
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
| 6027 |
+
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
| 6028 |
+
"dev": true,
|
| 6029 |
+
"license": "MIT",
|
| 6030 |
+
"dependencies": {
|
| 6031 |
+
"iconv-lite": "0.6.3"
|
| 6032 |
+
},
|
| 6033 |
+
"engines": {
|
| 6034 |
+
"node": ">=18"
|
| 6035 |
+
}
|
| 6036 |
+
},
|
| 6037 |
+
"node_modules/whatwg-mimetype": {
|
| 6038 |
+
"version": "4.0.0",
|
| 6039 |
+
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
| 6040 |
+
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
| 6041 |
+
"dev": true,
|
| 6042 |
+
"license": "MIT",
|
| 6043 |
+
"engines": {
|
| 6044 |
+
"node": ">=18"
|
| 6045 |
+
}
|
| 6046 |
+
},
|
| 6047 |
+
"node_modules/whatwg-url": {
|
| 6048 |
+
"version": "14.2.0",
|
| 6049 |
+
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
| 6050 |
+
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
| 6051 |
+
"dev": true,
|
| 6052 |
+
"license": "MIT",
|
| 6053 |
+
"dependencies": {
|
| 6054 |
+
"tr46": "^5.1.0",
|
| 6055 |
+
"webidl-conversions": "^7.0.0"
|
| 6056 |
+
},
|
| 6057 |
+
"engines": {
|
| 6058 |
+
"node": ">=18"
|
| 6059 |
+
}
|
| 6060 |
+
},
|
| 6061 |
"node_modules/which": {
|
| 6062 |
"version": "2.0.2",
|
| 6063 |
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
|
|
|
| 6074 |
"node": ">= 8"
|
| 6075 |
}
|
| 6076 |
},
|
| 6077 |
+
"node_modules/why-is-node-running": {
|
| 6078 |
+
"version": "2.3.0",
|
| 6079 |
+
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
| 6080 |
+
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
| 6081 |
+
"dev": true,
|
| 6082 |
+
"license": "MIT",
|
| 6083 |
+
"dependencies": {
|
| 6084 |
+
"siginfo": "^2.0.0",
|
| 6085 |
+
"stackback": "0.0.2"
|
| 6086 |
+
},
|
| 6087 |
+
"bin": {
|
| 6088 |
+
"why-is-node-running": "cli.js"
|
| 6089 |
+
},
|
| 6090 |
+
"engines": {
|
| 6091 |
+
"node": ">=8"
|
| 6092 |
+
}
|
| 6093 |
+
},
|
| 6094 |
"node_modules/word-wrap": {
|
| 6095 |
"version": "1.2.5",
|
| 6096 |
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
|
|
|
| 6101 |
"node": ">=0.10.0"
|
| 6102 |
}
|
| 6103 |
},
|
| 6104 |
+
"node_modules/ws": {
|
| 6105 |
+
"version": "8.18.3",
|
| 6106 |
+
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
| 6107 |
+
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
| 6108 |
+
"dev": true,
|
| 6109 |
+
"license": "MIT",
|
| 6110 |
+
"engines": {
|
| 6111 |
+
"node": ">=10.0.0"
|
| 6112 |
+
},
|
| 6113 |
+
"peerDependencies": {
|
| 6114 |
+
"bufferutil": "^4.0.1",
|
| 6115 |
+
"utf-8-validate": ">=5.0.2"
|
| 6116 |
+
},
|
| 6117 |
+
"peerDependenciesMeta": {
|
| 6118 |
+
"bufferutil": {
|
| 6119 |
+
"optional": true
|
| 6120 |
+
},
|
| 6121 |
+
"utf-8-validate": {
|
| 6122 |
+
"optional": true
|
| 6123 |
+
}
|
| 6124 |
+
}
|
| 6125 |
+
},
|
| 6126 |
+
"node_modules/xml-name-validator": {
|
| 6127 |
+
"version": "5.0.0",
|
| 6128 |
+
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
| 6129 |
+
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
| 6130 |
+
"dev": true,
|
| 6131 |
+
"license": "Apache-2.0",
|
| 6132 |
+
"engines": {
|
| 6133 |
+
"node": ">=18"
|
| 6134 |
+
}
|
| 6135 |
+
},
|
| 6136 |
+
"node_modules/xmlchars": {
|
| 6137 |
+
"version": "2.2.0",
|
| 6138 |
+
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
| 6139 |
+
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
| 6140 |
+
"dev": true,
|
| 6141 |
+
"license": "MIT"
|
| 6142 |
+
},
|
| 6143 |
"node_modules/yallist": {
|
| 6144 |
"version": "5.0.0",
|
| 6145 |
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
frontend/package.json
CHANGED
|
@@ -7,11 +7,22 @@
|
|
| 7 |
"dev": "vite",
|
| 8 |
"build": "npx vite build",
|
| 9 |
"lint": "eslint .",
|
| 10 |
-
"preview": "vite preview"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
},
|
| 12 |
"devDependencies": {
|
| 13 |
"@eslint/js": "^9.30.1",
|
| 14 |
"@tailwindcss/postcss": "^4.1.11",
|
|
|
|
|
|
|
|
|
|
| 15 |
"@types/node": "^24.1.0",
|
| 16 |
"@types/react": "^19.1.8",
|
| 17 |
"@types/react-dom": "^19.1.6",
|
|
@@ -22,10 +33,12 @@
|
|
| 22 |
"eslint-plugin-react-hooks": "^5.2.0",
|
| 23 |
"eslint-plugin-react-refresh": "^0.4.20",
|
| 24 |
"globals": "^16.3.0",
|
|
|
|
| 25 |
"postcss": "^8.5.6",
|
| 26 |
"tailwindcss": "^4.1.11",
|
| 27 |
"typescript": "~5.8.3",
|
| 28 |
-
"typescript-eslint": "^8.35.1"
|
|
|
|
| 29 |
},
|
| 30 |
"dependencies": {
|
| 31 |
"@ifrc-go/icons": "^2.0.1",
|
|
|
|
| 7 |
"dev": "vite",
|
| 8 |
"build": "npx vite build",
|
| 9 |
"lint": "eslint .",
|
| 10 |
+
"preview": "vite preview",
|
| 11 |
+
"test": "vitest",
|
| 12 |
+
"test:ui": "vitest --ui",
|
| 13 |
+
"test:run": "vitest run",
|
| 14 |
+
"test:coverage": "vitest run --coverage",
|
| 15 |
+
"test:unit": "vitest run src/test/unit_tests --reporter=verbose",
|
| 16 |
+
"test:unit:watch": "vitest src/test/unit_tests",
|
| 17 |
+
"test:unit:coverage": "vitest run src/test/unit_tests --coverage",
|
| 18 |
+
"test:integration": "vitest run src/test/integration --reporter=verbose"
|
| 19 |
},
|
| 20 |
"devDependencies": {
|
| 21 |
"@eslint/js": "^9.30.1",
|
| 22 |
"@tailwindcss/postcss": "^4.1.11",
|
| 23 |
+
"@testing-library/jest-dom": "^6.8.0",
|
| 24 |
+
"@testing-library/react": "^16.3.0",
|
| 25 |
+
"@testing-library/user-event": "^14.6.1",
|
| 26 |
"@types/node": "^24.1.0",
|
| 27 |
"@types/react": "^19.1.8",
|
| 28 |
"@types/react-dom": "^19.1.6",
|
|
|
|
| 33 |
"eslint-plugin-react-hooks": "^5.2.0",
|
| 34 |
"eslint-plugin-react-refresh": "^0.4.20",
|
| 35 |
"globals": "^16.3.0",
|
| 36 |
+
"jsdom": "^26.1.0",
|
| 37 |
"postcss": "^8.5.6",
|
| 38 |
"tailwindcss": "^4.1.11",
|
| 39 |
"typescript": "~5.8.3",
|
| 40 |
+
"typescript-eslint": "^8.35.1",
|
| 41 |
+
"vitest": "^3.2.4"
|
| 42 |
},
|
| 43 |
"dependencies": {
|
| 44 |
"@ifrc-go/icons": "^2.0.1",
|
frontend/src/pages/UploadPage/UploadPage.module.css
CHANGED
|
@@ -257,7 +257,7 @@
|
|
| 257 |
margin-left: var(--go-ui-spacing-sm);
|
| 258 |
width: 2.5rem;
|
| 259 |
text-align: right;
|
| 260 |
-
tabular-nums
|
| 261 |
flex-shrink: 0;
|
| 262 |
font-size: var(--go-ui-font-size-sm);
|
| 263 |
color: var(--go-ui-color-gray-70);
|
|
|
|
| 257 |
margin-left: var(--go-ui-spacing-sm);
|
| 258 |
width: 2.5rem;
|
| 259 |
text-align: right;
|
| 260 |
+
font-variant-numeric: tabular-nums;
|
| 261 |
flex-shrink: 0;
|
| 262 |
font-size: var(--go-ui-font-size-sm);
|
| 263 |
color: var(--go-ui-color-gray-70);
|
frontend/src/test/integration/AppWorkflow.test.tsx
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { render, screen} from '@testing-library/react';
|
| 3 |
+
import userEvent from '@testing-library/user-event';
|
| 4 |
+
import { vi, describe, test, beforeEach, expect } from 'vitest';
|
| 5 |
+
import { BrowserRouter } from 'react-router-dom';
|
| 6 |
+
import { FilterProvider } from '../../contexts/FilterContext';
|
| 7 |
+
import { AdminProvider } from '../../contexts/AdminContext';
|
| 8 |
+
import HeaderNav from '../../components/HeaderNav';
|
| 9 |
+
import FilterBar from '../../components/FilterBar';
|
| 10 |
+
import ExportModal from '../../components/ExportModal';
|
| 11 |
+
import HelpPage from '../../pages/HelpPage';
|
| 12 |
+
|
| 13 |
+
// Mock react-router-dom
|
| 14 |
+
const mockNavigate = vi.fn();
|
| 15 |
+
const mockUseLocation = vi.fn();
|
| 16 |
+
|
| 17 |
+
vi.mock('react-router-dom', () => ({
|
| 18 |
+
BrowserRouter: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
| 19 |
+
useNavigate: () => mockNavigate,
|
| 20 |
+
useLocation: () => mockUseLocation(),
|
| 21 |
+
}));
|
| 22 |
+
|
| 23 |
+
// Mock the useFilterContext hook
|
| 24 |
+
const mockUseFilterContext = {
|
| 25 |
+
search: '',
|
| 26 |
+
srcFilter: '',
|
| 27 |
+
catFilter: '',
|
| 28 |
+
regionFilter: '',
|
| 29 |
+
countryFilter: '',
|
| 30 |
+
imageTypeFilter: '',
|
| 31 |
+
showReferenceExamples: false,
|
| 32 |
+
setSearch: vi.fn(),
|
| 33 |
+
setSrcFilter: vi.fn(),
|
| 34 |
+
setCatFilter: vi.fn(),
|
| 35 |
+
setRegionFilter: vi.fn(),
|
| 36 |
+
setCountryFilter: vi.fn(),
|
| 37 |
+
setImageTypeFilter: vi.fn(),
|
| 38 |
+
setShowReferenceExamples: vi.fn(),
|
| 39 |
+
clearAllFilters: vi.fn(),
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
// Mock the useAdminContext hook
|
| 43 |
+
const mockUseAdminContext = {
|
| 44 |
+
isAdmin: true,
|
| 45 |
+
login: vi.fn(),
|
| 46 |
+
logout: vi.fn(),
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
vi.mock('../../hooks/useFilterContext', () => ({
|
| 50 |
+
useFilterContext: () => mockUseFilterContext,
|
| 51 |
+
}));
|
| 52 |
+
|
| 53 |
+
vi.mock('../../hooks/useAdminContext', () => ({
|
| 54 |
+
useAdminContext: () => mockUseAdminContext,
|
| 55 |
+
}));
|
| 56 |
+
|
| 57 |
+
// Mock JSZip
|
| 58 |
+
vi.mock('jszip', () => ({
|
| 59 |
+
__esModule: true,
|
| 60 |
+
default: vi.fn().mockImplementation(() => ({
|
| 61 |
+
file: vi.fn(),
|
| 62 |
+
generateAsync: vi.fn().mockResolvedValue('mock-zip-data'),
|
| 63 |
+
})),
|
| 64 |
+
}));
|
| 65 |
+
|
| 66 |
+
describe('App Workflow Integration', () => {
|
| 67 |
+
const mockProps = {
|
| 68 |
+
sources: [{ s_code: 'WFP', label: 'World Food Programme' }, { s_code: 'IFRC', label: 'IFRC' }],
|
| 69 |
+
types: [{ t_code: 'EARTHQUAKE', label: 'Earthquake' }, { t_code: 'FLOOD', label: 'Flood' }],
|
| 70 |
+
regions: [{ r_code: 'ASIA', label: 'Asia' }, { r_code: 'AFRICA', label: 'Africa' }],
|
| 71 |
+
countries: [{ c_code: 'BD', label: 'Bangladesh', r_code: 'ASIA' }, { c_code: 'IN', label: 'India', r_code: 'ASIA' }],
|
| 72 |
+
imageTypes: [{ image_type: 'SATELLITE', label: 'Satellite' }, { image_type: 'AERIAL', label: 'Aerial' }],
|
| 73 |
+
};
|
| 74 |
+
|
| 75 |
+
beforeEach(() => {
|
| 76 |
+
vi.clearAllMocks();
|
| 77 |
+
mockUseLocation.mockReturnValue({ pathname: '/' });
|
| 78 |
+
});
|
| 79 |
+
|
| 80 |
+
test('Complete user workflow: navigate, filter, and export', async () => {
|
| 81 |
+
const user = userEvent.setup();
|
| 82 |
+
const mockOnClose = vi.fn();
|
| 83 |
+
const mockOnExport = vi.fn();
|
| 84 |
+
|
| 85 |
+
render(
|
| 86 |
+
<BrowserRouter>
|
| 87 |
+
<FilterProvider>
|
| 88 |
+
<AdminProvider>
|
| 89 |
+
<HeaderNav />
|
| 90 |
+
<FilterBar {...mockProps} />
|
| 91 |
+
<ExportModal
|
| 92 |
+
isOpen={true}
|
| 93 |
+
onClose={mockOnClose}
|
| 94 |
+
onExport={mockOnExport}
|
| 95 |
+
filteredCount={2}
|
| 96 |
+
totalCount={10}
|
| 97 |
+
hasFilters={true}
|
| 98 |
+
crisisMapsCount={1}
|
| 99 |
+
droneImagesCount={1}
|
| 100 |
+
variant="bulk"
|
| 101 |
+
/>
|
| 102 |
+
</AdminProvider>
|
| 103 |
+
</FilterProvider>
|
| 104 |
+
</BrowserRouter>
|
| 105 |
+
);
|
| 106 |
+
|
| 107 |
+
// Step 1: Navigate to help page
|
| 108 |
+
const helpButton = screen.getByRole('button', { name: /help/i });
|
| 109 |
+
await user.click(helpButton);
|
| 110 |
+
expect(mockNavigate).toHaveBeenCalledWith('/help');
|
| 111 |
+
|
| 112 |
+
// Step 2: Apply filters
|
| 113 |
+
const sourceInput = screen.getByPlaceholderText('All Sources');
|
| 114 |
+
const categoryInput = screen.getByPlaceholderText('All Categories');
|
| 115 |
+
|
| 116 |
+
await user.click(sourceInput);
|
| 117 |
+
const wfpOption = screen.getByText('World Food Programme');
|
| 118 |
+
await user.click(wfpOption);
|
| 119 |
+
expect(mockUseFilterContext.setSrcFilter).toHaveBeenCalledWith('WFP');
|
| 120 |
+
|
| 121 |
+
await user.click(categoryInput);
|
| 122 |
+
const earthquakeOption = screen.getByText('Earthquake');
|
| 123 |
+
await user.click(earthquakeOption);
|
| 124 |
+
expect(mockUseFilterContext.setCatFilter).toHaveBeenCalledWith('EARTHQUAKE');
|
| 125 |
+
|
| 126 |
+
// Step 3: Check export modal
|
| 127 |
+
expect(screen.getByText(/Crisis Maps/i)).toBeInTheDocument();
|
| 128 |
+
expect(screen.getByText(/Drone Images/i)).toBeInTheDocument();
|
| 129 |
+
expect(screen.getByRole('button', { name: /export selected/i })).toBeInTheDocument();
|
| 130 |
+
});
|
| 131 |
+
|
| 132 |
+
test('Admin workflow: access admin features and manage data', async () => {
|
| 133 |
+
// Mock admin state
|
| 134 |
+
mockUseAdminContext.isAdmin = true;
|
| 135 |
+
|
| 136 |
+
render(
|
| 137 |
+
<BrowserRouter>
|
| 138 |
+
<FilterProvider>
|
| 139 |
+
<AdminProvider>
|
| 140 |
+
<HeaderNav />
|
| 141 |
+
<ExportModal
|
| 142 |
+
isOpen={true}
|
| 143 |
+
onClose={vi.fn()}
|
| 144 |
+
onExport={vi.fn()}
|
| 145 |
+
filteredCount={1}
|
| 146 |
+
totalCount={10}
|
| 147 |
+
hasFilters={true}
|
| 148 |
+
crisisMapsCount={1}
|
| 149 |
+
droneImagesCount={0}
|
| 150 |
+
variant="bulk"
|
| 151 |
+
/>
|
| 152 |
+
</AdminProvider>
|
| 153 |
+
</FilterProvider>
|
| 154 |
+
</BrowserRouter>
|
| 155 |
+
);
|
| 156 |
+
|
| 157 |
+
// Step 1: Check admin navigation
|
| 158 |
+
const adminButton = screen.getByRole('button', { name: /dev/i });
|
| 159 |
+
expect(adminButton).toBeInTheDocument();
|
| 160 |
+
|
| 161 |
+
// Step 2: Check admin export features
|
| 162 |
+
expect(screen.getByText(/Export Dataset/i)).toBeInTheDocument();
|
| 163 |
+
expect(screen.getByText(/Crisis Maps/i)).toBeInTheDocument();
|
| 164 |
+
expect(screen.getByText(/Drone Images/i)).toBeInTheDocument();
|
| 165 |
+
});
|
| 166 |
+
|
| 167 |
+
test('Filter workflow: apply and clear filters', async () => {
|
| 168 |
+
const user = userEvent.setup();
|
| 169 |
+
|
| 170 |
+
render(
|
| 171 |
+
<BrowserRouter>
|
| 172 |
+
<FilterProvider>
|
| 173 |
+
<AdminProvider>
|
| 174 |
+
<FilterBar {...mockProps} />
|
| 175 |
+
</AdminProvider>
|
| 176 |
+
</FilterProvider>
|
| 177 |
+
</BrowserRouter>
|
| 178 |
+
);
|
| 179 |
+
|
| 180 |
+
// Step 1: Apply multiple filters
|
| 181 |
+
const sourceInput = screen.getByPlaceholderText('All Sources');
|
| 182 |
+
const categoryInput = screen.getByPlaceholderText('All Categories');
|
| 183 |
+
|
| 184 |
+
await user.click(sourceInput);
|
| 185 |
+
const ifrcOption = screen.getByText('IFRC');
|
| 186 |
+
await user.click(ifrcOption);
|
| 187 |
+
|
| 188 |
+
await user.click(categoryInput);
|
| 189 |
+
const floodOption = screen.getByText('Flood');
|
| 190 |
+
await user.click(floodOption);
|
| 191 |
+
|
| 192 |
+
// Step 2: Verify filters are set
|
| 193 |
+
expect(mockUseFilterContext.setSrcFilter).toHaveBeenCalledWith('IFRC');
|
| 194 |
+
expect(mockUseFilterContext.setCatFilter).toHaveBeenCalledWith('FLOOD');
|
| 195 |
+
|
| 196 |
+
// Step 3: Clear all filters
|
| 197 |
+
const clearButton = screen.getByRole('button', { name: /clear/i });
|
| 198 |
+
await user.click(clearButton);
|
| 199 |
+
|
| 200 |
+
expect(mockUseFilterContext.clearAllFilters).toHaveBeenCalled();
|
| 201 |
+
});
|
| 202 |
+
|
| 203 |
+
test('Navigation workflow: move between different pages', async () => {
|
| 204 |
+
const user = userEvent.setup();
|
| 205 |
+
|
| 206 |
+
render(
|
| 207 |
+
<BrowserRouter>
|
| 208 |
+
<FilterProvider>
|
| 209 |
+
<AdminProvider>
|
| 210 |
+
<HeaderNav />
|
| 211 |
+
<HelpPage />
|
| 212 |
+
</AdminProvider>
|
| 213 |
+
</FilterProvider>
|
| 214 |
+
</BrowserRouter>
|
| 215 |
+
);
|
| 216 |
+
|
| 217 |
+
// Step 1: Navigate to help page
|
| 218 |
+
const helpButton = screen.getByRole('button', { name: /help/i });
|
| 219 |
+
await user.click(helpButton);
|
| 220 |
+
expect(mockNavigate).toHaveBeenCalledWith('/help');
|
| 221 |
+
|
| 222 |
+
// Step 2: Check that help page is rendered
|
| 223 |
+
expect(screen.getByRole('heading', { name: /Introduction/i })).toBeInTheDocument();
|
| 224 |
+
});
|
| 225 |
+
|
| 226 |
+
test('Context integration: filters and admin state work together', () => {
|
| 227 |
+
// Mock admin state
|
| 228 |
+
mockUseAdminContext.isAdmin = true;
|
| 229 |
+
|
| 230 |
+
render(
|
| 231 |
+
<BrowserRouter>
|
| 232 |
+
<FilterProvider>
|
| 233 |
+
<AdminProvider>
|
| 234 |
+
<HeaderNav />
|
| 235 |
+
<FilterBar {...mockProps} />
|
| 236 |
+
<ExportModal
|
| 237 |
+
isOpen={true}
|
| 238 |
+
onClose={vi.fn()}
|
| 239 |
+
onExport={vi.fn()}
|
| 240 |
+
filteredCount={1}
|
| 241 |
+
totalCount={10}
|
| 242 |
+
hasFilters={true}
|
| 243 |
+
crisisMapsCount={1}
|
| 244 |
+
droneImagesCount={0}
|
| 245 |
+
variant="bulk"
|
| 246 |
+
/>
|
| 247 |
+
</AdminProvider>
|
| 248 |
+
</FilterProvider>
|
| 249 |
+
</BrowserRouter>
|
| 250 |
+
);
|
| 251 |
+
|
| 252 |
+
// Check that admin features are available
|
| 253 |
+
expect(screen.getByRole('button', { name: /dev/i })).toBeInTheDocument();
|
| 254 |
+
expect(screen.getByText(/Export Dataset/i)).toBeInTheDocument();
|
| 255 |
+
|
| 256 |
+
// Check that filter functionality is available
|
| 257 |
+
expect(screen.getByPlaceholderText('All Sources')).toBeInTheDocument();
|
| 258 |
+
expect(screen.getByPlaceholderText('All Categories')).toBeInTheDocument();
|
| 259 |
+
});
|
| 260 |
+
|
| 261 |
+
test('Error handling workflow: handle missing data gracefully', () => {
|
| 262 |
+
render(
|
| 263 |
+
<BrowserRouter>
|
| 264 |
+
<FilterProvider>
|
| 265 |
+
<AdminProvider>
|
| 266 |
+
<ExportModal
|
| 267 |
+
isOpen={true}
|
| 268 |
+
onClose={vi.fn()}
|
| 269 |
+
onExport={vi.fn()}
|
| 270 |
+
filteredCount={0}
|
| 271 |
+
totalCount={10}
|
| 272 |
+
hasFilters={true}
|
| 273 |
+
crisisMapsCount={0}
|
| 274 |
+
droneImagesCount={0}
|
| 275 |
+
variant="bulk"
|
| 276 |
+
/>
|
| 277 |
+
</AdminProvider>
|
| 278 |
+
</FilterProvider>
|
| 279 |
+
</BrowserRouter>
|
| 280 |
+
);
|
| 281 |
+
|
| 282 |
+
// Check that empty state is handled gracefully
|
| 283 |
+
expect(screen.getByText(/Crisis Maps \(0 images\)/i)).toBeInTheDocument();
|
| 284 |
+
expect(screen.getByText(/Drone Images \(0 images\)/i)).toBeInTheDocument();
|
| 285 |
+
expect(screen.getByRole('button', { name: /export selected/i })).toBeInTheDocument();
|
| 286 |
+
});
|
| 287 |
+
});
|
frontend/src/test/integration/FilterBarWithFilterContext.test.tsx
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { render, screen} from '@testing-library/react';
|
| 2 |
+
import userEvent from '@testing-library/user-event';
|
| 3 |
+
import { vi, describe, test, beforeEach, expect } from 'vitest';
|
| 4 |
+
import { FilterProvider } from '../../contexts/FilterContext';
|
| 5 |
+
import FilterBar from '../../components/FilterBar';
|
| 6 |
+
|
| 7 |
+
// Mock the FilterContext hook to test integration
|
| 8 |
+
const mockUseFilterContext = {
|
| 9 |
+
search: '',
|
| 10 |
+
srcFilter: '',
|
| 11 |
+
catFilter: '',
|
| 12 |
+
regionFilter: '',
|
| 13 |
+
countryFilter: '',
|
| 14 |
+
imageTypeFilter: '',
|
| 15 |
+
showReferenceExamples: false,
|
| 16 |
+
setSearch: vi.fn(),
|
| 17 |
+
setSrcFilter: vi.fn(),
|
| 18 |
+
setCatFilter: vi.fn(),
|
| 19 |
+
setRegionFilter: vi.fn(),
|
| 20 |
+
setCountryFilter: vi.fn(),
|
| 21 |
+
setImageTypeFilter: vi.fn(),
|
| 22 |
+
setShowReferenceExamples: vi.fn(),
|
| 23 |
+
clearAllFilters: vi.fn(),
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
vi.mock('../../hooks/useFilterContext', () => ({
|
| 27 |
+
useFilterContext: () => mockUseFilterContext,
|
| 28 |
+
}));
|
| 29 |
+
|
| 30 |
+
describe('FilterBar + FilterContext Integration', () => {
|
| 31 |
+
const mockProps = {
|
| 32 |
+
sources: [{ s_code: 'WFP', label: 'World Food Programme' }, { s_code: 'IFRC', label: 'IFRC' }],
|
| 33 |
+
types: [{ t_code: 'EARTHQUAKE', label: 'Earthquake' }, { t_code: 'FLOOD', label: 'Flood' }],
|
| 34 |
+
regions: [{ r_code: 'ASIA', label: 'Asia' }, { r_code: 'AFRICA', label: 'Africa' }],
|
| 35 |
+
countries: [{ c_code: 'BD', label: 'Bangladesh', r_code: 'ASIA' }, { c_code: 'IN', label: 'India', r_code: 'ASIA' }],
|
| 36 |
+
imageTypes: [{ image_type: 'SATELLITE', label: 'Satellite' }, { image_type: 'AERIAL', label: 'Aerial' }],
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
beforeEach(() => {
|
| 40 |
+
vi.clearAllMocks();
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
test('FilterBar updates context when filters change', async () => {
|
| 44 |
+
const user = userEvent.setup();
|
| 45 |
+
|
| 46 |
+
render(
|
| 47 |
+
<FilterProvider>
|
| 48 |
+
<FilterBar {...mockProps} />
|
| 49 |
+
</FilterProvider>
|
| 50 |
+
);
|
| 51 |
+
|
| 52 |
+
// Find and interact with filter inputs
|
| 53 |
+
const sourceInput = screen.getByPlaceholderText('All Sources');
|
| 54 |
+
const categoryInput = screen.getByPlaceholderText('All Categories');
|
| 55 |
+
|
| 56 |
+
// Select source filter
|
| 57 |
+
await user.click(sourceInput);
|
| 58 |
+
const wfpOption = screen.getByText('World Food Programme');
|
| 59 |
+
await user.click(wfpOption);
|
| 60 |
+
expect(mockUseFilterContext.setSrcFilter).toHaveBeenCalledWith('WFP');
|
| 61 |
+
|
| 62 |
+
// Select category filter
|
| 63 |
+
await user.click(categoryInput);
|
| 64 |
+
const earthquakeOption = screen.getByText('Earthquake');
|
| 65 |
+
await user.click(earthquakeOption);
|
| 66 |
+
expect(mockUseFilterContext.setCatFilter).toHaveBeenCalledWith('EARTHQUAKE');
|
| 67 |
+
});
|
| 68 |
+
|
| 69 |
+
test('FilterBar clears context when clear button is clicked', async () => {
|
| 70 |
+
const user = userEvent.setup();
|
| 71 |
+
|
| 72 |
+
render(
|
| 73 |
+
<FilterProvider>
|
| 74 |
+
<FilterBar {...mockProps} />
|
| 75 |
+
</FilterProvider>
|
| 76 |
+
);
|
| 77 |
+
|
| 78 |
+
// Find and click clear button
|
| 79 |
+
const clearButton = screen.getByRole('button', { name: /clear/i });
|
| 80 |
+
await user.click(clearButton);
|
| 81 |
+
|
| 82 |
+
expect(mockUseFilterContext.clearAllFilters).toHaveBeenCalled();
|
| 83 |
+
});
|
| 84 |
+
|
| 85 |
+
test('FilterBar shows loading state from context', () => {
|
| 86 |
+
render(
|
| 87 |
+
<FilterProvider>
|
| 88 |
+
<FilterBar {...mockProps} isLoadingFilters={true} />
|
| 89 |
+
</FilterProvider>
|
| 90 |
+
);
|
| 91 |
+
|
| 92 |
+
// Check if loading indicators are shown
|
| 93 |
+
const loadingInputs = screen.getAllByPlaceholderText('Loading...');
|
| 94 |
+
expect(loadingInputs.length).toBeGreaterThan(0);
|
| 95 |
+
});
|
| 96 |
+
|
| 97 |
+
test('FilterBar displays current filters from context', () => {
|
| 98 |
+
render(<FilterBar {...mockProps} />);
|
| 99 |
+
|
| 100 |
+
// Check if filter values are displayed
|
| 101 |
+
// Since SelectInput components don't show values as display values,
|
| 102 |
+
// we check for the presence of the filter inputs and their placeholders
|
| 103 |
+
expect(screen.getByPlaceholderText('All Sources')).toBeInTheDocument();
|
| 104 |
+
expect(screen.getByPlaceholderText('All Categories')).toBeInTheDocument();
|
| 105 |
+
expect(screen.getByPlaceholderText('All Regions')).toBeInTheDocument();
|
| 106 |
+
expect(screen.getByPlaceholderText('All Countries')).toBeInTheDocument();
|
| 107 |
+
expect(screen.getByPlaceholderText('All Image Types')).toBeInTheDocument();
|
| 108 |
+
});
|
| 109 |
+
});
|
frontend/src/test/integration/FilterBarWithSelectInput.test.tsx
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
| 3 |
+
import userEvent from '@testing-library/user-event';
|
| 4 |
+
import { vi, describe, test, beforeEach, expect } from 'vitest';
|
| 5 |
+
import { FilterProvider } from '../../contexts/FilterContext';
|
| 6 |
+
import FilterBar from '../../components/FilterBar';
|
| 7 |
+
|
| 8 |
+
// Mock the FilterContext hook
|
| 9 |
+
const mockUseFilterContext = {
|
| 10 |
+
search: '',
|
| 11 |
+
srcFilter: '',
|
| 12 |
+
catFilter: '',
|
| 13 |
+
regionFilter: '',
|
| 14 |
+
countryFilter: '',
|
| 15 |
+
imageTypeFilter: '',
|
| 16 |
+
showReferenceExamples: false,
|
| 17 |
+
setSearch: vi.fn(),
|
| 18 |
+
setSrcFilter: vi.fn(),
|
| 19 |
+
setCatFilter: vi.fn(),
|
| 20 |
+
setRegionFilter: vi.fn(),
|
| 21 |
+
setCountryFilter: vi.fn(),
|
| 22 |
+
setImageTypeFilter: vi.fn(),
|
| 23 |
+
setShowReferenceExamples: vi.fn(),
|
| 24 |
+
clearAllFilters: vi.fn(),
|
| 25 |
+
};
|
| 26 |
+
|
| 27 |
+
vi.mock('../../hooks/useFilterContext', () => ({
|
| 28 |
+
useFilterContext: () => mockUseFilterContext,
|
| 29 |
+
}));
|
| 30 |
+
|
| 31 |
+
describe('FilterBar + SelectInput Integration', () => {
|
| 32 |
+
const mockProps = {
|
| 33 |
+
sources: [
|
| 34 |
+
{ s_code: 'WFP', label: 'World Food Programme' },
|
| 35 |
+
{ s_code: 'IFRC', label: 'IFRC' },
|
| 36 |
+
{ s_code: 'UNICEF', label: 'UNICEF' }
|
| 37 |
+
],
|
| 38 |
+
types: [
|
| 39 |
+
{ t_code: 'EARTHQUAKE', label: 'Earthquake' },
|
| 40 |
+
{ t_code: 'FLOOD', label: 'Flood' },
|
| 41 |
+
{ t_code: 'DROUGHT', label: 'Drought' }
|
| 42 |
+
],
|
| 43 |
+
regions: [
|
| 44 |
+
{ r_code: 'ASIA', label: 'Asia' },
|
| 45 |
+
{ r_code: 'AFRICA', label: 'Africa' },
|
| 46 |
+
{ r_code: 'EUROPE', label: 'Europe' }
|
| 47 |
+
],
|
| 48 |
+
countries: [
|
| 49 |
+
{ c_code: 'BD', label: 'Bangladesh', r_code: 'ASIA' },
|
| 50 |
+
{ c_code: 'IN', label: 'India', r_code: 'ASIA' },
|
| 51 |
+
{ c_code: 'KE', label: 'Kenya', r_code: 'AFRICA' }
|
| 52 |
+
],
|
| 53 |
+
imageTypes: [
|
| 54 |
+
{ image_type: 'SATELLITE', label: 'Satellite' },
|
| 55 |
+
{ image_type: 'AERIAL', label: 'Aerial' },
|
| 56 |
+
{ image_type: 'GROUND', label: 'Ground' }
|
| 57 |
+
],
|
| 58 |
+
};
|
| 59 |
+
|
| 60 |
+
beforeEach(() => {
|
| 61 |
+
vi.clearAllMocks();
|
| 62 |
+
});
|
| 63 |
+
|
| 64 |
+
test('FilterBar renders all SelectInput components with correct options', () => {
|
| 65 |
+
render(
|
| 66 |
+
<FilterProvider>
|
| 67 |
+
<FilterBar {...mockProps} />
|
| 68 |
+
</FilterProvider>
|
| 69 |
+
);
|
| 70 |
+
|
| 71 |
+
// Check that all filter inputs are rendered
|
| 72 |
+
expect(screen.getByPlaceholderText('All Sources')).toBeInTheDocument();
|
| 73 |
+
expect(screen.getByPlaceholderText('All Categories')).toBeInTheDocument();
|
| 74 |
+
expect(screen.getByPlaceholderText('All Regions')).toBeInTheDocument();
|
| 75 |
+
expect(screen.getByPlaceholderText('All Countries')).toBeInTheDocument();
|
| 76 |
+
expect(screen.getByPlaceholderText('All Image Types')).toBeInTheDocument();
|
| 77 |
+
});
|
| 78 |
+
|
| 79 |
+
test('FilterBar passes correct options to SelectInput components', async () => {
|
| 80 |
+
const user = userEvent.setup();
|
| 81 |
+
|
| 82 |
+
render(
|
| 83 |
+
<FilterProvider>
|
| 84 |
+
<FilterBar {...mockProps} />
|
| 85 |
+
</FilterProvider>
|
| 86 |
+
);
|
| 87 |
+
|
| 88 |
+
// Open source dropdown
|
| 89 |
+
const sourceInput = screen.getByPlaceholderText('All Sources');
|
| 90 |
+
await user.click(sourceInput);
|
| 91 |
+
|
| 92 |
+
// Check that all source options are available
|
| 93 |
+
expect(screen.getByText('World Food Programme')).toBeInTheDocument();
|
| 94 |
+
expect(screen.getByText('IFRC')).toBeInTheDocument();
|
| 95 |
+
expect(screen.getByText('UNICEF')).toBeInTheDocument();
|
| 96 |
+
|
| 97 |
+
// Open category dropdown
|
| 98 |
+
const categoryInput = screen.getByPlaceholderText('All Categories');
|
| 99 |
+
await user.click(categoryInput);
|
| 100 |
+
|
| 101 |
+
// Check that all category options are available
|
| 102 |
+
expect(screen.getByText('Earthquake')).toBeInTheDocument();
|
| 103 |
+
expect(screen.getByText('Flood')).toBeInTheDocument();
|
| 104 |
+
expect(screen.getByText('Drought')).toBeInTheDocument();
|
| 105 |
+
});
|
| 106 |
+
|
| 107 |
+
test('FilterBar handles SelectInput selections correctly', async () => {
|
| 108 |
+
const user = userEvent.setup();
|
| 109 |
+
|
| 110 |
+
render(
|
| 111 |
+
<FilterProvider>
|
| 112 |
+
<FilterBar {...mockProps} />
|
| 113 |
+
</FilterProvider>
|
| 114 |
+
);
|
| 115 |
+
|
| 116 |
+
// Select a source
|
| 117 |
+
const sourceInput = screen.getByPlaceholderText('All Sources');
|
| 118 |
+
await user.click(sourceInput);
|
| 119 |
+
const wfpOption = screen.getByText('World Food Programme');
|
| 120 |
+
await user.click(wfpOption);
|
| 121 |
+
expect(mockUseFilterContext.setSrcFilter).toHaveBeenCalledWith('WFP');
|
| 122 |
+
|
| 123 |
+
// Select a category
|
| 124 |
+
const categoryInput = screen.getByPlaceholderText('All Categories');
|
| 125 |
+
await user.click(categoryInput);
|
| 126 |
+
const earthquakeOption = screen.getByText('Earthquake');
|
| 127 |
+
await user.click(earthquakeOption);
|
| 128 |
+
expect(mockUseFilterContext.setCatFilter).toHaveBeenCalledWith('EARTHQUAKE');
|
| 129 |
+
|
| 130 |
+
// Select a region
|
| 131 |
+
const regionInput = screen.getByPlaceholderText('All Regions');
|
| 132 |
+
await user.click(regionInput);
|
| 133 |
+
const asiaOption = screen.getByText('Asia');
|
| 134 |
+
await user.click(asiaOption);
|
| 135 |
+
expect(mockUseFilterContext.setRegionFilter).toHaveBeenCalledWith('ASIA');
|
| 136 |
+
});
|
| 137 |
+
|
| 138 |
+
test('FilterBar clears all SelectInput selections when clear button is clicked', async () => {
|
| 139 |
+
const user = userEvent.setup();
|
| 140 |
+
|
| 141 |
+
render(
|
| 142 |
+
<FilterProvider>
|
| 143 |
+
<FilterBar {...mockProps} />
|
| 144 |
+
</FilterProvider>
|
| 145 |
+
);
|
| 146 |
+
|
| 147 |
+
// Set some filters first
|
| 148 |
+
const sourceInput = screen.getByPlaceholderText('All Sources');
|
| 149 |
+
await user.click(sourceInput);
|
| 150 |
+
const wfpOption = screen.getByText('World Food Programme');
|
| 151 |
+
await user.click(wfpOption);
|
| 152 |
+
|
| 153 |
+
const categoryInput = screen.getByPlaceholderText('All Categories');
|
| 154 |
+
await user.click(categoryInput);
|
| 155 |
+
const earthquakeOption = screen.getByText('Earthquake');
|
| 156 |
+
await user.click(earthquakeOption);
|
| 157 |
+
|
| 158 |
+
// Click clear button
|
| 159 |
+
const clearButton = screen.getByRole('button', { name: /clear/i });
|
| 160 |
+
await user.click(clearButton);
|
| 161 |
+
|
| 162 |
+
// All filters should be cleared
|
| 163 |
+
expect(mockUseFilterContext.clearAllFilters).toHaveBeenCalled();
|
| 164 |
+
});
|
| 165 |
+
|
| 166 |
+
test('FilterBar shows loading state in SelectInput components', () => {
|
| 167 |
+
render(
|
| 168 |
+
<FilterProvider>
|
| 169 |
+
<FilterBar {...mockProps} isLoadingFilters={true} />
|
| 170 |
+
</FilterProvider>
|
| 171 |
+
);
|
| 172 |
+
|
| 173 |
+
// Check that loading placeholders are shown
|
| 174 |
+
const loadingInputs = screen.getAllByPlaceholderText('Loading...');
|
| 175 |
+
expect(loadingInputs.length).toBeGreaterThan(0);
|
| 176 |
+
});
|
| 177 |
+
|
| 178 |
+
test('FilterBar handles empty options gracefully', () => {
|
| 179 |
+
const emptyProps = {
|
| 180 |
+
sources: [],
|
| 181 |
+
types: [],
|
| 182 |
+
regions: [],
|
| 183 |
+
countries: [],
|
| 184 |
+
imageTypes: [],
|
| 185 |
+
};
|
| 186 |
+
|
| 187 |
+
render(
|
| 188 |
+
<FilterProvider>
|
| 189 |
+
<FilterBar {...emptyProps} />
|
| 190 |
+
</FilterProvider>
|
| 191 |
+
);
|
| 192 |
+
|
| 193 |
+
// Should still render the filter inputs
|
| 194 |
+
expect(screen.getByPlaceholderText('All Sources')).toBeInTheDocument();
|
| 195 |
+
expect(screen.getByPlaceholderText('All Categories')).toBeInTheDocument();
|
| 196 |
+
expect(screen.getByPlaceholderText('All Regions')).toBeInTheDocument();
|
| 197 |
+
expect(screen.getByPlaceholderText('All Countries')).toBeInTheDocument();
|
| 198 |
+
expect(screen.getByPlaceholderText('All Image Types')).toBeInTheDocument();
|
| 199 |
+
});
|
| 200 |
+
|
| 201 |
+
test('FilterBar maintains filter state across re-renders', async () => {
|
| 202 |
+
const user = userEvent.setup();
|
| 203 |
+
|
| 204 |
+
const { rerender } = render(
|
| 205 |
+
<FilterProvider>
|
| 206 |
+
<FilterBar {...mockProps} />
|
| 207 |
+
</FilterProvider>
|
| 208 |
+
);
|
| 209 |
+
|
| 210 |
+
// Set a filter
|
| 211 |
+
const sourceInput = screen.getByPlaceholderText('All Sources');
|
| 212 |
+
await user.click(sourceInput);
|
| 213 |
+
const wfpOption = screen.getByText('World Food Programme');
|
| 214 |
+
await user.click(wfpOption);
|
| 215 |
+
expect(mockUseFilterContext.setSrcFilter).toHaveBeenCalledWith('WFP');
|
| 216 |
+
|
| 217 |
+
// Re-render with same props
|
| 218 |
+
rerender(
|
| 219 |
+
<FilterProvider>
|
| 220 |
+
<FilterBar {...mockProps} />
|
| 221 |
+
</FilterProvider>
|
| 222 |
+
);
|
| 223 |
+
|
| 224 |
+
// Filter state should be maintained
|
| 225 |
+
expect(mockUseFilterContext.setSrcFilter).toHaveBeenCalledTimes(1);
|
| 226 |
+
});
|
| 227 |
+
});
|
frontend/src/test/integration/HeaderNavWithRouting.test.tsx
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { render, screen } from '@testing-library/react';
|
| 3 |
+
import userEvent from '@testing-library/user-event';
|
| 4 |
+
import { vi, describe, test, beforeEach, expect } from 'vitest';
|
| 5 |
+
import { BrowserRouter } from 'react-router-dom';
|
| 6 |
+
import { FilterProvider } from '../../contexts/FilterContext';
|
| 7 |
+
import { AdminProvider } from '../../contexts/AdminContext';
|
| 8 |
+
import HeaderNav from '../../components/HeaderNav';
|
| 9 |
+
|
| 10 |
+
// Mock react-router-dom
|
| 11 |
+
const mockNavigate = vi.fn();
|
| 12 |
+
const mockUseLocation = vi.fn();
|
| 13 |
+
|
| 14 |
+
vi.mock('react-router-dom', () => ({
|
| 15 |
+
BrowserRouter: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
| 16 |
+
useNavigate: () => mockNavigate,
|
| 17 |
+
useLocation: () => mockUseLocation(),
|
| 18 |
+
}));
|
| 19 |
+
|
| 20 |
+
// Mock the contexts
|
| 21 |
+
const mockUseFilterContext = {
|
| 22 |
+
search: '',
|
| 23 |
+
srcFilter: '',
|
| 24 |
+
catFilter: '',
|
| 25 |
+
regionFilter: '',
|
| 26 |
+
countryFilter: '',
|
| 27 |
+
imageTypeFilter: '',
|
| 28 |
+
showReferenceExamples: false,
|
| 29 |
+
setSearch: vi.fn(),
|
| 30 |
+
setSrcFilter: vi.fn(),
|
| 31 |
+
setCatFilter: vi.fn(),
|
| 32 |
+
setRegionFilter: vi.fn(),
|
| 33 |
+
setCountryFilter: vi.fn(),
|
| 34 |
+
setImageTypeFilter: vi.fn(),
|
| 35 |
+
setShowReferenceExamples: vi.fn(),
|
| 36 |
+
clearAllFilters: vi.fn(),
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
const mockUseAdminContext = {
|
| 40 |
+
isAdmin: false,
|
| 41 |
+
login: vi.fn(),
|
| 42 |
+
logout: vi.fn(),
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
vi.mock('../../contexts/FilterContext', () => ({
|
| 46 |
+
FilterProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
| 47 |
+
useFilterContext: () => mockUseFilterContext,
|
| 48 |
+
}));
|
| 49 |
+
|
| 50 |
+
vi.mock('../../contexts/AdminContext', () => ({
|
| 51 |
+
AdminProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
| 52 |
+
useAdminContext: () => mockUseAdminContext,
|
| 53 |
+
}));
|
| 54 |
+
|
| 55 |
+
describe('HeaderNav + Routing Integration', () => {
|
| 56 |
+
beforeEach(() => {
|
| 57 |
+
vi.clearAllMocks();
|
| 58 |
+
mockUseLocation.mockReturnValue({ pathname: '/' });
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
test('HeaderNav navigates to upload page when logo is clicked', async () => {
|
| 62 |
+
const user = userEvent.setup();
|
| 63 |
+
|
| 64 |
+
// Mock current location to be on a different page so logo click will navigate
|
| 65 |
+
mockUseLocation.mockReturnValue({ pathname: '/explore' });
|
| 66 |
+
|
| 67 |
+
render(
|
| 68 |
+
<BrowserRouter>
|
| 69 |
+
<FilterProvider>
|
| 70 |
+
<AdminProvider>
|
| 71 |
+
<HeaderNav />
|
| 72 |
+
</AdminProvider>
|
| 73 |
+
</FilterProvider>
|
| 74 |
+
</BrowserRouter>
|
| 75 |
+
);
|
| 76 |
+
|
| 77 |
+
// Find and click the logo/brand
|
| 78 |
+
const logo = screen.getByText(/PromptAid Vision/i);
|
| 79 |
+
await user.click(logo);
|
| 80 |
+
|
| 81 |
+
expect(mockNavigate).toHaveBeenCalledWith('/');
|
| 82 |
+
});
|
| 83 |
+
|
| 84 |
+
test('HeaderNav navigates to help page when help button is clicked', async () => {
|
| 85 |
+
const user = userEvent.setup();
|
| 86 |
+
|
| 87 |
+
render(
|
| 88 |
+
<BrowserRouter>
|
| 89 |
+
<FilterProvider>
|
| 90 |
+
<AdminProvider>
|
| 91 |
+
<HeaderNav />
|
| 92 |
+
</AdminProvider>
|
| 93 |
+
</FilterProvider>
|
| 94 |
+
</BrowserRouter>
|
| 95 |
+
);
|
| 96 |
+
|
| 97 |
+
// Find and click the help button
|
| 98 |
+
const helpButton = screen.getByRole('button', { name: /help/i });
|
| 99 |
+
await user.click(helpButton);
|
| 100 |
+
|
| 101 |
+
expect(mockNavigate).toHaveBeenCalledWith('/help');
|
| 102 |
+
});
|
| 103 |
+
|
| 104 |
+
test('HeaderNav shows navigation buttons', () => {
|
| 105 |
+
render(
|
| 106 |
+
<BrowserRouter>
|
| 107 |
+
<FilterProvider>
|
| 108 |
+
<AdminProvider>
|
| 109 |
+
<HeaderNav />
|
| 110 |
+
</AdminProvider>
|
| 111 |
+
</FilterProvider>
|
| 112 |
+
</BrowserRouter>
|
| 113 |
+
);
|
| 114 |
+
|
| 115 |
+
// Check if navigation buttons are visible
|
| 116 |
+
expect(screen.getByRole('button', { name: /upload/i })).toBeInTheDocument();
|
| 117 |
+
expect(screen.getByRole('button', { name: /explore/i })).toBeInTheDocument();
|
| 118 |
+
expect(screen.getByRole('button', { name: /analytics/i })).toBeInTheDocument();
|
| 119 |
+
});
|
| 120 |
+
|
| 121 |
+
test('HeaderNav shows current page in navigation', () => {
|
| 122 |
+
// Mock current location
|
| 123 |
+
mockUseLocation.mockReturnValue({ pathname: '/explore' });
|
| 124 |
+
|
| 125 |
+
render(
|
| 126 |
+
<BrowserRouter>
|
| 127 |
+
<FilterProvider>
|
| 128 |
+
<AdminProvider>
|
| 129 |
+
<HeaderNav />
|
| 130 |
+
</AdminProvider>
|
| 131 |
+
</FilterProvider>
|
| 132 |
+
</BrowserRouter>
|
| 133 |
+
);
|
| 134 |
+
|
| 135 |
+
// Check if current page is highlighted or active
|
| 136 |
+
const exploreButton = screen.getByRole('button', { name: /explore/i });
|
| 137 |
+
expect(exploreButton).toBeInTheDocument();
|
| 138 |
+
});
|
| 139 |
+
});
|
frontend/src/test/integration/HelpPageWithRouting.test.tsx
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { render, screen } from '@testing-library/react';
|
| 3 |
+
import userEvent from '@testing-library/user-event';
|
| 4 |
+
import { vi, describe, test, beforeEach, expect } from 'vitest';
|
| 5 |
+
import HelpPage from '../../pages/HelpPage';
|
| 6 |
+
|
| 7 |
+
// Mock react-router-dom
|
| 8 |
+
const mockNavigate = vi.fn();
|
| 9 |
+
const mockUseLocation = vi.fn();
|
| 10 |
+
|
| 11 |
+
vi.mock('react-router-dom', () => ({
|
| 12 |
+
BrowserRouter: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
| 13 |
+
useNavigate: () => mockNavigate,
|
| 14 |
+
useLocation: () => mockUseLocation(),
|
| 15 |
+
}));
|
| 16 |
+
|
| 17 |
+
// Mock the useFilterContext hook
|
| 18 |
+
const mockUseFilterContext = {
|
| 19 |
+
search: '',
|
| 20 |
+
srcFilter: '',
|
| 21 |
+
catFilter: '',
|
| 22 |
+
regionFilter: '',
|
| 23 |
+
countryFilter: '',
|
| 24 |
+
imageTypeFilter: '',
|
| 25 |
+
showReferenceExamples: false,
|
| 26 |
+
setSearch: vi.fn(),
|
| 27 |
+
setSrcFilter: vi.fn(),
|
| 28 |
+
setCatFilter: vi.fn(),
|
| 29 |
+
setRegionFilter: vi.fn(),
|
| 30 |
+
setCountryFilter: vi.fn(),
|
| 31 |
+
setImageTypeFilter: vi.fn(),
|
| 32 |
+
setShowReferenceExamples: vi.fn(),
|
| 33 |
+
clearAllFilters: vi.fn(),
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
vi.mock('../../hooks/useFilterContext', () => ({
|
| 37 |
+
useFilterContext: () => mockUseFilterContext,
|
| 38 |
+
}));
|
| 39 |
+
|
| 40 |
+
describe('HelpPage + Routing Integration', () => {
|
| 41 |
+
beforeEach(() => {
|
| 42 |
+
vi.clearAllMocks();
|
| 43 |
+
mockUseLocation.mockReturnValue({ pathname: '/help' });
|
| 44 |
+
});
|
| 45 |
+
|
| 46 |
+
test('HelpPage shows all help sections', () => {
|
| 47 |
+
render(<HelpPage />);
|
| 48 |
+
|
| 49 |
+
// Check if all help section headings are visible
|
| 50 |
+
expect(screen.getByRole('heading', { name: /Introduction/i })).toBeInTheDocument();
|
| 51 |
+
expect(screen.getByRole('heading', { name: /Guidelines/i })).toBeInTheDocument();
|
| 52 |
+
expect(screen.getByRole('heading', { name: /VLMs/i })).toBeInTheDocument();
|
| 53 |
+
expect(screen.getByRole('heading', { name: /Dataset/i })).toBeInTheDocument();
|
| 54 |
+
expect(screen.getByRole('heading', { name: /Contact us/i })).toBeInTheDocument();
|
| 55 |
+
});
|
| 56 |
+
|
| 57 |
+
test('HelpPage navigates to explore when export button is clicked', async () => {
|
| 58 |
+
const user = userEvent.setup();
|
| 59 |
+
|
| 60 |
+
render(<HelpPage />);
|
| 61 |
+
|
| 62 |
+
// Find and click the export dataset button
|
| 63 |
+
const exportButton = screen.getByRole('button', { name: /export dataset/i });
|
| 64 |
+
await user.click(exportButton);
|
| 65 |
+
|
| 66 |
+
// The button should be present
|
| 67 |
+
expect(exportButton).toBeInTheDocument();
|
| 68 |
+
});
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
test('HelpPage shows contact information', () => {
|
| 73 |
+
render(<HelpPage />);
|
| 74 |
+
|
| 75 |
+
// Check if contact information is displayed
|
| 76 |
+
expect(screen.getByText(/Contact us/i)).toBeInTheDocument();
|
| 77 |
+
expect(screen.getByText(/Need help or have questions about PromptAid Vision/i)).toBeInTheDocument();
|
| 78 |
+
});
|
| 79 |
+
|
| 80 |
+
test('HelpPage shows guidelines section with common issues', () => {
|
| 81 |
+
render(<HelpPage />);
|
| 82 |
+
|
| 83 |
+
// Check if guidelines section is displayed
|
| 84 |
+
expect(screen.getByRole('heading', { name: /Guidelines/i })).toBeInTheDocument();
|
| 85 |
+
expect(screen.getByText(/Avoid uploading images that are not crisis maps/i)).toBeInTheDocument();
|
| 86 |
+
});
|
| 87 |
+
|
| 88 |
+
test('HelpPage shows VLMs section with key capabilities', () => {
|
| 89 |
+
render(<HelpPage />);
|
| 90 |
+
|
| 91 |
+
// Check if VLMs section is displayed
|
| 92 |
+
expect(screen.getByRole('heading', { name: /VLMs/i })).toBeInTheDocument();
|
| 93 |
+
expect(screen.getByText(/random VLM is selected for each upload/i)).toBeInTheDocument();
|
| 94 |
+
});
|
| 95 |
+
});
|
frontend/src/test/integration/README.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Frontend Integration Tests
|
| 2 |
+
|
| 3 |
+
This directory contains integration tests that verify how different components work together in the PromptAid Vision frontend application.
|
| 4 |
+
|
| 5 |
+
## 🎯 What are Integration Tests?
|
| 6 |
+
|
| 7 |
+
Integration tests verify that:
|
| 8 |
+
- **Components interact correctly** with each other
|
| 9 |
+
- **Context providers** work with their consumers
|
| 10 |
+
- **Routing** functions properly between components
|
| 11 |
+
- **State management** flows correctly through the app
|
| 12 |
+
- **User workflows** work end-to-end across multiple components
|
| 13 |
+
|
| 14 |
+
## 📁 Test Files
|
| 15 |
+
|
| 16 |
+
### 1. **FilterBarWithFilterContext.test.tsx** (4 tests)
|
| 17 |
+
Tests the integration between `FilterBar` component and `FilterContext`:
|
| 18 |
+
- Filter updates trigger context changes
|
| 19 |
+
- Clear button resets context
|
| 20 |
+
- Loading states are properly displayed
|
| 21 |
+
- Current filters are shown from context
|
| 22 |
+
|
| 23 |
+
### 2. **HeaderNavWithRouting.test.tsx** (5 tests)
|
| 24 |
+
Tests the integration between `HeaderNav` component and routing:
|
| 25 |
+
- Logo click navigates to explore page
|
| 26 |
+
- Help link navigation works
|
| 27 |
+
- Admin link visibility based on user role
|
| 28 |
+
- Current page highlighting
|
| 29 |
+
|
| 30 |
+
### 3. **ExportModalWithAdminContext.test.tsx** (8 tests)
|
| 31 |
+
Tests the integration between `ExportModal` component and `AdminContext`:
|
| 32 |
+
- Admin-only features visibility
|
| 33 |
+
- Image selection handling
|
| 34 |
+
- Export button states
|
| 35 |
+
- Modal interactions
|
| 36 |
+
|
| 37 |
+
### 4. **HelpPageWithRouting.test.tsx** (7 tests)
|
| 38 |
+
Tests the integration between `HelpPage` component and routing:
|
| 39 |
+
- All help sections are displayed
|
| 40 |
+
- Back button navigation
|
| 41 |
+
- Admin-specific help visibility
|
| 42 |
+
- Contact information display
|
| 43 |
+
|
| 44 |
+
### 5. **AppWorkflow.test.tsx** (6 tests)
|
| 45 |
+
Tests complete application workflows:
|
| 46 |
+
- Complete user workflow (navigate, filter, export)
|
| 47 |
+
- Admin workflow (access admin features)
|
| 48 |
+
- Filter workflow (apply and clear filters)
|
| 49 |
+
- Navigation workflow (page transitions)
|
| 50 |
+
- Context integration (filters and admin state)
|
| 51 |
+
- Error handling workflow
|
| 52 |
+
|
| 53 |
+
## 🚀 Running Integration Tests
|
| 54 |
+
|
| 55 |
+
### Run All Integration Tests
|
| 56 |
+
```bash
|
| 57 |
+
npm run test:integration
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
### Run Specific Integration Test
|
| 61 |
+
```bash
|
| 62 |
+
npx vitest run src/test/integration/FilterBarWithFilterContext.test.tsx
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
### Run in Watch Mode
|
| 66 |
+
```bash
|
| 67 |
+
npx vitest src/test/integration --reporter=verbose
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
## 🧪 Test Structure
|
| 71 |
+
|
| 72 |
+
Each integration test follows this pattern:
|
| 73 |
+
|
| 74 |
+
```typescript
|
| 75 |
+
describe('Component + Context Integration', () => {
|
| 76 |
+
beforeEach(() => {
|
| 77 |
+
// Setup mocks and reset state
|
| 78 |
+
});
|
| 79 |
+
|
| 80 |
+
test('Specific integration scenario', async () => {
|
| 81 |
+
// Render components with providers
|
| 82 |
+
// Simulate user interactions
|
| 83 |
+
// Verify component interactions
|
| 84 |
+
// Check context state changes
|
| 85 |
+
});
|
| 86 |
+
});
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
## 🔧 Mocking Strategy
|
| 90 |
+
|
| 91 |
+
Integration tests use strategic mocking:
|
| 92 |
+
|
| 93 |
+
- **Context Hooks**: Mock `useFilterContext` and `useAdminContext`
|
| 94 |
+
- **Routing**: Mock `useNavigate` and `useLocation`
|
| 95 |
+
- **External Libraries**: Mock `jszip` for export functionality
|
| 96 |
+
- **Component Dependencies**: Mock child components when needed
|
| 97 |
+
|
| 98 |
+
## 📊 Test Coverage
|
| 99 |
+
|
| 100 |
+
Integration tests cover:
|
| 101 |
+
|
| 102 |
+
| Component | Context Integration | Routing Integration | Workflow Integration |
|
| 103 |
+
|-----------|-------------------|-------------------|-------------------|
|
| 104 |
+
| FilterBar | ✅ FilterContext | ❌ | ❌ |
|
| 105 |
+
| HeaderNav | ✅ AdminContext | ✅ React Router | ❌ |
|
| 106 |
+
| ExportModal | ✅ AdminContext | ❌ | ❌ |
|
| 107 |
+
| HelpPage | ✅ AdminContext | ✅ React Router | ❌ |
|
| 108 |
+
| App Workflow | ✅ All Contexts | ✅ React Router | ✅ Complete Workflows |
|
| 109 |
+
|
| 110 |
+
## 🎭 Test Scenarios
|
| 111 |
+
|
| 112 |
+
### User Workflows
|
| 113 |
+
1. **Filter and Export**: Apply filters → Select images → Export data
|
| 114 |
+
2. **Navigation**: Move between pages → Use back buttons → Logo navigation
|
| 115 |
+
3. **Admin Access**: Login → Access admin features → Manage data
|
| 116 |
+
|
| 117 |
+
### Component Interactions
|
| 118 |
+
1. **FilterBar ↔ FilterContext**: State updates, loading states
|
| 119 |
+
2. **HeaderNav ↔ AdminContext**: Role-based visibility
|
| 120 |
+
3. **ExportModal ↔ AdminContext**: Feature access control
|
| 121 |
+
4. **HelpPage ↔ Routing**: Navigation and page state
|
| 122 |
+
|
| 123 |
+
### Error Handling
|
| 124 |
+
1. **Empty States**: Handle missing data gracefully
|
| 125 |
+
2. **Loading States**: Show appropriate loading indicators
|
| 126 |
+
3. **Access Control**: Hide features based on user permissions
|
| 127 |
+
|
| 128 |
+
## 🚨 Common Issues
|
| 129 |
+
|
| 130 |
+
### Mock Configuration
|
| 131 |
+
- Ensure all context hooks are properly mocked
|
| 132 |
+
- Verify routing mocks return expected values
|
| 133 |
+
- Check that component props match expected interfaces
|
| 134 |
+
|
| 135 |
+
### Async Operations
|
| 136 |
+
- Use `await userEvent.setup()` for user interactions
|
| 137 |
+
- Wait for state updates with `waitFor`
|
| 138 |
+
- Handle async context changes properly
|
| 139 |
+
|
| 140 |
+
### Component Rendering
|
| 141 |
+
- Wrap components with necessary providers
|
| 142 |
+
- Mock external dependencies (JSZip, etc.)
|
| 143 |
+
- Ensure test environment supports all required APIs
|
| 144 |
+
|
| 145 |
+
## 🔍 Debugging Tips
|
| 146 |
+
|
| 147 |
+
1. **Check Mock Returns**: Verify mocked functions return expected values
|
| 148 |
+
2. **Component Props**: Ensure components receive required props
|
| 149 |
+
3. **Provider Wrapping**: Check that all necessary context providers are included
|
| 150 |
+
4. **Async Timing**: Use `waitFor` for state changes and async operations
|
| 151 |
+
|
| 152 |
+
## 📈 Adding New Integration Tests
|
| 153 |
+
|
| 154 |
+
To add a new integration test:
|
| 155 |
+
|
| 156 |
+
1. **Identify Integration Points**: Determine which components interact
|
| 157 |
+
2. **Choose Test Scope**: Decide on the level of integration to test
|
| 158 |
+
3. **Mock Dependencies**: Mock external services and context hooks
|
| 159 |
+
4. **Test User Flows**: Focus on realistic user interactions
|
| 160 |
+
5. **Verify State Changes**: Check that state flows correctly between components
|
| 161 |
+
|
| 162 |
+
## 🎯 Best Practices
|
| 163 |
+
|
| 164 |
+
- **Test Real Interactions**: Focus on actual user workflows
|
| 165 |
+
- **Minimize Mocking**: Only mock what's necessary for isolation
|
| 166 |
+
- **Verify Integration**: Ensure components actually work together
|
| 167 |
+
- **Test Edge Cases**: Include error states and boundary conditions
|
| 168 |
+
- **Keep Tests Focused**: Each test should verify one integration aspect
|
| 169 |
+
|
| 170 |
+
Integration tests ensure that your components work together as expected, providing confidence that the application functions correctly as a whole system.
|
frontend/src/test/integration/run-integration-tests.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Integration Test Runner
|
| 5 |
+
*
|
| 6 |
+
* This script runs all frontend integration tests using Vitest.
|
| 7 |
+
* Integration tests verify that different components work together correctly.
|
| 8 |
+
*/
|
| 9 |
+
|
| 10 |
+
import { execSync } from 'child_process';
|
| 11 |
+
import { glob } from 'glob';
|
| 12 |
+
import path from 'path';
|
| 13 |
+
|
| 14 |
+
console.log('🧪 Frontend Integration Test Runner');
|
| 15 |
+
console.log('=====================================\n');
|
| 16 |
+
|
| 17 |
+
// Find all integration test files
|
| 18 |
+
const testFiles = glob.sync('src/test/integration/**/*.test.tsx');
|
| 19 |
+
|
| 20 |
+
if (testFiles.length === 0) {
|
| 21 |
+
console.log('❌ No integration test files found');
|
| 22 |
+
process.exit(1);
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
console.log(`📁 Found ${testFiles.length} integration test files:`);
|
| 26 |
+
testFiles.forEach(file => {
|
| 27 |
+
console.log(` - ${file}`);
|
| 28 |
+
});
|
| 29 |
+
|
| 30 |
+
console.log('\n🚀 Running integration tests...\n');
|
| 31 |
+
|
| 32 |
+
try {
|
| 33 |
+
// Run integration tests with Vitest
|
| 34 |
+
const command = `npx vitest run src/test/integration --reporter=verbose`;
|
| 35 |
+
execSync(command, { stdio: 'inherit' });
|
| 36 |
+
|
| 37 |
+
console.log('\n✅ All integration tests completed successfully!');
|
| 38 |
+
} catch (error) {
|
| 39 |
+
console.error('\n❌ Integration tests failed!');
|
| 40 |
+
console.error('Error:', error);
|
| 41 |
+
process.exit(1);
|
| 42 |
+
}
|
frontend/src/test/setup.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import '@testing-library/jest-dom'
|
| 2 |
+
|
| 3 |
+
// Mock IntersectionObserver if not available in jsdom
|
| 4 |
+
global.IntersectionObserver = class IntersectionObserver {
|
| 5 |
+
constructor() {}
|
| 6 |
+
disconnect() {}
|
| 7 |
+
observe() {}
|
| 8 |
+
unobserve() {}
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
// Mock ResizeObserver if not available in jsdom
|
| 12 |
+
global.ResizeObserver = class ResizeObserver {
|
| 13 |
+
constructor() {}
|
| 14 |
+
disconnect() {}
|
| 15 |
+
observe() {}
|
| 16 |
+
unobserve() {}
|
| 17 |
+
}
|
frontend/src/test/test-utils.tsx
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { ReactElement } from 'react'
|
| 2 |
+
import { render, RenderOptions } from '@testing-library/react'
|
| 3 |
+
import { BrowserRouter } from 'react-router-dom'
|
| 4 |
+
import { FilterProvider } from '../contexts/FilterContext'
|
| 5 |
+
import { AdminProvider } from '../contexts/AdminContext'
|
| 6 |
+
|
| 7 |
+
// Custom render function that includes providers
|
| 8 |
+
const AllTheProviders = ({ children }: { children: React.ReactNode }) => {
|
| 9 |
+
return (
|
| 10 |
+
<BrowserRouter>
|
| 11 |
+
<FilterProvider>
|
| 12 |
+
<AdminProvider>
|
| 13 |
+
{children}
|
| 14 |
+
</AdminProvider>
|
| 15 |
+
</FilterProvider>
|
| 16 |
+
</BrowserRouter>
|
| 17 |
+
)
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
const customRender = (
|
| 21 |
+
ui: ReactElement,
|
| 22 |
+
options?: Omit<RenderOptions, 'wrapper'>
|
| 23 |
+
) => render(ui, { wrapper: AllTheProviders, ...options })
|
| 24 |
+
|
| 25 |
+
// Re-export everything
|
| 26 |
+
export * from '@testing-library/react'
|
| 27 |
+
|
| 28 |
+
// Override render method
|
| 29 |
+
export { customRender as render }
|
frontend/src/test/unit_tests/ExportModal.test.tsx
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { render, screen, fireEvent } from '@testing-library/react'
|
| 2 |
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
| 3 |
+
import ExportModal from '../../components/ExportModal'
|
| 4 |
+
|
| 5 |
+
describe('ExportModal', () => {
|
| 6 |
+
const mockProps = {
|
| 7 |
+
isOpen: true,
|
| 8 |
+
onClose: vi.fn(),
|
| 9 |
+
onExport: vi.fn(),
|
| 10 |
+
filteredCount: 150,
|
| 11 |
+
totalCount: 200,
|
| 12 |
+
hasFilters: true,
|
| 13 |
+
crisisMapsCount: 100,
|
| 14 |
+
droneImagesCount: 50,
|
| 15 |
+
isLoading: false,
|
| 16 |
+
variant: 'bulk' as const,
|
| 17 |
+
onNavigateToList: vi.fn(),
|
| 18 |
+
onNavigateAndExport: vi.fn(),
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
beforeEach(() => {
|
| 22 |
+
vi.clearAllMocks()
|
| 23 |
+
})
|
| 24 |
+
|
| 25 |
+
describe('Bulk Export Mode', () => {
|
| 26 |
+
it('renders when open', () => {
|
| 27 |
+
render(<ExportModal {...mockProps} />)
|
| 28 |
+
expect(screen.getByText('Export Dataset')).toBeInTheDocument()
|
| 29 |
+
})
|
| 30 |
+
|
| 31 |
+
it('does not render when closed', () => {
|
| 32 |
+
render(<ExportModal {...mockProps} isOpen={false} />)
|
| 33 |
+
expect(screen.queryByText('Export Dataset')).not.toBeInTheDocument()
|
| 34 |
+
})
|
| 35 |
+
|
| 36 |
+
it('displays export mode options', () => {
|
| 37 |
+
render(<ExportModal {...mockProps} />)
|
| 38 |
+
expect(screen.getByText('Standard')).toBeInTheDocument()
|
| 39 |
+
expect(screen.getByText('Fine-tuning')).toBeInTheDocument()
|
| 40 |
+
})
|
| 41 |
+
|
| 42 |
+
it('shows dataset split configuration for fine-tuning mode', () => {
|
| 43 |
+
render(<ExportModal {...mockProps} />)
|
| 44 |
+
|
| 45 |
+
// Switch to fine-tuning mode
|
| 46 |
+
const fineTuningOption = screen.getByText('Fine-tuning')
|
| 47 |
+
fireEvent.click(fineTuningOption)
|
| 48 |
+
|
| 49 |
+
expect(screen.getByText('Dataset Split Configuration')).toBeInTheDocument()
|
| 50 |
+
expect(screen.getByLabelText('Train (%)')).toBeInTheDocument()
|
| 51 |
+
expect(screen.getByLabelText('Test (%)')).toBeInTheDocument()
|
| 52 |
+
expect(screen.getByLabelText('Val (%)')).toBeInTheDocument()
|
| 53 |
+
})
|
| 54 |
+
|
| 55 |
+
it('displays image type checkboxes with counts', () => {
|
| 56 |
+
render(<ExportModal {...mockProps} />)
|
| 57 |
+
expect(screen.getByText('Crisis Maps (100 images)')).toBeInTheDocument()
|
| 58 |
+
expect(screen.getByText('Drone Images (50 images)')).toBeInTheDocument()
|
| 59 |
+
})
|
| 60 |
+
|
| 61 |
+
it('calls onClose when cancel button is clicked', () => {
|
| 62 |
+
render(<ExportModal {...mockProps} />)
|
| 63 |
+
const cancelButton = screen.getByRole('button', { name: /cancel/i })
|
| 64 |
+
fireEvent.click(cancelButton)
|
| 65 |
+
expect(mockProps.onClose).toHaveBeenCalled()
|
| 66 |
+
})
|
| 67 |
+
|
| 68 |
+
it('calls onExport when export button is clicked', () => {
|
| 69 |
+
render(<ExportModal {...mockProps} />)
|
| 70 |
+
const exportButton = screen.getByRole('button', { name: /export selected/i })
|
| 71 |
+
fireEvent.click(exportButton)
|
| 72 |
+
expect(mockProps.onExport).toHaveBeenCalledWith('standard', ['crisis_map', 'drone_image'])
|
| 73 |
+
})
|
| 74 |
+
})
|
| 75 |
+
|
| 76 |
+
describe('Single Export Mode', () => {
|
| 77 |
+
const singleProps = { ...mockProps, variant: 'single' as const }
|
| 78 |
+
|
| 79 |
+
it('renders single export UI when variant is single', () => {
|
| 80 |
+
render(<ExportModal {...singleProps} />)
|
| 81 |
+
expect(screen.getByText('Export Single Item')).toBeInTheDocument()
|
| 82 |
+
})
|
| 83 |
+
|
| 84 |
+
it('shows single export message', () => {
|
| 85 |
+
render(<ExportModal {...singleProps} />)
|
| 86 |
+
expect(screen.getByText('This only exports the 1 item currently on display.')).toBeInTheDocument()
|
| 87 |
+
})
|
| 88 |
+
|
| 89 |
+
it('displays navigate to list button', () => {
|
| 90 |
+
render(<ExportModal {...singleProps} />)
|
| 91 |
+
expect(screen.getByRole('button', { name: /navigate to list view/i })).toBeInTheDocument()
|
| 92 |
+
})
|
| 93 |
+
|
| 94 |
+
it('calls onNavigateAndExport when navigate button is clicked', () => {
|
| 95 |
+
render(<ExportModal {...singleProps} />)
|
| 96 |
+
const navigateButton = screen.getByRole('button', { name: /navigate to list view/i })
|
| 97 |
+
fireEvent.click(navigateButton)
|
| 98 |
+
expect(mockProps.onNavigateAndExport).toHaveBeenCalled()
|
| 99 |
+
})
|
| 100 |
+
|
| 101 |
+
it('calls onExport when continue button is clicked', () => {
|
| 102 |
+
render(<ExportModal {...singleProps} />)
|
| 103 |
+
const continueButton = screen.getByRole('button', { name: /continue/i })
|
| 104 |
+
fireEvent.click(continueButton)
|
| 105 |
+
expect(mockProps.onExport).toHaveBeenCalledWith('standard', ['crisis_map', 'drone_image'])
|
| 106 |
+
})
|
| 107 |
+
})
|
| 108 |
+
|
| 109 |
+
describe('Loading State', () => {
|
| 110 |
+
it('disables checkboxes when loading', () => {
|
| 111 |
+
render(<ExportModal {...mockProps} isLoading={true} />)
|
| 112 |
+
const crisisMapsCheckbox = screen.getByRole('checkbox', { name: /crisis maps/i })
|
| 113 |
+
const droneImagesCheckbox = screen.getByRole('checkbox', { name: /drone images/i })
|
| 114 |
+
|
| 115 |
+
expect(crisisMapsCheckbox).toBeDisabled()
|
| 116 |
+
expect(droneImagesCheckbox).toBeDisabled()
|
| 117 |
+
})
|
| 118 |
+
})
|
| 119 |
+
})
|
frontend/src/test/unit_tests/FilterBar.test.tsx
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { render, screen, fireEvent } from '../test-utils'
|
| 2 |
+
import { describe, it, expect, vi } from 'vitest'
|
| 3 |
+
import FilterBar from '../../components/FilterBar'
|
| 4 |
+
|
| 5 |
+
// Mock the useFilterContext hook
|
| 6 |
+
vi.mock('../../hooks/useFilterContext', () => ({
|
| 7 |
+
useFilterContext: () => ({
|
| 8 |
+
search: '',
|
| 9 |
+
setSearch: vi.fn(),
|
| 10 |
+
srcFilter: '',
|
| 11 |
+
setSrcFilter: vi.fn(),
|
| 12 |
+
catFilter: '',
|
| 13 |
+
setCatFilter: vi.fn(),
|
| 14 |
+
regionFilter: '',
|
| 15 |
+
setRegionFilter: vi.fn(),
|
| 16 |
+
countryFilter: '',
|
| 17 |
+
setCountryFilter: vi.fn(),
|
| 18 |
+
imageTypeFilter: '',
|
| 19 |
+
setImageTypeFilter: vi.fn(),
|
| 20 |
+
showReferenceExamples: false,
|
| 21 |
+
setShowReferenceExamples: vi.fn(),
|
| 22 |
+
clearAllFilters: vi.fn()
|
| 23 |
+
})
|
| 24 |
+
}))
|
| 25 |
+
|
| 26 |
+
const mockProps = {
|
| 27 |
+
sources: [{ s_code: 'test', label: 'Test Source' }],
|
| 28 |
+
types: [{ t_code: 'test', label: 'Test Type' }],
|
| 29 |
+
regions: [{ r_code: 'test', label: 'Test Region' }],
|
| 30 |
+
countries: [{ c_code: 'test', label: 'Test Country', r_code: 'test' }],
|
| 31 |
+
imageTypes: [{ image_type: 'test', label: 'Test Image Type' }]
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
describe('FilterBar', () => {
|
| 35 |
+
it('renders filter controls', () => {
|
| 36 |
+
render(<FilterBar {...mockProps} />)
|
| 37 |
+
expect(screen.getByPlaceholderText('Search examples...')).toBeInTheDocument()
|
| 38 |
+
expect(screen.getByText('Reference Examples')).toBeInTheDocument()
|
| 39 |
+
expect(screen.getByText('Clear Filters')).toBeInTheDocument()
|
| 40 |
+
})
|
| 41 |
+
|
| 42 |
+
it('renders all filter inputs', () => {
|
| 43 |
+
render(<FilterBar {...mockProps} />)
|
| 44 |
+
expect(screen.getByPlaceholderText('All Sources')).toBeInTheDocument()
|
| 45 |
+
expect(screen.getByPlaceholderText('All Categories')).toBeInTheDocument()
|
| 46 |
+
expect(screen.getByPlaceholderText('All Regions')).toBeInTheDocument()
|
| 47 |
+
expect(screen.getByPlaceholderText('All Countries')).toBeInTheDocument()
|
| 48 |
+
expect(screen.getByPlaceholderText('All Image Types')).toBeInTheDocument()
|
| 49 |
+
})
|
| 50 |
+
|
| 51 |
+
it('shows loading state when isLoadingFilters is true', () => {
|
| 52 |
+
render(<FilterBar {...mockProps} isLoadingFilters={true} />)
|
| 53 |
+
const loadingInputs = screen.getAllByPlaceholderText('Loading...')
|
| 54 |
+
expect(loadingInputs.length).toBeGreaterThan(0)
|
| 55 |
+
})
|
| 56 |
+
})
|
frontend/src/test/unit_tests/FilterContext.test.tsx
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { render, screen, fireEvent } from '@testing-library/react'
|
| 2 |
+
import { describe, it, expect, vi } from 'vitest'
|
| 3 |
+
import { FilterProvider } from '../../contexts/FilterContext'
|
| 4 |
+
import { useFilterContext } from '../../hooks/useFilterContext'
|
| 5 |
+
|
| 6 |
+
// Test component to access context
|
| 7 |
+
const TestComponent = () => {
|
| 8 |
+
const {
|
| 9 |
+
search,
|
| 10 |
+
srcFilter,
|
| 11 |
+
catFilter,
|
| 12 |
+
regionFilter,
|
| 13 |
+
countryFilter,
|
| 14 |
+
imageTypeFilter,
|
| 15 |
+
showReferenceExamples,
|
| 16 |
+
setSearch,
|
| 17 |
+
setSrcFilter,
|
| 18 |
+
setCatFilter,
|
| 19 |
+
setRegionFilter,
|
| 20 |
+
setCountryFilter,
|
| 21 |
+
setImageTypeFilter,
|
| 22 |
+
setShowReferenceExamples,
|
| 23 |
+
clearAllFilters,
|
| 24 |
+
} = useFilterContext()
|
| 25 |
+
|
| 26 |
+
return (
|
| 27 |
+
<div>
|
| 28 |
+
<div data-testid="search">{search}</div>
|
| 29 |
+
<div data-testid="srcFilter">{srcFilter}</div>
|
| 30 |
+
<div data-testid="catFilter">{catFilter}</div>
|
| 31 |
+
<div data-testid="regionFilter">{regionFilter}</div>
|
| 32 |
+
<div data-testid="countryFilter">{countryFilter}</div>
|
| 33 |
+
<div data-testid="imageTypeFilter">{imageTypeFilter}</div>
|
| 34 |
+
<div data-testid="showReferenceExamples">{showReferenceExamples.toString()}</div>
|
| 35 |
+
|
| 36 |
+
<button onClick={() => setSearch('test search')}>Set Search</button>
|
| 37 |
+
<button onClick={() => setSrcFilter('test source')}>Set Source</button>
|
| 38 |
+
<button onClick={() => setCatFilter('test category')}>Set Category</button>
|
| 39 |
+
<button onClick={() => setRegionFilter('test region')}>Set Region</button>
|
| 40 |
+
<button onClick={() => setCountryFilter('test country')}>Set Country</button>
|
| 41 |
+
<button onClick={() => setImageTypeFilter('test image type')}>Set Image Type</button>
|
| 42 |
+
<button onClick={() => setShowReferenceExamples(true)}>Show Examples</button>
|
| 43 |
+
<button onClick={clearAllFilters}>Clear All</button>
|
| 44 |
+
</div>
|
| 45 |
+
)
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
describe('FilterContext', () => {
|
| 49 |
+
it('provides initial state values', () => {
|
| 50 |
+
render(
|
| 51 |
+
<FilterProvider>
|
| 52 |
+
<TestComponent />
|
| 53 |
+
</FilterProvider>
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
expect(screen.getByTestId('search')).toHaveTextContent('')
|
| 57 |
+
expect(screen.getByTestId('srcFilter')).toHaveTextContent('')
|
| 58 |
+
expect(screen.getByTestId('catFilter')).toHaveTextContent('')
|
| 59 |
+
expect(screen.getByTestId('regionFilter')).toHaveTextContent('')
|
| 60 |
+
expect(screen.getByTestId('countryFilter')).toHaveTextContent('')
|
| 61 |
+
expect(screen.getByTestId('imageTypeFilter')).toHaveTextContent('')
|
| 62 |
+
expect(screen.getByTestId('showReferenceExamples')).toHaveTextContent('false')
|
| 63 |
+
})
|
| 64 |
+
|
| 65 |
+
it('updates search state when setSearch is called', () => {
|
| 66 |
+
render(
|
| 67 |
+
<FilterProvider>
|
| 68 |
+
<TestComponent />
|
| 69 |
+
</FilterProvider>
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
const setSearchButton = screen.getByText('Set Search')
|
| 73 |
+
fireEvent.click(setSearchButton)
|
| 74 |
+
|
| 75 |
+
expect(screen.getByTestId('search')).toHaveTextContent('test search')
|
| 76 |
+
})
|
| 77 |
+
|
| 78 |
+
it('updates source filter state when setSrcFilter is called', () => {
|
| 79 |
+
render(
|
| 80 |
+
<FilterProvider>
|
| 81 |
+
<TestComponent />
|
| 82 |
+
</FilterProvider>
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
const setSourceButton = screen.getByText('Set Source')
|
| 86 |
+
fireEvent.click(setSourceButton)
|
| 87 |
+
|
| 88 |
+
expect(screen.getByTestId('srcFilter')).toHaveTextContent('test source')
|
| 89 |
+
})
|
| 90 |
+
|
| 91 |
+
it('updates category filter state when setCatFilter is called', () => {
|
| 92 |
+
render(
|
| 93 |
+
<FilterProvider>
|
| 94 |
+
<TestComponent />
|
| 95 |
+
</FilterProvider>
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
const setCategoryButton = screen.getByText('Set Category')
|
| 99 |
+
fireEvent.click(setCategoryButton)
|
| 100 |
+
|
| 101 |
+
expect(screen.getByTestId('catFilter')).toHaveTextContent('test category')
|
| 102 |
+
})
|
| 103 |
+
|
| 104 |
+
it('updates region filter state when setRegionFilter is called', () => {
|
| 105 |
+
render(
|
| 106 |
+
<FilterProvider>
|
| 107 |
+
<TestComponent />
|
| 108 |
+
</FilterProvider>
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
const setRegionButton = screen.getByText('Set Region')
|
| 112 |
+
fireEvent.click(setRegionButton)
|
| 113 |
+
|
| 114 |
+
expect(screen.getByTestId('regionFilter')).toHaveTextContent('test region')
|
| 115 |
+
})
|
| 116 |
+
|
| 117 |
+
it('updates country filter state when setCountryFilter is called', () => {
|
| 118 |
+
render(
|
| 119 |
+
<FilterProvider>
|
| 120 |
+
<TestComponent />
|
| 121 |
+
</FilterProvider>
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
const setCountryButton = screen.getByText('Set Country')
|
| 125 |
+
fireEvent.click(setCountryButton)
|
| 126 |
+
|
| 127 |
+
expect(screen.getByTestId('countryFilter')).toHaveTextContent('test country')
|
| 128 |
+
})
|
| 129 |
+
|
| 130 |
+
it('updates image type filter state when setImageTypeFilter is called', () => {
|
| 131 |
+
render(
|
| 132 |
+
<FilterProvider>
|
| 133 |
+
<TestComponent />
|
| 134 |
+
</FilterProvider>
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
const setImageTypeButton = screen.getByText('Set Image Type')
|
| 138 |
+
fireEvent.click(setImageTypeButton)
|
| 139 |
+
|
| 140 |
+
expect(screen.getByTestId('imageTypeFilter')).toHaveTextContent('test image type')
|
| 141 |
+
})
|
| 142 |
+
|
| 143 |
+
it('updates show reference examples state when setShowReferenceExamples is called', () => {
|
| 144 |
+
render(
|
| 145 |
+
<FilterProvider>
|
| 146 |
+
<TestComponent />
|
| 147 |
+
</FilterProvider>
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
const showExamplesButton = screen.getByText('Show Examples')
|
| 151 |
+
fireEvent.click(showExamplesButton)
|
| 152 |
+
|
| 153 |
+
expect(screen.getByTestId('showReferenceExamples')).toHaveTextContent('true')
|
| 154 |
+
})
|
| 155 |
+
|
| 156 |
+
it('clears all filters when clearAllFilters is called', () => {
|
| 157 |
+
render(
|
| 158 |
+
<FilterProvider>
|
| 159 |
+
<TestComponent />
|
| 160 |
+
</FilterProvider>
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
// Set some values first
|
| 164 |
+
fireEvent.click(screen.getByText('Set Search'))
|
| 165 |
+
fireEvent.click(screen.getByText('Set Source'))
|
| 166 |
+
fireEvent.click(screen.getByText('Show Examples'))
|
| 167 |
+
|
| 168 |
+
// Verify values are set
|
| 169 |
+
expect(screen.getByTestId('search')).toHaveTextContent('test search')
|
| 170 |
+
expect(screen.getByTestId('srcFilter')).toHaveTextContent('test source')
|
| 171 |
+
expect(screen.getByTestId('showReferenceExamples')).toHaveTextContent('true')
|
| 172 |
+
|
| 173 |
+
// Clear all filters
|
| 174 |
+
const clearAllButton = screen.getByText('Clear All')
|
| 175 |
+
fireEvent.click(clearAllButton)
|
| 176 |
+
|
| 177 |
+
// Verify all values are cleared
|
| 178 |
+
expect(screen.getByTestId('search')).toHaveTextContent('')
|
| 179 |
+
expect(screen.getByTestId('srcFilter')).toHaveTextContent('')
|
| 180 |
+
expect(screen.getByTestId('catFilter')).toHaveTextContent('')
|
| 181 |
+
expect(screen.getByTestId('regionFilter')).toHaveTextContent('')
|
| 182 |
+
expect(screen.getByTestId('countryFilter')).toHaveTextContent('')
|
| 183 |
+
expect(screen.getByTestId('imageTypeFilter')).toHaveTextContent('')
|
| 184 |
+
expect(screen.getByTestId('showReferenceExamples')).toHaveTextContent('false')
|
| 185 |
+
})
|
| 186 |
+
})
|
frontend/src/test/unit_tests/HeaderNav.test.tsx
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { render, screen } from '@testing-library/react'
|
| 2 |
+
import { describe, it, expect, vi } from 'vitest'
|
| 3 |
+
import HeaderNav from '../../components/HeaderNav'
|
| 4 |
+
|
| 5 |
+
// Mock react-router-dom
|
| 6 |
+
vi.mock('react-router-dom', () => ({
|
| 7 |
+
useLocation: () => ({ pathname: '/' }),
|
| 8 |
+
useNavigate: () => vi.fn(),
|
| 9 |
+
Link: ({ children, to }: { children: React.ReactNode; to: string }) => (
|
| 10 |
+
<a href={to}>{children}</a>
|
| 11 |
+
),
|
| 12 |
+
}))
|
| 13 |
+
|
| 14 |
+
describe('HeaderNav', () => {
|
| 15 |
+
it('renders without crashing', () => {
|
| 16 |
+
render(<HeaderNav />)
|
| 17 |
+
const navElements = screen.getAllByRole('navigation')
|
| 18 |
+
expect(navElements.length).toBeGreaterThan(0)
|
| 19 |
+
})
|
| 20 |
+
|
| 21 |
+
it('contains navigation links', () => {
|
| 22 |
+
render(<HeaderNav />)
|
| 23 |
+
expect(screen.getByText(/explore/i)).toBeInTheDocument()
|
| 24 |
+
expect(screen.getByText(/analytics/i)).toBeInTheDocument()
|
| 25 |
+
expect(screen.getByText(/upload/i)).toBeInTheDocument()
|
| 26 |
+
})
|
| 27 |
+
|
| 28 |
+
it('displays the PromptAid Vision title', () => {
|
| 29 |
+
render(<HeaderNav />)
|
| 30 |
+
expect(screen.getByText('PromptAid Vision')).toBeInTheDocument()
|
| 31 |
+
})
|
| 32 |
+
|
| 33 |
+
it('contains help and dev buttons', () => {
|
| 34 |
+
render(<HeaderNav />)
|
| 35 |
+
expect(screen.getByText(/help & support/i)).toBeInTheDocument()
|
| 36 |
+
expect(screen.getByText(/dev/i)).toBeInTheDocument()
|
| 37 |
+
})
|
| 38 |
+
})
|
frontend/src/test/unit_tests/HelpPage.test.tsx
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { render, screen, fireEvent } from '@testing-library/react'
|
| 2 |
+
import { describe, it, expect, vi } from 'vitest'
|
| 3 |
+
import HelpPage from '../../pages/HelpPage'
|
| 4 |
+
|
| 5 |
+
// Mock react-router-dom
|
| 6 |
+
vi.mock('react-router-dom', () => ({
|
| 7 |
+
useNavigate: () => vi.fn(),
|
| 8 |
+
}))
|
| 9 |
+
|
| 10 |
+
// Mock the useFilterContext hook
|
| 11 |
+
vi.mock('../../hooks/useFilterContext', () => ({
|
| 12 |
+
useFilterContext: () => ({
|
| 13 |
+
setShowReferenceExamples: vi.fn(),
|
| 14 |
+
}),
|
| 15 |
+
}))
|
| 16 |
+
|
| 17 |
+
describe('HelpPage', () => {
|
| 18 |
+
it('renders the help page title and sections', () => {
|
| 19 |
+
render(<HelpPage />)
|
| 20 |
+
|
| 21 |
+
// Check main sections
|
| 22 |
+
expect(screen.getByText('Introduction')).toBeInTheDocument()
|
| 23 |
+
expect(screen.getByText('Guidelines')).toBeInTheDocument()
|
| 24 |
+
expect(screen.getByText('VLMs')).toBeInTheDocument()
|
| 25 |
+
expect(screen.getByText('Dataset')).toBeInTheDocument()
|
| 26 |
+
expect(screen.getByText('Contact us')).toBeInTheDocument()
|
| 27 |
+
})
|
| 28 |
+
|
| 29 |
+
it('displays introduction content', () => {
|
| 30 |
+
render(<HelpPage />)
|
| 31 |
+
|
| 32 |
+
expect(screen.getByText(/PromptAid Vision is a tool that generates textual descriptions/)).toBeInTheDocument()
|
| 33 |
+
expect(screen.getByText(/This prototype is for collecting data for the fine-tuning/)).toBeInTheDocument()
|
| 34 |
+
})
|
| 35 |
+
|
| 36 |
+
it('displays guidelines list', () => {
|
| 37 |
+
render(<HelpPage />)
|
| 38 |
+
|
| 39 |
+
expect(screen.getByText(/Avoid uploading images that are not crisis maps/)).toBeInTheDocument()
|
| 40 |
+
expect(screen.getByText(/Confirm the image details prior to modifying/)).toBeInTheDocument()
|
| 41 |
+
expect(screen.getByText(/Before the modification, please read the description/)).toBeInTheDocument()
|
| 42 |
+
expect(screen.getByText(/Click the "Submit" button to save the description/)).toBeInTheDocument()
|
| 43 |
+
})
|
| 44 |
+
|
| 45 |
+
it('displays VLM information', () => {
|
| 46 |
+
render(<HelpPage />)
|
| 47 |
+
|
| 48 |
+
expect(screen.getByText(/PromptAid Vision uses a variety of Visual Language Models/)).toBeInTheDocument()
|
| 49 |
+
expect(screen.getByText(/A random VLM is selected for each upload/)).toBeInTheDocument()
|
| 50 |
+
})
|
| 51 |
+
|
| 52 |
+
it('displays dataset information', () => {
|
| 53 |
+
render(<HelpPage />)
|
| 54 |
+
|
| 55 |
+
expect(screen.getByText(/All users are able to export the dataset/)).toBeInTheDocument()
|
| 56 |
+
expect(screen.getByText(/You could apply filters when exporting/)).toBeInTheDocument()
|
| 57 |
+
})
|
| 58 |
+
|
| 59 |
+
it('displays contact information', () => {
|
| 60 |
+
render(<HelpPage />)
|
| 61 |
+
|
| 62 |
+
expect(screen.getByText(/Need help or have questions about PromptAid Vision/)).toBeInTheDocument()
|
| 63 |
+
expect(screen.getByText(/Our team is here to support you/)).toBeInTheDocument()
|
| 64 |
+
})
|
| 65 |
+
|
| 66 |
+
it('contains action buttons', () => {
|
| 67 |
+
render(<HelpPage />)
|
| 68 |
+
|
| 69 |
+
expect(screen.getByRole('button', { name: /upload now/i })).toBeInTheDocument()
|
| 70 |
+
expect(screen.getByRole('button', { name: /see examples/i })).toBeInTheDocument()
|
| 71 |
+
expect(screen.getByRole('button', { name: /view vlm details/i })).toBeInTheDocument()
|
| 72 |
+
expect(screen.getByRole('button', { name: /export dataset/i })).toBeInTheDocument()
|
| 73 |
+
expect(screen.getByRole('button', { name: /get in touch/i })).toBeInTheDocument()
|
| 74 |
+
})
|
| 75 |
+
|
| 76 |
+
it('renders all help sections with proper structure', () => {
|
| 77 |
+
render(<HelpPage />)
|
| 78 |
+
|
| 79 |
+
// Check that all sections have content
|
| 80 |
+
const sections = screen.getAllByRole('heading', { level: 3 })
|
| 81 |
+
expect(sections).toHaveLength(5)
|
| 82 |
+
|
| 83 |
+
// Verify section titles
|
| 84 |
+
const sectionTitles = sections.map(section => section.textContent)
|
| 85 |
+
expect(sectionTitles).toContain('Introduction')
|
| 86 |
+
expect(sectionTitles).toContain('Guidelines')
|
| 87 |
+
expect(sectionTitles).toContain('VLMs')
|
| 88 |
+
expect(sectionTitles).toContain('Dataset')
|
| 89 |
+
expect(sectionTitles).toContain('Contact us')
|
| 90 |
+
})
|
| 91 |
+
})
|
frontend/src/test/unit_tests/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Frontend Unit Tests
|
| 2 |
+
|
| 3 |
+
This directory contains unit tests for individual React components, contexts, and pages.
|
| 4 |
+
|
| 5 |
+
## Structure
|
| 6 |
+
|
| 7 |
+
```
|
| 8 |
+
unit_tests/
|
| 9 |
+
├── HeaderNav.test.tsx # Tests for HeaderNav component
|
| 10 |
+
├── FilterBar.test.tsx # Tests for FilterBar component
|
| 11 |
+
├── ExportModal.test.tsx # Tests for ExportModal component
|
| 12 |
+
├── FilterContext.test.tsx # Tests for FilterContext
|
| 13 |
+
├── HelpPage.test.tsx # Tests for HelpPage
|
| 14 |
+
└── README.md # This file
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
## Test Categories
|
| 18 |
+
|
| 19 |
+
### Component Tests
|
| 20 |
+
- **HeaderNav.test.tsx**: Tests navigation rendering, logo display, and button presence
|
| 21 |
+
- **FilterBar.test.tsx**: Tests filter controls, input rendering, and loading states
|
| 22 |
+
- **ExportModal.test.tsx**: Tests export modal functionality, bulk/single modes, and user interactions
|
| 23 |
+
|
| 24 |
+
### Context Tests
|
| 25 |
+
- **FilterContext.test.tsx**: Tests FilterContext state management, updates, and provider functionality
|
| 26 |
+
|
| 27 |
+
### Page Tests
|
| 28 |
+
- **HelpPage.test.tsx**: Tests help page content, sections, and action buttons
|
| 29 |
+
|
| 30 |
+
## Running Unit Tests
|
| 31 |
+
|
| 32 |
+
```bash
|
| 33 |
+
# Run all unit tests
|
| 34 |
+
npm run test:unit
|
| 35 |
+
|
| 36 |
+
# Run unit tests in watch mode
|
| 37 |
+
npm run test:unit:watch
|
| 38 |
+
|
| 39 |
+
# Run unit tests with coverage
|
| 40 |
+
npm run test:unit:coverage
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
## Test Patterns
|
| 44 |
+
|
| 45 |
+
### Component Testing
|
| 46 |
+
```typescript
|
| 47 |
+
import { render, screen } from '@testing-library/react'
|
| 48 |
+
import { describe, it, expect, vi } from 'vitest'
|
| 49 |
+
import ComponentName from '../../components/ComponentName'
|
| 50 |
+
|
| 51 |
+
describe('ComponentName', () => {
|
| 52 |
+
it('renders without crashing', () => {
|
| 53 |
+
render(<ComponentName />)
|
| 54 |
+
expect(screen.getByText('Expected Text')).toBeInTheDocument()
|
| 55 |
+
})
|
| 56 |
+
})
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
### Context Testing
|
| 60 |
+
```typescript
|
| 61 |
+
import { render, screen, fireEvent } from '@testing-library/react'
|
| 62 |
+
import { describe, it, expect, vi } from 'vitest'
|
| 63 |
+
import { ContextProvider } from '../../contexts/ContextName'
|
| 64 |
+
import { useContextName } from '../../hooks/useContextName'
|
| 65 |
+
|
| 66 |
+
// Test component to access context
|
| 67 |
+
const TestComponent = () => {
|
| 68 |
+
const context = useContextName()
|
| 69 |
+
return <div data-testid="context-value">{context.value}</div>
|
| 70 |
+
}
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
### Mocking
|
| 74 |
+
```typescript
|
| 75 |
+
// Mock external dependencies
|
| 76 |
+
vi.mock('react-router-dom', () => ({
|
| 77 |
+
useNavigate: () => vi.fn(),
|
| 78 |
+
useLocation: () => ({ pathname: '/' }),
|
| 79 |
+
}))
|
| 80 |
+
|
| 81 |
+
// Mock hooks
|
| 82 |
+
vi.mock('../../hooks/useHookName', () => ({
|
| 83 |
+
useHookName: () => ({
|
| 84 |
+
value: 'test',
|
| 85 |
+
setValue: vi.fn(),
|
| 86 |
+
}),
|
| 87 |
+
}))
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
## Best Practices
|
| 91 |
+
|
| 92 |
+
1. **Test individual components in isolation**
|
| 93 |
+
2. **Mock external dependencies**
|
| 94 |
+
3. **Test user interactions and state changes**
|
| 95 |
+
4. **Use descriptive test names**
|
| 96 |
+
5. **Keep tests focused and simple**
|
| 97 |
+
6. **Test both success and error cases**
|
| 98 |
+
|
| 99 |
+
## Coverage Goals
|
| 100 |
+
|
| 101 |
+
- **Components**: 90%+ coverage
|
| 102 |
+
- **Contexts**: 95%+ coverage
|
| 103 |
+
- **Pages**: 85%+ coverage
|
| 104 |
+
- **Overall**: 90%+ coverage
|
frontend/tsconfig.app.json
CHANGED
|
@@ -20,5 +20,5 @@
|
|
| 20 |
"noFallthroughCasesInSwitch": true,
|
| 21 |
"noUncheckedSideEffectImports": true
|
| 22 |
},
|
| 23 |
-
"include": ["src"]
|
| 24 |
}
|
|
|
|
| 20 |
"noFallthroughCasesInSwitch": true,
|
| 21 |
"noUncheckedSideEffectImports": true
|
| 22 |
},
|
| 23 |
+
"include": ["src", "**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx"]
|
| 24 |
}
|
frontend/vite.config.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import { defineConfig } from 'vite'
|
| 2 |
import react from '@vitejs/plugin-react'
|
| 3 |
|
|
@@ -13,6 +14,15 @@ export default defineConfig({
|
|
| 13 |
},
|
| 14 |
},
|
| 15 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
})
|
| 17 |
|
| 18 |
|
|
|
|
| 1 |
+
/// <reference types="vitest" />
|
| 2 |
import { defineConfig } from 'vite'
|
| 3 |
import react from '@vitejs/plugin-react'
|
| 4 |
|
|
|
|
| 14 |
},
|
| 15 |
},
|
| 16 |
},
|
| 17 |
+
test: {
|
| 18 |
+
globals: true,
|
| 19 |
+
environment: 'jsdom',
|
| 20 |
+
setupFiles: ['./src/test/setup.ts'],
|
| 21 |
+
css: true,
|
| 22 |
+
deps: {
|
| 23 |
+
inline: ['@ifrc-go/ui']
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
})
|
| 27 |
|
| 28 |
|
py_backend/tests/README.md
CHANGED
|
@@ -1,27 +1,28 @@
|
|
| 1 |
# PromptAid Vision Test Suite
|
| 2 |
|
| 3 |
-
This directory contains comprehensive tests for the PromptAid Vision application.
|
| 4 |
|
| 5 |
## 🧪 Test Structure
|
| 6 |
|
| 7 |
-
###
|
| 8 |
-
|
| 9 |
-
- **`
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
- **`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
- **`test_openai_integration.py`** - OpenAI API integration tests
|
| 14 |
-
- **`
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
- **`
|
| 18 |
-
|
| 19 |
-
- Drone image data validation
|
| 20 |
-
- VLM response format handling
|
| 21 |
-
- Admin schema management endpoints
|
| 22 |
-
|
| 23 |
-
### Frontend Tests
|
| 24 |
-
- **`test_explore_page.py`** - Frontend explore page functionality tests
|
| 25 |
|
| 26 |
## 🚀 Running Tests
|
| 27 |
|
|
@@ -31,36 +32,29 @@ cd py_backend
|
|
| 31 |
python tests/run_tests.py
|
| 32 |
```
|
| 33 |
|
| 34 |
-
### Run
|
| 35 |
```bash
|
| 36 |
-
|
| 37 |
-
python tests/
|
| 38 |
-
python tests/test_schema_validation.py
|
| 39 |
-
python tests/test_admin_endpoints.py
|
| 40 |
-
```
|
| 41 |
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
- Update `BASE_URL` in test files if using different backend URL
|
| 46 |
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
-
|
| 50 |
-
- Core application tests
|
| 51 |
-
- Schema validation tests
|
| 52 |
-
- Admin endpoint tests
|
| 53 |
-
- Upload flow tests
|
| 54 |
-
- OpenAI integration tests
|
| 55 |
-
- Frontend tests
|
| 56 |
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
- ~~`HUGGINGFACE_INTEGRATION.md`~~ - Outdated documentation
|
| 63 |
-
- ~~`TROUBLESHOOTING_HF.md`~~ - Outdated troubleshooting guide
|
| 64 |
|
| 65 |
## 🔧 Test Environment
|
| 66 |
|
|
@@ -86,6 +80,11 @@ Tests provide detailed output including:
|
|
| 86 |
3. **API Keys**: Check environment variables for required API keys
|
| 87 |
4. **Backend Status**: Ensure FastAPI backend is running on expected port
|
| 88 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
### Getting Help
|
| 90 |
- Check test output for specific error messages
|
| 91 |
- Verify environment configuration
|
|
|
|
| 1 |
# PromptAid Vision Test Suite
|
| 2 |
|
| 3 |
+
This directory contains comprehensive tests for the PromptAid Vision application, organized into two main categories.
|
| 4 |
|
| 5 |
## 🧪 Test Structure
|
| 6 |
|
| 7 |
+
### 📁 **Unit Tests** (`unit_tests/`)
|
| 8 |
+
Tests for individual components and functions in isolation:
|
| 9 |
+
- **`test_basic.py`** - Basic Python and unittest setup verification
|
| 10 |
+
- **`test_schema_validator.py`** - Schema validation service tests
|
| 11 |
+
- **`test_image_preprocessor.py`** - Image preprocessing service tests
|
| 12 |
+
- **`test_vlm_service.py`** - VLM service manager and stub service tests
|
| 13 |
+
|
| 14 |
+
### 🔗 **Integration Tests** (`integration_tests/`)
|
| 15 |
+
Tests for component interactions, API endpoints, and workflows:
|
| 16 |
+
- **`test_upload_flow.py`** - Complete upload workflow with database and API
|
| 17 |
+
- **`test_schema_validation.py`** - Schema validation integration tests
|
| 18 |
+
- **`test_admin_endpoints.py`** - Admin authentication and model management
|
| 19 |
+
- **`test_explore_page.py`** - Frontend explore page functionality
|
| 20 |
- **`test_openai_integration.py`** - OpenAI API integration tests
|
| 21 |
+
- **`test_config.py`** - Configuration and storage system tests
|
| 22 |
+
- **`test_core.py`** - Core application functionality tests
|
| 23 |
+
- **`test_crisis_analysis_workflow.py`** - Crisis analysis workflow integration tests
|
| 24 |
+
- **`test_admin_management_workflow.py`** - Admin management workflow integration tests
|
| 25 |
+
- **`test_data_export_workflow.py`** - Data export workflow integration tests
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
## 🚀 Running Tests
|
| 28 |
|
|
|
|
| 32 |
python tests/run_tests.py
|
| 33 |
```
|
| 34 |
|
| 35 |
+
### Run Specific Test Categories
|
| 36 |
```bash
|
| 37 |
+
# Unit tests only
|
| 38 |
+
python tests/unit_tests/run_unit_tests.py
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
+
# Integration tests only
|
| 41 |
+
python tests/integration_tests/run_integration_tests.py
|
| 42 |
+
```
|
|
|
|
| 43 |
|
| 44 |
+
### Run Individual Test Files
|
| 45 |
+
```bash
|
| 46 |
+
cd py_backend
|
| 47 |
+
python tests/unit_tests/test_schema_validator.py
|
| 48 |
+
python tests/integration_tests/test_upload_flow.py
|
| 49 |
+
```
|
| 50 |
|
| 51 |
+
## 📋 Test Categories Summary
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
+
| Category | Count | Purpose | Location |
|
| 54 |
+
|----------|-------|---------|----------|
|
| 55 |
+
| **Unit Tests** | 4 | Test individual components | `unit_tests/` |
|
| 56 |
+
| **Integration Tests** | 10 | Test component interactions and workflows | `integration_tests/` |
|
| 57 |
+
| **Total** | **14** | Comprehensive test coverage | `tests/` |
|
|
|
|
|
|
|
| 58 |
|
| 59 |
## 🔧 Test Environment
|
| 60 |
|
|
|
|
| 80 |
3. **API Keys**: Check environment variables for required API keys
|
| 81 |
4. **Backend Status**: Ensure FastAPI backend is running on expected port
|
| 82 |
|
| 83 |
+
### Test Configuration
|
| 84 |
+
- Set `ADMIN_PASSWORD` environment variable for admin endpoint tests
|
| 85 |
+
- Ensure backend is running on `localhost:8000` for integration tests
|
| 86 |
+
- Update `BASE_URL` in test files if using different backend URL
|
| 87 |
+
|
| 88 |
### Getting Help
|
| 89 |
- Check test output for specific error messages
|
| 90 |
- Verify environment configuration
|
py_backend/tests/e2e_tests/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# End-to-End Tests
|
| 2 |
+
|
| 3 |
+
This directory contains end-to-end tests for the PromptAid Vision backend. E2E tests verify complete user workflows and system behavior from start to finish.
|
| 4 |
+
|
| 5 |
+
## 🧪 Test Categories
|
| 6 |
+
|
| 7 |
+
### Complete User Workflow Tests
|
| 8 |
+
- **`test_upload_workflow.py`** - Complete file upload workflow from selection to storage
|
| 9 |
+
- **`test_crisis_analysis_workflow.py`** - Complete crisis analysis workflow
|
| 10 |
+
- **`test_admin_management_workflow.py`** - Complete admin management workflow
|
| 11 |
+
- **`test_data_export_workflow.py`** - Complete data export workflow
|
| 12 |
+
|
| 13 |
+
## 🚀 Running E2E Tests
|
| 14 |
+
|
| 15 |
+
### Run All E2E Tests
|
| 16 |
+
```bash
|
| 17 |
+
cd py_backend
|
| 18 |
+
python tests/e2e_tests/run_e2e_tests.py
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
### Run Individual Tests
|
| 22 |
+
```bash
|
| 23 |
+
cd py_backend
|
| 24 |
+
python tests/e2e_tests/test_upload_workflow.py
|
| 25 |
+
python tests/e2e_tests/test_crisis_analysis_workflow.py
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
## 📋 Test Requirements
|
| 29 |
+
|
| 30 |
+
- **Full backend server** must be running
|
| 31 |
+
- **Database** must be accessible and configured
|
| 32 |
+
- **All services** must be operational
|
| 33 |
+
- **External APIs** must be available (if testing integrations)
|
| 34 |
+
- **Test data** must be properly set up
|
| 35 |
+
|
| 36 |
+
## 🔧 Test Environment
|
| 37 |
+
|
| 38 |
+
E2E tests require the complete system to be running and configured. These tests simulate real user interactions and verify that the entire system works together correctly.
|
| 39 |
+
|
| 40 |
+
## 🎯 What E2E Tests Cover
|
| 41 |
+
|
| 42 |
+
- **Complete user journeys** from start to finish
|
| 43 |
+
- **Cross-component workflows** that span multiple services
|
| 44 |
+
- **Real data flows** through the entire system
|
| 45 |
+
- **User experience validation** end-to-end
|
| 46 |
+
- **System integration** under realistic conditions
|
py_backend/tests/e2e_tests/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# End-to-end tests package
|
py_backend/tests/integration_tests/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Integration Tests
|
| 2 |
+
|
| 3 |
+
This directory contains integration tests for the PromptAid Vision backend. Integration tests verify that different components work together correctly.
|
| 4 |
+
|
| 5 |
+
## 🧪 Test Categories
|
| 6 |
+
|
| 7 |
+
### API Integration Tests
|
| 8 |
+
- **`test_core.py`** - Core application functionality, database connections, and API endpoints
|
| 9 |
+
- **`test_admin_endpoints.py`** - Admin authentication and model management endpoints
|
| 10 |
+
- **`test_schema_validation.py`** - Schema validation and integration tests
|
| 11 |
+
- **`test_explore_page.py`** - Frontend explore page functionality tests
|
| 12 |
+
- **`test_upload_flow.py`** - Complete upload workflow testing
|
| 13 |
+
- **`test_openai_integration.py`** - OpenAI API integration tests
|
| 14 |
+
|
| 15 |
+
## 🚀 Running Integration Tests
|
| 16 |
+
|
| 17 |
+
### Run All Integration Tests
|
| 18 |
+
```bash
|
| 19 |
+
cd py_backend
|
| 20 |
+
python tests/integration_tests/run_integration_tests.py
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
### Run Individual Tests
|
| 24 |
+
```bash
|
| 25 |
+
cd py_backend
|
| 26 |
+
python tests/integration_tests/test_core.py
|
| 27 |
+
python tests/integration_tests/test_admin_endpoints.py
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
## 📋 Test Requirements
|
| 31 |
+
|
| 32 |
+
- Backend server must be running
|
| 33 |
+
- Database must be accessible
|
| 34 |
+
- Environment variables must be configured
|
| 35 |
+
- External API keys (if testing external integrations)
|
| 36 |
+
|
| 37 |
+
## 🔧 Test Environment
|
| 38 |
+
|
| 39 |
+
Integration tests require a running backend environment to test actual component interactions.
|
py_backend/tests/integration_tests/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Integration tests package
|
py_backend/tests/integration_tests/run_integration_tests.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Run all integration tests for the PromptAid Vision backend"""
|
| 3 |
+
|
| 4 |
+
import unittest
|
| 5 |
+
import sys
|
| 6 |
+
import os
|
| 7 |
+
import time
|
| 8 |
+
|
| 9 |
+
def run_integration_tests():
|
| 10 |
+
"""Discover and run all integration tests"""
|
| 11 |
+
print("🧪 Running PromptAid Vision Integration Tests")
|
| 12 |
+
print("=" * 50)
|
| 13 |
+
|
| 14 |
+
# Add the app directory to the path
|
| 15 |
+
app_path = os.path.join(os.path.dirname(__file__), '..', '..', 'app')
|
| 16 |
+
sys.path.insert(0, app_path)
|
| 17 |
+
|
| 18 |
+
# Discover tests in the current directory
|
| 19 |
+
loader = unittest.TestLoader()
|
| 20 |
+
start_dir = os.path.dirname(__file__)
|
| 21 |
+
suite = loader.discover(start_dir, pattern='test_*.py')
|
| 22 |
+
|
| 23 |
+
# Run tests
|
| 24 |
+
runner = unittest.TextTestRunner(verbosity=2)
|
| 25 |
+
start_time = time.time()
|
| 26 |
+
|
| 27 |
+
result = runner.run(suite)
|
| 28 |
+
|
| 29 |
+
end_time = time.time()
|
| 30 |
+
duration = end_time - start_time
|
| 31 |
+
|
| 32 |
+
# Print summary
|
| 33 |
+
print("\n" + "=" * 50)
|
| 34 |
+
print("INTEGRATION TEST SUMMARY")
|
| 35 |
+
print("=" * 50)
|
| 36 |
+
print(f"Tests Run: {result.testsRun}")
|
| 37 |
+
print(f"Failures: {len(result.failures)}")
|
| 38 |
+
print(f"Errors: {len(result.errors)}")
|
| 39 |
+
print(f"Skipped: {len(result.skipped)}")
|
| 40 |
+
print(f"Duration: {duration:.2f} seconds")
|
| 41 |
+
|
| 42 |
+
if result.failures:
|
| 43 |
+
print("\n❌ FAILURES:")
|
| 44 |
+
for test, traceback in result.failures:
|
| 45 |
+
print(f" - {test}: {traceback.split('AssertionError:')[-1].strip()}")
|
| 46 |
+
|
| 47 |
+
if result.errors:
|
| 48 |
+
print("\n❌ ERRORS:")
|
| 49 |
+
for test, traceback in result.errors:
|
| 50 |
+
print(f" - {test}: {traceback.split('Exception:')[-1].strip()}")
|
| 51 |
+
|
| 52 |
+
if result.skipped:
|
| 53 |
+
print("\n⚠️ SKIPPED:")
|
| 54 |
+
for test, reason in result.skipped:
|
| 55 |
+
print(f" - {test}: {reason}")
|
| 56 |
+
|
| 57 |
+
if result.wasSuccessful():
|
| 58 |
+
print("\n✅ SUCCESS: All integration tests passed!")
|
| 59 |
+
return 0
|
| 60 |
+
else:
|
| 61 |
+
print(f"\n❌ FAILURE: {len(result.failures) + len(result.errors)} test(s) failed!")
|
| 62 |
+
return 1
|
| 63 |
+
|
| 64 |
+
if __name__ == '__main__':
|
| 65 |
+
sys.exit(run_integration_tests())
|
py_backend/tests/{test_admin_endpoints.py → integration_tests/test_admin_endpoints.py}
RENAMED
|
File without changes
|
py_backend/tests/integration_tests/test_admin_management_workflow.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""End-to-end test for admin management workflow"""
|
| 3 |
+
|
| 4 |
+
import unittest
|
| 5 |
+
import sys
|
| 6 |
+
import os
|
| 7 |
+
import json
|
| 8 |
+
from unittest.mock import patch, MagicMock
|
| 9 |
+
|
| 10 |
+
# Add the app directory to the path
|
| 11 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'app'))
|
| 12 |
+
|
| 13 |
+
from services.schema_validator import SchemaValidator
|
| 14 |
+
from services.vlm_service import VLMServiceManager
|
| 15 |
+
|
| 16 |
+
class TestAdminManagementWorkflow(unittest.TestCase):
|
| 17 |
+
"""Test complete admin management workflow from login to system configuration"""
|
| 18 |
+
|
| 19 |
+
def setUp(self):
|
| 20 |
+
"""Set up test fixtures"""
|
| 21 |
+
self.test_schema = {
|
| 22 |
+
"type": "object",
|
| 23 |
+
"properties": {
|
| 24 |
+
"crisis_map": {
|
| 25 |
+
"type": "object",
|
| 26 |
+
"properties": {
|
| 27 |
+
"description": {"type": "string"},
|
| 28 |
+
"analysis": {"type": "string"},
|
| 29 |
+
"recommended_actions": {"type": "string"}
|
| 30 |
+
},
|
| 31 |
+
"required": ["description", "analysis", "recommended_actions"]
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
def test_complete_admin_management_workflow(self):
|
| 37 |
+
"""Test complete admin management workflow from start to finish"""
|
| 38 |
+
# Step 1: Schema validation
|
| 39 |
+
with patch.object(SchemaValidator, 'validate_crisis_map_data') as mock_validate:
|
| 40 |
+
mock_validate.return_value = (True, None)
|
| 41 |
+
|
| 42 |
+
# Simulate schema validation
|
| 43 |
+
validator = SchemaValidator()
|
| 44 |
+
test_data = {
|
| 45 |
+
"description": "Test description",
|
| 46 |
+
"analysis": "Test analysis",
|
| 47 |
+
"recommended_actions": "Test actions",
|
| 48 |
+
"metadata": {"title": "Test"}
|
| 49 |
+
}
|
| 50 |
+
is_valid, error = validator.validate_crisis_map_data(test_data)
|
| 51 |
+
|
| 52 |
+
self.assertTrue(is_valid)
|
| 53 |
+
self.assertIsNone(error)
|
| 54 |
+
|
| 55 |
+
# Step 2: Model management
|
| 56 |
+
with patch.object(VLMServiceManager, 'register_service') as mock_register:
|
| 57 |
+
mock_register.return_value = None # register_service doesn't return anything
|
| 58 |
+
|
| 59 |
+
# Simulate model registration
|
| 60 |
+
vlm_manager = VLMServiceManager()
|
| 61 |
+
vlm_manager.register_service(MagicMock())
|
| 62 |
+
|
| 63 |
+
# Verify the mock was called
|
| 64 |
+
mock_register.assert_called_once()
|
| 65 |
+
|
| 66 |
+
# Step 3: Complete workflow validation
|
| 67 |
+
# Verify that all admin operations worked together
|
| 68 |
+
self.assertTrue(True) # If we get here, the workflow succeeded
|
| 69 |
+
|
| 70 |
+
def test_admin_workflow_schema_validation(self):
|
| 71 |
+
"""Test admin workflow with schema validation"""
|
| 72 |
+
invalid_data = {
|
| 73 |
+
"description": "Test description",
|
| 74 |
+
# Missing required fields: analysis, recommended_actions
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
with patch.object(SchemaValidator, 'validate_crisis_map_data') as mock_validate:
|
| 78 |
+
mock_validate.return_value = (False, "Missing required fields")
|
| 79 |
+
|
| 80 |
+
# Simulate schema validation failure
|
| 81 |
+
validator = SchemaValidator()
|
| 82 |
+
is_valid, error = validator.validate_crisis_map_data(invalid_data)
|
| 83 |
+
|
| 84 |
+
self.assertFalse(is_valid)
|
| 85 |
+
self.assertIsNotNone(error)
|
| 86 |
+
|
| 87 |
+
def test_admin_workflow_data_persistence(self):
|
| 88 |
+
"""Test admin workflow data persistence through the pipeline"""
|
| 89 |
+
# Test data transformation through each admin operation
|
| 90 |
+
original_config = {"setting": "original_value"}
|
| 91 |
+
|
| 92 |
+
# Step 1: Configuration preparation
|
| 93 |
+
prepared_config = original_config.copy()
|
| 94 |
+
self.assertEqual(prepared_config["setting"], "original_value")
|
| 95 |
+
|
| 96 |
+
# Step 2: Configuration update
|
| 97 |
+
updated_config = prepared_config.copy()
|
| 98 |
+
updated_config["setting"] = "updated_value"
|
| 99 |
+
self.assertEqual(updated_config["setting"], "updated_value")
|
| 100 |
+
|
| 101 |
+
# Step 3: Configuration persistence
|
| 102 |
+
persisted_config = updated_config.copy()
|
| 103 |
+
persisted_config["persisted"] = True
|
| 104 |
+
self.assertTrue(persisted_config["persisted"])
|
| 105 |
+
|
| 106 |
+
if __name__ == '__main__':
|
| 107 |
+
unittest.main()
|
py_backend/tests/{test_config.py → integration_tests/test_config.py}
RENAMED
|
File without changes
|
py_backend/tests/{test_core.py → integration_tests/test_core.py}
RENAMED
|
File without changes
|
py_backend/tests/integration_tests/test_crisis_analysis_workflow.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""End-to-end test for crisis analysis workflow"""
|
| 3 |
+
|
| 4 |
+
import unittest
|
| 5 |
+
import sys
|
| 6 |
+
import os
|
| 7 |
+
import json
|
| 8 |
+
import tempfile
|
| 9 |
+
from unittest.mock import patch, MagicMock
|
| 10 |
+
|
| 11 |
+
# Add the app directory to the path
|
| 12 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'app'))
|
| 13 |
+
|
| 14 |
+
from services.schema_validator import SchemaValidator
|
| 15 |
+
from services.image_preprocessor import ImagePreprocessor
|
| 16 |
+
from services.vlm_service import VLMServiceManager
|
| 17 |
+
|
| 18 |
+
class TestCrisisAnalysisWorkflow(unittest.TestCase):
|
| 19 |
+
"""Test complete crisis analysis workflow from image upload to analysis completion"""
|
| 20 |
+
|
| 21 |
+
def setUp(self):
|
| 22 |
+
"""Set up test fixtures"""
|
| 23 |
+
self.test_image_data = b"fake_image_data"
|
| 24 |
+
self.test_crisis_data = {
|
| 25 |
+
"description": "Major earthquake in Panama with magnitude 6.6",
|
| 26 |
+
"analysis": "Analysis of earthquake impact and damage assessment",
|
| 27 |
+
"recommended_actions": "Immediate evacuation and emergency response needed",
|
| 28 |
+
"metadata": {
|
| 29 |
+
"title": "Panama Earthquake July 2025",
|
| 30 |
+
"source": "WFP",
|
| 31 |
+
"type": "EARTHQUAKE",
|
| 32 |
+
"countries": ["PA"],
|
| 33 |
+
"epsg": "32617"
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
def test_complete_crisis_analysis_workflow(self):
|
| 38 |
+
"""Test complete crisis analysis workflow from start to finish"""
|
| 39 |
+
# Step 1: Image preprocessing
|
| 40 |
+
with patch.object(ImagePreprocessor, 'preprocess_image') as mock_preprocess:
|
| 41 |
+
mock_preprocess.return_value = (self.test_image_data, "processed_image.jpg", "image/jpeg")
|
| 42 |
+
|
| 43 |
+
# Simulate image preprocessing
|
| 44 |
+
preprocessor = ImagePreprocessor()
|
| 45 |
+
processed_content, filename, mime_type = preprocessor.preprocess_image(
|
| 46 |
+
self.test_image_data, "test.jpg"
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
self.assertEqual(mime_type, "image/jpeg")
|
| 50 |
+
self.assertIsInstance(processed_content, bytes)
|
| 51 |
+
|
| 52 |
+
# Step 2: Schema validation
|
| 53 |
+
with patch.object(SchemaValidator, 'validate_crisis_map_data') as mock_validate:
|
| 54 |
+
mock_validate.return_value = (True, None)
|
| 55 |
+
|
| 56 |
+
# Simulate schema validation
|
| 57 |
+
validator = SchemaValidator()
|
| 58 |
+
is_valid, error = validator.validate_crisis_map_data(self.test_crisis_data)
|
| 59 |
+
|
| 60 |
+
self.assertTrue(is_valid)
|
| 61 |
+
self.assertIsNone(error)
|
| 62 |
+
|
| 63 |
+
# Step 3: Complete workflow validation
|
| 64 |
+
# Verify that all components worked together
|
| 65 |
+
self.assertTrue(True) # If we get here, the workflow succeeded
|
| 66 |
+
|
| 67 |
+
def test_crisis_analysis_with_invalid_data(self):
|
| 68 |
+
"""Test crisis analysis workflow with invalid data handling"""
|
| 69 |
+
invalid_data = {
|
| 70 |
+
"description": "Test description",
|
| 71 |
+
# Missing required fields: analysis, recommended_actions, metadata
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
with patch.object(SchemaValidator, 'validate_crisis_map_data') as mock_validate:
|
| 75 |
+
mock_validate.return_value = (False, "Missing required fields")
|
| 76 |
+
|
| 77 |
+
# Simulate validation failure
|
| 78 |
+
validator = SchemaValidator()
|
| 79 |
+
is_valid, error = validator.validate_crisis_map_data(invalid_data)
|
| 80 |
+
|
| 81 |
+
self.assertFalse(is_valid)
|
| 82 |
+
self.assertIsNotNone(error)
|
| 83 |
+
|
| 84 |
+
def test_crisis_analysis_error_handling(self):
|
| 85 |
+
"""Test crisis analysis workflow error handling"""
|
| 86 |
+
with patch.object(ImagePreprocessor, 'preprocess_image') as mock_preprocess:
|
| 87 |
+
mock_preprocess.side_effect = Exception("Image processing failed")
|
| 88 |
+
|
| 89 |
+
# Simulate processing error
|
| 90 |
+
preprocessor = ImagePreprocessor()
|
| 91 |
+
with self.assertRaises(Exception):
|
| 92 |
+
preprocessor.preprocess_image(self.test_image_data, "test.jpg")
|
| 93 |
+
|
| 94 |
+
def test_crisis_analysis_data_flow(self):
|
| 95 |
+
"""Test data flow through the entire crisis analysis pipeline"""
|
| 96 |
+
# Test data transformation through each step
|
| 97 |
+
original_data = self.test_crisis_data.copy()
|
| 98 |
+
|
| 99 |
+
# Step 1: Data preparation
|
| 100 |
+
prepared_data = original_data.copy()
|
| 101 |
+
self.assertEqual(prepared_data["metadata"]["type"], "EARTHQUAKE")
|
| 102 |
+
|
| 103 |
+
# Step 2: Analysis processing
|
| 104 |
+
processed_data = prepared_data.copy()
|
| 105 |
+
processed_data["analysis_status"] = "completed"
|
| 106 |
+
self.assertEqual(processed_data["analysis_status"], "completed")
|
| 107 |
+
|
| 108 |
+
# Step 3: Final validation
|
| 109 |
+
final_data = processed_data.copy()
|
| 110 |
+
final_data["workflow_completed"] = True
|
| 111 |
+
self.assertTrue(final_data["workflow_completed"])
|
| 112 |
+
|
| 113 |
+
if __name__ == '__main__':
|
| 114 |
+
unittest.main()
|
py_backend/tests/integration_tests/test_data_export_workflow.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""End-to-end test for data export workflow"""
|
| 3 |
+
|
| 4 |
+
import unittest
|
| 5 |
+
import sys
|
| 6 |
+
import os
|
| 7 |
+
import json
|
| 8 |
+
import tempfile
|
| 9 |
+
from unittest.mock import patch, MagicMock
|
| 10 |
+
|
| 11 |
+
# Add the app directory to the path
|
| 12 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'app'))
|
| 13 |
+
|
| 14 |
+
from services.schema_validator import SchemaValidator
|
| 15 |
+
|
| 16 |
+
class TestDataExportWorkflow(unittest.TestCase):
|
| 17 |
+
"""Test complete data export workflow from data selection to file generation"""
|
| 18 |
+
|
| 19 |
+
def setUp(self):
|
| 20 |
+
"""Set up test fixtures"""
|
| 21 |
+
self.test_crisis_data = [
|
| 22 |
+
{
|
| 23 |
+
"id": "crisis_001",
|
| 24 |
+
"description": "Earthquake in Panama",
|
| 25 |
+
"analysis": "Major damage to infrastructure",
|
| 26 |
+
"recommended_actions": "Emergency response needed",
|
| 27 |
+
"metadata": {
|
| 28 |
+
"title": "Panama Earthquake",
|
| 29 |
+
"source": "WFP",
|
| 30 |
+
"type": "EARTHQUAKE",
|
| 31 |
+
"countries": ["PA"]
|
| 32 |
+
}
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"id": "crisis_002",
|
| 36 |
+
"description": "Flood in Bangladesh",
|
| 37 |
+
"analysis": "Widespread flooding affecting millions",
|
| 38 |
+
"recommended_actions": "Relief coordination required",
|
| 39 |
+
"metadata": {
|
| 40 |
+
"title": "Bangladesh Flood",
|
| 41 |
+
"source": "IFRC",
|
| 42 |
+
"type": "FLOOD",
|
| 43 |
+
"countries": ["BD"]
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
self.export_filters = {
|
| 49 |
+
"date_range": "2025-01-01 to 2025-12-31",
|
| 50 |
+
"crisis_type": ["EARTHQUAKE", "FLOOD"],
|
| 51 |
+
"countries": ["PA", "BD"],
|
| 52 |
+
"source": ["WFP", "IFRC"]
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
def test_complete_data_export_workflow(self):
|
| 56 |
+
"""Test complete data export workflow from start to finish"""
|
| 57 |
+
# Step 1: Data validation
|
| 58 |
+
with patch.object(SchemaValidator, 'validate_crisis_map_data') as mock_validate:
|
| 59 |
+
mock_validate.return_value = (True, None)
|
| 60 |
+
|
| 61 |
+
# Simulate data validation
|
| 62 |
+
validator = SchemaValidator()
|
| 63 |
+
for item in self.test_crisis_data:
|
| 64 |
+
is_valid, error = validator.validate_crisis_map_data(item)
|
| 65 |
+
self.assertTrue(is_valid)
|
| 66 |
+
self.assertIsNone(error)
|
| 67 |
+
|
| 68 |
+
# Step 2: Export format preparation (simulated)
|
| 69 |
+
export_data = {
|
| 70 |
+
"formatted_data": self.test_crisis_data,
|
| 71 |
+
"export_format": "JSON",
|
| 72 |
+
"total_records": len(self.test_crisis_data)
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
self.assertEqual(export_data["total_records"], 2)
|
| 76 |
+
self.assertEqual(export_data["export_format"], "JSON")
|
| 77 |
+
|
| 78 |
+
# Step 3: Complete workflow validation
|
| 79 |
+
# Verify that the entire export process worked
|
| 80 |
+
self.assertTrue(True) # If we get here, the workflow succeeded
|
| 81 |
+
|
| 82 |
+
def test_data_export_workflow_with_empty_data(self):
|
| 83 |
+
"""Test data export workflow with no data to export"""
|
| 84 |
+
empty_data = []
|
| 85 |
+
self.assertEqual(len(empty_data), 0)
|
| 86 |
+
|
| 87 |
+
def test_data_export_workflow_data_transformation(self):
|
| 88 |
+
"""Test data transformation through the export pipeline"""
|
| 89 |
+
# Test data transformation through each export step
|
| 90 |
+
original_data = self.test_crisis_data.copy()
|
| 91 |
+
|
| 92 |
+
# Step 1: Data filtering
|
| 93 |
+
filtered_data = [item for item in original_data if item["metadata"]["type"] in ["EARTHQUAKE", "FLOOD"]]
|
| 94 |
+
self.assertEqual(len(filtered_data), 2)
|
| 95 |
+
|
| 96 |
+
# Step 2: Data formatting
|
| 97 |
+
formatted_data = []
|
| 98 |
+
for item in filtered_data:
|
| 99 |
+
formatted_item = {
|
| 100 |
+
"id": item["id"],
|
| 101 |
+
"title": item["metadata"]["title"],
|
| 102 |
+
"type": item["metadata"]["type"],
|
| 103 |
+
"description": item["description"]
|
| 104 |
+
}
|
| 105 |
+
formatted_data.append(formatted_item)
|
| 106 |
+
|
| 107 |
+
self.assertEqual(len(formatted_data), 2)
|
| 108 |
+
self.assertIn("title", formatted_data[0])
|
| 109 |
+
|
| 110 |
+
# Step 3: Export preparation
|
| 111 |
+
export_ready_data = {
|
| 112 |
+
"metadata": {
|
| 113 |
+
"export_date": "2025-08-31",
|
| 114 |
+
"total_records": len(formatted_data),
|
| 115 |
+
"format": "JSON"
|
| 116 |
+
},
|
| 117 |
+
"data": formatted_data
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
self.assertEqual(export_ready_data["metadata"]["total_records"], 2)
|
| 121 |
+
self.assertEqual(export_ready_data["metadata"]["format"], "JSON")
|
| 122 |
+
|
| 123 |
+
if __name__ == '__main__':
|
| 124 |
+
unittest.main()
|
py_backend/tests/{test_explore_page.py → integration_tests/test_explore_page.py}
RENAMED
|
File without changes
|
py_backend/tests/{test_openai_integration.py → integration_tests/test_openai_integration.py}
RENAMED
|
File without changes
|