T9.5 - C1+S8: the help-tip is real, the audit is written, BL-001 is dead
S8, finished where the plan said it would be: every .help-tip badge is a <button> - upgraded by the component itself at load (help.js), with helpTipUpgrade() for late renders, so a badge added tomorrow is born reachable. The count the task warned about came true: 15 at wave 0, 18 at the wave 6 exit, 20 at the start of this task - all 20 buttons now, and the fix being in the component is what stops the number growing again. One viewport-clamped role=tooltip bubble serves every badge: focus shows it, Escape hides it, tap toggles it, tap-elsewhere closes it - the touch path Field View's tablets never had. The injected styles now use theme tokens (four raw hexes of the S5 kind, gone). BL-001, CLOSED after three causes and nine waves: the old CSS ::after escaped its badge to the right and was the creator's last 390px overflow. The clamped bubble ends it - scrollWidth 390 vs clientWidth 390 - and frame_check's pin FLIPPED, exactly as designed: it asserted the failure until the fix landed, and now asserts the fix so a regression reopens the entry loudly. The audit (docs/reference/accessibility-audit.md), every number probe-backed: - div/span click handlers: 12/2 at wave 0 -> 0 (the wizard's constraint library entries and the dashboard chips became buttons here; the comments backdrop stopped pretending to be a control) - outline:none without replacement: 0 (wp-chrome's one is the documented S12 exception - its ring is on :focus-within, one ring not two) - aria-live: every toast system and banner announces - native dialogs: 79 -> 21, all on surfaces no S1 task named (admin, users, launcher) - documented as BL-024 with the T7.9 kit ready for them - keyboard-only primary flow: covered leg by leg by the probes that dispatch real CDP key events, cited in the document Three stale count-pins re-pointed to the numbers this task reached (stepper's baseline-minus-10, form_structure's one-span-left, frame_check's BL-001 pin) - each now pins the TARGET so slack cannot hide a regression. Verification (each probe run alone): NEW tests/helptip_check.py 13/13. Regressions: a11y_check 22/22, stepper_check 71/71, form_structure_check 50/51 (BL-022's product question), pipeline_check 44/44, frame_check 38/38. Items: C1, S8 (BL-001 closed, BL-024 opened) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -190,7 +190,10 @@ def run(page, base, tok, db_path):
|
||||
spans = len(re.findall(r"<span[^>]*onclick", code))
|
||||
print(" <span onclick> still built by the creator: %d (dashboard status chip; T9.5)"
|
||||
% spans)
|
||||
chk("...and the only one left in this file is the dashboard's", spans == 1, spans)
|
||||
# T9.5 converted the dashboard chip to a button, so the count is 0 now -
|
||||
# pinned there, because a new span-with-onclick would be a C1 regression.
|
||||
chk("...and none is left in this file at all (the chip became a button at T9.5)",
|
||||
spans == 0, spans)
|
||||
|
||||
# ── 2. the rail ──────────────────────────────────────────────────────────
|
||||
print("\n2. the rail is built from the cards")
|
||||
|
||||
@@ -366,13 +366,13 @@ def measurement(page, base, tok):
|
||||
% (over["scroll"], over["client"], over["navw"] or "(unset)"))
|
||||
if over["scroll"] > over["client"] + 2:
|
||||
print(" still reproduces. Widest boxes: %s" % page.eval(WIDEST_JS))
|
||||
# Pinned, not fixed. BL-001 says to verify it at T7.1 and give it its own item
|
||||
# if it survives the rebuild; T7.1 says to bundle nothing into this diff. So
|
||||
# this asserts what is true TODAY and turns red the moment T7.2 lays the form
|
||||
# out again - which is the point of pinning rather than printing.
|
||||
chk("BL-001 is pinned: the creator still overflows at 390px, so this check "
|
||||
"fails when it is fixed",
|
||||
over["scroll"] > over["client"] + 2, over)
|
||||
# CLOSED at T9.5. The pin below held this failure in view from T7.1 until the
|
||||
# cause was actually removed: the help-tip's CSS ::after escaped its badge to
|
||||
# the right, and rebuilding the component (S8) with a viewport-clamped bubble
|
||||
# ended the overflow. The check now asserts the FIX, so a regression reopens
|
||||
# BL-001 loudly instead of quietly re-widening the page.
|
||||
chk("BL-001 is closed: the creator does not overflow at 390px",
|
||||
over["scroll"] <= over["client"] + 2, over)
|
||||
|
||||
# BL-013: outline:none on every input, replaced by a 3px #edf5ff glow on white -
|
||||
# a 1.05:1 edge. T7.2 owns the fix; this records whether the rebuild changed it,
|
||||
|
||||
189
tests/helptip_check.py
Normal file
189
tests/helptip_check.py
Normal file
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Is every help-tip reachable by keyboard and by touch? — C1 + S8, T9.5.
|
||||
|
||||
The badges were <span> elements whose :focus CSS was dead code (no tabindex)
|
||||
and whose touch path did not exist - on tablets, Field View's surface. Now the
|
||||
component upgrades every badge to a button at load, and one viewport-clamped
|
||||
role=tooltip bubble serves them all. This probe drives a badge with REAL key
|
||||
events and a tap at 390px, then greps the app-wide metrics the audit document
|
||||
cites.
|
||||
|
||||
Exit 0 all passed, 1 a failure, 2 could not run.
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
import cdp # noqa: E402
|
||||
from browser_check import seed, start_server, chk, _PASS, _FAIL # noqa: E402
|
||||
from sections_check import set_sop # noqa: E402
|
||||
from stepper_check import dismiss_dialogs # noqa: E402
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
HTML = os.path.join(ROOT, "html")
|
||||
|
||||
|
||||
def ascii_(v, n=280):
|
||||
return re.sub(r"\s+", " ", str(v)).encode("ascii", "replace").decode()[:n]
|
||||
|
||||
|
||||
def settle(seconds=0.5):
|
||||
time.sleep(seconds)
|
||||
|
||||
|
||||
def strip_js(src):
|
||||
src = re.sub(r"/\*.*?\*/", "", src, flags=re.S)
|
||||
return "\n".join(re.sub(r"(?<![:'\"])//.*$", "", ln) for ln in src.split("\n"))
|
||||
|
||||
|
||||
def wait_creator(page, tries=40):
|
||||
for _ in range(tries):
|
||||
if page.eval("!!window.wpCreatorReady"):
|
||||
return True
|
||||
time.sleep(0.3)
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
exe = cdp.find_browser()
|
||||
if not exe:
|
||||
print("no headless-capable browser found; set WP_BROWSER.")
|
||||
return 2
|
||||
|
||||
# ── 1. the greps the audit cites ──────────────────────────────────────────
|
||||
print("\n1. the audit's grep metrics")
|
||||
divspan = 0
|
||||
outline_bad = []
|
||||
for name in sorted(os.listdir(HTML)):
|
||||
if not name.endswith((".html", ".js", ".css")):
|
||||
continue
|
||||
src = io.open(os.path.join(HTML, name), encoding="utf-8").read()
|
||||
code = strip_js(src) if not name.endswith(".css") else re.sub(r"/\*.*?\*/", "", src, flags=re.S)
|
||||
divspan += len(re.findall(r"<(div|span)[^>]*\bonclick=", code))
|
||||
for m in re.finditer(r"outline:\s*none", code):
|
||||
# The replacement can sit in the rule ABOVE (wp-chrome's search shell
|
||||
# rings on :focus-within, and ringing input + shell would draw two),
|
||||
# so the window looks both ways.
|
||||
ctx = code[max(0, m.start() - 400):m.start() + 260]
|
||||
tail = ctx[ctx.find("outline:") + 12:]
|
||||
if ("outline" not in tail and "box-shadow" not in ctx
|
||||
and "focus-within" not in ctx and "border" not in tail):
|
||||
outline_bad.append(name)
|
||||
chk("div/span click handlers app-wide: 0 (baseline 12/2)", divspan == 0, divspan)
|
||||
chk("outline:none without a replacement: 0", not outline_bad, outline_bad[:4])
|
||||
chk("the audit document exists with the per-page table",
|
||||
"## Per-page results" in io.open(os.path.join(ROOT, "docs", "reference",
|
||||
"accessibility-audit.md"), encoding="utf-8").read())
|
||||
|
||||
tmpdir = tempfile.mkdtemp(prefix="wpsuite-tip-")
|
||||
db_path = os.path.join(tmpdir, "check.db")
|
||||
server = None
|
||||
browser = None
|
||||
try:
|
||||
tok = seed(db_path)
|
||||
set_sop(db_path, {})
|
||||
port = cdp.free_port()
|
||||
base = "http://127.0.0.1:%d" % port
|
||||
server = start_server(port, db_path)
|
||||
|
||||
browser = cdp.Browser(exe)
|
||||
page = browser.page()
|
||||
page.clear_cookies()
|
||||
page.set_cookie("wp_session", tok["root"])
|
||||
page.viewport(390, 844, mobile=True)
|
||||
page.goto(base + "/wp-creation-index.html?project=projA")
|
||||
dismiss_dialogs(page)
|
||||
chk("the creator boots at 390px", wait_creator(page))
|
||||
settle(1.8)
|
||||
|
||||
# ── 2. every badge is a real button ──────────────────────────────────
|
||||
print("\n2. the component, upgraded")
|
||||
counts = json.loads(page.eval("""JSON.stringify({
|
||||
total: document.querySelectorAll('.help-tip').length,
|
||||
buttons: document.querySelectorAll('button.help-tip').length,
|
||||
spans: document.querySelectorAll('span.help-tip').length,
|
||||
})"""))
|
||||
chk("every help-tip on the page is a <button>; zero spans remain",
|
||||
counts["total"] > 0 and counts["spans"] == 0
|
||||
and counts["buttons"] == counts["total"], ascii_(counts))
|
||||
chk("...with an accessible name and a declared state",
|
||||
page.eval("""[...document.querySelectorAll('button.help-tip')]
|
||||
.every(b => b.getAttribute('aria-label') && b.getAttribute('aria-expanded') !== null)"""))
|
||||
|
||||
# ── 3. keyboard ───────────────────────────────────────────────────────
|
||||
print("\n3. keyboard")
|
||||
# Programmatic focus() fires no focusin unless the document HAS focus -
|
||||
# the exact trap form_structure_check documents. Emulate it, loudly.
|
||||
page.ws.call("Emulation.setFocusEmulationEnabled", {"enabled": True})
|
||||
chk("focus emulation is on, so a focus reading means something",
|
||||
page.eval("document.hasFocus()") is True)
|
||||
page.eval("""(() => {
|
||||
const b = [...document.querySelectorAll('button.help-tip')]
|
||||
.find(x => x.offsetParent !== null) || document.querySelector('button.help-tip');
|
||||
b.scrollIntoView({block:'center'}); b.focus();
|
||||
})()""")
|
||||
settle(0.4)
|
||||
chk("focusing a badge shows the tooltip",
|
||||
page.eval("!!(document.getElementById('wp-tip-bubble') && !document.getElementById('wp-tip-bubble').hidden)")
|
||||
and page.eval("(document.getElementById('wp-tip-bubble')||{}).textContent.length > 0"))
|
||||
chk("...as a role=tooltip the badge points at",
|
||||
page.eval("document.getElementById('wp-tip-bubble').getAttribute('role')") == "tooltip"
|
||||
and page.eval("document.activeElement.getAttribute('aria-describedby')") == "wp-tip-bubble")
|
||||
bubble = json.loads(page.eval("""JSON.stringify((() => {
|
||||
const r = document.getElementById('wp-tip-bubble').getBoundingClientRect();
|
||||
return {left: r.left, right: r.right};
|
||||
})())"""))
|
||||
chk("390px: the bubble is CLAMPED to the viewport (BL-001's cause, dead)",
|
||||
bubble["left"] >= 0 and bubble["right"] <= 390, ascii_(bubble))
|
||||
|
||||
# ── 4. touch ──────────────────────────────────────────────────────────
|
||||
print("\n4. touch")
|
||||
page.eval("document.activeElement.blur()")
|
||||
settle(0.3)
|
||||
page.eval("""(() => {
|
||||
const b = [...document.querySelectorAll('button.help-tip')]
|
||||
.find(x => x.offsetParent !== null);
|
||||
b.click();
|
||||
})()""")
|
||||
settle(0.4)
|
||||
chk("tapping a badge opens the tooltip and says so with aria-expanded",
|
||||
page.eval("!!(document.getElementById('wp-tip-bubble') && !document.getElementById('wp-tip-bubble').hidden)")
|
||||
and page.eval("!!document.querySelector(%s)"
|
||||
% json.dumps('button.help-tip[aria-expanded="true"]')))
|
||||
page.eval("document.body.click()")
|
||||
settle(0.3)
|
||||
chk("tapping elsewhere closes it",
|
||||
page.eval("!document.getElementById('wp-tip-bubble') || document.getElementById('wp-tip-bubble').hidden"))
|
||||
|
||||
js_errors = [e for e in page.js_errors() if "beforeunload" not in e]
|
||||
chk("no JavaScript errors anywhere in this run", not js_errors,
|
||||
ascii_(js_errors[:2]))
|
||||
|
||||
finally:
|
||||
if browser is not None:
|
||||
try:
|
||||
browser.close()
|
||||
except Exception:
|
||||
pass
|
||||
if server is not None:
|
||||
try:
|
||||
server.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
print("\n" + "-" * 54)
|
||||
print("%d/%d checks passed." % (len(_PASS), len(_PASS) + len(_FAIL)))
|
||||
for f in _FAIL:
|
||||
print(" - " + f)
|
||||
return 1 if _FAIL else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -232,9 +232,13 @@ def run(page, base, tok):
|
||||
# ── 6. the div-onclick baseline moved ─────────────────────────────────────
|
||||
print("\n6. the wave 0 <div onclick> count dropped by 10")
|
||||
divs, spans = source_counts()
|
||||
chk("app-wide div-with-onclick is %d, down 10 from %d"
|
||||
% (divs, BASELINE_DIV_ONCLICK), divs == BASELINE_DIV_ONCLICK - 10,
|
||||
"counted %d" % divs)
|
||||
# Re-pointed at T9.5: the C1 audit drove the app-wide count to ZERO (the
|
||||
# last two - the wizard's constraint-library entries and the dashboard
|
||||
# chips - became buttons). "Exactly baseline-10" was right while wave 9 was
|
||||
# future; asserting <= that now would let regressions hide under the slack,
|
||||
# so the pin is the final number.
|
||||
chk("app-wide div-with-onclick is %d - the C1 target, reached at T9.5"
|
||||
% divs, divs == 0, "counted %d" % divs)
|
||||
chk("...and none of the survivors is in the wizard's rail",
|
||||
page.eval("document.querySelectorAll('#step-rail div').length") == 0)
|
||||
print(" span-with-onclick unchanged at %d (wave 9 owns those)" % spans)
|
||||
|
||||
Reference in New Issue
Block a user