162 lines
7.6 KiB
JavaScript
162 lines
7.6 KiB
JavaScript
'use strict';
|
||
|
||
/**
|
||
* 针对性诊断:键盘动作(S / R / #)为什么没生效。
|
||
* 做法:无头打开页面,把 app.js 里的内部状态(state.selected / state.open)与
|
||
* 当前焦点元素、toast 文案都直接读出来 —— 比猜快得多。
|
||
*/
|
||
|
||
const { spawn } = require('node:child_process');
|
||
const fs = require('node:fs');
|
||
const os = require('node:os');
|
||
const path = require('node:path');
|
||
|
||
const URL_ = 'http://127.0.0.1:8788/';
|
||
const PORT = 9223;
|
||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||
|
||
class Cdp {
|
||
constructor(ws) { this.ws = ws; this.id = 0; this.pending = new Map(); this.errors = []; }
|
||
static async connect(url) {
|
||
const ws = new WebSocket(url);
|
||
await new Promise((res, rej) => {
|
||
ws.addEventListener('open', res, { once: true });
|
||
ws.addEventListener('error', () => rej(new Error('CDP connect failed')), { once: true });
|
||
});
|
||
const c = new Cdp(ws);
|
||
ws.addEventListener('message', (ev) => {
|
||
let m; try { m = JSON.parse(ev.data); } catch { return; }
|
||
if (m.id && c.pending.has(m.id)) {
|
||
const { resolve, reject } = c.pending.get(m.id);
|
||
c.pending.delete(m.id);
|
||
if (m.error) reject(new Error(m.error.message)); else resolve(m.result);
|
||
} else if (m.method === 'Runtime.exceptionThrown') {
|
||
const d = m.params.exceptionDetails;
|
||
c.errors.push((d.exception && d.exception.description ? d.exception.description.split('\n')[0] : d.text));
|
||
} else if (m.method === 'Runtime.consoleAPICalled' && m.params.type === 'error') {
|
||
c.errors.push('console.error: ' + m.params.args.map((a) => a.value || a.description || '').join(' '));
|
||
}
|
||
});
|
||
return c;
|
||
}
|
||
send(method, params = {}) {
|
||
const id = ++this.id;
|
||
return new Promise((resolve, reject) => {
|
||
this.pending.set(id, { resolve, reject });
|
||
this.ws.send(JSON.stringify({ id, method, params }));
|
||
setTimeout(() => { if (this.pending.has(id)) { this.pending.delete(id); reject(new Error(method + ' timeout')); } }, 20000);
|
||
});
|
||
}
|
||
async eval(expression) {
|
||
const r = await this.send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
|
||
if (r.exceptionDetails) return { __err: r.exceptionDetails.text || 'eval error' };
|
||
return r.result ? r.result.value : undefined;
|
||
}
|
||
}
|
||
|
||
(async () => {
|
||
const edge = ['C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
||
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe'].find((p) => fs.existsSync(p));
|
||
const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'wpyw-diag-'));
|
||
const child = spawn(edge, ['--headless=new', `--remote-debugging-port=${PORT}`, `--user-data-dir=${profile}`,
|
||
'--no-first-run', '--disable-extensions', '--window-size=1600,1000', URL_], { stdio: 'ignore' });
|
||
|
||
let cdp = null;
|
||
const out = [];
|
||
const log = (s) => { out.push(s); process.stdout.write(s + '\n'); };
|
||
try {
|
||
let target = null;
|
||
for (let i = 0; i < 40 && !target; i++) {
|
||
await sleep(400);
|
||
try {
|
||
const list = await (await fetch(`http://127.0.0.1:${PORT}/json/list`)).json();
|
||
target = list.find((t) => t.type === 'page' && t.webSocketDebuggerUrl);
|
||
} catch { /* 等 */ }
|
||
}
|
||
cdp = await Cdp.connect(target.webSocketDebuggerUrl);
|
||
await cdp.send('Runtime.enable');
|
||
await cdp.send('Page.navigate', { url: URL_ });
|
||
|
||
// 等界面渲染
|
||
let rows = 0;
|
||
for (let i = 0; i < 40 && rows === 0; i++) { await sleep(500); rows = await cdp.eval('document.querySelectorAll(".row").length'); }
|
||
log(`邮件行 = ${rows}`);
|
||
|
||
// state 能不能从外部读到?
|
||
const probe = await cdp.eval('typeof state');
|
||
log(`typeof state = ${probe}`);
|
||
|
||
log('');
|
||
log('── 步骤 1:点击第一行 ──────────────────────────');
|
||
await cdp.eval('document.querySelector(".row").click()');
|
||
await sleep(1500);
|
||
log(JSON.stringify(await cdp.eval(`({
|
||
selected: (typeof state !== 'undefined' ? state.selected : 'N/A'),
|
||
openUid: (typeof state !== 'undefined' && state.open ? state.open.uid : null),
|
||
activeTag: document.activeElement ? document.activeElement.tagName : null,
|
||
activeId: document.activeElement ? document.activeElement.id : null,
|
||
readerSubject: (document.querySelector('.reader__subject')||{}).textContent || '',
|
||
toast: (document.getElementById('toastText')||{}).textContent || '',
|
||
rowsNow: document.querySelectorAll('.row').length,
|
||
})`)));
|
||
|
||
log('');
|
||
log('── 步骤 2:派发 keydown "s" ────────────────────');
|
||
const uid = await cdp.eval('document.querySelector(".row")?.dataset.uid');
|
||
log(`目标 UID = ${uid}`);
|
||
await cdp.eval(`document.dispatchEvent(new KeyboardEvent('keydown', { key: 's', bubbles: true }))`);
|
||
await sleep(1800);
|
||
log(JSON.stringify(await cdp.eval(`({
|
||
flaggedInState: (() => { const m = state.messages.find(x => String(x.uid) === '${uid}'); return m ? m.flagged : 'not-found'; })(),
|
||
rowHasFlagClass: !!(document.querySelector('.row[data-uid="${uid}"]')?.classList.contains('is-flagged')),
|
||
openFlagged: (state.open ? state.open.flagged : null),
|
||
toast: (document.getElementById('toastText')||{}).textContent || '',
|
||
})`)));
|
||
|
||
log('');
|
||
log('── 步骤 3:派发 keydown "r" ────────────────────');
|
||
await cdp.eval(`document.dispatchEvent(new KeyboardEvent('keydown', { key: 'r', bubbles: true }))`);
|
||
await sleep(800);
|
||
log(JSON.stringify(await cdp.eval(`({
|
||
composeHidden: document.getElementById('compose').hidden,
|
||
composeTitle: document.getElementById('composeTitle').textContent,
|
||
openUid: (state.open ? state.open.uid : null),
|
||
})`)));
|
||
|
||
log('');
|
||
log('── 步骤 4:直接在 document 上监听,验证事件到底有没有到达 ──');
|
||
await cdp.eval(`(() => {
|
||
window.__probe = [];
|
||
document.addEventListener('keydown', (e) => window.__probe.push(e.key + '|' + (document.activeElement ? document.activeElement.tagName : '?')), true);
|
||
return true;
|
||
})()`);
|
||
await cdp.eval(`document.dispatchEvent(new KeyboardEvent('keydown', { key: 's', bubbles: true }))`);
|
||
await sleep(600);
|
||
log('捕获到的 keydown:' + JSON.stringify(await cdp.eval('window.__probe')));
|
||
|
||
// 对比:真实用户按键(CDP Input.dispatchKeyEvent 走浏览器输入管线)
|
||
log('');
|
||
log('── 步骤 5:用 CDP 真实键盘事件(Input.dispatchKeyEvent)──');
|
||
await cdp.eval('document.querySelector(".row").click()');
|
||
await sleep(1200);
|
||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 's', code: 'KeyS', windowsVirtualKeyCode: 83, nativeVirtualKeyCode: 83, text: 's', unmodifiedText: 's' });
|
||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 's', code: 'KeyS', windowsVirtualKeyCode: 83, nativeVirtualKeyCode: 83 });
|
||
await sleep(1800);
|
||
log(JSON.stringify(await cdp.eval(`({
|
||
rowHasFlagClass: !!(document.querySelector('.row[data-uid="${uid}"]')?.classList.contains('is-flagged')),
|
||
toast: (document.getElementById('toastText')||{}).textContent || '',
|
||
})`)));
|
||
|
||
log('');
|
||
log('运行时报错:' + JSON.stringify(cdp.errors.slice(0, 10)));
|
||
} catch (err) {
|
||
log('FATAL ' + (err && err.stack ? err.stack : err));
|
||
} finally {
|
||
if (cdp) try { cdp.ws.close(); } catch { /* 忽略 */ }
|
||
try { child.kill(); } catch { /* 忽略 */ }
|
||
await sleep(400);
|
||
try { fs.rmSync(profile, { recursive: true, force: true }); } catch { /* 忽略 */ }
|
||
fs.writeFileSync('E:\\deepseek\\artifacts\\client-ui-diag.txt', out.join('\n'), 'utf8');
|
||
}
|
||
})();
|