const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const http = require('node:http'); const { chromium } = require('playwright'); const root = path.resolve(__dirname, '..'); let mode = 'issues'; const records = ['人员证书', '项目信息'].map((template, index) => ({template, file:template+'.xlsx', sheet:'数据', row:7, fields:['编号'], message:'缺少主键字段:编号', action:'已跳过,未进入知识图谱', data:{编号:null, 名称:index ? '项目示例乙' : '证书示例甲', 备注:'', 数量:0}})); const server = http.createServer((req, res) => { const url = new URL(req.url, 'http://localhost'); if (url.pathname === '/api/data-quality') { res.setHeader('Content-Type','application/json; charset=utf-8'); if (mode === 'error') { res.statusCode=503; res.end(JSON.stringify({detail:'报告暂不可用'})); return; } const payload = mode === 'zero' ? {data_version:'v2',total:0,datasets:{},records:[]} : {data_version:'v1',total:2,datasets:{人员证书:1,项目信息:1},records}; if (!url.searchParams.has('details')) { const {records, ...summary}=payload; res.end(JSON.stringify(summary)); } else res.end(JSON.stringify(payload)); return; } if (url.pathname.startsWith('/output/')) { res.setHeader('Content-Type','application/json'); res.end(JSON.stringify({nodes:[],relations:[],meta:{build_id:'v1'}})); return; } const file = path.join(root,'html',url.pathname === '/' ? 'knowledge_graph_3d.html' : url.pathname); if (!file.startsWith(path.join(root,'html')) || !fs.existsSync(file)) {res.statusCode=404; res.end();return;} res.setHeader('Content-Type',file.endsWith('.js') ? 'application/javascript; charset=utf-8' : file.endsWith('.css') ? 'text/css; charset=utf-8' : 'text/html; charset=utf-8'); res.end(fs.readFileSync(file)); }); (async () => { await new Promise(resolve => server.listen(0,'127.0.0.1',resolve)); const browser = await chromium.launch({headless:true, channel:'msedge'}); try { const page = await browser.newPage({viewport:{width:1280,height:900}}); const base = `http://127.0.0.1:${server.address().port}`; let dialogs=0; page.on('dialog', d => { dialogs++; d.dismiss(); }); for (const entry of ['/index.html','/']) { mode='issues'; await page.goto(base+entry); const alert=page.locator('#dataQualityAlert'); await alert.waitFor({state:'visible'}); assert.match(await alert.textContent(), /2/); await alert.click(); await page.getByRole('button',{name:'项目信息.xlsx (1)',exact:true}).waitFor(); assert.match(await page.locator('.dq-card h3').textContent(), /人员证书.xlsx.*第 7 行/); assert.equal(await page.locator('.dq-data dd').count(),4); assert.equal(await page.locator('.dq-data dd.dq-missing').textContent(),'(空)'); assert.equal(await page.locator('.dq-data img').count(),0); await page.getByRole('button',{name:'项目信息.xlsx (1)',exact:true}).click(); assert.match(await page.locator('.dq-card h3').textContent(),/项目信息.xlsx/); assert.match(await page.locator('.dq-card').textContent(),/项目示例乙/); fs.mkdirSync(path.join(root,'.runtime/qa'),{recursive:true}); if(entry==='/') await page.screenshot({path:path.join(root,'.runtime/qa/data-quality-desktop.png')}); await page.keyboard.press('Escape'); assert.equal(await page.locator('#dataQualityDialog').evaluate(el=>el.open),false); } await page.setViewportSize({width:390,height:844}); await page.locator('#dataQualityAlert').click(); await page.locator('.dq-card').waitFor(); assert.ok(await page.locator('.dq-dialog').evaluate(el=>el.scrollWidth<=el.clientWidth)); await page.screenshot({path:path.join(root,'.runtime/qa/data-quality-mobile.png')}); mode='zero'; await page.evaluate(()=>window.dispatchEvent(new Event('focus'))); await page.locator('#dataQualityAlert').waitFor({state:'hidden'}); await page.getByText('当前版本没有缺少主键的记录。',{exact:true}).waitFor(); await page.keyboard.press('Escape'); mode='error'; await page.evaluate(()=>window.dispatchEvent(new Event('focus'))); await page.getByText('数据提醒暂不可用',{exact:true}).waitFor(); await page.locator('#dataQualityAlert').click(); await page.getByRole('button',{name:'重新加载',exact:true}).waitFor(); mode='issues'; await page.getByRole('button',{name:'重新加载',exact:true}).click(); await page.locator('.dq-card').waitFor(); assert.equal(dialogs,0); console.log('PASS: both pages, file grouping, full fields, missing highlight, text escaping, Escape, mobile, zero/version refresh, error/retry'); } finally { await browser.close(); await new Promise(resolve=>server.close(resolve)); } })().catch(error=>{console.error(error);process.exitCode=1;server.close();});