"""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()