Integration tests (350 passing): - test_evidence_summary.py: HTML/PDF generation, XSS safety, anchor rendering - test_tor.py: Tor module unit tests (mocked, no Tor needed) - test_c2pa_importer.py: Import result dataclass, trust evaluation, graceful degradation - test_file_attestation.py: All file types (PNG, PDF, CSV, empty, large), determinism - test_paths.py: Registry correctness, env var override, all paths under BASE_DIR - test_killswitch_coverage.py: Tor keys, trusted keys, carrier history destruction Playwright e2e infrastructure: - tests/e2e/ with conftest (live server, auth fixtures), helpers (test file generators) - test_auth.py: Setup flow, login/logout, protected routes - test_attest.py: Image/PDF/CSV attestation, verify, attestation log - test_dropbox.py: Token creation, source upload, branding check - test_keys.py: Identity display, trust store - test_fieldkit.py: Status dashboard, killswitch page - test_navigation.py: All nav links, responsive layout Run: pytest (unit/integration) or pytest -m e2e tests/e2e/ (browser) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
162 lines
5.6 KiB
Python
162 lines
5.6 KiB
Python
"""
|
|
e2e tests for general navigation and page health.
|
|
|
|
These tests verify that:
|
|
- The homepage loads after authentication.
|
|
- All primary navigation links resolve without 5xx errors.
|
|
- The layout is accessible at a mobile viewport width.
|
|
|
|
The navigation link test does NOT follow every link exhaustively — it checks
|
|
the primary links that appear in the base navigation bar (the links that every
|
|
page shares). Blueprint-specific internal links are covered by their own test
|
|
files.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from playwright.sync_api import Page, expect
|
|
|
|
|
|
# Primary navigation links as rendered by base.html.
|
|
# Each entry is (link text substring | href fragment, expected URL fragment).
|
|
# We match by href since the link text includes Bootstrap icons which vary.
|
|
PRIMARY_NAV_HREFS = [
|
|
"/",
|
|
"/encode",
|
|
"/decode",
|
|
"/generate",
|
|
"/attest",
|
|
"/verify",
|
|
"/keys/",
|
|
"/fieldkit/",
|
|
"/dropbox/admin",
|
|
"/federation/",
|
|
]
|
|
|
|
|
|
@pytest.mark.e2e
|
|
def test_homepage_loads(live_server: str, authenticated_page: Page) -> None:
|
|
"""The index page loads after login and shows the main feature cards."""
|
|
page = authenticated_page
|
|
page.goto(f"{live_server}/")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
# Core headings from index.html
|
|
expect(page.locator("body")).to_contain_text("FieldWitness")
|
|
# At least one of the feature card links is visible
|
|
expect(page.locator("a[href='/encode']")).to_be_visible()
|
|
|
|
|
|
@pytest.mark.e2e
|
|
def test_all_nav_links_no_server_error(live_server: str, authenticated_page: Page) -> None:
|
|
"""Every primary navigation link returns a non-5xx response."""
|
|
page = authenticated_page
|
|
|
|
errors: list[str] = []
|
|
|
|
for href in PRIMARY_NAV_HREFS:
|
|
url = f"{live_server}{href}"
|
|
response = page.goto(url)
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
status = response.status if response else None
|
|
if status is not None and status >= 500:
|
|
errors.append(f"{href} → HTTP {status}")
|
|
|
|
assert not errors, "Navigation links returned server errors:\n" + "\n".join(errors)
|
|
|
|
|
|
@pytest.mark.e2e
|
|
def test_health_endpoint_authenticated(live_server: str, authenticated_page: Page) -> None:
|
|
"""The /health endpoint returns JSON with full details when authenticated."""
|
|
import json
|
|
|
|
page = authenticated_page
|
|
|
|
# Use fetch() so we get the JSON body (page.goto would return HTML shell)
|
|
result = page.evaluate("""async () => {
|
|
const resp = await fetch('/health');
|
|
return {status: resp.status, body: await resp.json()};
|
|
}""")
|
|
|
|
assert result["status"] == 200, f"Health check failed with status {result['status']}"
|
|
data = result["body"]
|
|
assert "status" in data, f"Unexpected health response: {data}"
|
|
assert data["status"] in ("ok", "degraded"), f"Unknown health status: {data['status']}"
|
|
|
|
|
|
@pytest.mark.e2e
|
|
def test_health_endpoint_unauthenticated(live_server: str, page: Page) -> None:
|
|
"""The /health endpoint returns minimal JSON for unauthenticated callers."""
|
|
result = page.evaluate("""async () => {
|
|
const resp = await fetch('/health');
|
|
return {status: resp.status, body: await resp.json()};
|
|
}""")
|
|
|
|
assert result["status"] == 200
|
|
data = result["body"]
|
|
# Unauthenticated response must have only status and version, not operational details
|
|
assert "status" in data
|
|
assert "modules" not in data, "Health leaked module details to unauthenticated caller"
|
|
assert "keys" not in data, "Health leaked key details to unauthenticated caller"
|
|
|
|
|
|
@pytest.mark.e2e
|
|
def test_responsive_layout_mobile(live_server: str, authenticated_page: Page) -> None:
|
|
"""The index page renders without horizontal overflow at 375px viewport width."""
|
|
page = authenticated_page
|
|
page.set_viewport_size({"width": 375, "height": 812})
|
|
|
|
page.goto(f"{live_server}/")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
# The page must render — verify the main heading is still in the DOM
|
|
expect(page.locator("body")).to_contain_text("FieldWitness")
|
|
|
|
# Check for horizontal overflow: scrollWidth should not exceed clientWidth
|
|
overflow = page.evaluate("""() => {
|
|
return document.documentElement.scrollWidth > document.documentElement.clientWidth;
|
|
}""")
|
|
|
|
assert not overflow, (
|
|
"Page has horizontal overflow at 375px viewport — layout breaks on mobile"
|
|
)
|
|
|
|
|
|
@pytest.mark.e2e
|
|
def test_page_titles_are_set(live_server: str, authenticated_page: Page) -> None:
|
|
"""Key pages have non-empty <title> elements (not the default Flask title)."""
|
|
page = authenticated_page
|
|
|
|
pages_and_expected = [
|
|
("/", "FieldWitness"),
|
|
("/attest", "Attest"),
|
|
("/verify", "Verify"),
|
|
("/keys/", "Keys"),
|
|
("/fieldkit/", "Fieldkit"),
|
|
]
|
|
|
|
for href, expected_fragment in pages_and_expected:
|
|
page.goto(f"{live_server}{href}")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
title = page.title()
|
|
assert expected_fragment.lower() in title.lower(), (
|
|
f"Page {href}: expected title to contain '{expected_fragment}', got '{title}'"
|
|
)
|
|
|
|
|
|
@pytest.mark.e2e
|
|
def test_logout_link_present_when_authenticated(
|
|
live_server: str, authenticated_page: Page
|
|
) -> None:
|
|
"""The navigation bar shows a logout affordance when the user is logged in."""
|
|
page = authenticated_page
|
|
page.goto(f"{live_server}/")
|
|
page.wait_for_load_state("networkidle")
|
|
|
|
# Logout is a POST form in the navbar; we just confirm the form/button exists
|
|
logout = page.locator("form[action*='logout'], a[href*='logout']")
|
|
assert logout.count() > 0, "No logout link/form found in navigation when authenticated"
|