Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,6 @@ jobs:
path: |
reports/a11y_report.html
reports/accessibility.json
reports/accessibility_report.json
reports/accessibility_login_report.json
retention-days: 30

Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,6 @@ After each run, reports are written to `reports/`:
| `reports/e2e_combined.json` | Merged shard JSON (CI test-summary job) |
| `reports/e2e_summary.md` | Merged Markdown posted to GitHub Step Summary |
| `reports/a11y_report.html` | Accessibility HTML report |
| `reports/accessibility_report.json` | Structured axe violations JSON |
| `reports/accessibility_*_report.json` | Authenticated-route axe results |
| `reports/performance_metrics.json` | Public-page timing metrics |
| `reports/performance_metrics_auth.json` | Authenticated-route timing metrics |
Expand Down
101 changes: 1 addition & 100 deletions tests/accessibility/test_accessibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@
Accessibility tests for the Parakh platform using axe-playwright-python.
Targets WCAG 2.1 AA compliance.

Reports are saved to reports/accessibility_report.json.
The login page report is saved to reports/accessibility_login_report.json.
"""

import json

import pytest
from playwright.sync_api import Page

Expand Down Expand Up @@ -48,27 +46,6 @@ def _critical_violations(axe_results: dict) -> list[dict]:


class TestAxeScans:
def test_homepage_has_no_critical_axe_violations(self, page: Page):
"""Homepage must pass axe WCAG 2.1 AA scan with no critical/serious issues."""
_require_axe()
home = HomePage(page)
home.go_to_home()

results = _run_axe(page)
critical = _critical_violations(results)

# Save full report
report = build_axe_report(results.get("violations", []), page.url)
save_json_report(report, "accessibility_report.json")

assert len(critical) == 0, (
f"Found {len(critical)} critical/serious axe violations on homepage:\n"
+ json.dumps(
[{"id": v["id"], "impact": v["impact"], "description": v["description"]} for v in critical],
indent=2,
)
)

def test_login_page_has_no_critical_axe_violations(self, page: Page):
"""Login page must pass axe WCAG 2.1 AA scan with no critical/serious issues.

Expand Down Expand Up @@ -108,30 +85,6 @@ def test_login_page_has_no_critical_axe_violations(self, page: Page):


class TestImagesAndLinks:
def test_images_have_alt_text(self, page: Page):
"""Every <img> element must have a non-empty alt attribute."""
home = HomePage(page)
home.go_to_home()

images = page.locator("img")
count = images.count()
if count == 0:
pytest.skip("No <img> elements found on the page")

missing_alt = []
for i in range(count):
img = images.nth(i)
alt = img.get_attribute("alt")
src = img.get_attribute("src") or f"img[{i}]"
if alt is None:
missing_alt.append(f"Missing alt: {src}")
# Note: alt="" is acceptable for decorative images

assert len(missing_alt) == 0, (
f"{len(missing_alt)} image(s) missing alt attribute:\n"
+ "\n".join(missing_alt)
)

@pytest.mark.xfail(reason="App bug #9 — see docs/app_bugs.md", strict=False)
def test_social_links_have_accessible_names(self, page: Page):
"""Footer icon-only links must have aria-label or visible text."""
Expand Down Expand Up @@ -214,58 +167,6 @@ def test_lang_attribute_set(self, page: Page):
f"<html> element must have a non-empty lang attribute, got: '{lang}'"
)

def test_buttons_have_accessible_names(self, page: Page):
"""Every button must have either visible text or an aria-label."""
home = HomePage(page)
home.go_to_home()

buttons = page.locator("button")
count = buttons.count()
if count == 0:
pytest.skip("No <button> elements found on the page")

unlabelled = []
for i in range(count):
btn = buttons.nth(i)
# Skip hidden buttons
if not btn.is_visible():
continue
text = btn.inner_text().strip()
aria_label = btn.get_attribute("aria-label") or ""
aria_labelledby = btn.get_attribute("aria-labelledby") or ""
title = btn.get_attribute("title") or ""

if not text and not aria_label and not aria_labelledby and not title:
class_name = btn.get_attribute("class") or f"button[{i}]"
unlabelled.append(class_name)

assert len(unlabelled) == 0, (
f"{len(unlabelled)} visible button(s) have no accessible name:\n"
+ "\n".join(unlabelled)
)

def test_feature_tabs_have_aria_states(self, page: Page):
"""Tab buttons in the feature tabs widget must use aria-selected."""
home = HomePage(page)
home.go_to_home()

tabs = page.locator("[role='tab']")
count = tabs.count()
if count == 0:
pytest.skip("No [role='tab'] elements found — feature tabs may not use ARIA roles")

missing_aria = []
for i in range(count):
tab = tabs.nth(i)
aria_selected = tab.get_attribute("aria-selected")
if aria_selected is None:
missing_aria.append(tab.inner_text().strip() or f"tab[{i}]")

assert len(missing_aria) == 0, (
f"Tab(s) missing aria-selected attribute: {missing_aria}"
)


class TestKeyboardNavigation:
def test_keyboard_navigation_tab_order(self, page: Page):
"""Tab through the page and verify interactive elements receive focus."""
Expand Down
3 changes: 0 additions & 3 deletions tests/accessibility/test_accessibility_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,9 +443,6 @@ def test_ux010_new_evaluation_dialog_aria_describedby_resolves(
)

@pytest.mark.auth
@pytest.mark.xfail(
reason="UX-011: Select fields in New Evaluation dialog have no accessible label — known bug"
)
def test_ux011_new_evaluation_dialog_selects_have_labels(
self, authenticated_page_fast: Page
):
Expand Down
Loading