Name dialogs and contain native overlay keyboard focus
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
"""Focused native lifecycle regression against the actual local overlay script."""
|
||||
import argparse,json
|
||||
from pathlib import Path
|
||||
from playwright.sync_api import sync_playwright
|
||||
args=argparse.ArgumentParser();args.add_argument('--output',required=True);opts=args.parse_args()
|
||||
script=Path('src/ZB.MOM.WW.ScadaBridge.CentralUI/wwwroot/js/dialog.js').read_text();results=[]
|
||||
with sync_playwright() as p:
|
||||
browser=p.chromium.launch(channel='chromium')
|
||||
for state in ['disabled','hidden','unfocusable','disconnected']:
|
||||
page=browser.new_page();page.set_content('<main><h1>Stable heading</h1><button id="trigger">Open</button><button id="next">Next</button></main><section id="dialog" tabindex="-1"><button>Close</button></section>');page.add_script_tag(content=script);
|
||||
if state=='unfocusable':page.evaluate('()=>{const span=document.createElement("span");span.id="trigger";span.tabIndex=0;span.textContent="Open";document.querySelector("#trigger").replaceWith(span)}')
|
||||
page.focus('#trigger');page.evaluate('sbDialog.activate("dialog")');page.evaluate('''s=>{const t=document.querySelector('#trigger');if(s==='disabled')t.disabled=true;if(s==='hidden')t.hidden=true;if(s==='unfocusable')t.removeAttribute('tabindex');if(s==='disconnected')t.remove();document.querySelector('#dialog').remove();}''',state);page.wait_for_timeout(30)
|
||||
result=page.evaluate('({heading:document.activeElement===document.querySelector("h1"),locked:document.body.classList.contains("modal-open"),inert:!!document.querySelector("[inert]")})');page.focus('#next');result['cleaned']=page.get_attribute('h1','tabindex') is None;results.append({'state':state,**result});page.close()
|
||||
browser.close()
|
||||
Path(opts.output).write_text(json.dumps(results,indent=2)+'\n');print(results)
|
||||
raise SystemExit(0 if all(r['heading'] and not r['locked'] and not r['inert'] and r['cleaned'] for r in results) else 1)
|
||||
@@ -0,0 +1,33 @@
|
||||
"""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))
|
||||
Reference in New Issue
Block a user