34 lines
4.0 KiB
Python
34 lines
4.0 KiB
Python
"""Bounded loopback fixture overlay check. Does not submit application writes."""
|
|
import argparse,json
|
|
from pathlib import Path
|
|
from urllib.parse import urlparse
|
|
from playwright.sync_api import sync_playwright
|
|
parser=argparse.ArgumentParser();parser.add_argument('--base-url',required=True);parser.add_argument('--storage-state');parser.add_argument('--output-dir',required=True);parser.add_argument('--case',default='overlays');args=parser.parse_args()
|
|
if urlparse(args.base_url).hostname not in ('127.0.0.1','localhost'):raise SystemExit('This synthetic fixture harness accepts loopback origins only.')
|
|
out=Path(args.output_dir);out.mkdir(parents=True,exist_ok=True);results=[]
|
|
with sync_playwright() as p:
|
|
browser=p.chromium.launch(channel='chromium',headless=True)
|
|
for width in (1440,390):
|
|
context=browser.new_context(viewport={'width':width,'height':1000},reduced_motion='reduce',**({'storage_state':args.storage_state} if args.storage_state else {}));page=context.new_page();page.goto(args.base_url+'/overlays',wait_until='networkidle')
|
|
for kind in ('confirm','prompt','drawer'):
|
|
trigger='#open-'+kind;page.locator(trigger).click();dialog=page.locator('[role=dialog]');dialog.wait_for();page.wait_for_function('document.body.classList.contains("modal-open")')
|
|
record={'width':width,'kind':kind,'title':dialog.get_attribute('aria-labelledby'),'checks':{}}
|
|
record['checks']['name']=bool(record['title']) and bool(page.locator('#'+record['title']).inner_text())
|
|
if kind=='prompt':record['checks']['promptLabel']=dialog.locator('label').get_attribute('for')==dialog.locator('input').get_attribute('id')
|
|
# Hidden/disabled descendants do not become keyboard boundaries.
|
|
dialog.evaluate('e=>{let b=document.createElement("button");b.hidden=true;b.textContent="Hidden sample";e.append(b);let d=document.createElement("button");d.disabled=true;d.textContent="Disabled sample";e.append(d)}')
|
|
selectors='a[href],button,input,select,textarea,[tabindex]'
|
|
items=dialog.locator(selectors).all();items=[i for i in items if i.is_visible() and i.is_enabled() and int(i.get_attribute('tabindex') or '0')>=0]
|
|
items[-1].focus();page.keyboard.press('Tab');record['checks']['lastWraps']=items[0].evaluate('e=>e===document.activeElement')
|
|
items[0].focus();page.keyboard.press('Shift+Tab');record['checks']['firstWraps']=items[-1].evaluate('e=>e===document.activeElement')
|
|
record['checks']['backgroundInert']=page.locator(trigger).evaluate('e=>!!e.closest("[inert]")')
|
|
page.screenshot(path=str(out/f'{kind}-{width}.png'),full_page=True)
|
|
page.keyboard.press('Escape');dialog.wait_for(state='detached');record['checks']['restored']=page.locator(trigger).evaluate('e=>e===document.activeElement');record['checks']['unlocked']=page.evaluate('!document.body.classList.contains("modal-open") && !document.querySelector("[inert]")')
|
|
# Repeat opening and disconnected-trigger fallback; no server write involved.
|
|
page.locator(trigger).click();dialog.wait_for();page.wait_for_function('document.body.classList.contains("modal-open")');page.locator(trigger).evaluate('e=>e.remove()');page.keyboard.press('Escape');dialog.wait_for(state='detached');record['checks']['disconnectedFallback']=page.locator('h1').evaluate('e=>e===document.activeElement')
|
|
results.append(record);page.reload(wait_until='networkidle')
|
|
# Real host lifecycle with temporarily nonfocusable descendants.
|
|
page.locator('#open-confirm').click();dialog=page.locator('[role=dialog]');dialog.wait_for();page.wait_for_function('document.body.classList.contains("modal-open")');dialog.locator('button').evaluate_all('xs=>xs.forEach(e=>e.disabled=true)');dialog.focus();page.keyboard.press('Tab');results.append({'width':width,'kind':'no-focusable','checks':{'contained':dialog.evaluate('e=>e===document.activeElement')}});page.keyboard.press('Escape');dialog.wait_for(state='detached');context.close()
|
|
browser.close()
|
|
(out/'overlays.json').write_text(json.dumps({'results':results,'passed':all(all(r['checks'].values()) for r in results)},indent=2)+'\n');print('Passed',sum(all(r['checks'].values()) for r in results),'/',len(results))
|