import { chromium } from 'playwright'; import fs from 'fs'; import path from 'path'; const outDir = path.resolve('.ui-review/accept-2026-09-19'); const result = { pass: [], fail: [], notes: [] }; const shot = async (page, name) => { await page.screenshot({ path: path.join(outDir, name) }); }; async function main() { const browser = await chromium.launch({ headless: true, channel: 'chrome', }); const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); page.setDefaultTimeout(15000); await page.goto('http://127.0.0.1:5173/', { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(1500); await shot(page, 'web-01-home.png'); const title = await page.title(); result.notes.push('title=' + title); // Channel: look for select near 通道 try { const selects = page.locator('select'); const sc = await selects.count(); result.notes.push('selectCount=' + sc); let channelDone = false; for (let i = 0; i < sc; i++) { const sel = selects.nth(i); const opts = await sel.locator('option').allTextContents(); result.notes.push('select' + i + '=' + opts.join('/')); if (opts.some((o) => /自动|代理|浏览器|桌面|Web|browser|tauri|auto/i.test(o))) { const values = await sel.locator('option').evaluateAll((os) => os.map((o) => o.value)); if (values.length > 1) { await sel.selectOption(values[1]); await page.waitForTimeout(300); await sel.selectOption(values[0]); result.pass.push('通道切换往返: ' + opts.join('|')); channelDone = true; break; } } } if (!channelDone) { // click text 通道 then options await page.getByText('通道', { exact: true }).first().click().catch(() => {}); await page.waitForTimeout(300); for (const m of ['Web 代理', 'Web代理', '浏览器', '桌面', '自动']) { const o = page.getByText(m, { exact: true }); if (await o.count()) { await o.first().click(); result.pass.push('通道点选 ' + m); channelDone = true; break; } } } if (!channelDone) result.fail.push('通道切换失败'); await shot(page, 'web-02-channel.png'); } catch (e) { result.fail.push('通道: ' + e.message); } // Env try { const envBtn = page.getByRole('button', { name: /变量/ }); if (await envBtn.count()) { await envBtn.first().click(); await page.waitForTimeout(500); result.pass.push('打开环境变量面板'); await page.keyboard.press('Escape'); } else result.fail.push('无环境变量按钮'); await shot(page, 'web-03-env.png'); } catch (e) { result.fail.push('环境: ' + e.message); } // Tree try { await page.getByText('回归 · GET Query').first().click({ timeout: 8000 }); result.pass.push('集合树切换到回归 GET'); await page.waitForTimeout(500); await shot(page, 'web-04-tree.png'); } catch (e) { result.fail.push('集合树: ' + e.message); } // Dirty try { let edited = false; const inputs = page.locator('input'); const n = await inputs.count(); for (let i = 0; i < n; i++) { const el = inputs.nth(i); const val = await el.inputValue().catch(() => ''); if (/https?:|127\.0\.0\.1|\/v1\//.test(val)) { await el.fill(val.replace(/([?&]_dirty=1)/g, '') + (val.includes('?') ? '&' : '?') + '_dirty=1'); edited = true; result.pass.push('URL 脏点已写入'); break; } } if (!edited) result.fail.push('未找到可编辑 URL'); // look for dirty marker / save await page.keyboard.press('Meta+s'); await page.waitForTimeout(400); const body = await page.locator('body').innerText(); if (/未保存|脏|dirty|\*/i.test(body) || (await page.getByRole('button', { name: /保存/ }).count())) { result.pass.push('脏点/保存相关 UI 可见或已 Cmd+S'); } else { result.notes.push('可能自动持久化,未见显式脏标记'); } await shot(page, 'web-05-dirty.png'); } catch (e) { result.fail.push('脏点: ' + e.message); } // First send HTTP to create history try { const send = page.getByRole('button', { name: /发送/ }).first(); if (await send.count()) { await send.click(); await page.waitForTimeout(2000); result.pass.push('发送 HTTP 以产生历史'); } } catch (e) { result.notes.push('发送: ' + e.message); } // New WS try { await page.getByRole('button', { name: '新建' }).first().click(); await page.waitForTimeout(500); await shot(page, 'web-06-new-menu.png'); const cand = page.locator('text=/WebSocket|Websocket|WS/i'); if (await cand.count()) { await cand.first().click(); result.pass.push('新建 WebSocket'); } else { result.fail.push('新建菜单无 WebSocket: ' + (await page.locator('body').innerText()).slice(0, 200)); } await page.waitForTimeout(600); await shot(page, 'web-07-ws-editor.png'); } catch (e) { result.fail.push('新建WS: ' + e.message); } // Fill WS try { // URL field const inputs = page.locator('input'); let ok = false; for (let i = 0; i < await inputs.count(); i++) { const el = inputs.nth(i); const ph = ((await el.getAttribute('placeholder')) || '') + (await el.getAttribute('aria-label') || ''); const val = await el.inputValue().catch(() => ''); if (/ws|url|地址|主机/i.test(ph + val) || val.startsWith('ws') || (i < 5 && val === '')) { // prefer empty or ws-looking near top of editor } } // Prefer the main URL bar: often largest width input with ws default const urlCandidates = await page.locator('input.ops-input, input').all(); for (const el of urlCandidates) { const val = await el.inputValue().catch(() => ''); const box = await el.boundingBox().catch(() => null); if (!box || box.width < 200) continue; if (val.startsWith('ws') || val === '' || /echo|localhost|127/.test(val)) { await el.fill('wss://ws.postman-echo.com/raw'); ok = true; break; } } if (ok) result.pass.push('填写 WS URL'); else { // force first wide input for (const el of urlCandidates) { const box = await el.boundingBox().catch(() => null); if (box && box.width > 300) { await el.fill('wss://ws.postman-echo.com/raw'); ok = true; result.pass.push('填写 WS URL(宽输入框)'); break; } } } if (!ok) result.fail.push('WS URL 填写失败'); // Headers const headerTab = page.getByText('Headers', { exact: true }).or(page.getByText('请求头')).or(page.getByRole('tab', { name: /Header/i })); if (await headerTab.count()) await headerTab.first().click().catch(() => {}); await page.waitForTimeout(300); // try add row const addBtns = page.getByRole('button', { name: /添加|+|\+/ }); if (await addBtns.count()) await addBtns.first().click().catch(() => {}); // fill key values in kv editor const placeholders = page.locator('input[placeholder*="Key"], input[placeholder*="键"], input[placeholder*="Header"], input[placeholder*="名称"]'); if (await placeholders.count()) { await placeholders.first().fill('Authorization'); const valPh = page.locator('input[placeholder*="Value"], input[placeholder*="值"]'); if (await valPh.count()) await valPh.first().fill('Bearer smoke'); result.pass.push('填写 Authorization Header'); } else { // generic: last two empty inputs in view result.notes.push('Header 占位符未命中,尝试通用 KV'); const all = page.locator('input'); const vals = []; for (let i = 0; i < await all.count(); i++) { const v = await all.nth(i).inputValue().catch(() => null); vals.push(v); } result.notes.push('inputs snapshot count=' + vals.length); } await shot(page, 'web-08-ws-filled.png'); const connect = page.getByRole('button', { name: /连接/ }); if (await connect.count()) { await connect.first().click(); await page.waitForTimeout(3000); const text = await page.locator('body').innerText(); if (/桌面原生|Gin 隧道|自定义 Header|open|已连接|连接成功|status/i.test(text)) { result.pass.push('连接后出现状态文案'); } else result.notes.push('连接后文案不明确'); const sendInput = page.locator('textarea, input[placeholder*="消息"], input[placeholder*="发送"]'); if (await sendInput.count()) { await sendInput.first().fill('ping-gui-reg'); const sendBtn = page.getByRole('button', { name: /发送/ }); if (await sendBtn.count()) { await sendBtn.first().click(); await page.waitForTimeout(1500); result.pass.push('已发送 ping-gui-reg'); } } await shot(page, 'web-09-ws-live.png'); } else result.fail.push('无连接按钮'); } catch (e) { result.fail.push('WS流程: ' + e.message); } // History replay try { await page.getByRole('button', { name: /历史/ }).first().click(); await page.waitForTimeout(600); await shot(page, 'web-10-history.png'); const rows = page.locator('.ops-tree-row, [class*="history"] >> ..'); // hover first history-like row const replayBtn = page.getByRole('button', { name: '重放' }); if (!(await replayBtn.count()) || !(await replayBtn.first().isVisible())) { const anyRow = page.locator('button:has-text("GET"), button:has-text("POST"), div.ops-tree-row').first(); if (await anyRow.count()) await anyRow.hover(); await page.waitForTimeout(400); } if (await replayBtn.count()) { await replayBtn.first().click({ force: true }); await page.waitForTimeout(2200); const toast = page.locator('.ops-toast'); const t = (await toast.allTextContents()).join(' | '); const body = await page.locator('body').innerText(); if (/正在重放|重放成功|重放完成|重放失败|重放连接/i.test(t + body)) { result.pass.push('重放 toast: ' + (t || 'body命中').slice(0, 160)); } else result.fail.push('重放后无 toast,toast=' + t); } else result.fail.push('无重放按钮'); await shot(page, 'web-11-replay.png'); } catch (e) { result.fail.push('重放: ' + e.message); } await browser.close(); const summary = { verdict: result.fail.length ? '有缺陷' : '通过', ...result }; fs.writeFileSync(path.join(outDir, 'web-reg-result.json'), JSON.stringify(summary, null, 2)); console.log(JSON.stringify(summary, null, 2)); } main().catch((e) => { console.error(e); process.exit(1); });