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'
2111import { resolve } from 'path'
2212
2313const ROOT = resolve ( __dirname , '..' )
@@ -37,46 +27,94 @@ function log(stage: string, message: string) {
3727
3828function 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+
6182async 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
82120async 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