Skip to content

Commit f6bc247

Browse files
author
HelloWorldU
committed
fix(auto-test): detailed CDP diagnostics + TCP port probe
- Replace HTTP GET with raw TCP connect for port detection - Forward Tauri stdout/stderr to console for visibility - Print WebView2 process args and netstat on timeout - Set env var on process.env before spawn (Windows inherit) - Remove detached mode to keep output visible
1 parent 56cba1d commit f6bc247

2 files changed

Lines changed: 88 additions & 48 deletions

File tree

docs/DESIGN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787
| bug-fix: 鼓励留痕 | AST `error-handling/missing-logger` (warn) + check-docs-sync(要求 docs/ 变更) | ⚡ 半硬(warn + 分支检查) |
8888
| new-task: 未验证代码禁止合入 | CI 流水线 + PR 门控 | ✅ 硬约束 |
8989
| new-task: 审阅通过才能合并 | PR review 机制 | ⚡ 半硬(Mock 模式可跳过) |
90-
| auto-test: E2E 验证 | Playwright + WebView2 CDP (`WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS` 环境变量启用调试端口) | ✅ 硬约束(UI 改动后必须跑通 smoke test) |
90+
| auto-test: E2E 验证 | Playwright + WebView2 CDP (`process.env` + TCP 端口探测) | ✅ 硬约束(UI 改动后必须跑通 smoke test) |
9191

9292
## 关键决策记录
9393

scripts/auto-test.ts

Lines changed: 87 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,10 @@
44
*
55
* 用法:
66
* npx tsx scripts/auto-test.ts
7-
*
8-
* 流程:
9-
* 1. npm run ci(快速验证:typecheck / lint / analyze / check-docs / test / build)
10-
* 2. 后台启动 Tauri 应用(cargo tauri dev)
11-
* 3. 等待 WebView2 CDP 端口就绪
12-
* 4. 运行 Playwright E2E
13-
* 5. 终止应用进程,输出结果
14-
*
15-
* 注意:
16-
* - Windows 上需要 PowerShell 执行权限
17-
* - Tauri 应用启动约需 15-30 秒(含 Rust 编译)
187
*/
198

20-
import { spawn, ChildProcess } from 'child_process'
9+
import { spawn, ChildProcess, execSync } from 'child_process'
10+
import { connect } from 'net'
2111
import { resolve } from 'path'
2212

