The deliberate mobile pass the original proposal never had. Driven page by page at 390px with mobile emulation (the media queries under test actually fire) by NEW tests/mobile_check.py, 24/24: - no page scrolls sideways - all seven (the creator joined at T9.5 when BL-001 died) - no visible control is clipped past the viewport (the probe learned frame_check's two lessons: an off-canvas drawer is PARKED, not clipped, and a row inside an overflow-x container is scrollable) - tap targets: the shared coarse-pointer block in wp-chrome.css puts every button, input, select, nav link and appbar control at a 44px minimum on phone widths and coarse pointers; checkboxes, radios and help-tip badges get the 24px WCAG floor with spacing doing the rest. Field View - the gloved-hands surface - measures 44px on EVERY control. Inline text links are exempt per WCAG 2.5.8's own exception. Even the deliberately unobtrusive dev toggle grew to the floor: subtle by opacity, not by size. - CR-007 attachments offline at 390px and T8.5 requests at 390px were already pinned by files_check and mreq_check; this pass cites rather than repeats. After-screenshots for all seven pages at 390px are committed in docs/reference/baseline/after-wave9, beside the wave 0 set, captured by the same baseline_shots.py fixture. Verification (each probe run alone): NEW tests/mobile_check.py 24/24. Regressions: form_structure_check 50/51 (BL-022's standing question), files_check 36/36. Items: C2 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
181 lines
6.7 KiB
Python
181 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Does the whole suite hold together at 390px? — C2, T9.6.
|
|
|
|
Nothing in the original proposal touched mobile, and it is where the worst
|
|
rendering was found. This drives all seven pages at 390px (mobile emulation,
|
|
so the media queries under test actually fire) and asserts:
|
|
|
|
- no page scrolls sideways
|
|
- no visible control is clipped past the viewport or collapsed to nothing
|
|
- every control meets the 24px WCAG floor; on the gloved-hands surfaces
|
|
(Field View, and the creator's rail / status / save controls) the bar is
|
|
44px, which is what the shared coarse-pointer sizing in wp-chrome.css
|
|
delivers
|
|
|
|
The after-screenshots live in docs/reference/baseline/after-wave9 (captured by
|
|
baseline_shots.py) beside the wave 0 set.
|
|
|
|
Exit 0 all passed, 1 a failure, 2 could not run.
|
|
"""
|
|
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__)))
|
|
|
|
PAGES = [
|
|
("login", "/login.html"),
|
|
("launcher", "/index.html"),
|
|
("wizard", "/work-package-suite.html?tab=sop&project=projA"),
|
|
("creator", "/wp-creation-index.html?project=projA"),
|
|
("field", "/field.html?project=projA"),
|
|
("admin", "/admin.html"),
|
|
("users", "/users.html"),
|
|
]
|
|
|
|
MEASURE = """(function(){
|
|
var out = {sw: document.documentElement.scrollWidth, total:0, under24:[],
|
|
clipped:[]};
|
|
function skip(el){
|
|
// An off-canvas drawer is PARKED outside the viewport by design, and a row
|
|
// inside an overflow-x container is scrollable, not clipped - the same two
|
|
// lessons frame_check's widest-box scan learned the hard way.
|
|
for (var n = el; n; n = n.parentElement){
|
|
var cs = getComputedStyle(n);
|
|
if (cs.overflowX === 'auto' || cs.overflowX === 'scroll') return true;
|
|
if (cs.position === 'fixed' && cs.transform && cs.transform !== 'none') return true;
|
|
if (n.getAttribute && n.getAttribute('aria-hidden') === 'true') return true;
|
|
}
|
|
return false;
|
|
}
|
|
var els = document.querySelectorAll('button, a[href], input, select, textarea, [role=button]');
|
|
for (var i=0;i<els.length;i++){
|
|
var el=els[i]; var r=el.getBoundingClientRect();
|
|
if (r.width===0 || r.height===0 || el.disabled || el.type==='hidden') continue;
|
|
if (skip(el)) continue;
|
|
out.total++;
|
|
var inlineText = el.tagName==='A' && getComputedStyle(el).display==='inline';
|
|
var m=Math.min(r.width,r.height);
|
|
var id=el.tagName+'.'+String(el.className).slice(0,24)+' '+Math.round(r.width)+'x'+Math.round(r.height);
|
|
if (m < 24 && !inlineText && out.under24.length < 6) out.under24.push(id);
|
|
if ((r.left < -2 || r.right > 392) && out.clipped.length < 6) out.clipped.push(id);
|
|
}
|
|
return JSON.stringify(out);
|
|
})()"""
|
|
|
|
FIELD44 = """(function(){
|
|
var out = {total:0, under:[]};
|
|
function skip(el){
|
|
for (var n = el; n; n = n.parentElement){
|
|
var cs = getComputedStyle(n);
|
|
if (cs.position === 'fixed' && cs.transform && cs.transform !== 'none') return true;
|
|
if (n.getAttribute && n.getAttribute('aria-hidden') === 'true') return true;
|
|
}
|
|
return false;
|
|
}
|
|
var els = document.querySelectorAll(
|
|
'button, a[href], input:not([type=checkbox]):not([type=radio]), select, textarea');
|
|
for (var i=0;i<els.length;i++){
|
|
var el=els[i]; var r=el.getBoundingClientRect();
|
|
if (r.width===0 || r.height===0 || el.disabled) continue;
|
|
if (skip(el)) continue;
|
|
out.total++;
|
|
if (Math.min(r.width, r.height) < 44 && out.under.length < 6)
|
|
out.under.push(el.tagName+'.'+String(el.className).slice(0,24)+' '
|
|
+Math.round(r.width)+'x'+Math.round(r.height));
|
|
}
|
|
return JSON.stringify(out);
|
|
})()"""
|
|
|
|
|
|
def ascii_(v, n=280):
|
|
return re.sub(r"\s+", " ", str(v)).encode("ascii", "replace").decode()[:n]
|
|
|
|
|
|
def main():
|
|
exe = cdp.find_browser()
|
|
if not exe:
|
|
print("no headless-capable browser found; set WP_BROWSER.")
|
|
return 2
|
|
|
|
shots = os.path.join(ROOT, "docs", "reference", "baseline", "after-wave9")
|
|
chk("the after-screenshots are committed beside the wave 0 baseline",
|
|
os.path.isdir(shots) and len([f for f in os.listdir(shots)
|
|
if f.endswith("-390.png")]) >= 7,
|
|
shots)
|
|
|
|
tmpdir = tempfile.mkdtemp(prefix="wpsuite-mobile-")
|
|
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)
|
|
|
|
for name, path in PAGES:
|
|
print("\n%s" % name)
|
|
page.goto(base + path)
|
|
dismiss_dialogs(page)
|
|
time.sleep(2.4)
|
|
m = json.loads(page.eval(MEASURE))
|
|
chk("%s: no sideways scrolling" % name, m["sw"] <= 392, m["sw"])
|
|
chk("%s: no control clipped past the viewport" % name,
|
|
not m["clipped"], ascii_(m["clipped"]))
|
|
chk("%s: every control meets the 24px floor" % name,
|
|
not m["under24"], ascii_(m["under24"]))
|
|
|
|
# the gloved-hands bar: Field View, everything 44px
|
|
print("\nfield view, the 44px bar")
|
|
page.goto(base + "/field.html?project=projA")
|
|
dismiss_dialogs(page)
|
|
time.sleep(2.4)
|
|
f = json.loads(page.eval(FIELD44))
|
|
chk("field view: every control is a 44px touch target",
|
|
f["total"] > 0 and not f["under"], ascii_(f))
|
|
|
|
js_errors = [e for e in page.js_errors() if "beforeunload" not in e]
|
|
chk("no JavaScript errors across the whole sweep", 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())
|