fieldwitness/tests/e2e/test_navigation.py
Aaron D. Lee 16318daea3
Some checks failed
CI / lint (push) Failing after 12s
CI / typecheck (push) Failing after 12s
Add comprehensive test suite: integration tests + Playwright e2e
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>
2026-04-02 20:22:12 -04:00

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"