Adding e2e testing
This commit is contained in:
@@ -85,3 +85,7 @@ coverage.xml
|
|||||||
*.cover
|
*.cover
|
||||||
.hypothesis/
|
.hypothesis/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Playwright artifacts
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
|
|||||||
@@ -208,6 +208,20 @@ Or in Docker:
|
|||||||
docker compose exec api pytest --cov=app --cov-report=term-missing
|
docker compose exec api pytest --cov=app --cov-report=term-missing
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### End-to-End Testing
|
||||||
|
Browser e2e tests use pytest plus Playwright. The recommended path is the containerized runner, which joins the same Compose network as the app and opens the web service at `http://web`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.e2e.yml up -d db api web
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.e2e.yml run --rm e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
Authenticated browser tests are skipped unless `E2E_ADMIN_USERNAME` and `E2E_ADMIN_PASSWORD` are supplied. See [`tests/e2e/README.md`](./tests/e2e/README.md) for credential examples, host-run instructions, and guidance for adding specs.
|
||||||
|
|
||||||
|
The e2e Compose override uses a separate MySQL container and volume, so tests do not use the normal dev/prod database configured in `.env`. It is still a real MySQL database, but isolated for e2e.
|
||||||
|
|
||||||
|
E2e reports are written to `test-results/e2e-report.html` and `test-results/e2e-junit.xml`.
|
||||||
|
|
||||||
## Additional Features
|
## Additional Features
|
||||||
|
|
||||||
### Email Notifications
|
### Email Notifications
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
container_name: ppr_e2e_db
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: tests/e2e/mysql.Dockerfile
|
||||||
|
image: pprdev-e2e-db
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: e2e_root_password
|
||||||
|
MYSQL_DATABASE: ppr_e2e
|
||||||
|
MYSQL_USER: ppr_e2e
|
||||||
|
MYSQL_PASSWORD: ppr_e2e_password
|
||||||
|
volumes:
|
||||||
|
- ppr_e2e_mysql_data:/var/lib/mysql
|
||||||
|
|
||||||
|
api:
|
||||||
|
container_name: ppr_e2e_api
|
||||||
|
ports:
|
||||||
|
- "${E2E_API_PORT_EXTERNAL:-18002}:8000"
|
||||||
|
environment:
|
||||||
|
DB_HOST: db
|
||||||
|
DB_PORT: 3306
|
||||||
|
DB_NAME: ppr_e2e
|
||||||
|
DB_USER: ppr_e2e
|
||||||
|
DB_PASSWORD: ppr_e2e_password
|
||||||
|
BASE_URL: http://web
|
||||||
|
MAIL_HOST: 127.0.0.1
|
||||||
|
MAIL_PORT: 1
|
||||||
|
MAIL_USERNAME: e2e
|
||||||
|
MAIL_PASSWORD: e2e
|
||||||
|
MAIL_FROM: e2e@example.com
|
||||||
|
MAIL_FROM_NAME: PPR E2E
|
||||||
|
|
||||||
|
web:
|
||||||
|
container_name: ppr_e2e_web
|
||||||
|
ports:
|
||||||
|
- "${E2E_WEB_PORT_EXTERNAL:-18055}:80"
|
||||||
|
environment:
|
||||||
|
BASE_URL: ""
|
||||||
|
command: >
|
||||||
|
sh -c "if [ -z \"$${BASE_URL}\" ]; then API_BASE='/api/v1'; else API_BASE=\"$${BASE_URL}/api/v1\"; fi;
|
||||||
|
printf 'window.PPR_CONFIG = { apiBase: \"%s\" };' \"$${API_BASE}\" > /usr/share/nginx/html/config.js;
|
||||||
|
nginx -g 'daemon off;'"
|
||||||
|
|
||||||
|
e2e:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: tests/e2e/Dockerfile
|
||||||
|
working_dir: /workspace
|
||||||
|
volumes:
|
||||||
|
- .:/workspace
|
||||||
|
environment:
|
||||||
|
E2E_BASE_URL: http://web
|
||||||
|
E2E_ADMIN_USERNAME: ${E2E_ADMIN_USERNAME:-}
|
||||||
|
E2E_ADMIN_PASSWORD: ${E2E_ADMIN_PASSWORD:-}
|
||||||
|
E2E_HEALTH_URL: http://api:8000/health
|
||||||
|
E2E_ARTIFACT_UID: ${E2E_ARTIFACT_UID:-1000}
|
||||||
|
E2E_ARTIFACT_GID: ${E2E_ARTIFACT_GID:-1000}
|
||||||
|
PYTHONDONTWRITEBYTECODE: "1"
|
||||||
|
depends_on:
|
||||||
|
- web
|
||||||
|
networks:
|
||||||
|
- public_network
|
||||||
|
command: >
|
||||||
|
bash -lc "mkdir -p test-results &&
|
||||||
|
python tests/e2e/wait_for_web.py &&
|
||||||
|
pytest tests/e2e
|
||||||
|
--browser chromium
|
||||||
|
--tracing=retain-on-failure
|
||||||
|
--screenshot=only-on-failure
|
||||||
|
--junitxml=test-results/e2e-junit.xml
|
||||||
|
--html=test-results/e2e-report.html
|
||||||
|
--self-contained-html;
|
||||||
|
status=$$?;
|
||||||
|
chown -R \"$${E2E_ARTIFACT_UID}:$${E2E_ARTIFACT_GID}\" test-results || true;
|
||||||
|
exit $$status"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
ppr_e2e_mysql_data:
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
FROM mcr.microsoft.com/playwright/python:v1.45.0-jammy
|
||||||
|
|
||||||
|
WORKDIR /workspace
|
||||||
|
|
||||||
|
COPY tests/e2e/requirements.txt /tmp/e2e-requirements.txt
|
||||||
|
RUN pip install --no-cache-dir -r /tmp/e2e-requirements.txt
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# End-to-End Tests
|
||||||
|
|
||||||
|
The e2e suite uses pytest plus Playwright. The preferred path is the containerized runner, which joins the same Docker Compose network as the web app and opens `http://web`.
|
||||||
|
|
||||||
|
## Containerized Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.e2e.yml up -d db api web
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.e2e.yml run --rm e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
The e2e override sets `E2E_BASE_URL=http://web`, makes the web container generate relative API config for browser-side requests, and forces the API container to use an e2e-only Compose database.
|
||||||
|
|
||||||
|
The e2e database is a real MySQL server, but it is isolated from the normal dev database:
|
||||||
|
|
||||||
|
- DB container: `ppr_e2e_db`
|
||||||
|
- API container: `ppr_e2e_api`
|
||||||
|
- Web container: `ppr_e2e_web`
|
||||||
|
- Database name: `ppr_e2e`
|
||||||
|
- Volume: `pprdev_ppr_e2e_mysql_data`
|
||||||
|
|
||||||
|
The e2e DB image is plain `mysql:8.0`, so the API should see a fresh empty database and create schema through Alembic migrations instead of stamping an older `db-init` schema.
|
||||||
|
|
||||||
|
Authenticated tests are skipped unless credentials are supplied:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
E2E_ADMIN_USERNAME=admin \
|
||||||
|
E2E_ADMIN_PASSWORD=admin123 \
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.e2e.yml run --rm e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rebuild The Test Image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.e2e.yml build e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Evidence
|
||||||
|
|
||||||
|
Containerized runs write evidence to `test-results/`:
|
||||||
|
|
||||||
|
- `test-results/e2e-report.html` for a human-readable report
|
||||||
|
- `test-results/e2e-junit.xml` for CI systems
|
||||||
|
- Playwright traces and screenshots on failures
|
||||||
|
|
||||||
|
If the report files are not owned by your host user, pass your UID/GID:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
E2E_ARTIFACT_UID=$(id -u) \
|
||||||
|
E2E_ARTIFACT_GID=$(id -g) \
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.e2e.yml run --rm e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
## Host Run
|
||||||
|
|
||||||
|
Running on the host is still supported for quick debugging if Python and Playwright are installed locally.
|
||||||
|
|
||||||
|
First-time host setup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r backend/requirements.txt
|
||||||
|
pip install -r tests/e2e/requirements.txt
|
||||||
|
python -m playwright install --with-deps chromium
|
||||||
|
```
|
||||||
|
|
||||||
|
Run against a host-exposed web port:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
E2E_BASE_URL=http://localhost:8082 pytest tests/e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
Run authenticated host tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
E2E_BASE_URL=http://localhost:8082 \
|
||||||
|
E2E_ADMIN_USERNAME=admin \
|
||||||
|
E2E_ADMIN_PASSWORD=admin123 \
|
||||||
|
pytest tests/e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding Tests
|
||||||
|
|
||||||
|
Put browser specs in `tests/e2e/test_*.py`. Start with user-visible behavior and stable selectors:
|
||||||
|
|
||||||
|
- Navigate through the same URLs users open.
|
||||||
|
- Prefer roles and labels, such as `get_by_role()` and `get_by_label()`.
|
||||||
|
- Use API setup only when a test needs specific records to exist.
|
||||||
|
- Keep specs independent so they can run in any order.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from playwright.sync_api import expect
|
||||||
|
|
||||||
|
|
||||||
|
BASE_URL = os.getenv("E2E_BASE_URL", "http://localhost:8082").rstrip("/")
|
||||||
|
ADMIN_USERNAME = os.getenv("E2E_ADMIN_USERNAME")
|
||||||
|
ADMIN_PASSWORD = os.getenv("E2E_ADMIN_PASSWORD")
|
||||||
|
|
||||||
|
|
||||||
|
def app_url(path):
|
||||||
|
return f"{BASE_URL}/{path.lstrip('/')}"
|
||||||
|
|
||||||
|
|
||||||
|
def login_as_admin(page):
|
||||||
|
if page.locator("#login-username").is_visible():
|
||||||
|
page.locator("#login-username").fill(ADMIN_USERNAME)
|
||||||
|
page.locator("#login-password").fill(ADMIN_PASSWORD)
|
||||||
|
page.locator("#login-btn").click()
|
||||||
|
expect(page.locator("#current-user")).to_have_text(ADMIN_USERNAME)
|
||||||
|
if page.get_by_role("heading", name="Login").count() > 0:
|
||||||
|
expect(page.get_by_role("heading", name="Login")).to_be_hidden()
|
||||||
|
|
||||||
|
|
||||||
|
def skip_without_admin_credentials(pytest):
|
||||||
|
if not ADMIN_USERNAME or not ADMIN_PASSWORD:
|
||||||
|
pytest.skip("Set E2E_ADMIN_USERNAME and E2E_ADMIN_PASSWORD to run authenticated e2e tests")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
FROM mysql:8.0
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
playwright==1.45.0
|
||||||
|
pytest-playwright>=0.5,<0.6
|
||||||
|
pytest-html>=4,<5
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from playwright.sync_api import expect
|
||||||
|
|
||||||
|
from helpers import app_url, login_as_admin, skip_without_admin_credentials
|
||||||
|
|
||||||
|
|
||||||
|
def open_admin_dropdown(page):
|
||||||
|
page.locator("#adminDropdownBtn").click()
|
||||||
|
admin_menu = page.locator("#adminDropdownMenu")
|
||||||
|
expect(admin_menu).to_have_class(re.compile("active"))
|
||||||
|
return admin_menu
|
||||||
|
|
||||||
|
|
||||||
|
def open_admin_page(page):
|
||||||
|
skip_without_admin_credentials(pytest)
|
||||||
|
page.goto(app_url("/admin"))
|
||||||
|
expect(page).to_have_title(re.compile("PPR Admin Interface"))
|
||||||
|
expect(page.get_by_role("heading", name=re.compile("Swansea Tower"))).to_be_visible()
|
||||||
|
login_as_admin(page)
|
||||||
|
|
||||||
|
|
||||||
|
def test_actions_and_admin_dropdowns_toggle_exclusively(page):
|
||||||
|
open_admin_page(page)
|
||||||
|
|
||||||
|
actions_menu = page.locator("#actionsDropdownMenu")
|
||||||
|
admin_menu = page.locator("#adminDropdownMenu")
|
||||||
|
|
||||||
|
expect(actions_menu).not_to_have_class(re.compile("active"))
|
||||||
|
expect(admin_menu).not_to_have_class(re.compile("active"))
|
||||||
|
|
||||||
|
page.locator("#actionsDropdownBtn").click()
|
||||||
|
expect(actions_menu).to_have_class(re.compile("active"))
|
||||||
|
expect(admin_menu).not_to_have_class(re.compile("active"))
|
||||||
|
expect(actions_menu.get_by_role("link", name=re.compile("New PPR"))).to_be_visible()
|
||||||
|
expect(actions_menu.get_by_role("link", name=re.compile("Book Out"))).to_be_visible()
|
||||||
|
expect(actions_menu.get_by_role("link", name=re.compile("Book In"))).to_be_visible()
|
||||||
|
expect(actions_menu.get_by_role("link", name=re.compile("Overflight"))).to_be_visible()
|
||||||
|
|
||||||
|
admin_menu = open_admin_dropdown(page)
|
||||||
|
expect(actions_menu).not_to_have_class(re.compile("active"))
|
||||||
|
expect(admin_menu.get_by_role("link", name=re.compile("Admin View"))).to_be_visible()
|
||||||
|
expect(admin_menu.get_by_role("link", name=re.compile("ATC View"))).to_be_visible()
|
||||||
|
expect(admin_menu.get_by_role("link", name=re.compile("Reports"))).to_be_visible()
|
||||||
|
expect(admin_menu.get_by_role("link", name=re.compile("Drone Requests"))).to_be_visible()
|
||||||
|
expect(admin_menu.get_by_role("link", name=re.compile("Journal Log"))).to_be_visible()
|
||||||
|
|
||||||
|
page.locator(".container").click()
|
||||||
|
expect(admin_menu).not_to_have_class(re.compile("active"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_admin_menu_links_navigate_to_expected_pages(page):
|
||||||
|
open_admin_page(page)
|
||||||
|
|
||||||
|
menu_expectations = [
|
||||||
|
("Admin View", re.compile(r"/admin$"), re.compile("PPR Admin Interface"), "title"),
|
||||||
|
("ATC View", re.compile(r"/atc$"), re.compile("ATC Management Interface"), "title"),
|
||||||
|
("Reports", re.compile(r"/reports$"), re.compile("PPR Reports"), "heading"),
|
||||||
|
("Drone Requests", re.compile(r"/drone-requests$"), re.compile("Drone Flight Requests"), "heading"),
|
||||||
|
("Journal Log", re.compile(r"/journal$"), re.compile("Journal Log"), "heading"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for label, url_pattern, expected_text, assertion_type in menu_expectations:
|
||||||
|
page.goto(app_url("/admin"))
|
||||||
|
login_as_admin(page)
|
||||||
|
admin_menu = open_admin_dropdown(page)
|
||||||
|
admin_menu.get_by_role("link", name=re.compile(label)).click()
|
||||||
|
expect(page).to_have_url(url_pattern)
|
||||||
|
if assertion_type == "title":
|
||||||
|
expect(page).to_have_title(expected_text)
|
||||||
|
else:
|
||||||
|
expect(page.get_by_role("heading", name=expected_text)).to_be_visible()
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import re
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from playwright.sync_api import expect
|
||||||
|
|
||||||
|
from helpers import app_url, login_as_admin, skip_without_admin_credentials
|
||||||
|
|
||||||
|
|
||||||
|
def drone_payload(operator_name):
|
||||||
|
return {
|
||||||
|
"operator_name": operator_name,
|
||||||
|
"operator_id": "E2E-OP",
|
||||||
|
"flyer_name": "E2E Remote Pilot",
|
||||||
|
"flyer_id": "E2E-FLYER",
|
||||||
|
"email": "drone-e2e@example.com",
|
||||||
|
"phone": "0123456789",
|
||||||
|
"flight_date": "2026-06-21",
|
||||||
|
"estimated_takeoff_time": "10:00",
|
||||||
|
"estimated_completion_time": "10:30",
|
||||||
|
"estimated_takeoff_at": "2026-06-21T10:00:00",
|
||||||
|
"estimated_completion_at": "2026-06-21T10:30:00",
|
||||||
|
"maximum_elevation_ft_amsl": 200,
|
||||||
|
"location_description": "E2E north apron survey",
|
||||||
|
"location_latitude": 51.623389,
|
||||||
|
"location_longitude": -4.069231,
|
||||||
|
"location_inside_frz": "yes",
|
||||||
|
"notes": "Created by Playwright e2e",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_drone_requests_requires_login_and_loads_queue(page):
|
||||||
|
skip_without_admin_credentials(pytest)
|
||||||
|
|
||||||
|
page.goto(app_url("/drone-requests"))
|
||||||
|
|
||||||
|
expect(page).to_have_title(re.compile("Drone Flight Requests"))
|
||||||
|
expect(page.get_by_role("heading", name="Drone Flight Requests")).to_be_visible()
|
||||||
|
expect(page.get_by_role("heading", name="Login")).to_be_visible()
|
||||||
|
|
||||||
|
login_as_admin(page)
|
||||||
|
|
||||||
|
expect(page.locator("#request-list-body")).not_to_contain_text("Loading requests...")
|
||||||
|
expect(page.locator("#request-count")).to_have_text(re.compile(r"\d+"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_public_drone_request_appears_in_tower_queue(page):
|
||||||
|
skip_without_admin_credentials(pytest)
|
||||||
|
|
||||||
|
operator_name = f"E2E Rotor Ops {datetime.utcnow().strftime('%H%M%S%f')}"
|
||||||
|
create_response = page.request.post(
|
||||||
|
app_url("/api/v1/drone-requests/public"),
|
||||||
|
data=json.dumps(drone_payload(operator_name)),
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
assert create_response.ok, create_response.text()
|
||||||
|
created = create_response.json()
|
||||||
|
|
||||||
|
page.goto(app_url("/drone-requests"))
|
||||||
|
login_as_admin(page)
|
||||||
|
|
||||||
|
expect(page.locator("#request-list-body")).to_contain_text(created["reference_number"])
|
||||||
|
expect(page.locator("#request-list-body")).to_contain_text(operator_name)
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from playwright.sync_api import expect
|
||||||
|
|
||||||
|
from helpers import app_url, login_as_admin, skip_without_admin_credentials
|
||||||
|
|
||||||
|
|
||||||
|
def local_flight_row(page, registration):
|
||||||
|
return page.locator("#local-flights-table-body tr").filter(has_text=registration)
|
||||||
|
|
||||||
|
|
||||||
|
def confirm_timestamp(page, button_text):
|
||||||
|
expect(page.locator("#timestampModal")).to_be_visible()
|
||||||
|
expect(page.locator("#timestamp-submit-btn")).to_contain_text(button_text)
|
||||||
|
page.locator("#timestamp-submit-btn").click()
|
||||||
|
expect(page.locator("#timestampModal")).to_be_hidden()
|
||||||
|
|
||||||
|
|
||||||
|
def test_operator_books_out_local_flight_records_touch_and_go_and_lands(page):
|
||||||
|
skip_without_admin_credentials(pytest)
|
||||||
|
registration = f"GE2E{datetime.utcnow().strftime('%H%M%S')}"
|
||||||
|
|
||||||
|
page.goto(app_url("/admin"))
|
||||||
|
login_as_admin(page)
|
||||||
|
|
||||||
|
page.locator("#actionsDropdownBtn").click()
|
||||||
|
page.locator("#actionsDropdownMenu").get_by_role("link", name="🛫 Book Out (L)").click()
|
||||||
|
|
||||||
|
expect(page.locator("#localFlightModal")).to_be_visible()
|
||||||
|
page.locator("#local_registration").fill(registration)
|
||||||
|
page.locator("#local_type").fill("PA28")
|
||||||
|
page.locator("#local_pob").fill("1")
|
||||||
|
page.locator("#local_flight_type").select_option("LOCAL")
|
||||||
|
page.locator("#local_duration").fill("45")
|
||||||
|
page.locator("#local_notes").fill("E2E local flight lifecycle")
|
||||||
|
page.locator("#local-flight-form").get_by_role("button", name="🛫 Book Out").click()
|
||||||
|
|
||||||
|
expect(page.locator("#localFlightModal")).to_be_hidden()
|
||||||
|
row = local_flight_row(page, registration)
|
||||||
|
expect(row).to_be_visible()
|
||||||
|
expect(row).to_contain_text("GROUND")
|
||||||
|
|
||||||
|
row.get_by_role("button", name="TAKE OFF").click()
|
||||||
|
confirm_timestamp(page, "Confirm Takeoff")
|
||||||
|
expect(row).to_contain_text("LOCAL")
|
||||||
|
|
||||||
|
row.get_by_role("button", name="T&G").click()
|
||||||
|
expect(page.locator("#circuitModal")).to_be_visible()
|
||||||
|
page.locator("#circuit-form").get_by_role("button", name="Record Circuit").click()
|
||||||
|
expect(page.locator("#circuitModal")).to_be_hidden()
|
||||||
|
expect(row.locator("td").nth(7)).to_have_text("1")
|
||||||
|
|
||||||
|
row.get_by_role("button", name="LAND").click()
|
||||||
|
confirm_timestamp(page, "Confirm Landing")
|
||||||
|
expect(local_flight_row(page, registration)).to_have_count(0)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
from playwright.sync_api import expect
|
||||||
|
|
||||||
|
|
||||||
|
BASE_URL = os.getenv("E2E_BASE_URL", "http://localhost:8082").rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
|
def app_url(path):
|
||||||
|
return f"{BASE_URL}/{path.lstrip('/')}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_flight_information_display_loads(page):
|
||||||
|
page.goto(app_url("/"))
|
||||||
|
|
||||||
|
expect(page).to_have_title(re.compile("Swansea Airport - Arrivals & Departures"))
|
||||||
|
expect(page.get_by_role("heading", name="Flight Information")).to_be_visible()
|
||||||
|
|
||||||
|
|
||||||
|
def test_public_ppr_form_loads(page):
|
||||||
|
page.goto(app_url("/ppr.html"))
|
||||||
|
|
||||||
|
expect(page).to_have_title(re.compile("Swansea PPR"))
|
||||||
|
expect(page.get_by_role("heading", name=re.compile("PPR Request"))).to_be_visible()
|
||||||
|
expect(page.get_by_role("button", name="Submit PPR Request")).to_be_visible()
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from playwright.sync_api import expect
|
||||||
|
|
||||||
|
|
||||||
|
BASE_URL = os.getenv("E2E_BASE_URL", "http://localhost:8082").rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
|
def app_url(path):
|
||||||
|
return f"{BASE_URL}/{path.lstrip('/')}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_public_ppr_form_submits_successfully(page):
|
||||||
|
unique_registration = f"GE{datetime.utcnow().strftime('%H%M')}"
|
||||||
|
|
||||||
|
page.goto(app_url("/ppr.html"))
|
||||||
|
|
||||||
|
page.locator("#ac_reg").fill(unique_registration)
|
||||||
|
page.locator("#ac_type").fill("PA28")
|
||||||
|
page.locator("#ac_call").fill(unique_registration)
|
||||||
|
page.locator("#captain").fill("E2E Test Pilot")
|
||||||
|
page.locator("#in_from").fill("EGLL")
|
||||||
|
page.locator("#eta-date").fill("2026-06-21")
|
||||||
|
page.locator("#eta-time").select_option("10:00")
|
||||||
|
page.locator("#pob_in").fill("2")
|
||||||
|
page.locator("#fuel").select_option("100LL")
|
||||||
|
page.locator("#out_to").fill("EGFF")
|
||||||
|
page.locator("#etd-date").fill("2026-06-21")
|
||||||
|
page.locator("#etd-time").select_option("12:00")
|
||||||
|
page.locator("#pob_out").fill("2")
|
||||||
|
page.locator("#phone").fill("0123456789")
|
||||||
|
page.locator("#notes").fill("Submitted by Playwright e2e")
|
||||||
|
|
||||||
|
page.get_by_role("button", name="Submit PPR Request").click()
|
||||||
|
|
||||||
|
expect(page.locator("#success-message")).to_be_visible()
|
||||||
|
expect(page.locator("#success-message")).to_contain_text("PPR Request Submitted")
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
|
||||||
|
base_url = os.getenv("E2E_BASE_URL", "http://web").rstrip("/")
|
||||||
|
health_url = os.getenv("E2E_HEALTH_URL", f"{base_url}/")
|
||||||
|
deadline = time.time() + int(os.getenv("E2E_WEB_TIMEOUT_SECONDS", "120"))
|
||||||
|
last_error = None
|
||||||
|
|
||||||
|
while time.time() < deadline:
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(health_url, timeout=5) as response:
|
||||||
|
if response.status < 500:
|
||||||
|
break
|
||||||
|
except Exception as exc:
|
||||||
|
last_error = exc
|
||||||
|
time.sleep(2)
|
||||||
|
else:
|
||||||
|
raise SystemExit(f"Timed out waiting for {health_url}: {last_error}")
|
||||||
|
|
||||||
|
with urllib.request.urlopen(f"{base_url}/", timeout=5) as response:
|
||||||
|
if response.status >= 500:
|
||||||
|
raise SystemExit(f"Web returned HTTP {response.status} at {base_url}/")
|
||||||
Reference in New Issue
Block a user