1- import fs from "fs" ;
21import path from "path" ;
3- import os from "os" ;
42import { test as base , chromium , type BrowserContext , type Route } from "@playwright/test" ;
53import { installScriptByCode } from "./utils" ;
64
@@ -76,27 +74,52 @@ export type AgentFixtures = {
7674
7775export { makeTextSSE , makeToolCallSSE } ;
7876
77+ /**
78+ * Agent test fixtures — 单 context 方案
79+ *
80+ * 与旧版两阶段方案(启动→关闭→重启)不同,这里在同一个 context 内完成
81+ * userScripts 启用和 model 配置写入,避免了 CI 上 profile 持久化不可靠的问题。
82+ */
7983export const test = base . extend < AgentFixtures > ( {
8084 // eslint-disable-next-line no-empty-pattern
8185 context : async ( { } , use ) => {
8286 const pathToExtension = path . resolve ( __dirname , "../dist/ext" ) ;
83- const userDataDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "pw-agent-" ) ) ;
84- const chromeArgs = [ `--disable-extensions-except=${ pathToExtension } ` , `--load-extension=${ pathToExtension } ` ] ;
85-
86- // Phase 1: Enable user scripts permission
87- const ctx1 = await chromium . launchPersistentContext ( userDataDir , {
87+ const context = await chromium . launchPersistentContext ( "" , {
8888 headless : false ,
89- args : [ "--headless=new" , ... chromeArgs ] ,
89+ args : [ "--headless=new" , `--disable-extensions-except= ${ pathToExtension } ` , `--load-extension= ${ pathToExtension } ` ] ,
9090 } ) ;
91- let [ bg ] = ctx1 . serviceWorkers ( ) ;
92- if ( ! bg ) bg = await ctx1 . waitForEvent ( "serviceworker" ) ;
91+
92+ await use ( context ) ;
93+ await context . close ( ) ;
94+ } ,
95+
96+ extensionId : async ( { context } , use ) => {
97+ // 等待 service worker 启动
98+ let [ bg ] = context . serviceWorkers ( ) ;
99+ if ( ! bg ) bg = await context . waitForEvent ( "serviceworker" ) ;
93100 const extensionId = bg . url ( ) . split ( "/" ) [ 2 ] ;
94- const extPage = await ctx1 . newPage ( ) ;
95- await extPage . goto ( "chrome://extensions/" ) ;
96- await extPage . waitForLoadState ( "domcontentloaded" ) ;
97- await extPage . waitForTimeout ( 1_000 ) ;
98- // 预写入 mock model 配置到 storage(在 userScripts 启用之前,SW 仍可用)
99- await bg . evaluate ( ( ) => {
101+
102+ // 在同一 context 内启用 userScripts 权限
103+ const setupPage = await context . newPage ( ) ;
104+ await setupPage . goto ( "chrome://extensions/" ) ;
105+ await setupPage . waitForLoadState ( "domcontentloaded" ) ;
106+ await setupPage . waitForTimeout ( 1_000 ) ;
107+ await setupPage . evaluate ( async ( id ) => {
108+ await ( chrome as any ) . developerPrivate . updateExtensionConfiguration ( {
109+ extensionId : id ,
110+ userScriptsAccess : true ,
111+ } ) ;
112+ } , extensionId ) ;
113+ await setupPage . close ( ) ;
114+
115+ // 启用 userScripts 后 SW 可能会重启,重新获取
116+ let currentBg = context . serviceWorkers ( ) . find ( ( w ) => w . url ( ) . includes ( extensionId ) ) ;
117+ if ( ! currentBg ) {
118+ currentBg = await context . waitForEvent ( "serviceworker" , { timeout : 15_000 } ) ;
119+ }
120+
121+ // 写入 mock model 配置到 storage
122+ await currentBg . evaluate ( ( ) => {
100123 const modelConfig = {
101124 id : "mock-model" ,
102125 name : "Mock LLM" ,
@@ -116,57 +139,15 @@ export const test = base.extend<AgentFixtures>({
116139 } ) ;
117140 } ) ;
118141
119- // 启用 userScripts 权限(会触发扩展重载,SW 可能被终止)
120- await extPage . evaluate ( async ( id ) => {
121- await ( chrome as any ) . developerPrivate . updateExtensionConfiguration ( {
122- extensionId : id ,
123- userScriptsAccess : true ,
124- } ) ;
125- } , extensionId ) ;
126- await extPage . close ( ) ;
127- await ctx1 . close ( ) ;
128-
129- // Phase 2: Relaunch with user scripts enabled
130- const context = await chromium . launchPersistentContext ( userDataDir , {
131- headless : false ,
132- args : [ "--headless=new" , ...chromeArgs ] ,
133- } ) ;
134-
135- // 先注册监听再检查,避免事件在 check 和 listen 之间丢失
136- // 如果 SW 已存在则忽略 promise(防止 dangling promise 在 context 关闭时报错)
137- const swPromise = context . waitForEvent ( "serviceworker" , { timeout : 30_000 } ) . catch ( ( ) => null ) ;
138- let [ bg2 ] = context . serviceWorkers ( ) ;
139- if ( ! bg2 ) bg2 = ( await swPromise ) ! ;
140-
141- ( context as any ) . __extensionId = extensionId ;
142-
143- await use ( context ) ;
144- await context . close ( ) ;
145- fs . rmSync ( userDataDir , { recursive : true , force : true } ) ;
146- } ,
147-
148- extensionId : async ( { context } , use ) => {
149- const extensionId : string = ( context as any ) . __extensionId ;
150-
151- // Dismiss first-use dialog(导航也会唤醒 SW)
152- // 扩展可能还在初始化中,重试导航以应对 ERR_BLOCKED_BY_CLIENT
142+ // 关闭首次使用引导
153143 const initPage = await context . newPage ( ) ;
154- for ( let attempt = 0 ; attempt < 5 ; attempt ++ ) {
155- try {
156- await initPage . goto ( `chrome-extension://${ extensionId } /src/options.html` , {
157- waitUntil : "domcontentloaded" ,
158- timeout : 15_000 ,
159- } ) ;
160- break ;
161- } catch {
162- if ( attempt === 4 ) throw new Error ( "Failed to load options page after 5 attempts" ) ;
163- await initPage . waitForTimeout ( 3_000 ) ;
164- }
165- }
144+ await initPage . goto ( `chrome-extension://${ extensionId } /src/options.html` , {
145+ waitUntil : "domcontentloaded" ,
146+ timeout : 30_000 ,
147+ } ) ;
166148 await initPage . evaluate ( ( ) => localStorage . setItem ( "firstUse" , "false" ) ) ;
167149 await initPage . close ( ) ;
168150
169- // mock model 已在 Phase 1 预写入 storage,Phase 2 SW 启动时缓存自动包含
170151 await use ( extensionId ) ;
171152 } ,
172153
0 commit comments