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:
@@ -0,0 +1,152 @@
|
||||
"""v1.1 WP-E: the contrast audit is a gate, not a report.
|
||||
|
||||
Before this package `tools/contrast_audit.py` measured control boundaries,
|
||||
printed that two of them were below 3:1, and exited 0 anyway — on the argument
|
||||
that a control is identified by its label rather than its edge. A check that
|
||||
cannot fail is not a check, and these tests are what make it one: the threshold
|
||||
is exercised from both sides, on a real tokens file, so a future palette change
|
||||
that dims a control's edge stops the run instead of adding a line to it.
|
||||
|
||||
python -m pytest tests/test_v11_e_contrast.py -v
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "tools"))
|
||||
|
||||
import contrast_audit as audit # noqa: E402
|
||||
|
||||
|
||||
# --------------------------------------------------------------- the maths
|
||||
|
||||
def test_the_ratio_is_the_wcag_ratio():
|
||||
"""Anchored on values with known answers, so a broken formula is visible."""
|
||||
assert audit.ratio("#ffffff", "#000000") == pytest.approx(21.0, abs=0.01)
|
||||
assert audit.ratio("#ffffff", "#ffffff") == pytest.approx(1.0, abs=0.001)
|
||||
# Order does not matter: contrast is symmetric.
|
||||
assert audit.ratio("#131320", "#676792") == pytest.approx(
|
||||
audit.ratio("#676792", "#131320"), abs=1e-9)
|
||||
|
||||
|
||||
# ------------------------------------------------------- the gate itself
|
||||
|
||||
def tokens_file(tmp_path: Path, **overrides: str) -> Path:
|
||||
"""A real tokens file with named colours replaced."""
|
||||
source = audit.TOKENS.read_text()
|
||||
for name, value in overrides.items():
|
||||
token = "--" + name.replace("_", "-")
|
||||
start = source.index(f"{token}: ")
|
||||
end = source.index(";", start)
|
||||
source = source[:start] + f"{token}: {value}" + source[end:]
|
||||
written = tmp_path / "tokens.css"
|
||||
written.write_text(source)
|
||||
return written
|
||||
|
||||
|
||||
def run_against(path: Path, monkeypatch) -> int:
|
||||
monkeypatch.setattr(audit, "TOKENS", path)
|
||||
return audit.main()
|
||||
|
||||
|
||||
def test_a_boundary_below_three_to_one_fails_the_run(tmp_path, monkeypatch, capsys):
|
||||
"""2.99:1 against --bg-input — the wrong side of the line by one hundredth.
|
||||
|
||||
This value clears 3:1 against --bg-panel (3.21:1), so it would have passed
|
||||
the audit as M11 wrote it. It fails now because the floor is taken against
|
||||
the background the control is actually drawn on.
|
||||
"""
|
||||
below = tokens_file(tmp_path, border="#58639a")
|
||||
assert run_against(below, monkeypatch) == 1
|
||||
printed = capsys.readouterr().out
|
||||
assert "2.99:1" in printed
|
||||
assert "FAIL" in printed
|
||||
assert "below 3:1 (WCAG 1.4.11)" in printed
|
||||
|
||||
|
||||
def test_a_boundary_at_three_to_one_passes(tmp_path, monkeypatch, capsys):
|
||||
"""3.00:1 — the right side of the same line, one hundredth from the value
|
||||
above, and not failed for arithmetic the reader cannot see."""
|
||||
at = tokens_file(tmp_path, border="#58639b")
|
||||
assert run_against(at, monkeypatch) == 0
|
||||
printed = capsys.readouterr().out
|
||||
assert "3.00:1" in printed
|
||||
assert "every control boundary clears 3:1" in printed
|
||||
|
||||
|
||||
def test_the_v1_0_0_boundary_would_now_fail(tmp_path, monkeypatch, capsys):
|
||||
"""The value v1.0.0 shipped. This is the defect WP-E closes, and the gate
|
||||
has to be the thing that would have caught it."""
|
||||
shipped = tokens_file(tmp_path, border="#2b2b3d")
|
||||
assert run_against(shipped, monkeypatch) == 1
|
||||
assert "1.24:1" in capsys.readouterr().out # against --bg-input, the worst case
|
||||
|
||||
|
||||
def test_a_text_pair_below_four_point_five_still_fails(tmp_path, monkeypatch, capsys):
|
||||
"""WP-E raised the boundary floor and must not have lowered the text one."""
|
||||
dimmed = tokens_file(tmp_path, text_dim="#5a5750")
|
||||
assert run_against(dimmed, monkeypatch) == 1
|
||||
assert "below WCAG AA (1.4.3)" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_a_missing_token_is_a_failure_not_a_skip(tmp_path, monkeypatch, capsys):
|
||||
source = audit.TOKENS.read_text().replace("--border-bright:", "--border-was-renamed:")
|
||||
written = tmp_path / "tokens.css"
|
||||
written.write_text(source)
|
||||
assert run_against(written, monkeypatch) == 1
|
||||
assert "MISSING TOKEN" in capsys.readouterr().out
|
||||
|
||||
|
||||
# ------------------------------------------------- the shipped palette
|
||||
|
||||
def test_the_real_tokens_pass_both_criteria(capsys):
|
||||
"""The palette as it stands, through the same gate CI would run."""
|
||||
assert audit.main() == 0
|
||||
printed = capsys.readouterr().out
|
||||
assert "every text pair clears WCAG AA (1.4.3)" in printed
|
||||
assert "every control boundary clears 3:1 (1.4.11)" in printed
|
||||
assert "FAIL" not in printed
|
||||
|
||||
|
||||
def test_every_boundary_pair_is_measured_against_the_background_it_is_drawn_on():
|
||||
"""The audit used to check borders only against --bg-panel, which is not
|
||||
where the bordered controls are: inputs and buttons sit on --bg-input
|
||||
(styles/forms.css), which is lighter and therefore harder. Checking only the
|
||||
easier background would let a token pass while the real control failed."""
|
||||
boundary = [(fg, bg) for kind, fg, bg, _, _ in audit.PAIRS if kind == "boundary"]
|
||||
for background in ("--bg-input", "--bg-panel", "--bg"):
|
||||
assert ("--border", background) in boundary, background
|
||||
assert ("--border-bright", "--bg-input") in boundary
|
||||
|
||||
|
||||
def test_the_text_baselines_are_unchanged_by_wp_e():
|
||||
"""WP-E changed only boundary tokens. These are the M11 text measurements,
|
||||
and they have to still be exactly what the earlier reports recorded."""
|
||||
tokens = audit.read_tokens(audit.TOKENS)
|
||||
measured = {
|
||||
"body text on the page": audit.ratio(tokens["--text"], tokens["--bg"]),
|
||||
"body text in a panel": audit.ratio(tokens["--text"], tokens["--bg-panel"]),
|
||||
"secondary text in a panel": audit.ratio(tokens["--text-dim"], tokens["--bg-panel"]),
|
||||
"secondary text on the page": audit.ratio(tokens["--text-dim"], tokens["--bg"]),
|
||||
}
|
||||
assert measured["body text on the page"] == pytest.approx(14.57, abs=0.01)
|
||||
assert measured["body text in a panel"] == pytest.approx(13.57, abs=0.01)
|
||||
assert measured["secondary text in a panel"] == pytest.approx(5.48, abs=0.01)
|
||||
assert measured["secondary text on the page"] == pytest.approx(5.88, abs=0.01)
|
||||
|
||||
|
||||
def test_the_hover_edge_stays_brighter_than_the_resting_edge():
|
||||
"""Rest and hover have to remain distinguishable from each other, not merely
|
||||
each clear the floor against the background."""
|
||||
tokens = audit.read_tokens(audit.TOKENS)
|
||||
panel = tokens["--bg-panel"]
|
||||
assert audit.ratio(tokens["--border-bright"], panel) > audit.ratio(tokens["--border"], panel)
|
||||
|
||||
|
||||
def test_a_boundary_never_becomes_as_loud_as_body_text():
|
||||
"""A control's edge that outshines the words inside it is its own defect."""
|
||||
tokens = audit.read_tokens(audit.TOKENS)
|
||||
panel = tokens["--bg-panel"]
|
||||
assert audit.ratio(tokens["--border-bright"], panel) < audit.ratio(tokens["--text"], panel)
|
||||
Reference in New Issue
Block a user