Initial commit: BiliDownloader Web 版:Python 零依赖后端 + shadcn/ui 前端
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* ui-check.mjs —— 浏览器端验收(Edge 无头 + CDP)
|
||||
*
|
||||
* 不只是"页面能打开":真的驱动界面做一次搜索、点选结果、等待清晰度解析,
|
||||
* 并读取渲染后的 DOM 断言每一处关键内容。
|
||||
*
|
||||
* 用法: node tools/ui-check.mjs [baseUrl]
|
||||
*/
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import WebSocket from '../ui/node_modules/ws/index.js';
|
||||
|
||||
const BASE = process.argv[2] || 'http://127.0.0.1:8799/';
|
||||
const PORT = 9333;
|
||||
|
||||
const EDGE_CANDIDATES = [
|
||||
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
||||
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
|
||||
];
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
const results = [];
|
||||
function check(name, ok, detail = '') {
|
||||
results.push({ name, ok, detail });
|
||||
console.log(`${ok ? ' ✓' : ' ✗'} ${name}${detail ? ` — ${detail}` : ''}`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const fs = await import('node:fs');
|
||||
const edge = EDGE_CANDIDATES.find((p) => fs.existsSync(p));
|
||||
if (!edge) throw new Error('找不到 Edge');
|
||||
|
||||
const profile = mkdtempSync(join(tmpdir(), 'bd-ui-'));
|
||||
const child = spawn(
|
||||
edge,
|
||||
[
|
||||
'--headless=new',
|
||||
'--disable-gpu',
|
||||
'--no-first-run',
|
||||
'--no-default-browser-check',
|
||||
`--remote-debugging-port=${PORT}`,
|
||||
`--user-data-dir=${profile}`,
|
||||
'--window-size=1280,860',
|
||||
BASE,
|
||||
],
|
||||
{ stdio: 'ignore' },
|
||||
);
|
||||
|
||||
let target = null;
|
||||
for (let i = 0; i < 40 && !target; i++) {
|
||||
await sleep(500);
|
||||
try {
|
||||
const res = await fetch(`http://127.0.0.1:${PORT}/json/list`);
|
||||
const list = await res.json();
|
||||
target = list.find((t) => t.type === 'page' && t.url.startsWith('http'));
|
||||
} catch {
|
||||
/* 还没起来 */
|
||||
}
|
||||
}
|
||||
if (!target) throw new Error('CDP 目标未就绪');
|
||||
|
||||
const ws = new WebSocket(target.webSocketDebuggerUrl, { maxPayload: 64 * 1024 * 1024 });
|
||||
await new Promise((res, rej) => {
|
||||
ws.once('open', res);
|
||||
ws.once('error', rej);
|
||||
});
|
||||
|
||||
let id = 0;
|
||||
const pending = new Map();
|
||||
ws.on('message', (raw) => {
|
||||
const msg = JSON.parse(raw.toString());
|
||||
if (msg.id && pending.has(msg.id)) {
|
||||
pending.get(msg.id)(msg);
|
||||
pending.delete(msg.id);
|
||||
}
|
||||
});
|
||||
const send = (method, params = {}) =>
|
||||
new Promise((res) => {
|
||||
const myId = ++id;
|
||||
pending.set(myId, res);
|
||||
ws.send(JSON.stringify({ id: myId, method, params }));
|
||||
});
|
||||
|
||||
const evaluate = async (expression) => {
|
||||
const r = await send('Runtime.evaluate', {
|
||||
expression,
|
||||
awaitPromise: true,
|
||||
returnByValue: true,
|
||||
});
|
||||
if (r.result?.exceptionDetails) {
|
||||
throw new Error(r.result.exceptionDetails.exception?.description || 'JS 异常');
|
||||
}
|
||||
return r.result?.result?.value;
|
||||
};
|
||||
|
||||
await send('Runtime.enable');
|
||||
await sleep(3500); // 等 React 挂载 + 首屏取配置/登录状态
|
||||
|
||||
console.log(`\n目标: ${BASE}\n`);
|
||||
|
||||
// ---------- 1. 首屏渲染 ----------
|
||||
const text = await evaluate('document.body.innerText');
|
||||
check('React 已挂载并渲染', !!text && text.length > 60, `${text?.length ?? 0} 字符`);
|
||||
for (const key of ['BiliDownloader', '搜索站内视频', '未登录', '还没有选择视频', '清晰度', '开始下载', '日志']) {
|
||||
check(`首屏含「${key}」`, text.includes(key));
|
||||
}
|
||||
|
||||
// ---------- 2. 暗色主题 ----------
|
||||
const isDark = await evaluate("document.documentElement.classList.contains('dark')");
|
||||
check('默认深色主题生效', isDark === true);
|
||||
const bg = await evaluate(
|
||||
"getComputedStyle(document.body).backgroundColor",
|
||||
);
|
||||
check('背景取自 shadcn token(非默认白)', bg !== 'rgb(255, 255, 255)' && bg !== 'rgba(0, 0, 0, 0)', bg);
|
||||
|
||||
// ---------- 3. 无横向溢出 ----------
|
||||
const overflow = await evaluate(
|
||||
'document.documentElement.scrollWidth - document.documentElement.clientWidth',
|
||||
);
|
||||
check('无横向溢出', overflow <= 0, `diff=${overflow}`);
|
||||
|
||||
// ---------- 4. 真的搜一次 ----------
|
||||
console.log('\n —— 驱动一次真实搜索 ——');
|
||||
await evaluate(`(() => {
|
||||
const input = document.querySelector('input');
|
||||
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
|
||||
setter.call(input, '航拍中国');
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
const form = input.closest('form');
|
||||
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
||||
return true;
|
||||
})()`);
|
||||
await sleep(6000);
|
||||
|
||||
const afterSearch = await evaluate('document.body.innerText');
|
||||
const cardCount = await evaluate(
|
||||
"document.querySelectorAll('button[type=button]').length",
|
||||
);
|
||||
check('搜索结果已渲染出卡片', cardCount >= 10, `${cardCount} 个可点卡片`);
|
||||
|
||||
// ---------- 5. 点选一个结果 → 清晰度解析 ----------
|
||||
console.log('\n —— 点选第一个结果,等待清晰度解析 ——');
|
||||
await evaluate(`(() => {
|
||||
const btn = document.querySelector('button[type=button]');
|
||||
btn.click();
|
||||
return true;
|
||||
})()`);
|
||||
await sleep(9000);
|
||||
|
||||
const afterPick = await evaluate('document.body.innerText');
|
||||
check('已进入待下载状态(出现「保存到」)', afterPick.includes('保存到'));
|
||||
check('详情面板显示 BV 号', /BV[0-9A-Za-z]{10}/.test(afterPick));
|
||||
check('清晰度已解析(出现「可用清晰度」日志或档位)', /可用清晰度|480P|360P|1080P|4K/.test(afterPick));
|
||||
const hasStartEnabled = await evaluate(`(() => {
|
||||
const b = [...document.querySelectorAll('button')].find(x => x.textContent.includes('开始下载'));
|
||||
return b ? !b.disabled : null;
|
||||
})()`);
|
||||
check('「开始下载」按钮可点', hasStartEnabled === true);
|
||||
|
||||
// ---------- 6. 控制台报错 ----------
|
||||
const errs = await evaluate('window.__bdErrors ? window.__bdErrors.length : 0');
|
||||
check('页面无致命脚本错误', errs === 0, `${errs} 个`);
|
||||
|
||||
console.log('\n —— 渲染文本采样 ——');
|
||||
console.log(
|
||||
afterPick
|
||||
.split('\n')
|
||||
.filter((l) => l.trim())
|
||||
.slice(0, 26)
|
||||
.map((l) => ' ' + l.slice(0, 96))
|
||||
.join('\n'),
|
||||
);
|
||||
|
||||
ws.close();
|
||||
child.kill();
|
||||
try {
|
||||
rmSync(profile, { recursive: true, force: true });
|
||||
} catch {}
|
||||
|
||||
const failed = results.filter((r) => !r.ok);
|
||||
console.log(`\n结果: ${results.length - failed.length}/${results.length} 通过`);
|
||||
if (failed.length) {
|
||||
console.log('失败项:');
|
||||
for (const f of failed) console.log(` - ${f.name} ${f.detail}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error('验收脚本异常:', e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user