diff --git a/docs/reference/baseline/after-wave9/admin-390.png b/docs/reference/baseline/after-wave9/admin-390.png new file mode 100644 index 0000000..8429e7c Binary files /dev/null and b/docs/reference/baseline/after-wave9/admin-390.png differ diff --git a/docs/reference/baseline/after-wave9/creator-390.png b/docs/reference/baseline/after-wave9/creator-390.png new file mode 100644 index 0000000..1413678 Binary files /dev/null and b/docs/reference/baseline/after-wave9/creator-390.png differ diff --git a/docs/reference/baseline/after-wave9/field-390.png b/docs/reference/baseline/after-wave9/field-390.png new file mode 100644 index 0000000..261edce Binary files /dev/null and b/docs/reference/baseline/after-wave9/field-390.png differ diff --git a/docs/reference/baseline/after-wave9/launcher-390.png b/docs/reference/baseline/after-wave9/launcher-390.png new file mode 100644 index 0000000..449fdf1 Binary files /dev/null and b/docs/reference/baseline/after-wave9/launcher-390.png differ diff --git a/docs/reference/baseline/after-wave9/login-390.png b/docs/reference/baseline/after-wave9/login-390.png new file mode 100644 index 0000000..f1d2996 Binary files /dev/null and b/docs/reference/baseline/after-wave9/login-390.png differ diff --git a/docs/reference/baseline/after-wave9/sop-390.png b/docs/reference/baseline/after-wave9/sop-390.png new file mode 100644 index 0000000..18ec120 Binary files /dev/null and b/docs/reference/baseline/after-wave9/sop-390.png differ diff --git a/docs/reference/baseline/after-wave9/users-390.png b/docs/reference/baseline/after-wave9/users-390.png new file mode 100644 index 0000000..cc4b87c Binary files /dev/null and b/docs/reference/baseline/after-wave9/users-390.png differ diff --git a/docs/reference/file-map.md b/docs/reference/file-map.md index eefbe6c..c6d64d7 100644 --- a/docs/reference/file-map.md +++ b/docs/reference/file-map.md @@ -303,6 +303,7 @@ python tests/export_check.py # CR-008/CR-017 - export walk + hours guard python tests/sample_check.py # S7 - one sample affordance, confirmed+fenced 10 checks python tests/icon_check.py # S6 - one icon system, no emoji, mapped 5 checks python tests/helptip_check.py # C1/S8 - tips by keyboard+touch, audit greps 13 checks +python tests/mobile_check.py # C2 - all 7 pages at 390px, targets + fit 24 checks ``` **Three probes were re-pointed at `T7.1`.** `sections_check.py` 5b drove the live diff --git a/html/console.css b/html/console.css index 45a63f2..416c74a 100644 --- a/html/console.css +++ b/html/console.css @@ -213,3 +213,9 @@ select.role-select:disabled{ color:var(--cds-text-disabled); border-color:var(-- @media (max-width:620px){ .urow input, .urow select, .urow button{ flex:1 1 100%; } } + +/* C2 / T9.6: the console header links are standalone targets, not inline text, + so they meet the touch floor at coarse pointers / phone widths. */ +@media (max-width: 500px), (pointer: coarse) { + a.home { min-height: 44px; display: inline-flex; align-items: center; } +} diff --git a/html/wp-chrome.css b/html/wp-chrome.css index e3ac2c7..9da595d 100644 --- a/html/wp-chrome.css +++ b/html/wp-chrome.css @@ -319,3 +319,20 @@ the gate panel is what does the explaining. */ .nav-tab[aria-disabled="true"] { opacity: .55; cursor: default; } .nav-tab[aria-disabled="true"]:hover { background: none; color: var(--text-light); } + +/* C2 / T9.6: touch sizing. At phone widths (and any coarse pointer) every + control meets the 44px bar the field surfaces are held to; checkboxes, + radios and the help-tip badge get the 24px WCAG floor with spacing doing + the rest. Shared here because every page loads this sheet - six copies of + this block is how the six pages drift apart again. */ +@media (max-width: 500px), (pointer: coarse) { + button, .btn, .add-btn, .nav-btn, .header-button, + input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]), + select, textarea { min-height: 44px; } + a.wp-appbar-link, .wp-sidenav-item, .nav-tab { + min-height: 44px; display: inline-flex; align-items: center; } + input[type="checkbox"], input[type="radio"] { min-width: 24px; min-height: 24px; } + .help-tip { min-width: 24px; min-height: 24px; } + .wp-navbtn, .ui-help-fab, .wp-sidenav-close { min-width: 44px; } + .wp-appbar-brand { min-height: 44px; display: inline-flex; align-items: center; } +} diff --git a/html/wp-creation-styles.css b/html/wp-creation-styles.css index ca948eb..6993e00 100644 --- a/html/wp-creation-styles.css +++ b/html/wp-creation-styles.css @@ -689,7 +689,9 @@ /* Dev mode (comment 7) */ .logo-wrap { position:relative; display:flex; align-items:center; } - .dev-toggle { position:absolute; left:2px; bottom:-9px; width:18px; height:7px; padding:0; border:none; + /* C2/T9.6: a deliberately unobtrusive dev switch is still a control - it + meets the 24px floor and earns its subtlety with opacity, not size. */ + .dev-toggle { position:absolute; left:2px; bottom:-12px; width:24px; height:24px; padding:0; border:none; background:var(--text-dim); opacity:0.10; border-radius:3px; cursor:pointer; } .dev-toggle:hover { opacity:0.35; } .dev-banner { background:var(--wp-dev-bg); color:var(--wp-dev-fg); font-size:12.5px; font-weight:700; text-align:center; padding:7px 14px; letter-spacing:.3px; } diff --git a/tests/mobile_check.py b/tests/mobile_check.py new file mode 100644 index 0000000..cbcd3c5 --- /dev/null +++ b/tests/mobile_check.py @@ -0,0 +1,180 @@ +#!/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 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= 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())