From 2638e6f5898889830b970b0c1b8005d37335635a Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Wed, 29 Jul 2026 20:04:26 -0400 Subject: [PATCH] test(form): cover a11y labels, status region, and desc toggle Structural checks for dual status nodes and no alert(); Playwright for init-time qty labels, aria-pressed chips, overflow More/Less, and failed-submit text landing in role=alert. --- tests/test_generate_form.py | 125 ++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/tests/test_generate_form.py b/tests/test_generate_form.py index c5d5fc3..99834ab 100644 --- a/tests/test_generate_form.py +++ b/tests/test_generate_form.py @@ -227,6 +227,26 @@ class TestGenerateFormStructural: assert "data-meal-index" in html assert "data-qty-delta" in html + def test_a11y_status_and_live_total_markup(self): + html = _render() + assert 'id="form-status-alert"' in html + assert 'id="form-status-info"' in html + assert 'id="admin-status-alert"' in html + assert 'id="admin-status-info"' in html + assert 'role="alert"' in html + assert 'role="status"' in html + assert 'aria-live="polite"' in html + assert 'id="success-heading"' in html + assert 'tabindex="-1"' in html + + def test_no_alert_dialogs_in_form_scripts(self): + form_js = (SERVER_DIR / "templates" / "form.js").read_text() + admin_js = (SERVER_DIR / "templates" / "admin.js").read_text() + assert "alert(" not in form_js + assert "alert(" not in admin_js + # confirm() retained for destructive admin delete / zero-qty save. + assert "confirm(" in admin_js + def test_design_tokens_only_in_definitions(self): css = (SERVER_DIR / "templates" / "form.css").read_text() # Strip :root block, then assert brand/muted/border hexes are gone @@ -414,6 +434,111 @@ class TestGenerateFormPlaywright: assert page.locator("#count-display").inner_text() == "2 meals" assert page.locator("#total-display").inner_text() == "$10.80" + def test_qty_labels_present_after_init(self, browser_page): + page = browser_page + card = page.locator('[data-meal-index="0"]') + assert ( + card.locator('[data-qty-delta="-1"]').get_attribute("aria-label") + == "Remove one Chicken Bowl" + ) + assert ( + card.locator('[data-qty-delta="1"]').get_attribute("aria-label") + == "Add one Chicken Bowl" + ) + assert ( + card.locator(".qty").get_attribute("aria-label") + == "Quantity of Chicken Bowl" + ) + + def test_filter_chip_aria_pressed(self, browser_page): + page = browser_page + all_btn = page.locator("#filters button", has_text="All Meals") + assert all_btn.get_attribute("aria-pressed") == "true" + other = page.locator("#filters button").nth(1) + other_label = other.inner_text() + other.click() + page.wait_for_function( + """(label) => { + const active = document.querySelector('#filters button.active'); + return active && active.textContent === label + && active.getAttribute('aria-pressed') === 'true'; + }""", + arg=other_label, + ) + assert all_btn.get_attribute("aria-pressed") == "false" + assert other.get_attribute("aria-pressed") == "true" + all_btn.click() + page.wait_for_function( + """() => { + const active = document.querySelector('#filters button.active'); + return active && active.textContent === 'All Meals' + && active.getAttribute('aria-pressed') === 'true'; + }""" + ) + + def test_desc_more_only_when_overflow(self, browser_page): + page = browser_page + # Ensure All Meals filter so both short and long cards are visible. + page.locator("#filters button", has_text="All Meals").click() + page.wait_for_selector('[data-meal-index="0"]') + page.wait_for_selector('[data-meal-index="1"]') + short_card = page.locator('[data-meal-index="0"]') + long_card = page.locator('[data-meal-index="1"]') + page.wait_for_function( + """() => { + const long = document.querySelector('[data-meal-index="1"] .meal-desc-toggle'); + return !!long; + }""" + ) + assert short_card.locator(".meal-desc-toggle").count() == 0 + toggle = long_card.locator(".meal-desc-toggle") + assert toggle.inner_text() == "More" + assert toggle.get_attribute("aria-expanded") == "false" + toggle.click() + assert toggle.inner_text() == "Less" + assert toggle.get_attribute("aria-expanded") == "true" + assert "is-expanded" in ( + long_card.locator(".meal-desc").get_attribute("class") or "" + ) + + def test_submit_failure_lands_in_alert_region(self, browser_page): + page = browser_page + page.route( + "**/api/submit-order**", + lambda route: route.fulfill( + status=500, + content_type="application/json", + body='{"error":"Simulated submit failure"}', + ), + ) + page.locator("#emp-name").select_option(label="Ada Lovelace") + page.evaluate( + """() => { + document.querySelectorAll('[data-meal-index] .qty').forEach((input) => { + input.value = '0'; + input.dispatchEvent(new Event('change', { bubbles: true })); + }); + }""" + ) + page.wait_for_function( + "() => document.getElementById('count-display').textContent === '0 meals'" + ) + page.locator('[data-meal-index="0"] [data-qty-delta="1"]').click() + page.wait_for_function( + "() => document.getElementById('count-display').textContent === '1 meal'" + ) + page.locator("#submit-btn").click() + page.wait_for_function( + """() => document.getElementById('form-status-alert').textContent + === 'Simulated submit failure'""" + ) + assert ( + page.locator("#form-status-alert").inner_text() + == "Simulated submit failure" + ) + assert page.locator("#form-status-info").inner_text() == "" + page.unroute("**/api/submit-order**") + def test_qty_typein_clamps_negative(self, browser_page): page = browser_page # Reset first card via type-in