Compare commits

..

7 commits

Author SHA1 Message Date
8c7d8d4723
feat(form): render admin orders as mobile cards 2026-07-31 09:40:16 -04:00
fa52b8c924
fix: address review comments 2026-07-31 09:27:15 -04:00
d5a54b2e78
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.
2026-07-30 20:39:12 -04:00
9f34cb093f
fix(form): wire a11y labels, status helper, and desc expand
Meal-scoped qty labels at card create time, aria-pressed filter chips,
dual-node showStatus replacing all alert() calls (confirm retained),
overflow-gated More/Less, and success-heading focus after submit.
2026-07-30 20:39:12 -04:00
c0f6316f3f
fix(form): add status regions, live total, and a11y CSS
Dual alert/status slots for form and admin, aria-live on the sticky
total, success heading focus target, More/Less affordance styles, and
44px coarse-pointer chip padding.
2026-07-30 20:38:59 -04:00
5ba3f00157
test(form): stub admin lookup in browser tests 2026-07-30 20:29:43 -04:00
91286fc922
fix(form): guard stale admin edit responses 2026-07-30 20:27:19 -04:00
3 changed files with 21 additions and 3 deletions

View file

@ -454,13 +454,15 @@ function downloadSummaryPdf() {
function adminStartEdit(email) {
window.FormUtils.clearStatus('admin');
const requestedWeek = currentAdminWeek;
setAdminActionDisabled('start-edit', true);
adminFetch(ADMIN_URL + '?week=' + encodeURIComponent(currentAdminWeek), { headers: adminHeaders() })
adminFetch(ADMIN_URL + '?week=' + encodeURIComponent(requestedWeek), { headers: adminHeaders() })
.then(r => {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(data => {
if (currentAdminWeek !== requestedWeek) return;
lastAdminData = data;
const order = data.orders.find(o => o.employee_email === email);
if (!order) return;

View file

@ -99,7 +99,7 @@
<div id="form-status-info" class="form-status-msg form-status-info" role="status"></div>
</div>
<div class="inner">
<div aria-live="polite">
<div aria-live="polite" aria-atomic="true">
<span class="total" id="total-display">$0.00</span>
<span class="count-display" id="count-display">0 meals</span>
</div>

View file

@ -101,6 +101,14 @@ def _mock_form_routes(page):
),
),
)
page.route(
"**/api/admin/orders",
lambda route: route.fulfill(
status=403,
content_type="application/json",
body='{"error":"Forbidden"}',
),
)
class TestGenerateFormStructural:
@ -391,10 +399,15 @@ class TestGenerateFormPlaywright:
bottom: box.bottom,
};
};
const footer = document.querySelector('#sticky-footer');
return {
clientWidth: document.documentElement.clientWidth,
scrollWidth: document.documentElement.scrollWidth,
footer: rect('#sticky-footer'),
viewportHeight: window.innerHeight,
footer: {
...rect('#sticky-footer'),
position: getComputedStyle(footer).position,
},
total: rect('#total-display'),
count: rect('#count-display'),
submit: rect('#submit-btn'),
@ -407,6 +420,9 @@ class TestGenerateFormPlaywright:
for selector in ("footer", "total", "count", "submit"):
assert layout[selector]["left"] >= 0
assert layout[selector]["right"] <= layout["clientWidth"]
assert layout["footer"]["position"] == "fixed"
assert layout["footer"]["top"] >= 0
assert layout["footer"]["bottom"] <= layout["viewportHeight"]
assert layout["lastCard"]["bottom"] <= layout["footer"]["top"]
finally:
browser.close()