#!/usr/bin/env python3 """Local synthetic keyboard test. Not an axe scan or screen-reader test. Run: python lab/run_keyboard_lab.py --browser /usr/bin/chromium Requires Python 3.10+ and playwright==1.57.0 or another compatible installed version. The exact tested package and browser versions are written to the results. """ from __future__ import annotations import argparse, hashlib, importlib.metadata, json from datetime import datetime, timezone from pathlib import Path from playwright.sync_api import sync_playwright ROOT = Path(__file__).resolve().parent.parent def sha256(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--browser', default='/usr/bin/chromium') args = parser.parse_args() executable = Path(args.browser) if not executable.is_file(): parser.error(f'Browser not found: {executable}') results = { 'schema': 'cometweb.synthetic-keyboard-lab/v1', 'run_at_utc': datetime.now(timezone.utc).isoformat(), 'dataset': 'two local, deliberately authored fixtures', 'document_delivery': 'Playwright page.set_content, no HTTP requests', 'not_assessed': ['live CometWeb site', 'axe-core', 'screen readers', 'WCAG conformance of a page or site', 'legal compliance', 'other browser engines', 'usability with disabled people'], 'python_playwright_version': importlib.metadata.version('playwright'), 'cases': [] } with sync_playwright() as p: browser = p.chromium.launch(executable_path=str(executable), headless=True, args=['--no-sandbox']) results['browser'] = browser.version for variant in ('before', 'after'): file = ROOT / 'lab' / 'fixtures' / f'{variant}.html' page = browser.new_page(viewport={'width': 1280, 'height': 800}) page.set_content(file.read_text(encoding='utf-8'), wait_until='load') button = page.get_by_role('button', name='Zamów', exact=True) assert button.count() == 1 # Chromium's AX tree is not a screen reader or a complete accessibility evaluation. cdp = page.context.new_cdp_session(page) ax = cdp.send('Accessibility.getFullAXTree') ax_buttons = [n for n in ax['nodes'] if n.get('role', {}).get('value') == 'button' and n.get('name', {}).get('value') == 'Zamów'] assert len(ax_buttons) == 1 forward, backward = [], [] for key, trace in [('Tab', forward), ('Shift+Tab', backward)]: button.focus() for _ in range(3): page.keyboard.press(key) trace.append(page.evaluate('document.activeElement.id || document.activeElement.tagName')) expected_first = {'Tab': 'next', 'Shift+Tab': 'previous'} observed = {'Tab': forward[0], 'Shift+Tab': backward[0]} case = { 'variant': variant, 'fixture_sha256': sha256(file), 'computed_accessibility_role': 'button', 'computed_accessibility_name': 'Zamów', 'role_and_name_observed': True, 'tab_trace': forward, 'shift_tab_trace': backward, 'expected_first_destination': expected_first, 'observed_first_destination': observed, 'forward_navigation_matches_expected': forward[0] == 'next', 'backward_navigation_matches_expected': backward[0] == 'previous' } if variant == 'before': assert forward == ['buy'] * 3 and backward == ['buy'] * 3 case['interpretation'] = 'Authored Tab trap reproduced; no in-page escape is implemented.' else: assert forward[0] == 'next' and backward[0] == 'previous' case['interpretation'] = 'First forward and backward focus destinations match the fixture contract.' results['cases'].append(case) page.close() browser.close() dest = ROOT / 'evidence' / 'keyboard-lab-results.json' dest.write_text(json.dumps(results, ensure_ascii=False, indent=2) + '\n', encoding='utf-8') print(json.dumps(results, ensure_ascii=False, indent=2)) if __name__ == '__main__': main()