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:
@@ -25,6 +25,7 @@ has actually finished arriving — never the click that started it.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
@@ -226,6 +227,45 @@ class Browser:
|
||||
def source(self) -> str:
|
||||
return self._call("GET", self._s("/source"))["value"]
|
||||
|
||||
def screenshot(self, path) -> Path:
|
||||
"""The viewport as a PNG, written where you ask (v1.1 WP-E).
|
||||
|
||||
Evidence for a change a reader judges by looking at it: a contrast ratio
|
||||
says a boundary is measurable, and a picture says what it looks like.
|
||||
"""
|
||||
encoded = self._call("GET", self._s("/screenshot"))["value"]
|
||||
target = Path(path)
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(base64.b64decode(encoded))
|
||||
return target
|
||||
|
||||
def hover(self, element: str) -> None:
|
||||
"""A real pointer over an element, so `:hover` actually applies.
|
||||
|
||||
Dispatching a mouseover event from JavaScript does not do this: CSS
|
||||
`:hover` follows the browser's own pointer state, not a synthetic event,
|
||||
so a measurement taken after `dispatchEvent` reads the resting style and
|
||||
reports it as the hover style. This moves the pointer (v1.1 WP-E).
|
||||
"""
|
||||
self._call("POST", self._s("/execute/sync"), {
|
||||
"script": "arguments[0].scrollIntoView({block: 'center', inline: 'nearest'})",
|
||||
"args": [{ELEMENT_KEY: element}]})
|
||||
self._call("POST", self._s("/actions"), {"actions": [{
|
||||
"type": "pointer", "id": "mouse", "parameters": {"pointerType": "mouse"},
|
||||
"actions": [{"type": "pointerMove", "duration": 60,
|
||||
"origin": {ELEMENT_KEY: element}, "x": 0, "y": 0}]}]})
|
||||
|
||||
def unhover(self) -> None:
|
||||
"""Move the pointer off whatever it was over, and forget the input state."""
|
||||
self._call("POST", self._s("/actions"), {"actions": [{
|
||||
"type": "pointer", "id": "mouse", "parameters": {"pointerType": "mouse"},
|
||||
"actions": [{"type": "pointerMove", "duration": 30,
|
||||
"origin": "viewport", "x": 0, "y": 0}]}]})
|
||||
try:
|
||||
self._call("DELETE", self._s("/actions"))
|
||||
except WebDriverError:
|
||||
pass
|
||||
|
||||
def js(self, script: str, *args):
|
||||
return self._call("POST", self._s("/execute/sync"),
|
||||
{"script": script, "args": list(args)})["value"]
|
||||
|
||||
Reference in New Issue
Block a user