diff options
Diffstat (limited to 'tools/ui_flow.py')
| -rw-r--r-- | tools/ui_flow.py | 74 |
1 files changed, 65 insertions, 9 deletions
diff --git a/tools/ui_flow.py b/tools/ui_flow.py index edfc9a4..a17d435 100644 --- a/tools/ui_flow.py +++ b/tools/ui_flow.py @@ -28,8 +28,11 @@ waiting for the PIN and confirmed from the menu (watches with buttons), the list, a TOTP entry, a HOTP entry and its next code, the PIN screen, a wrong PIN, a correct PIN opening a locked entry, auto-lock closing it, the menu, the About screen (watches with buttons; touch-only -menus have no focus to move), the lockout after 5 wrong PINs, and an EAN-13 -code whose screenshot must decode with zxing-cpp (README 12.4). +menus have no focus to move), the lockout after 5 wrong PINs, the barcodes +whose screenshots must decode with zxing-cpp (README 12.4). The glance with +the favourite's code is a separate launch (--shot glance_code, from +screenshots.sh): the simulator doesn't return to the glance when the app +exits. Each screenshot is compared with test/golden/<device>/<screen>.png like sim_screenshot.py does (or saved there with --approve). @@ -50,6 +53,7 @@ from pathlib import Path import sim_screenshot as shots import zxingcpp +from PIL import Image EXIT_OK, EXIT_DIFFERENT, EXIT_FLOW = 0, 1, 2 SCREEN_TIMEOUT_S = 20.0 @@ -90,6 +94,11 @@ class Key: hold: bool +def barcodes_on(screen: Image.Image) -> list[str]: + """The texts of every barcode zxing-cpp finds on a screenshot.""" + return [r.text for r in zxingcpp.read_barcodes(screen)] + + def load_keys(device_dir: Path) -> dict[str, Key]: """Button centres in device-drawing coordinates, by button id (enter, up, down, esc, menu).""" sim = json.loads((device_dir / "simulator.json").read_text(encoding="utf-8")) @@ -326,7 +335,7 @@ class Flow: return if expect_code is not None: # README 12.4 check 3: the code on the screenshot must decode. - read = [r.text for r in zxingcpp.read_barcodes(screen)] + read = barcodes_on(screen) if expect_code not in read: print(f"[{label}] the barcode didn't decode to {expect_code!r}: read {read}") self.results[name] = EXIT_DIFFERENT @@ -569,6 +578,50 @@ def open_from_glance(window: str, device_dir: Path, raw_dir: Path) -> None: sim.press("enter") +def decode_screen(window: str, device_dir: Path, out: Path, text: str) -> int: + """Screenshot the simulator; EXIT_OK when a barcode on it reads `text`.""" + name = device_dir.name + try: + sim = Simulator(window, device_dir, out / "raw") + screen = shots.extract_screen(sim.capture("decode"), device_dir, out / "decode.png") + except (FlowError, shots.ScreenshotError, subprocess.CalledProcessError, OSError) as err: + print(f"[{name}] no screenshot to decode: {err}") + return EXIT_FLOW + read = barcodes_on(screen) + if text not in read: + print(f"[{name}] the barcode on the screen didn't decode to {text!r}: read {read}") + return EXIT_DIFFERENT + print(f"[{name}] the barcode on the screen decodes to {text!r}") + return EXIT_OK + + +def open_app(window: str, device_dir: Path, out: Path) -> int: + """--open-from-glance: EXIT_OK once the input was sent.""" + try: + open_from_glance(window, device_dir, out / "raw") + except (FlowError, subprocess.CalledProcessError, OSError) as err: + print(f"[{device_dir.name}] could not open the app from its glance: {err}") + return EXIT_FLOW + return EXIT_OK + + +def single_shot(args: argparse.Namespace) -> int: + """Screenshot the simulator as args.shot and compare it with its golden image (or approve it).""" + window, device_dir, out, golden, name = args.window, args.device_dir, args.out, args.golden, args.shot + label = f"{device_dir.name}/{name}" + try: + sim = Simulator(window, device_dir, out / "raw") + screen = shots.extract_screen(sim.capture(name), device_dir, out / f"{name}.png") + except (FlowError, shots.ScreenshotError, subprocess.CalledProcessError, OSError) as err: + print(f"[{label}] {err}") + return EXIT_FLOW + if args.approve: + shots.save_png(screen, golden / f"{name}.png", f"Approved simulator screen '{name}' of {device_dir.name}") + print(f"[{label}] approved") + return EXIT_OK + return shots.check_golden(screen, label, out / f"{name}.png", golden / f"{name}.png") + + def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description="Scripted UI flow with screenshot checks.") parser.add_argument("--device-dir", type=Path, required=True) @@ -578,15 +631,18 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--golden", type=Path, help="directory of the approved images") parser.add_argument("--approve", action="store_true") parser.add_argument("--open-from-glance", action="store_true", help="only open the app from its glance") + parser.add_argument("--decode", metavar="TEXT", help="only check that the screen shows a barcode reading TEXT") + parser.add_argument("--shot", metavar="NAME", help="only screenshot the current screen as NAME (needs --golden)") args = parser.parse_args(argv) name = args.device_dir.name + if args.decode is not None: + return decode_screen(args.window, args.device_dir, args.out, args.decode) + if args.shot is not None: + if args.golden is None: + parser.error("--shot needs --golden") + return single_shot(args) if args.open_from_glance: - try: - open_from_glance(args.window, args.device_dir, args.out / "raw") - except (FlowError, subprocess.CalledProcessError, OSError) as err: - print(f"[{name}] could not open the app from its glance: {err}") - return EXIT_FLOW - return EXIT_OK + return open_app(args.window, args.device_dir, args.out) if args.log is None or args.golden is None: parser.error("--log and --golden are required for the flow") try: |
