#!/usr/bin/env node /** * 打断场景的 UI 回归验证(2026-11-10) * * 验证点: * ① 图文被容器拆成两轮时,两轮**各自**计时,先结束的那轮定格不倒退 * ② 同一时间不出现两条「正在思考 / 等待回复」占位 * ③ 页面无未捕获错误 * * 做法:真实发送「图 + 文字」,每 500ms 采样一次页面上的计时读数与占位文案, * 观察读数序列是否**单调不减**(倒退就是本次要修的 bug)。 */ import { chromium } from 'playwright'; import { mkdir } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const here = dirname(fileURLToPath(import.meta.url)); const root = resolve(here, '..'); const outDir = resolve(root, 'test-results/interrupt-ui'); const BASE = process.env.BASE_URL || 'http://127.0.0.1:4300'; const imgPath = process.argv[2]; await mkdir(outDir, { recursive: true }); const browser = await chromium.launch({ channel: 'chrome' }); const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); const errors = []; page.on('pageerror', (e) => errors.push(String(e))); await page.goto(`${BASE}/desktop/chat`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(8000); const s = page.locator('text=使用HTML根据我这个').first(); if (await s.count()) { await s.click(); await page.waitForTimeout(5000); } // 发送:图 + 文字(复现容器的两轮) await page.locator('input[type="file"]').first().setInputFiles(imgPath); await page.waitForTimeout(1000); const box = page.locator('textarea').first(); await box.click(); await box.fill('用终端执行 date 命令,然后用一句话告诉我当前时间。'); await page.keyboard.press('Enter'); console.log('已发送图文,开始采样…'); /** [{t, times:[..], waits:[..]}] */ const samples = []; const t0 = Date.now(); while (Date.now() - t0 < 120_000) { const snap = await page.evaluate(() => ({ times: [...document.querySelectorAll('.head-time, .seg-time')].map((e) => e.textContent.trim()), waits: [...document.querySelectorAll('.seg-wait, .card-wait')].map((e) => e.textContent.trim()), })); samples.push({ at: ((Date.now() - t0) / 1000).toFixed(1), ...snap }); await page.waitForTimeout(500); if (Date.now() - t0 > 30_000) { const noLive = !(await page.getByText('输出中').count()); if (noLive) break; } } await page.screenshot({ path: `${outDir}/after.png`, fullPage: false }); // 分析:只看最后一组头部读数(最新回合),检查是否单调不减 const parse = (s) => parseInt(String(s).replace(/[^\d]/g, ''), 10); const series = samples .map((x) => x.times.map(parse).filter((n) => Number.isFinite(n))) .filter((a) => a.length > 0); let regressions = 0; for (let i = 1; i < series.length; i++) { for (let k = 0; k < Math.min(series[i].length, series[i - 1].length); k++) { if (series[i][k] < series[i - 1][k]) { regressions++; console.log(` ✗ 倒退:${series[i - 1][k]}s → ${series[i][k]}s(${samples[i].at}s 处)`); } } } // 占位重复:同一时刻出现多于 1 条「正在思考/等待回复」且文案相同 const dup = samples.filter((x) => x.waits.length >= 2 && new Set(x.waits).size === 1).length; console.log(`\n采样 ${samples.length} 次,读数组 ${series.length} 组`); console.log(`读数倒退次数:${regressions}`); console.log(`重复占位采样数:${dup}`); console.log(`页面错误:${errors.length ? errors.slice(0, 3).join(' | ') : '无'}`); const pass = regressions === 0 && errors.length === 0; console.log(`\n${pass ? '✓ PASS' : '✗ FAIL'}:计时不退步 / 无页面错误`); await browser.close(); process.exit(pass ? 0 : 1);