check

Draw a demo in a headless browser and play it: no errors, no network, the gate hides the answer, keys move each control, and the text has enough contrast.

validate reads the spec. check runs it. It writes the static page, opens it in a headless browser with playwright, and plays the demo as a learner would. The browser is always headless, so no window opens and nothing takes the focus.

These checks run in light and in dark mode:

  1. The demo draws: the host gets data-rk-ready="1". Any console error, page error, or request that is not file:, data: or about: fails.
  2. Each visible block has a box that is not empty. Each block with after is hidden before its gate.
  3. Each text has a contrast of at least 4.5:1 against what is behind it (3:1 for large text), before and after the reveal.
  4. Play: for each gate, pick the first option and lock in with ⌘↩︎. The verdict shows, the rk-predict event fires, its answer matches the Python twin, and the blocks that wait show.
  5. Keys: each control gets a key press, and the state must move. The derived values must match the Python twins.

The browser

The check never uses the Chrome that you use every day: a headless run of that app shares its crash reporter, its updater and its app registration, and many runs can crash it. It uses, in this order: a path that you give, the path in $FASTCDP_CHROME, or the Chromium that playwright install chromium gives. One process runs one browser at a time, and each browser closes in a finally block.


source

browser

def browser(
    path:str=None
):

A headless browser for check; one at a time in this process, closed at the end.


source

browser_path

def browser_path(
    path:str=None
):

The browser to use: path, else $FASTCDP_CHROME, else None for playwright’s own Chromium. Never the everyday Chrome.

The default is playwright’s own Chromium, not the everyday Chrome, and a path into the everyday Chrome is refused.

import os
if not os.getenv('FASTCDP_CHROME'): test_eq(browser_path(), None)
test_fail(lambda: browser_path('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'), contains='everyday Chrome')

can_check says if a browser starts here. The notebook runs its browser tests only when one does.


source

can_check

def can_check()->bool:

True when playwright and a browser are there.


source

in_thread

def in_thread(
    f, *a, **kw
):

Call f; in a thread when an asyncio loop runs here (as in a notebook), because playwright’s sync API cannot run in a loop.

Contrast

This script runs in the page. For each element with its own text, it finds the colour behind the text: the backgrounds of the element and its parents, and for SVG text, a shape behind it in the same group (the dot of a picked wheel item, the cell of a grid). It mixes in opacity, then gives the WCAG ratio. Disabled controls and hidden text are skipped.


source

contrast

def contrast(
    pg
)->list:

The texts of the page whose contrast is under the WCAG need, as (text, ratio, need).

The parts of the check

_state reads the state of the demo from the page. _parity compares each derived value with the Python twin of its fn.

_boxes checks that each visible block has a box, and that each block with after is hidden before its gate. Chrome’s checkVisibility() does not see display: none on an SVG element, so VIS also looks at the element and its parents.

_play plays each gate: it picks the first option with the key 1, and locks in with ⌘↩︎.

_keys sends keys to each control and checks that the state moves. A control at one end of its range gets the opposite key too.

check

check puts the parts together. It returns AttrDict(ok, errors, info, shots). With shots, it saves screenshots at 2× before and after the play, in light and dark.

check runs in a thread when an asyncio loop runs, as in a notebook.


source

check

def check(
    spec, shots:NoneType=None, modes:tuple=('light', 'dark'), play:bool=True, b:NoneType=None, path:str=None
)->fastcore.basics.AttrDict:

Draw spec in a headless browser, play it, and return AttrDict(ok, errors, info, shots); b reuses an open browser.

Tests

These tests need a browser, so they run only where one starts.

ok = can_check()
ok

A good demo passes. The info has the options of the gate and the verdict.

from rupaka.spec import examples
if ok:
    r = check(examples()['gate'])
    test_eq(r.errors, [])
    test_eq(r.info['light'][0]['options'], ['4', '5', '6', '23'])
    assert r.info['light'][0]['verdict'].startswith('You said 4. It is 5.')

A spec with a broken fn name fails with the error text of the runtime.

if ok:
    bad = {**examples()['gate'], 'derive': [{'fn': 'sums', 'out': 'sum'}]}
    r = check(bad, modes=('light',))
    assert not r.ok and 'unknown fn "sums"' in r.errors[0], r.errors

Each block example passes the whole check, in light and dark.

def check_all(specs):
    with browser() as br: return {n: check(s, b=br).errors for n, s in specs.items()}
if ok: test_eq(in_thread(check_all, examples()), {n: [] for n in examples()})