Files
opsxapi/.ui-review/accept-2026-09-19/web-reg2.mjs
T
logan 39f4d5addc 桌面应用图标替换为 B-brackets 设计(深藏青 + 白/蓝花括号)
- icon.svg 取自 opsx-icon-options/api/B-brackets.svg,全套光栅重生成:
  icon.icns(iconutil)、icon.ico(PNG-in-ICO 6 尺寸)、16-1024 PNG、Windows Square*/StoreLogo
- 替换 Tauri 脚手架默认五角星图标;GUI 重启实测 Dock 图标已生效
- 服务全量重启:server 8787(官网/文档 200)+ web 5173 + 桌面 GUI
2026-09-20 00:14:32 +08:00

276 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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); });