2313
const ROOT = resolve(__dirname, '..')
@@ -37,46 +27,94 @@ function log(stage: string, message: string) {
3727

3828
function run(cmd: string, args: string[], cwd: string): Promise<void> {
3929
return new Promise((resolve, reject) => {
40-
const child = spawn(cmd, args, {
41-
cwd,
42-
stdio: 'inherit',
43-
shell: true,
44-
})
30+
const child = spawn(cmd, args, { cwd, stdio: 'inherit', shell: true })
4531
child.on('close', (code) => {
4632
if (code === 0) resolve()
4733
else reject(new Error(`命令退出码: ${code}`))
4834
})
4935
})
5036
}
5137

52-
function spawnDetached(cmd: string, args: string[], cwd: string): ChildProcess {
53-
return spawn(cmd, args, {
54-
cwd,
55-
detached: true,
56-
stdio: 'ignore',
57-
shell: true,
38+
/** TCP 层探测端口是否可连 */
39+
function tcpConnect(port: number, timeout: number): Promise<void> {
40+
return new Promise((resolve, reject) => {
41+
const socket = connect(port, '127.0.0.1')
42+
const timer = setTimeout(() => {
43+
socket.destroy()
44+
reject(new Error('timeout'))
45+
}, timeout)
46+
socket.on('connect', () => {
47+
clearTimeout(timer)
48+
socket.destroy()
49+
resolve()
50+
})
51+
socket.on('error', (err) => {
52+
clearTimeout(timer)
53+
socket.destroy()
54+
reject(err)
55+
})
5856
})
5957
}
6058

59+
/** 检查 WebView2 进程是否带 CDP 参数 */
60+
function diagnoseWebView2(): string {
61+
try {
62+
const out = execSync(
63+
'wmic process where "name like \'%msedgewebview2%\'" get CommandLine /format:csv 2>nul',
64+
{ encoding: 'utf-8', shell: 'cmd.exe' }
65+
)
66+
return out || '(无 WebView2 进程)'
67+
} catch (e) {
68+
return `诊断失败: ${e instanceof Error ? e.message : String(e)}`
69+
}
70+
}
71+
72+
/** 检查端口监听状态 */
73+
function diagnosePort(port: number): string {
74+
try {
75+
const out = execSync(`netstat -an | findstr "${port}"`, { encoding: 'utf-8', shell: 'cmd.exe' })
76+
return out || '(端口未监听)'
77+
} catch {
78+
return '(端口未监听)'
79+
}
80+
}
81+
6182
async function waitForCdp(port: number, timeoutMs: number): Promise<void> {
6283
const deadline = Date.now() + timeoutMs
63-
const http = await import('http')
84+
let lastErr = ''
85+
let checkCount = 0
86+
6487
while (Date.now() < deadline) {
88+
checkCount++
6589
try {
66-
await new Promise<void>((resolve, reject) => {
67-
const req = http.get(`http://localhost:${port}/json`, (res) => {
68-
if (res.statusCode === 200) resolve()
69-
else reject(new Error(`status ${res.statusCode}`))
70-
})
71-
req.on('error', reject)
72-
req.setTimeout(1000, () => reject(new Error('timeout')))
73-
})
90+
await tcpConnect(port, 2000)
91+
log('CDP', `端口 ${port} TCP 可连(尝试 ${checkCount} 次)`)
7492
return
75-
} catch {
76-
await new Promise((r) => setTimeout(r, 1000))
93+
} catch (e) {
94+
lastErr = e instanceof Error ? e.message : String(e)
95+
}
96+
97+
// 每 5 秒打印一次诊断
98+
if (checkCount % 5 === 0) {
99+
log('CDP', `仍在等待端口 ${port}... (${Math.round((Date.now() - (deadline - timeoutMs)) / 1000)}s)`)
100+
log('DIAG', '端口状态: ' + diagnosePort(port).trim().replace(/\n/g, ', '))
77101
}
102+
103+
await new Promise((r) => setTimeout(r, 1000))
78104
}
79-
throw new Error(`CDP 端口 ${port}${timeoutMs}ms 内未就绪`)
105+
106+
// 最终诊断
107+
log('DIAG', '=== 最终诊断 ===')
108+
log('DIAG', '端口状态: ' + diagnosePort(port).trim().replace(/\n/g, ', '))
109+
log('DIAG', 'WebView2 进程: ' + diagnoseWebView2().trim().replace(/\n/g, ', ').slice(0, 500))
110+
111+
throw new Error(
112+
`CDP 端口 ${port}${timeoutMs}ms 内未就绪。` +
113+
`最后错误: ${lastErr}。` +
114+
`可能原因: (1) WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS 未生效 ` +
115+
`(2) WebView2 运行时版本过旧 ` +
116+
`(3) 端口被占用`
117+
)
80118
}
81119

82120
async function main() {
@@ -89,21 +127,20 @@ async function main() {
89127
await run('npm', ['run', 'ci'], SWARM)
90128
log('CI', `${COLORS.green}通过${COLORS.reset}`)
91129

92-
// 2. 启动 Tauri 应用
93-
log('TAURI', '后台启动 Tauri 应用...')
94-
// Windows: WebView2 CDP port must be set via env var, not additionalBrowserArgs
130+
// 2. 启动 Tauri 应用(不 detached,保留输出用于调试)
131+
log('TAURI', '启动 Tauri 应用...')
132+
process.env.WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS = '--remote-debugging-port=9222'
95133
tauriProcess = spawn('npx', ['tauri', 'dev'], {
96134
cwd: SWARM,
97-
detached: true,
98-
stdio: 'ignore',
135+
stdio: 'pipe',
99136
shell: true,
100-
env: {
101-
...process.env,
102-
WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS: '--remote-debugging-port=9222',
103-
},
104137
})
105138
log('TAURI', `PID: ${tauriProcess.pid}`)
106139

140+
// 把 stdout/stderr 转发出来,方便看启动进度
141+
tauriProcess.stdout?.on('data', (d) => process.stdout.write(d))
142+
tauriProcess.stderr?.on('data', (d) => process.stderr.write(d))
143+
107144
// 3. 等待 CDP 就绪
108145
log('CDP', '等待 WebView2 调试端口 (9222)...')
109146
await waitForCdp(9222, 60000)
@@ -121,10 +158,13 @@ async function main() {
121158
if (tauriProcess && tauriProcess.pid) {
122159
log('CLEANUP', `终止 Tauri 进程 ${tauriProcess.pid}...`)
123160
try {
124-
process.kill(-tauriProcess.pid, 'SIGTERM')
161+
spawn('taskkill', ['/PID', String(tauriProcess.pid), '/T', '/F'], {
162+
shell: true,
163+
stdio: 'ignore',
164+
detached: true,
165+
})
125166
} catch {
126-
// Windows 上 process group kill 可能失败,尝试 taskkill
127-
spawn('taskkill', ['/PID', String(tauriProcess.pid), '/T', '/F'], { shell: true })
167+
// ignore
128168
}
129169
}
130170
}

0 commit comments

Comments
 (0)