124 lines
5.2 KiB
Python
124 lines
5.2 KiB
Python
"""
|
|
Step 10 — PWA Finalization & Polish tests.
|
|
|
|
Tests manifest presence and validity, service worker registration,
|
|
icon availability, and install prompt absence in test environment.
|
|
"""
|
|
|
|
import json
|
|
import pytest
|
|
|
|
|
|
def test_manifest_link_present(page, app_url):
|
|
"""HTML page includes a <link rel=manifest> pointing to /manifest.json."""
|
|
page.goto(app_url)
|
|
handle = page.query_selector('link[rel="manifest"]')
|
|
assert handle is not None, "<link rel='manifest'> not found in <head>"
|
|
href = handle.get_attribute("href")
|
|
assert href == "/manifest.json", f"Unexpected manifest href: {href}"
|
|
|
|
|
|
def test_manifest_json_valid(page, app_url):
|
|
"""manifest.json is reachable, valid JSON, and has required PWA fields."""
|
|
response = page.request.get(f"{app_url}/manifest.json")
|
|
assert response.status == 200, f"manifest.json returned {response.status}"
|
|
manifest = json.loads(response.text())
|
|
assert manifest.get("name"), "manifest missing 'name'"
|
|
assert manifest.get("display") == "standalone", "manifest display should be 'standalone'"
|
|
icons = manifest.get("icons", [])
|
|
assert len(icons) >= 2, "manifest should declare at least 2 icons"
|
|
sizes = {icon["sizes"] for icon in icons}
|
|
assert "192x192" in sizes, "192x192 icon missing from manifest"
|
|
assert "512x512" in sizes, "512x512 icon missing from manifest"
|
|
|
|
|
|
def test_icon_192_accessible(page, app_url):
|
|
"""icon-192.png is served with a 200 status."""
|
|
response = page.request.get(f"{app_url}/icon-192.png")
|
|
assert response.status == 200, f"icon-192.png returned {response.status}"
|
|
|
|
|
|
def test_icon_512_accessible(page, app_url):
|
|
"""icon-512.png is served with a 200 status."""
|
|
response = page.request.get(f"{app_url}/icon-512.png")
|
|
assert response.status == 200, f"icon-512.png returned {response.status}"
|
|
|
|
|
|
def test_service_worker_registered(page, app_url):
|
|
"""Service worker is registered in the browser after page load."""
|
|
page.goto(app_url)
|
|
page.wait_for_selector('[data-testid="db-loading"]', state="detached", timeout=15000)
|
|
registration = page.evaluate(
|
|
"() => navigator.serviceWorker.getRegistration().then(r => !!r)"
|
|
)
|
|
assert registration, "No service worker registration found"
|
|
|
|
|
|
def test_install_prompt_hidden_by_default(page, app_url):
|
|
"""Install prompt button is not shown in the test environment (beforeinstallprompt not fired)."""
|
|
page.goto(app_url)
|
|
page.wait_for_selector('[data-testid="db-loading"]', state="detached", timeout=15000)
|
|
btn = page.query_selector('[data-testid="install-prompt-btn"]')
|
|
assert btn is None, "Install prompt button should not appear without beforeinstallprompt"
|
|
|
|
|
|
def test_favicon_linked(page, app_url):
|
|
"""HTML page includes a <link rel=icon> for the favicon."""
|
|
page.goto(app_url)
|
|
handle = page.query_selector('link[rel="icon"]')
|
|
assert handle is not None, "<link rel='icon'> not found in <head>"
|
|
|
|
|
|
def test_apple_touch_icon_linked(page, app_url):
|
|
"""HTML page includes a <link rel=apple-touch-icon>."""
|
|
page.goto(app_url)
|
|
handle = page.query_selector('link[rel="apple-touch-icon"]')
|
|
assert handle is not None, "<link rel='apple-touch-icon'> not found in <head>"
|
|
|
|
|
|
def test_nav_has_aria_label(page, app_url):
|
|
"""Main navigation has an aria-label for screen readers."""
|
|
page.goto(app_url)
|
|
page.wait_for_selector('[data-testid="db-loading"]', state="detached", timeout=15000)
|
|
nav = page.query_selector("nav.navbar")
|
|
assert nav is not None, "<nav class='navbar'> not found"
|
|
aria_label = nav.get_attribute("aria-label")
|
|
assert aria_label, "nav.navbar is missing aria-label"
|
|
|
|
|
|
def test_form_required_fields_marked(page, app_url):
|
|
"""Exercise form title input is marked as required."""
|
|
page.goto(f"{app_url}/#/exercises/new")
|
|
page.wait_for_selector('[data-testid="exercise-title"]', timeout=10000)
|
|
required = page.get_attribute('[data-testid="exercise-title"]', "required")
|
|
assert required is not None, "Exercise title input should have required attribute"
|
|
|
|
|
|
def test_progress_bar_has_aria_role(page, app_url):
|
|
"""Execution progress bar has role=progressbar with ARIA value attributes."""
|
|
from playwright.sync_api import expect
|
|
|
|
# Create an exercise and session first — skip if none exist, just verify structure
|
|
page.goto(f"{app_url}/#/exercises")
|
|
page.wait_for_selector('[data-testid="db-loading"]', state="detached", timeout=15000)
|
|
|
|
exercises = page.query_selector_all('[data-testid^="exercise-card"]')
|
|
sessions = []
|
|
if exercises:
|
|
page.goto(f"{app_url}/#/sessions")
|
|
sessions = page.query_selector_all('[data-testid^="session-card"]')
|
|
|
|
if not sessions:
|
|
pytest.skip("No sessions available to test execution view progress bar")
|
|
|
|
session_link = page.query_selector('[data-testid^="session-card"] a')
|
|
session_link.click()
|
|
page.wait_for_selector('[data-testid="session-execute-btn"]', timeout=5000)
|
|
page.click('[data-testid="session-execute-btn"]')
|
|
page.wait_for_selector('[data-testid="execute-progress"]', timeout=5000)
|
|
|
|
bar = page.query_selector('[role="progressbar"]')
|
|
assert bar is not None, "Progress bar should have role='progressbar'"
|
|
assert bar.get_attribute("aria-valuemin") == "0"
|
|
assert bar.get_attribute("aria-valuemax") is not None
|