"""Optional Chromium fixture QA, not deployment/remote/production-HTMX acceptance. Uses the real supplied Core for discovery against a temporary synthetic inventory. The external assets are embedded only in about:blank test documents. With no pinned assets available, --bootstrap (or the local default) is a declared substitute. No substitutions are written into production static/vendor. """ from pathlib import Path import argparse import json import re import sys import tempfile import time sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'src')) from fastapi.testclient import TestClient from playwright.sync_api import sync_playwright from aim_webgui.app import create_app from aim_webgui.auth.service import Auth from conftest import core_root as core_fixture, settings as settings_fixture from test_read_experience import add_job ROOT=Path(__file__).resolve().parents[1] def main(): parser=argparse.ArgumentParser() parser.add_argument('--out',type=Path,required=True) parser.add_argument('--bootstrap',type=Path,default=Path('/opt/imagemagick/share/doc/ImageMagick-7/www/assets/bootstrap.min.css')) args=parser.parse_args();args.out.mkdir(parents=True,exist_ok=True) results=[];screenshots=[] with tempfile.TemporaryDirectory(prefix='aim-browser-readonly-') as tmp: tmp=Path(tmp);core=core_fixture.__wrapped__(tmp);settings=settings_fixture.__wrapped__(tmp,core) (core/'inventories/example/hosts.yml').write_text('''all: children: example: hosts: edge-01.example: ansible_host: 192.0.2.1 children: linux: hosts: api-01.example: ansible_host: 192.0.2.10 api-02.example: ansible_host: 192.0.2.11 children: database: hosts: db-01.example: ansible_host: 192.0.2.12 db-02.example: ansible_host: 192.0.2.13 windows: hosts: desktop-01.example: ansible_host: 192.0.2.20 children: servers: hosts: mail-01.example: ansible_host: 192.0.2.21 app-01.example: ansible_host: 192.0.2.22 staging: hosts: api-02.example: {} ''') print('Fixture core ready',flush=True) auth=Auth(settings);auth.bootstrap() print('Fixture auth ready',flush=True) with auth.store.transaction() as db: db.execute("UPDATE users SET must_change_password=0 WHERE username='admin'") uid=db.execute("SELECT id FROM users WHERE username='admin'").fetchone()[0] now=int(time.time()) hosts=['api-01.example','api-02.example','db-01.example','db-02.example','desktop-01.example','mail-01.example','app-01.example'] for i in range(18): facts={h:('unreachable' if i%4==0 and h=='db-02.example' else 'successful') for h in hosts} add_job(auth,owner='admin',book=['checkmk_install_agent','debug_test_connection','maintenance_patch_os'][i%3], hosts=facts,status='failed' if i%4==0 else 'successful',when=now-3600*(i+1)) active=add_job(auth,owner='admin',status='running',hosts={'api-01.example':'successful'}) token,session=auth.new_session(uid) paths={'overview':'/','new-run':'/plan?customer=example&playbook=debug_test_connection', 'explorer':'/inventory/example/explore','map':'/inventory/example/explore?view=map', 'branch':'/inventory/example/explore?view=map&branch=%5B%22linux%22%5D', 'activity':'/inventory/example/activity?host=api-01.example', 'insights':'/insights?customer=example','audit':'/audit'} pages={} with TestClient(create_app(settings),base_url=settings.public_url) as client: client.cookies.set(settings.cookie_name,token) for name,path in paths.items(): print('Render',name,path,flush=True) response=client.get(path);assert response.status_code==200,(name,response.text) pages[name]=response.text # Every SVG host link really points to an authenticated host page, not an action. import html links=re.findall(r''+file.read_text()+'' content=re.sub(r']*>',css,content) content=re.sub(r'','',content) fixture='''window.fetch=async()=>({ok:true,json:async()=>({saved:true})}); window.__sources=[]; window.EventSource=class { static CLOSED=2; constructor(url){this.url=url;this.readyState=1;this.handlers={};window.__sources.push(this);} addEventListener(k,fn){this.handlers[k]=fn;} close(){this.readyState=2;} emit(k,text){if(this.handlers[k])this.handlers[k]({data:JSON.stringify({text})});} };''' script='\n'.join((ROOT/'src/aim_webgui/static/js'/x).read_text() for x in ('theme.js','aim.js','experience.js','credentials.js')) return content.replace('','') with sync_playwright() as pw: browser=pw.chromium.launch(executable_path='/usr/bin/chromium',headless=True,args=['--no-sandbox','--disable-dev-shm-usage']) try: for width,height in ((320,750),(390,844),(760,900),(1440,1000),(740,390)): for theme in ('light','dark'): context=browser.new_context(viewport={'width':width,'height':height},locale='de-DE',timezone_id='Europe/Berlin',reduced_motion='reduce') errors=[] for name,content in pages.items(): print('QA',width,height,theme,name,flush=True) page=context.new_page();page.on('pageerror',lambda e:errors.append(str(e))) page.set_content(inline(content),wait_until='load') page.locator('[data-theme-option='+theme+']:visible').click() page.wait_for_timeout(100) assert page.evaluate('document.documentElement.scrollWidth<=innerWidth'),(name,width,height,'overflow') if width<=760: assert not page.locator('.sidebar').is_visible() toggle=page.locator('[data-mobile-menu] summary');toggle.focus();page.keyboard.press('Enter') assert page.locator('[data-mobile-menu]').get_attribute('open') is not None assert page.locator('#mobile-navigation').get_by_role('link',name='Playbook insights',exact=True).is_visible() page.keyboard.press('Escape') assert page.locator('[data-mobile-menu]').get_attribute('open') is None assert toggle.evaluate('(e)=>e===document.activeElement') a=page.locator('.mobile-brand').bounding_box();b=page.locator('.mobile-actions .theme-control').bounding_box();c=toggle.bounding_box() assert abs((a['y']+a['height']/2)-(c['y']+c['height']/2))<2 assert abs((b['y']+b['height']/2)-(c['y']+c['height']/2))<2 assert b['x']+b['width']<=c['x'] and c['x']+c['width']<=width assert page.locator('.mobile-topbar').bounding_box()['height']<80 assert page.locator('[data-nav-forward]').count()==0 else: assert page.locator('.sidebar').is_visible() and not page.locator('.mobile-topbar').is_visible() if name=='explorer': assert page.locator('.explorer-map').is_visible()==(width>760) if name=='map': graph=page.locator('[data-inventory-graph]');old=float(graph.get_attribute('width')) page.locator('[data-graph-zoom="in"]').click() assert float(graph.get_attribute('width'))>old page.locator('[data-graph-zoom="reset"]').click() assert page.locator('svg .graph-host').count()>0 assert page.locator('.inventory-canvas').evaluate('(e)=>e.scrollHeight>=e.clientHeight') if name=='insights': assert page.locator('.matrix-mobile').is_visible()==(width<=760) if name=='new-run': page.locator('[data-select-all-hosts]').check() assert page.locator('[data-host-target]:checked').count()==7 if name=='job': output=page.locator('[data-console-output]');output.scroll_into_view_if_needed();before=page.evaluate('scrollY') page.evaluate("()=>{for(let i=0;i<240;i++)window.__sources[0].emit('line','TASK [Synthetic fixture '+i+']');}") page.wait_for_timeout(100) assert output.evaluate('(e)=>e.scrollTop>0 && e.scrollHeight>e.clientHeight') assert abs(page.evaluate('scrollY')-before)<3 output.evaluate('(e)=>e.scrollTop=0');page.wait_for_timeout(100) assert not page.locator('[data-console-autoscroll]').is_checked() page.locator('[data-console-autoscroll]').check();page.wait_for_timeout(100) assert output.evaluate('(e)=>e.scrollTop>0') if name in ('map','activity','insights','explorer') and width in (390,1440) and theme=='dark': page.evaluate('scrollTo(0,0)');file=f'{name}-{width}-{theme}.png';page.screenshot(path=str(args.out/file),full_page=True);screenshots.append(file) if name=='activity': shot=f'activity-{width}-viewport.png';page.screenshot(path=str(args.out/shot),full_page=False);screenshots.append(shot) if name=='overview' and width==390 and theme=='dark': page.locator('[data-mobile-menu] summary').click();page.screenshot(path=str(args.out/'mobile-menu.png'),full_page=False);screenshots.append('mobile-menu.png') # Formatter includes an HTMX-replaced