// Acceptance against the real built frontend, HTTP adapter, and native eight-branch solver. import { chromium } from '../../frontend/node_modules/playwright/index.mjs'; import { stripTypeScriptTypes } from 'node:module'; import fs from 'node:fs/promises'; import path from 'node:path'; import assert from 'node:assert/strict'; const [baseURL='http://127.0.0.1:8036', output='test/project-contract-20260912/browser'] = process.argv.slice(2); await fs.mkdir(output,{recursive:true}); const original=JSON.parse(await fs.readFile('tests/data/test-mql-8-corrected.json','utf8')); const catalog=await (await fetch(`${baseURL}/api/components/catalog`)).json(); const definitions=catalog.libraries.flatMap(l=>l.components).map(d=>({...d,parameters:Object.fromEntries(d.parameters.map(p=>[p.name,p]))})); const index=new Map(definitions.map(d=>[d.type,d])); const browser=await chromium.launch({headless:true}); const page=await browser.newPage({viewport:{width:1600,height:1000},acceptDownloads:true}); page.setDefaultTimeout(30000); const errors=[];page.on('pageerror',e=>errors.push(String(e))); const unitsOnly=process.argv.includes('--units-only'); const summary=unitsOnly ? JSON.parse(await fs.readFile(path.join(output,'partial-summary.json'),'utf8')) : {rows:[],unitCases:[],browser:browser.version(),nodeCount:original.nodes.length}; async function expand(){const button=page.getByRole('button',{name:'展开仿真控制台',exact:true});if(await button.count())await button.click();} async function importProject(project,name){ await page.locator('input[type="file"][accept*=".json"]').setInputFiles({name:`${name}.json`,mimeType:'application/json',buffer:Buffer.from(JSON.stringify(project))}); await page.waitForFunction(name=>[...document.querySelectorAll('input')].some(input=>input.value===name),name); } async function exportJson(filename){ const waiting=page.waitForEvent('download'); await page.getByRole('button',{name:'导出工程 JSON',exact:true}).click(); await(await waiting).saveAs(path.join(output,filename)); return JSON.parse(await fs.readFile(path.join(output,filename),'utf8')); } try{ await page.goto(baseURL); await page.getByRole('button',{name:'运行仿真',exact:true}).waitFor(); if(!unitsOnly){ // Run the exact source function in Chromium, separately from React rendering and XML validation. let source=await fs.readFile('frontend/src/projectCompatibility.ts','utf8'); source=source.replace(/^import unitTable[^\n]+/m,`const unitTable=${await fs.readFile('schemas/parameter-units.json','utf8')};`); const js=stripTypeScriptTypes(source); summary.versionCheck=await page.evaluate(async({js,definitions,original})=>{ const api=await import(`data:text/javascript;base64,${btoa(unescape(encodeURIComponent(js)))}`); const stats=(fn)=>{const samples=[];for(let i=0;i<100;i++)fn();for(let i=0;i<200;i++){const start=performance.now();for(let j=0;j<10;j++)fn();samples.push((performance.now()-start)/10);}samples.sort((a,b)=>a-b);return{medianMs:samples[100],p95Ms:samples[190]};}; const start=performance.now();api.componentIndex(definitions);const coldIndexMs=performance.now()-start; return{coldIndexMs,cases:[157,1000,10000].map(count=>{ const current=Array.from({length:count},(_,i)=>({...original.nodes[i%original.nodes.length],id:`node-${i}`})); const old=current.map(n=>({...n,data:{...n.data,modelVersion:'0.0.1'}})); return{count,current:stats(()=>api.componentVersionNotices(current,definitions)),old:stats(()=>api.componentVersionNotices(old,definitions)),oldWithMessage:stats(()=>api.componentVersionWarning(old,definitions,true))}; })}; },{js,definitions,original}); await fs.writeFile(path.join(output,'version-check.json'),JSON.stringify(summary.versionCheck,null,2)); // Full eight-branch run and result persistence; repeat with old model versions, then exported v2. let baseline=null; const stale=structuredClone(original);stale.nodes.forEach(n=>n.data.modelVersion='0.0.1'); const projects=[['current',original],['old',stale]]; for(let i=0;i<3;i++){ const [name,project]=projects[i]; await page.getByRole('tab',{name:'建模',exact:true}).click(); const importedAt=performance.now();await importProject(project,name);const importMs=performance.now()-importedAt; await expand(); const consolePanel=page.getByRole('complementary',{name:'仿真控制台',exact:true}); if(name==='old')assert((await consolePanel.innerText()).includes('工程包含旧版或版本未知的组件')); const previous=await page.evaluate(()=>sessionStorage.getItem('system-simulation-flow:latest-result')); let xml; const requestPromise=page.waitForRequest(r=>r.url().endsWith('/api/system-xml/simulate-stream')); const responsePromise=page.waitForResponse(r=>r.url().endsWith('/api/system-xml/simulate-stream'),{timeout:180000}); const started=performance.now();await page.getByRole('button',{name:'运行仿真',exact:true}).click(); xml=(await requestPromise).postData(); const response=await responsePromise;await response.finished();assert(response.ok()); const receiveMs=performance.now()-started; await page.waitForFunction(previous=>{const current=sessionStorage.getItem('system-simulation-flow:latest-result');return current&¤t!==previous&&JSON.parse(current).storage==='indexeddb';},previous,{timeout:180000}); const savedMs=performance.now()-started; await page.getByRole('tab',{name:/^结果/}).click(); const resultDownload=page.waitForEvent('download');await page.getByRole('button',{name:'下载结果文件',exact:true}).click(); const resultFile=path.join(output,`${name}.simresult`);await(await resultDownload).saveAs(resultFile); const result=JSON.parse(await fs.readFile(resultFile,'utf8')).snapshot.result; assert(result.success,result.message);assert.equal(result.simulatedUntil,original.simulation.t_stop); if(!baseline)baseline=result; else { let differences=0; assert.equal(Object.keys(result.series).length,Object.keys(baseline.series).length); for(const [key,values] of Object.entries(result.series)) { assert.equal(values.length,baseline.series[key].length,key); for(let j=0;jassert.equal(n.data.modelVersion,index.get(n.data.componentType).modelVersion)); projects.push(['exported-v2',exported]); } } await fs.writeFile(path.join(output,'partial-summary.json'),JSON.stringify(summary,null,2)); } // Lightweight real browser + HTTP checks isolate p0 numbers, strings, expression, and degC. const chamber=definitions.find(d=>d.modelType==='amesim_pnch012'); assert(chamber); for(const value of [2.5,'2.5','=2.5','2+0.5','sqrt(6.25)']){ const project={projectSchemaVersion:2,name:'units',simulation:{t_start:0,t_stop:0.002,step:0.001,max_step:0.001,method:'BDF'},edges:[],nodes:[{id:'chamber_1',type:'simulationComponent',position:{x:0,y:0},data:{label:'chamber_1',rotation:0,mirrored:false,componentType:chamber.type,modelType:chamber.modelType,modelVersion:chamber.modelVersion,ports:chamber.ports,parameters:{p0:value,T0:'=10+10'},parameterUnits:{p0:'bar',T0:'degC'}}}]}; await importProject(project,`units-${summary.unitCases.length}`); await page.getByRole('button',{name:'生成系统 XML',exact:true}).click();await expand(); const blocks=page.getByLabel('生成的系统 XML'); const browserXml=await blocks.last().innerText(); const response=await fetch(`${baseURL}/api/reactflow/system-xml`,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(project)}); assert(response.ok,await response.clone().text());const httpXml=await response.text(); const read=xml=>[...xml.matchAll(/[m[1],Number(m[2])]); const webParams=Object.fromEntries(read(browserXml));const httpParams=Object.fromEntries(read(httpXml)); assert.equal(webParams.p0,250000);assert.equal(webParams.T0,293.15);assert.deepEqual(webParams,httpParams); const exported=await exportJson(`units-${summary.unitCases.length}.json`);assert.equal(exported.projectSchemaVersion,2); summary.unitCases.push({value,siPressure:webParams.p0,siTemperature:webParams.T0,exportedPressure:exported.nodes[0].data.parameters.p0}); } summary.errors=errors;assert.deepEqual(errors,[]); await page.screenshot({path:path.join(output,'acceptance.png'),fullPage:true}); await fs.writeFile(path.join(output,'summary.json'),JSON.stringify(summary,null,2)); await fs.rm(path.join(output,'failure.txt'),{force:true}); await fs.rm(path.join(output,'failure.png'),{force:true}); console.log(JSON.stringify(summary,null,2)); }catch(error){await fs.writeFile(path.join(output,'failure.txt'),`${error.stack}\n${await page.locator('body').innerText()}`);await page.screenshot({path:path.join(output,'failure.png'),fullPage:true});throw error;} finally{await browser.close();}