"""Chromium fixture evidence QA. ASGI GET with simulated EventSource, no native tasks. Default CSS is the explicitly identified Bootstrap 5.3.6 fixture substitute. No fixture stylesheet is copied into the release. HTMX is deliberately unavailable unless the operator supplies the pinned asset; this is not proxy/HTTPS acceptance. """ from pathlib import Path import argparse,json,os,socket,sys,tempfile,time,re sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'src')) from dataclasses import replace 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 aim_webgui.config import Settings from aim_webgui.journal import Journal,project from aim_webgui.reports import persist, TITLES from evidence_fixtures import declared,sample,plan,result,job,ev def main(): p=argparse.ArgumentParser();p.add_argument('--out',type=Path,required=True);p.add_argument('--bootstrap',type=Path,default=Path('/opt/imagemagick/share/doc/ImageMagick-7/www/assets/bootstrap.min.css'));args=p.parse_args();args.out.mkdir(parents=True,exist_ok=True) with tempfile.TemporaryDirectory(prefix='aim-evidence-browser-')as td: state=Path(td)/'state' sock=socket.socket();sock.bind(('127.0.0.1',0));port=sock.getsockname()[1];sock.close() url=f'http://127.0.0.1:{port}' s=Settings(state_dir=state,public_url=url,core_transport='stdio');a=Auth(s);a.bootstrap() with a.store.transaction()as db: db.execute('UPDATE users SET must_change_password=0');uid=db.execute('SELECT id FROM users').fetchone()[0] paths={};jobs={} for name in TITLES: d=declared(name);data=sample(d['data_schema']) if name=='host_capabilities_v1':data['is_unifi_controller']=True if name=='filesystem_usage_v1': data={'platform':'linux','filesystems':[{'name':'/dev/synthetic1','mount':'/','filesystem_type':'ext4','used_bytes':42949672960,'total_bytes':128849018880,'available_bytes':85899345920,'used_percent':33.3,'status':'available'},{'name':'/dev/synthetic2','mount':'/srv/archive','filesystem_type':None,'used_bytes':None,'total_bytes':None,'available_bytes':None,'used_percent':None,'status':'unavailable'}]} ident=job(a,uid,plan(d),status='successful');r=result(d,report_data=data) with a.store.transaction()as db: small=persist(db,s,ident,plan(d),r) db.execute('UPDATE jobs SET core_result=?,finished_at=? WHERE id=?',(json.dumps(small),time.time(),ident)) paths[name]='/jobs/'+ident+'/reports';jobs[name]=ident running=job(a,uid,plan(declared()),status='running');j=Journal(s);j.begin(running) def add(start,end):j.append(running,[(*project(ev(i),{'test01.example'}),time.time())for i in range(start,end+1)]) add(1,300) paths['progress']='/jobs/'+running client=TestClient(create_app(s),base_url=url) token,_=a.new_session(uid);client.cookies.set(s.cookie_name,token);records=[] static=Path(__file__).resolve().parents[1]/'src/aim_webgui/static' def html(path): response=client.get(path);assert response.status_code==200,(path,response.text[:400]) text=response.text def css(m): name=m.group(1).split('?')[0] source=args.bootstrap if 'bootstrap.min.css' in name else static/name.removeprefix('/static/') return '' def js(m): name=m.group(1).split('?')[0] if 'htmx.min.js'in name:return '' source=static/name.removeprefix('/static/') return '' text=re.sub(r']+href="(/static/[^"]+\.css(?:\?[^"]*)?)"[^>]*>',css,text) text=re.sub(r']+src="(/static/[^"]+\.js(?:\?[^"]*)?)"[^>]*>',js,text) return text def bridge_read(source,path): response=client.get(path) return {'status':response.status_code,'text':response.text} fixture_js=""" window.__sources=[]; window.fetch=async function(url,opts){const r=await window.fixtureRead(String(url));return {ok:r.status>=200&&r.status<300,status:r.status,json:async()=>JSON.parse(r.text),text:async()=>r.text};}; window.EventSource=class{constructor(url){this.url=url;this.listeners={};window.__sources.push(this);setTimeout(()=>this.emit('open',{}),0);}addEventListener(k,f){this.listeners[k]=f;}close(){this.closed=true;}emit(k,d){if(this.listeners[k]&&!this.closed)this.listeners[k]({data:JSON.stringify(d)});}}; window.__emit=function(kind,data){window.__sources.forEach(s=>s.emit(kind,data));}; """ def mount(page,path): content=html(path) page.set_content(content.replace('',''),wait_until='domcontentloaded') def emit(page,start): data=j.snapshot(uid,running,after=start) page.evaluate('(d)=>window.__emit("snapshot",d)',data) for row in data['events']:page.evaluate('(r)=>window.__emit("line",{record:r,text:r.text})',row) try: with sync_playwright()as p: browser=p.chromium.launch(executable_path=os.environ.get('AIM_BROWSER_EXECUTABLE','/usr/bin/chromium'),args=['--no-sandbox']) for w,h in ((320,750),(390,844),(760,900),(1440,1000),(740,390)): for theme in ('light','dark'): ctx=browser.new_context(viewport={'width':w,'height':h},reduced_motion='reduce') ctx.add_cookies([{'name':s.cookie_name,'value':token,'url':url}]) ctx.add_init_script("localStorage.setItem('aim-web-display-theme',"+json.dumps(theme)+");") page=ctx.new_page();page.expose_binding("fixtureRead",bridge_read);errors=[];page.on('pageerror',lambda e:errors.append(str(e))) for name,path in paths.items(): print('Browser',name,w,h,theme,flush=True) mount(page,path);page.evaluate("(t)=>document.querySelectorAll('[data-theme-option]').forEach(b=>{if(b.dataset.themeOption===t)b.click()})",theme);page.wait_for_timeout(120) assert page.locator('h1').count()==1,(name,page.content()[:300]) assert page.evaluate('document.documentElement.scrollWidth<=innerWidth+1'),(name,w,h,'horizontal overflow') if name=='progress': page.wait_for_function("document.querySelector('[data-journal-output]').textContent.includes('Synthetic task 300')") box=page.locator('[data-journal-output]');height=box.evaluate('(e)=>e.clientHeight') assert heighte.scrollHeight>e.clientHeight') else: box=page.locator('[data-report-json]');box.locator('summary').click();page.wait_for_function("document.querySelector('[data-json-status]').textContent.includes('Retained')") assert page.locator('[data-json-output]').inner_text() not in ('','null'),name if name=='checkmk_user_config_v1':assert 'full configuration sections were not retained' in page.locator('body').inner_text() records.append({'page':name,'width':w,'height':h,'theme':theme,'passed':True}) if (w==1440 and theme=='dark' and name in ('host_capabilities_v1','filesystem_usage_v1')) or(w==390 and theme=='dark' and name in ('host_capabilities_v1','progress')): page.evaluate('window.scrollTo(0,0)');page.wait_for_timeout(80);page.screenshot(path=str(args.out/f'{name}-{w}-{theme}.png'),full_page=True) assert not errors,errors ctx.close() # Two independent browser sessions, HTTP snapshots and real EventSource. ctx1=browser.new_context();ctx2=browser.new_context() for ctx in (ctx1,ctx2):ctx.add_cookies([{'name':s.cookie_name,'value':token,'url':url}]) one=ctx1.new_page();two=ctx2.new_page() for page in(one,two):page.expose_binding('fixtureRead',bridge_read) for page in (one,two):mount(page,paths['progress']) for page in (one,two):page.wait_for_function("document.querySelector('[data-journal-output]').textContent.includes('Synthetic task 300')") add(301,310) for page in(one,two):emit(page,300) for page in(one,two):page.wait_for_function("document.querySelector('[data-journal-output]').textContent.includes('Synthetic task 310')") mount(one,paths['progress']);one.wait_for_function("document.querySelector('[data-journal-output]').textContent.includes('Synthetic task 310')") add(311,320) for page in(one,two):emit(page,310) for page in(one,two):page.wait_for_function("document.querySelector('[data-journal-output]').textContent.includes('Synthetic task 320')") assert one.locator('[data-journal-output]').inner_text().count('Synthetic task 320')==1 # Manual upward scrolling pauses following without moving the page. one.locator('[data-journal-output]').evaluate('(e)=>{e.scrollTop=0;e.dispatchEvent(new Event("scroll"));}') assert not one.locator('[data-journal-follow]').is_checked() position=one.evaluate('scrollY');add(321,321);emit(one,320);emit(two,320);one.wait_for_timeout(700) assert one.evaluate('scrollY')==position with a.store.transaction()as db:db.execute("UPDATE jobs SET status='successful',finished_at=? WHERE id=?",(time.time(),running)) for page in(one,two):page.evaluate('()=>window.__emit("end",{cursor:321,job_status:"successful"})') for page in(one,two):page.wait_for_function("document.querySelector('[data-journal-state]').textContent.includes('Ended')") mount(one,paths['progress']);one.wait_for_function("document.querySelector('[data-journal-output]').textContent.includes('Synthetic task 321')") records.append({'scenario':'two fixture views remount replay final reread internal follow','passed':True}) ctx1.close();ctx2.close();browser.close() finally:client.close() (args.out/'results.json').write_text(json.dumps({'fixture':'ASGI-backed reads, synthetic EventSource/remount; Bootstrap5.3.6 substitute; no HTMX/HTTPS/native operations','results':records},indent=2)) print(len(records),'browser cases passed') if __name__=='__main__':main()