123 lines
9.3 KiB
JavaScript
123 lines
9.3 KiB
JavaScript
// 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;j<values.length;j++) if(values[j]!==baseline.series[key][j]) differences++;
|
|
}
|
|
assert.equal(differences,0,`${name}: ${differences} result values differ`);
|
|
}
|
|
await fs.writeFile(path.join(output,`${name}.xml`),xml);
|
|
summary.rows.push({name,importMs,receiveMs,savedMs,sampleCount:result.series.time.length,identicalSeries:true,native:result.diagnostics.native});
|
|
await page.getByRole('tab',{name:'建模',exact:true}).click();await expand();
|
|
if(name==='old'){
|
|
assert((await consolePanel.innerText()).includes('可能出现仿真失败或仿真结果与实际不符'));
|
|
const exported=await exportJson('upgraded-eight.json');assert.equal(exported.projectSchemaVersion,2);
|
|
exported.nodes.forEach(n=>assert.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(/<Parameter name="([^"]+)" value="([^"]+)"/g)].map(m=>[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();}
|