import asyncio, pathlib, json from playwright.async_api import async_playwright ROOT=pathlib.Path(__file__).resolve().parent OUT=pathlib.Path('/mnt/data') async def main(): async with async_playwright() as p: browser=await p.chromium.launch(headless=True, executable_path='/usr/bin/chromium', args=['--no-sandbox','--disable-dev-shm-usage']) ctx=await browser.new_context(viewport={'width':1512,'height':980}, accept_downloads=True) page=await ctx.new_page();errors=[];page.on('pageerror', lambda e:errors.append(str(e))) import base64,hashlib await page.set_content((ROOT/'index.html').read_text(),wait_until='domcontentloaded') await page.add_style_tag(content=(ROOT/'styles.css').read_text()) await page.expose_function('__shahex',lambda b64:hashlib.sha256(base64.b64decode(b64)).hexdigest()) await page.evaluate('''() => { Object.defineProperty(window.crypto,'subtle',{configurable:true,value:{digest:async (_alg,buffer)=>{ let arr=new Uint8Array(buffer), bin=''; for(let i=0;iparseInt(x,16)).buffer; }}}); }''') assets={f.name:base64.b64encode(f.read_bytes()).decode() for f in ROOT.iterdir() if f.is_file() and f.suffix in ('.json','.js','.css','.html','.md')} await page.evaluate('''(assets)=>{ window.__ASSETS=assets; window.fetch=async uri=>{ const id=String(uri).split('/').pop().split('?')[0],base=window.__ASSETS[id]; if(!base)return {ok:false,status:404}; const bin=atob(base),arr=Uint8Array.from(bin,c=>c.charCodeAt(0)); return {ok:true,status:200,arrayBuffer:async()=>arr.buffer,json:async()=>JSON.parse(new TextDecoder().decode(arr))}; }; }''',assets) await page.add_script_tag(content=(ROOT/'evalflow_engine.js').read_text()) await page.add_script_tag(content=(ROOT/'reprochain_engine.js').read_text()) await page.add_script_tag(content=(ROOT/'app.js').read_text()) await page.wait_for_function("document.querySelector('#statusBadge').textContent.includes('PASS')",timeout=45000) checks=[] def ck(name,condition): if not condition: raise AssertionError(name+' errors='+repr(errors)) checks.append(name);print('PASS',name) ck('hero ReproChain AI', 'ReproChain' in await page.locator('h1').inner_text()) ck('22 hashed artifact nodes',await page.locator('#kArtifact').inner_text()=='22') ck('4416 prediction records',await page.locator('#kRecords').inner_text()=='4,416') ck('manifest root verified',await page.locator('#kRoot').inner_text()=='VALID') ck('default research gate PASS',await page.locator('#statusBadge').inner_text()=='RESEARCH GATES PASS') ck('candidate 77.8% Accuracy', '77.8%' in await page.locator('#content').inner_text()) ck('nine navigation modules',await page.locator('.nav-btn').count()==9) await page.locator('[data-view=lineage]').click() ck('lineage artifact nodes shown',await page.locator('[data-node]').count()==22) await page.locator('[data-node=predictions]').click() ck('predictions upstream text', 'train' in (await page.locator('#content').inner_text()).lower()) ck('sha prefix shown', 'SHA-256' in await page.locator('#content').inner_text()) async with page.expect_download() as d: await page.locator('[data-export=graph]').click() download=await d.value; ck('graphml export',download.suggested_filename.endswith('.graphml')) await page.locator('[data-view=vault]').click() ck('vault 22 verified', '22' in await page.locator('#content').inner_text()) await page.locator('#tamper').click() ck('simulated tamper BLOCK',await page.locator('#statusBadge').inner_text()=='RESEARCH GATES BLOCK') ck('synthetic tamper warning', 'SIMULATION ONLY' in await page.locator('#content').inner_text()) await page.locator('#tamper').click() ck('recover gate after simulation',await page.locator('#statusBadge').inner_text()=='RESEARCH GATES PASS') await page.locator('#rescan').click() await page.wait_for_function("document.querySelector('#statusLine').textContent.startsWith('22/22')") ck('rescan matches 22 hashes',(await page.locator('#statusLine').inner_text()).startswith('22/22')) await page.locator('#baseline').select_option('hybrid') await page.locator('#candidate').select_option('nb') await page.locator('#replay').click() await page.wait_for_function("document.querySelector('#statusBadge').textContent.includes('BLOCK')") ck('reversed classifier comparison BLOCK',await page.locator('#statusBadge').inner_text()=='RESEARCH GATES BLOCK') await page.locator('#baseline').select_option('nb') await page.locator('#candidate').select_option('hybrid') await page.locator('#replay').click() await page.wait_for_function("document.querySelector('#statusBadge').textContent.includes('PASS')") ck('restore candidate PASS',await page.locator('#statusBadge').inner_text()=='RESEARCH GATES PASS') await page.locator('[data-view=governance]').click() ck('editable policy eight entries',await page.locator('[id^=p_]').count()==8) await page.locator('#p_minAccuracy').fill('0.95') await page.locator('#apply').click() await page.wait_for_function("document.querySelector('#statusBadge').textContent.includes('BLOCK')") ck('strict policy BLOCK',await page.locator('#statusBadge').inner_text()=='RESEARCH GATES BLOCK') await page.locator('#defaults').click() await page.wait_for_function("document.querySelector('#statusBadge').textContent.includes('PASS')") ck('policy reset restores PASS',await page.locator('#statusBadge').inner_text()=='RESEARCH GATES PASS') await page.locator('[data-view=replay]').click() ck('paired bootstrap interval displayed','bootstrap' in (await page.locator('#content').inner_text()).lower()) await page.locator('[data-view=runs]').click() ck('run registry contains >= 4 runs',await page.locator('table tbody tr').count()>=4) await page.locator('#regA').select_option('1') await page.locator('#regB').select_option('3') ck('run selector compares snapshots', 'Difference (B' in await page.locator('#content').inner_text()) await page.locator('[data-view=audit]').click() await page.locator('#auditVerify').click() await page.wait_for_function("document.querySelector('#auditMessage').textContent.startsWith('PASS:') || document.querySelector('#auditMessage').textContent.startsWith('FAILED:')") ck('audit chain verify successful','PASS:' in await page.locator('#auditMessage').inner_text()) await page.locator('[data-view=export]').click() for kind,ext in [('report','.md'),('run','.json'),('inventory','.csv'),('runs','.csv'),('graph','.graphml'),('bundle','.json'),('manifest','.json')]: async with page.expect_download() as d:await page.locator(f'[data-export={kind}]').click() item=await d.value;ck('export '+kind,item.suggested_filename.endswith(ext)) snap={'schema':'reprochain.run.v1','baseline':'nb','candidate':'hybrid','split':'challenge','accuracy':.7777,'bundleRoot':'f'*64} await page.locator('#importRun').set_input_files({'name':'test-run.json','mimeType':'application/json','buffer':json.dumps(snap).encode()}) await page.wait_for_function("document.querySelector('#importMsg').textContent.includes('UNVERIFIED')") ck('import unverified run warning', 'UNVERIFIED' in await page.locator('#importMsg').inner_text()) await page.locator('[data-view=runs]').click() ck('import run shown unverified', 'UNVERIFIED' in await page.locator('#content').inner_text()) await page.locator('[data-view=methods]').click() ck('limitations visible','not a trusted signature' in (await page.locator('#content').inner_text()).lower()) await page.locator('[data-view=overview]').click() await page.evaluate('window.scrollTo(0,0)') await page.screenshot(path=str(OUT/'ReproChain_AI_DESKTOP_PREVIEW.png'),full_page=True) ck('desktop no sideways scroll',await page.evaluate('document.documentElement.scrollWidth<=window.innerWidth+3')) await page.set_viewport_size({'width':390,'height':844});await page.evaluate('window.scrollTo(0,0)');await page.screenshot(path=str(OUT/'ReproChain_AI_MOBILE_PREVIEW.png'),full_page=True) ck('mobile no sideways scroll',await page.evaluate('document.documentElement.scrollWidth<=window.innerWidth+3')) ck('mobile navigation accessible',await page.locator('[data-view=export]').count()==1) ck('no browser JavaScript errors',len(errors)==0) print('BROWSER CHECKS PASSED',len(checks));await browser.close() asyncio.run(main())