import {chromium} from 'playwright';import assert from 'node:assert/strict';import fs from 'node:fs/promises';import path from 'node:path';
const url=process.env.APP_URL||'http://127.0.0.1:4318';const out=path.resolve(process.env.QA_OUT||'../../qa');await fs.mkdir(out,{recursive:true});
const browser=await chromium.launch({channel:'chrome',headless:true,args:['--enable-webgl','--ignore-gpu-blocklist']});const page=await browser.newPage({viewport:{width:1440,height:1000},deviceScaleFactor:1});const errors=[];page.on('pageerror',e=>errors.push(e.message));const checks=process.env.QA_RESUME?JSON.parse(await fs.readFile(path.join(out,'partial-checks.json'),'utf8')):[];const pass=(name,detail='')=>{checks.push({name,status:'pass',detail});console.log('PASS',name)};
const snap=()=>page.evaluate(()=>window.engineExhibit.getState());
try{
 await page.goto(url);await page.waitForFunction(()=>!!window.engineExhibit);const slider=page.getByRole('slider');if(!process.env.QA_RESUME){await page.getByRole('button',{name:'Pause',exact:true}).click();const a=(await snap()).angle;await page.waitForTimeout(300);assert.equal((await snap()).angle,a);pass('Pause holds deterministic crank angle');
 await page.getByRole('button',{name:'Step forward 15 degrees'}).click();assert.ok(Math.abs((await snap()).angle-((a+15)%720))<1e-8);await page.getByRole('button',{name:'Step back 15 degrees'}).click();assert.ok(Math.abs((await snap()).angle-a)<1e-8);pass('Forward/backward stepping');
 assert.equal(await slider.count(),1);await slider.focus();await page.keyboard.press('Home');assert.equal((await snap()).angle,0);await page.keyboard.press('End');assert.equal((await snap()).angle,720);await page.keyboard.press('ArrowLeft');assert.equal((await snap()).angle,719);pass('Keyboard scrub reaches 0 and 720 and steps correctly');
 await page.getByRole('button',{name:'Follow cylinder 8',exact:true}).click();assert.equal((await snap()).selected,8);assert.ok((await page.getByRole('heading',{level:2}).textContent()).length>0);pass('Cylinder selection updates linked view');
 for(const name of ['Assembled','Exploded','Section']){await page.getByRole('tab',{name,exact:true}).click();assert.equal((await snap()).view,name.toLowerCase());}pass('All three inspection views');
 await page.getByRole('combobox',{name:'Playback speed'}).click();await page.getByRole('option',{name:'2× fast',exact:true}).click();assert.equal((await snap()).speed,2);await page.getByRole('button',{name:'Play',exact:true}).click();const fast=(await snap()).angle;await page.waitForTimeout(400);await page.getByRole('button',{name:'Pause',exact:true}).click();assert.notEqual((await snap()).angle,fast);pass('Speed selector and playback advance');
 const before=(await snap()).scene.camera;await page.getByRole('button',{name:'Zoom in',exact:true}).click();assert.notDeepEqual((await snap()).scene.camera,before);await page.getByRole('button',{name:'Zoom out',exact:true}).click();await page.getByRole('button',{name:'Reset camera',exact:true}).click();const reset=(await snap()).scene.camera;await page.mouse.move(530,430);await page.mouse.down();await page.mouse.move(650,480,{steps:8});await page.mouse.up();assert.notDeepEqual((await snap()).scene.camera,reset);await page.getByRole('button',{name:'Reset camera',exact:true}).click();assert.deepEqual((await snap()).scene.camera,reset);pass('Orbit, zoom and camera reset');}else{await page.getByRole('button',{name:'Pause',exact:true}).click();}
 await page.getByRole('button',{name:'About this model'}).click();await page.getByRole('dialog').waitFor();assert.ok((await page.getByRole('dialog').innerText()).includes('not pressure'));await page.keyboard.press('Escape');await page.getByRole('dialog').waitFor({state:'hidden'});pass('Accessible assumptions dialog opens/closes');
 await page.getByRole('button',{name:'Take a guided look',exact:false}).click();assert.equal((await snap()).tour,true);await page.waitForTimeout(300);await page.getByRole('button',{name:'Return to manual control'}).click();assert.equal((await snap()).tour,false);pass('Guided sequence start and manual exit');
 const geometry=await page.evaluate(()=>{
 let maxRod=0,maxJournal=0,maxPiston=0,maxValveAxis=0,maxPush=0;const baseline={};
 for(const angle of [0,37,90,180,270,359,360,450,540,630,719,720]){
   const s=window.engineExhibit.setState({angle,playing:false,tour:false,view:'section'});const g=s.scene;
   for(const rod of g.linked){maxRod=Math.max(maxRod,Math.abs(rod.length-2.65));const p=g.pistons.find(p=>p.cylinder===rod.cylinder);maxPiston=Math.max(maxPiston,...rod.small.map((v,i)=>Math.abs(v-p.wrist[i])));const j=g.journals[Math.floor((rod.cylinder-1)/2)];maxJournal=Math.max(maxJournal,Math.hypot(rod.big[0]-j[0],rod.big[1]-j[1]));}
   for(const q of g.valves){const b=q.cylinder%2?1:-1,beta=b*Math.PI/4;const axis=q.valve[0]*Math.cos(beta)-q.valve[1]*Math.sin(beta);const key=q.cylinder+q.type;if(baseline[key]===undefined)baseline[key]=axis;maxValveAxis=Math.max(maxValveAxis,Math.abs(axis-baseline[key]));maxPush=Math.max(maxPush,Math.abs(q.pushLength-3.504));}
 }
 return {maxRod,maxJournal,maxPiston,maxValveAxis,maxPush};});
 for(const [k,v] of Object.entries(geometry))assert.ok(v<1e-7,`${k}: ${v}`);pass('Rendered crank/rod/piston alignment and fixed valve axes at 12 crank angles',geometry);
 await page.evaluate(()=>window.engineExhibit.setState({angle:35,selected:1,view:'section',playing:false}));await page.screenshot({path:path.join(out,'desktop-section.png'),fullPage:true});
 for(const width of [390,360]){
 await page.setViewportSize({width,height:844});assert.ok(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth));await page.getByRole('button',{name:'Follow cylinder 3',exact:true}).click();assert.equal((await snap()).selected,3);await page.getByRole('tab',{name:'Exploded',exact:true}).click();assert.equal((await snap()).view,'exploded');await page.getByRole('tab',{name:'Section',exact:true}).click();await page.getByRole('button',{name:'Follow cylinder 1',exact:true}).click();await slider.scrollIntoViewIfNeeded();await slider.focus();await page.keyboard.press('Home');assert.equal((await snap()).angle,0);await page.getByRole('button',{name:'Play',exact:true}).click();await page.waitForTimeout(150);await page.getByRole('button',{name:'Pause',exact:true}).click();assert.ok((await snap()).angle>0);pass(`Phone ${width}×844: no horizontal overflow; selection, view, scrub, playback`);
 }
 await page.setViewportSize({width:390,height:844});await page.evaluate(()=>window.engineExhibit.setState({angle:35,selected:1,view:'section',playing:false}));await page.evaluate(()=>scrollTo(0,0));await page.screenshot({path:path.join(out,'phone-section.png'),fullPage:true});
 await page.setViewportSize({width:1440,height:1000});await page.evaluate(()=>window.engineExhibit.setState({angle:35,selected:1,view:'section',playing:false}));
 assert.deepEqual(errors,[]);pass('No application JavaScript errors');
 await fs.writeFile(path.join(out,'browser-qa.json'),JSON.stringify({utc:new Date().toISOString(),browser:browser.version(),checks,errors},null,2));
}finally{await browser.close();}
