Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

335 lines
12 KiB
Python

"""
Accessibility Backend Tests
Tests for HTML structure and semantic markup accessibility
"""
import re
import pytest
from bs4 import BeautifulSoup
class TestHTMLAccessibility:
"""Test HTML structure for accessibility compliance"""
@pytest.fixture
def research_page_html(self, authenticated_client):
"""Fetch the research page HTML"""
response = authenticated_client.get("/")
assert response.status_code == 200
return BeautifulSoup(response.data, "html.parser")
def test_form_has_proper_labels(self, research_page_html):
"""Test that all form inputs have proper labels"""
# audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: STRENGTHEN).
soup = research_page_html
# Find all input elements that should have labels
inputs = soup.find_all(["input", "textarea", "select"])
# Track which inputs need labels
inputs_needing_labels = []
for input_element in inputs:
input_id = input_element.get("id")
input_type = input_element.get("type", "text")
input_name = input_element.get("name", "")
# Skip hidden inputs, buttons, and CSRF tokens
if (
input_type in ["hidden", "submit", "button"]
or input_name == "csrf_token"
):
continue
# Check for associated label
if input_id:
label = soup.find("label", {"for": input_id})
if label is None:
inputs_needing_labels.append(
f"Input with id '{input_id}' (type: {input_type})"
)
# Allow some flexibility - not all inputs may need labels in modern UI
if len(inputs_needing_labels) > 0:
print(
f"Note: {len(inputs_needing_labels)} inputs without labels found: {inputs_needing_labels}"
)
def test_radio_button_structure(self, research_page_html):
"""Test that radio buttons are properly structured"""
soup = research_page_html
# Find radio button group - might be named differently
radio_inputs = soup.find_all("input", {"type": "radio"})
if len(radio_inputs) == 0:
pytest.skip(
"No radio buttons found on page - might be using different UI"
)
# Check that radio buttons have proper structure
for radio in radio_inputs:
assert radio.get("name") is not None, (
"Radio button should have a name attribute"
)
radio_id = radio.get("id")
if radio_id:
# Check for associated label
label = soup.find("label", {"for": radio_id})
assert label is not None, (
f"Radio button with id '{radio_id}' should have a label"
)
def test_fieldset_and_legend(self, research_page_html):
"""Test that form groups have proper fieldset and legend"""
soup = research_page_html
# Check if there are any fieldsets
fieldsets = soup.find_all("fieldset")
# Modern forms might not use fieldsets
if len(fieldsets) == 0:
# Check for alternative grouping methods
form_groups = soup.find_all(
class_=re.compile(r"form-group|field-group|input-group")
)
assert len(form_groups) > 0 or len(soup.find_all("form")) > 0, (
"Should have some form of input grouping (fieldset, form-group, or form)"
)
else:
# If fieldsets exist, they should have legends
for fieldset in fieldsets:
legend = fieldset.find("legend")
if legend is None:
# Check for aria-label as alternative
aria_label = fieldset.get("aria-label") or fieldset.get(
"aria-labelledby"
)
assert aria_label is not None, (
"Fieldset should have a legend or aria-label"
)
def test_aria_attributes(self, research_page_html):
"""Test that ARIA attributes are properly set where needed"""
soup = research_page_html
# Check for any interactive elements
interactive_elements = soup.find_all(
["button", "a", "input", "select", "textarea"]
)
# Check that interactive elements have appropriate accessibility attributes
for element in interactive_elements:
element_type = element.name
element_text = element.get_text(strip=True)
# Buttons and links should have accessible text
if element_type in ["button", "a"]:
aria_label = element.get("aria-label")
title = element.get("title")
# Should have text content, aria-label, or title
assert element_text or aria_label or title, (
f"{element_type} element should have accessible text"
)
def test_keyboard_hints_present(self, research_page_html):
"""Test that keyboard navigation hints are present"""
soup = research_page_html
# Look for keyboard hint indicators - not all apps show these
# Modern apps might show keyboard hints differently
# Check for focusable elements at minimum
focusable_elements = soup.find_all(
["a", "button", "input", "select", "textarea"]
)
assert len(focusable_elements) > 0, (
"Page should have focusable elements"
)
def test_form_structure(self, research_page_html):
"""Test that forms have proper structure"""
soup = research_page_html
forms = soup.find_all("form")
if len(forms) == 0:
# Might be a single-page app with AJAX
# Check for input elements at least
inputs = soup.find_all(["input", "textarea", "select"])
assert len(inputs) > 0, "Page should have some form inputs"
else:
for form in forms:
# Check form has action or is handled by JavaScript
action = form.get("action")
form_id = form.get("id")
form_class = form.get("class", [])
# Form should have some identifier or action
assert action or form_id or form_class, (
"Form should have action, id, or class"
)
def test_semantic_markup(self, research_page_html):
"""Test that semantic HTML5 elements are used"""
soup = research_page_html
# Check for semantic elements
semantic_elements = [
"header",
"nav",
"main",
"footer",
"section",
"article",
]
found_semantic = []
for element in semantic_elements:
if soup.find(element):
found_semantic.append(element)
# Should use at least some semantic elements
assert len(found_semantic) > 0, (
f"Should use semantic HTML5 elements. Found: {found_semantic}"
)
def test_required_fields_marked(self, research_page_html):
"""Test that required fields are properly marked"""
soup = research_page_html
# Find all required inputs
required_inputs = soup.find_all(attrs={"required": True})
for input_elem in required_inputs:
input_id = input_elem.get("id")
# Check for visual indicator
if input_id:
label = soup.find("label", {"for": input_id})
if label:
label_text = label.get_text()
# Should have some indicator like *, (required), etc.
has_indicator = (
"*" in label_text
or "required" in label_text.lower()
or label.find(class_=re.compile(r"required"))
)
# Also check for aria-required
aria_required = input_elem.get("aria-required") == "true"
assert has_indicator or aria_required, (
f"Required field '{input_id}' should have visual indicator or aria-required"
)
def test_error_handling_structure(self, research_page_html):
"""Test that error messages have proper structure"""
soup = research_page_html
# Look for error message containers
error_containers = soup.find_all(
class_=re.compile(r"error|alert|message|flash")
)
# Page should have some way to display errors
assert len(error_containers) > 0 or soup.find(
attrs={"role": "alert"}
), (
"Should have containers for error messages (class containing 'error', 'alert', or role='alert')"
)
class TestAccessibilityConfiguration:
"""Test accessibility-related configuration and styling"""
def test_css_focus_styles(self, authenticated_client):
"""Test that CSS includes focus styles"""
# Try to get main CSS file
response = authenticated_client.get("/static/css/style.css")
if response.status_code == 200:
css_content = response.data.decode("utf-8")
# Check for focus styles
has_focus_styles = (
":focus" in css_content
or "focus-visible" in css_content
or "outline" in css_content
)
assert has_focus_styles, (
"CSS should include focus styles for keyboard navigation"
)
else:
# CSS might be bundled or in different location
pytest.skip("Could not access main CSS file")
def test_responsive_meta_tag(self, authenticated_client):
"""Test that pages have responsive viewport meta tag"""
response = authenticated_client.get("/")
assert response.status_code == 200
soup = BeautifulSoup(response.data, "html.parser")
viewport_meta = soup.find("meta", {"name": "viewport"})
assert viewport_meta is not None, "Should have viewport meta tag"
content = viewport_meta.get("content", "")
assert "width=device-width" in content, "Viewport should be responsive"
def test_lang_attribute(self, authenticated_client):
"""Test that HTML has lang attribute"""
response = authenticated_client.get("/")
assert response.status_code == 200
soup = BeautifulSoup(response.data, "html.parser")
html_tag = soup.find("html")
assert html_tag is not None, "Should have html tag"
lang = html_tag.get("lang")
assert lang is not None, "HTML tag should have lang attribute"
assert lang in ["en", "en-US", "en-GB"], (
f"Lang should be English variant, got: {lang}"
)
def test_skip_navigation_link(self, authenticated_client):
"""Test for skip navigation link"""
response = authenticated_client.get("/")
assert response.status_code == 200
soup = BeautifulSoup(response.data, "html.parser")
# Look for skip link
skip_link = soup.find("a", text=re.compile(r"skip|main content", re.I))
# Skip link is a best practice but not always present
if skip_link:
href = skip_link.get("href")
assert href and href.startswith("#"), (
"Skip link should be an anchor link"
)
def test_heading_hierarchy(self, authenticated_client):
"""Test that headings follow proper hierarchy"""
response = authenticated_client.get("/")
assert response.status_code == 200
soup = BeautifulSoup(response.data, "html.parser")
# Find all headings
headings = soup.find_all(["h1", "h2", "h3", "h4", "h5", "h6"])
if len(headings) > 0:
# Should have at least one h1
h1_tags = soup.find_all("h1")
assert len(h1_tags) >= 1, "Page should have at least one h1 tag"
# Check heading order doesn't skip levels
heading_levels = [int(h.name[1]) for h in headings]
for i in range(1, len(heading_levels)):
level_diff = heading_levels[i] - heading_levels[i - 1]
assert level_diff <= 1, (
f"Heading hierarchy should not skip levels. "
f"Found h{heading_levels[i - 1]} followed by h{heading_levels[i]}"
)