Files
ScadaBridge/infra/tools/inspect_operations.py
T

55 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Bounded synthetic actual-component SB10–12 check; no live/fleet operations."""
import asyncio,json,sys
from pathlib import Path
from playwright.sync_api import sync_playwright
sys.path.insert(0,'/Users/dohertj2/.codex/worktrees/scada-ui-consistency/scadaproj/components/ui-theme/validation/theme-1.0.0/canary')
from browser_zoom import zoom_browser,set_browser_zoom
OUT=Path('docs/ui-consistency/evidence/sb10-12/browser-final');OUT.mkdir(parents=True,exist_ok=True)
def main():
results=[]
with sync_playwright() as p:
with zoom_browser(p) as (context,worker):
for kind in ['topology','health']:
for scheme in ['light','dark']:
for w,h,zoom in [(1440,900,1),(768,480,1),(390,844,1),(1440,900,2)]:
page=context.new_page();page.set_viewport_size({'width':w,'height':h})
errors=[];page.on('pageerror',lambda e:errors.append(str(e)))
page.add_init_script("localStorage.setItem('sb-theme', '"+scheme+"')")
route='topology-canary?role=Deployer' if kind=='topology' else 'operations-canary?role=Viewer'
page.goto('http://127.0.0.1:5201/'+route,wait_until='networkidle');page.evaluate('s => window.sbTheme.set(s)',scheme)
page.locator('.tv-root' if kind=='topology' else '[data-site-id]').first.wait_for()
page.wait_for_timeout(500)
set_browser_zoom(worker,page,zoom)
data=page.evaluate('''() => ({width:innerWidth,dpr:devicePixelRatio,document:document.documentElement.scrollWidth,scheme:document.documentElement.dataset.bsTheme,headings:[...document.querySelectorAll('h1')].map(e=>e.textContent),sites:[...document.querySelectorAll('[data-site-id]')].map(e=>e.dataset.siteId),summary:document.querySelector('[aria-label="Site observations"]')?.innerText,tree:document.querySelector('.topology-tree')&&{height:document.querySelector('.topology-tree').clientHeight,scroll:document.querySelector('.topology-tree').scrollHeight,overflow:getComputedStyle(document.querySelector('.topology-tree')).overflowY},rows:[...document.querySelectorAll('.tv-row')].slice(0,5).map(e=>e.getBoundingClientRect().height)})''')
if kind=='topology':
item=page.locator('[role=treeitem][tabindex="0"]').first;item.focus();page.keyboard.press('End');page.wait_for_function("document.activeElement === [...document.querySelectorAll('[role=treeitem]')].at(-1)")
data['endFocus']=page.evaluate('''() => ({role:document.activeElement.getAttribute('role'),bottom:document.activeElement.querySelector('.tv-row')?.getBoundingClientRect().bottom,viewport:innerHeight})''')
page.keyboard.press('Home');page.wait_for_function("document.activeElement === document.querySelector('[role=treeitem]')")
page.get_by_role('button',name='Load instances',exact=True).first.click()
page.get_by_text('Page 1',exact=True).first.wait_for();data['lazyLoad']=True
page.get_by_role('textbox',name='Search loaded sites, areas and instances').fill('facility 01');data['loadedSearchQualifier']=page.get_by_role('textbox',name='Search loaded sites, areas and instances').get_attribute('placeholder')
else: data['orderPass']=data['sites']==['offline','warning','unknown','healthy']
data.update(kind=kind,zoom=zoom,requested=[w,h],errors=errors)
data['passed']=data['document']<=data['width'] and data['scheme']==scheme and not errors and len(data['headings'])==1 and (data.get('orderPass',True)) and (kind!='topology' or data['endFocus']['bottom']<=data['endFocus']['viewport'])
if zoom==2: page.screenshot(path=str(OUT/f'{kind}-{scheme}-{w}-{h}-zoom{zoom}.png'),full_page=False)
else: page.screenshot(path=str(OUT/f'{kind}-{scheme}-{w}-{h}-zoom{zoom}.png'),full_page=False)
results.append(data);(OUT/'progress.json').write_text(json.dumps(results,indent=2));page.close()
for role,templates,audit in [('Viewer',False,True),('Designer',True,False),('Deployer',False,False),('Administrator,Designer,Deployer',True,True)]:
page=context.new_page();page.goto('http://127.0.0.1:5201/batch-two?Sample=dashboard&role='+role)
page.get_by_role('region',name='Sites configured',exact=True).wait_for();page.wait_for_timeout(300)
data={target:page.locator('main a[href="'+target+'"]').count() for target in ['/design/templates','/audit/configuration']}
# Rail uses the same real policies; hidden native navigation remains in DOM for comparison.
nav={target:page.locator('nav a[href="'+target+'"]').count() for target in data}
results.append(dict(kind='policy',role=role,shortcuts=data,rail=nav,passed=data['/design/templates']==int(templates) and data['/audit/configuration']==int(audit) and data==nav));(OUT/'progress.json').write_text(json.dumps(results,indent=2))
page.close()
touch_browser=p.chromium.launch(channel='chromium',args=['--touch-events=enabled'])
for w,h in [(390,844),(768,480)]:
touch=touch_browser.new_context(viewport={'width':w,'height':h},is_mobile=True,has_touch=True)
page=touch.new_page();page.goto('http://127.0.0.1:5201/topology-canary?role=Deployer');page.locator('.tv-root').wait_for();page.wait_for_function("matchMedia('(pointer:coarse)').matches")
record=page.evaluate("""() => ({width:innerWidth,document:document.documentElement.scrollWidth,coarse:matchMedia('(pointer:coarse)').matches,rows:[...document.querySelectorAll('.tv-row')].slice(0,8).map(e=>e.getBoundingClientRect().height),toggles:[...document.querySelectorAll('.tv-toggle')].slice(0,8).map(e=>({width:e.getBoundingClientRect().width,height:e.getBoundingClientRect().height})),overflow:getComputedStyle(document.querySelector('.topology-tree')).overflowY})""")
record.update(kind='touch',requested=[w,h]);record['passed']=record['coarse'] and min(record['rows'])>=44 and all(t['width']>=44 and t['height']>=44 for t in record['toggles']) and record['document']<=record['width'] and record['overflow']=='visible'
results.append(record);page.screenshot(path=str(OUT/f'topology-touch-{w}-{h}.png'));touch.close()
touch_browser.close()
(OUT/'results.json').write_text(json.dumps(results,indent=2));print(json.dumps({'cases':len(results),'passed':sum(x['passed'] for x in results),'failures':[r for r in results if not r['passed']]},indent=2))
main()