v1.1: harden recovery and control boundaries
WP-D and WP-E complete the planned v1.1 implementation packages. WP-D — recovery honesty: - backups verify the completed copy with PRAGMA integrity_check - corruption missed by quick_check is detected by the full check - existing good backups remain protected - oversized exports are still delivered but declare whether this version can import them, while the 20 MB import limit remains unchanged - backup was exercised through the real browser UI on both the normal campaign database and a campaign-shaped database over 100 MB WP-E — control-boundary contrast: - interactive control boundaries meet the WCAG 1.4.11 3:1 target - the contrast audit is now a failing gate rather than an advisory - rendered browser measurements pass for the composer, controls, tabs and nav - text contrast and focus visibility remain intact - owner reviewed and approved the before/after screenshots Reports: - planning/reports/v1.1/V1.1-WP-D-REPORT.md - planning/reports/v1.1/V1.1-WP-E-REPORT.md All planned v1.1 work packages A-E are now complete. Release validation has not yet begun.
This commit is contained in:
@@ -26,16 +26,24 @@ TOKENS = Path(__file__).resolve().parent.parent.parent / "frontend/src/styles/to
|
||||
#: rather than combinatorial, because "every colour against every other" reports
|
||||
#: pairs that never meet on screen.
|
||||
#:
|
||||
#: The `kind` matters and is not a way of grading on a curve. **text** pairs are
|
||||
#: WCAG 1.4.3 Contrast (Minimum) and are what §21 of the M11 brief asks about;
|
||||
#: they are pass/fail. **boundary** pairs are WCAG 1.4.11 Non-text Contrast,
|
||||
#: which applies to "visual information required to identify user interface
|
||||
#: components" — and in this design a control is identified by its *label*,
|
||||
#: which is measured above and passes, not by its edge. So a boundary below 3:1
|
||||
#: is reported with its number and does not fail the run; what it would take to
|
||||
#: turn it into a real failure is a control with no visible label, and there is
|
||||
#: no such control (`tools/m11_browser.py` asserts every visible control has an
|
||||
#: accessible name, and the story controls are text buttons).
|
||||
#: The `kind` records which success criterion a pair is measured against —
|
||||
#: **text** is WCAG 1.4.3 Contrast (Minimum), **boundary** is WCAG 1.4.11
|
||||
#: Non-text Contrast — and **both are pass/fail**.
|
||||
#:
|
||||
#: v1.1 WP-E overturned the earlier position here, which was that a boundary
|
||||
#: below 3:1 could be recorded rather than failed because "a control is
|
||||
#: identified by its label, not by its edge". That argument understates what
|
||||
#: 1.4.11 asks: the criterion covers the visual information needed to identify
|
||||
#: a component *and its boundary*, and a reader who cannot see where a text box
|
||||
#: ends cannot see that there is a text box to type into, label or no label.
|
||||
#: The edges were at 1.33:1 and 1.75:1 — the tokens were raised instead.
|
||||
#:
|
||||
#: Borders are measured against **every background they are drawn on**, and the
|
||||
#: floor is the worst of them. Inputs and buttons sit on --bg-input, which is
|
||||
#: lighter than --bg-panel and so the harder case; checking only --bg-panel
|
||||
#: would have let a token pass the audit while the real control failed.
|
||||
#: `--bg-panel-glass` is translucent and cannot be resolved from tokens alone;
|
||||
#: that edge is measured on the rendered page by `tools/m11_browser.py`.
|
||||
PAIRS = [
|
||||
("text", "--text", "--bg", 4.5, "body text on the page"),
|
||||
("text", "--text", "--bg-panel", 4.5, "body text in a panel"),
|
||||
@@ -48,8 +56,14 @@ PAIRS = [
|
||||
("text", "--danger", "--bg-panel", 4.5, "an error message"),
|
||||
("text", "--warning", "--bg-panel", 4.5, "a caution message"),
|
||||
("text", "--player", "--bg", 4.5, "the player's own words"),
|
||||
("boundary", "--border", "--bg-panel", 3.0, "a control's resting edge"),
|
||||
("boundary", "--border-bright", "--bg-panel", 3.0, "a control's hover edge"),
|
||||
("boundary", "--border", "--bg-input", 3.0, "a field or button's resting edge"),
|
||||
("boundary", "--border-bright", "--bg-input", 3.0, "a field or button's hover edge"),
|
||||
("boundary", "--border", "--bg-panel", 3.0, "a control's resting edge in a panel"),
|
||||
("boundary", "--border-bright", "--bg-panel", 3.0, "a control's hover edge in a panel"),
|
||||
("boundary", "--border", "--bg", 3.0, "a divider on the page"),
|
||||
("boundary", "--border-bright", "--bg", 3.0, "the scrollbar thumb"),
|
||||
("boundary", "--accent-dim", "--bg-input", 3.0, "a focused field's edge"),
|
||||
("boundary", "--accent-dim", "--bg-panel", 3.0, "a focused control's edge in a panel"),
|
||||
("boundary", "--chart-1", "--bg-panel", 3.0, "a chart bar"),
|
||||
("boundary", "--chart-2", "--bg-panel", 3.0, "a chart bar"),
|
||||
("boundary", "--chart-3", "--bg-panel", 3.0, "a chart bar"),
|
||||
@@ -81,37 +95,44 @@ def ratio(a: str, b: str) -> float:
|
||||
|
||||
def main() -> int:
|
||||
tokens = read_tokens(TOKENS)
|
||||
print(f"{TOKENS.relative_to(TOKENS.parents[3])}: {len(tokens)} colour tokens\n")
|
||||
# Named defensively: the tests run this against a temporary tokens file,
|
||||
# which need not sit four directories deep the way the real one does.
|
||||
label = TOKENS.name
|
||||
if len(TOKENS.parents) > 3:
|
||||
label = TOKENS.relative_to(TOKENS.parents[3])
|
||||
print(f"{label}: {len(tokens)} colour tokens\n")
|
||||
print(f"{'pair':44} {'kind':9} {'ratio':>7} {'floor':>6} verdict")
|
||||
print("-" * 82)
|
||||
failures, advisories = 0, 0
|
||||
text_failures, boundary_failures = 0, 0
|
||||
for kind, foreground, background, floor, description in PAIRS:
|
||||
if foreground not in tokens or background not in tokens:
|
||||
print(f"{description:44} {kind:9} {'—':>7} {floor:>6.1f} MISSING TOKEN")
|
||||
failures += 1
|
||||
text_failures += 1
|
||||
continue
|
||||
measured = ratio(tokens[foreground], tokens[background])
|
||||
ok = measured >= floor
|
||||
if not ok:
|
||||
if kind == "text":
|
||||
failures += 1
|
||||
verdict = "FAIL"
|
||||
else:
|
||||
advisories += 1
|
||||
verdict = "below 1.4.11 (label carries it)"
|
||||
else:
|
||||
# Rounded to the two decimals printed, so the verdict matches what the
|
||||
# reader is shown: a pair reported as 3.00:1 is not failed for arithmetic
|
||||
# the output does not display.
|
||||
if round(measured, 2) >= floor:
|
||||
verdict = "pass"
|
||||
else:
|
||||
verdict = "FAIL"
|
||||
if kind == "text":
|
||||
text_failures += 1
|
||||
else:
|
||||
boundary_failures += 1
|
||||
print(f"{description:44} {kind:9} {measured:>6.2f}:1 {floor:>6.1f} {verdict}")
|
||||
print()
|
||||
if failures:
|
||||
print(f"{failures} text pair(s) below WCAG AA — this is a defect")
|
||||
if text_failures:
|
||||
print(f"{text_failures} text pair(s) below WCAG AA (1.4.3) — this is a defect")
|
||||
else:
|
||||
print("every text pair clears WCAG AA (1.4.3)")
|
||||
if advisories:
|
||||
print(f"{advisories} boundary pair(s) below 3:1 (1.4.11). Recorded rather "
|
||||
"than failed: every control in this design carries a visible text "
|
||||
"label, which is measured above and passes.")
|
||||
return 1 if failures else 0
|
||||
if boundary_failures:
|
||||
print(f"{boundary_failures} boundary pair(s) below 3:1 (WCAG 1.4.11) — "
|
||||
"this is a defect")
|
||||
else:
|
||||
print("every control boundary clears 3:1 (1.4.11)")
|
||||
return 1 if (text_failures or boundary_failures) else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user