11import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
2+ import { createExistingSessionAgentSharedModule } from "./existing-session.test-support.js" ;
23import { createBrowserRouteApp , createBrowserRouteResponse } from "./test-helpers.js" ;
3- import type { BrowserRequest } from "./types.js" ;
4-
5- const routeState = vi . hoisted ( ( ) => ( {
6- profileCtx : {
7- profile : {
8- driver : "existing-session" as const ,
9- name : "chrome-live" ,
10- } ,
11- ensureTabAvailable : vi . fn ( async ( ) => ( {
12- targetId : "7" ,
13- url : "https://example.com" ,
14- } ) ) ,
15- } ,
16- tab : {
17- targetId : "7" ,
18- url : "https://example.com" ,
19- } ,
20- } ) ) ;
214
225const chromeMcpMocks = vi . hoisted ( ( ) => ( {
236 clickChromeMcpElement : vi . fn ( async ( ) => { } ) ,
@@ -49,26 +32,7 @@ vi.mock("../chrome-mcp.js", () => ({
4932
5033vi . mock ( "../navigation-guard.js" , ( ) => navigationGuardMocks ) ;
5134
52- vi . mock ( "./agent.shared.js" , ( ) => ( {
53- getPwAiModule : vi . fn ( async ( ) => null ) ,
54- handleRouteError : vi . fn ( ) ,
55- readBody : vi . fn ( ( req : BrowserRequest ) => req . body ?? { } ) ,
56- requirePwAi : vi . fn ( async ( ) => {
57- throw new Error ( "Playwright should not be used for existing-session tests" ) ;
58- } ) ,
59- resolveProfileContext : vi . fn ( ( ) => routeState . profileCtx ) ,
60- resolveTargetIdFromBody : vi . fn ( ( body : Record < string , unknown > ) =>
61- typeof body . targetId === "string" ? body . targetId : undefined ,
62- ) ,
63- withPlaywrightRouteContext : vi . fn ( ) ,
64- withRouteTabContext : vi . fn ( async ( { run } : { run : ( args : unknown ) => Promise < void > } ) => {
65- await run ( {
66- profileCtx : routeState . profileCtx ,
67- cdpUrl : "http://127.0.0.1:18800" ,
68- tab : routeState . tab ,
69- } ) ;
70- } ) ,
71- } ) ) ;
35+ vi . mock ( "./agent.shared.js" , ( ) => createExistingSessionAgentSharedModule ( ) ) ;
7236
7337const DEFAULT_SSRF_POLICY = { allowPrivateNetwork : false } as const ;
7438
@@ -119,6 +83,31 @@ describe("existing-session interaction navigation guard", () => {
11983 return response ;
12084 }
12185
86+ async function expectActionToReject ( body : Record < string , unknown > ) {
87+ const handler = getActPostHandler ( ) ;
88+ const response = createBrowserRouteResponse ( ) ;
89+ const pending = handler ?.( { params : { } , query : { } , body } , response . res ) ?? Promise . resolve ( ) ;
90+ void pending . catch ( ( ) => { } ) ;
91+ const completion = ( async ( ) => {
92+ await vi . runAllTimersAsync ( ) ;
93+ await pending ;
94+ } ) ( ) ;
95+
96+ await expect ( completion ) . rejects . toThrow ( "Unable to verify stable post-interaction navigation" ) ;
97+ }
98+
99+ function expectNavigationProbeUrls ( urls : string [ ] ) {
100+ expect ( navigationGuardMocks . assertBrowserNavigationResultAllowed ) . toHaveBeenCalledTimes (
101+ urls . length ,
102+ ) ;
103+ for ( const [ index , url ] of urls . entries ( ) ) {
104+ expect ( navigationGuardMocks . assertBrowserNavigationResultAllowed ) . toHaveBeenNthCalledWith (
105+ index + 1 ,
106+ expect . objectContaining ( { url } ) ,
107+ ) ;
108+ }
109+ }
110+
122111 it ( "checks navigation after click and key-driven submit paths" , async ( ) => {
123112 const clickResponse = await runAction ( { kind : "click" , ref : "btn-1" } ) ;
124113 const typeResponse = await runAction ( {
@@ -134,31 +123,7 @@ describe("existing-session interaction navigation guard", () => {
134123 expect ( chromeMcpMocks . pressChromeMcpKey ) . toHaveBeenCalledWith (
135124 expect . objectContaining ( { key : "Enter" } ) ,
136125 ) ;
137- expect ( navigationGuardMocks . assertBrowserNavigationResultAllowed ) . toHaveBeenCalledTimes ( 6 ) ;
138- expect ( navigationGuardMocks . assertBrowserNavigationResultAllowed ) . toHaveBeenNthCalledWith (
139- 1 ,
140- expect . objectContaining ( { url : "https://example.com" } ) ,
141- ) ;
142- expect ( navigationGuardMocks . assertBrowserNavigationResultAllowed ) . toHaveBeenNthCalledWith (
143- 2 ,
144- expect . objectContaining ( { url : "https://example.com" } ) ,
145- ) ;
146- expect ( navigationGuardMocks . assertBrowserNavigationResultAllowed ) . toHaveBeenNthCalledWith (
147- 3 ,
148- expect . objectContaining ( { url : "https://example.com" } ) ,
149- ) ;
150- expect ( navigationGuardMocks . assertBrowserNavigationResultAllowed ) . toHaveBeenNthCalledWith (
151- 4 ,
152- expect . objectContaining ( { url : "https://example.com" } ) ,
153- ) ;
154- expect ( navigationGuardMocks . assertBrowserNavigationResultAllowed ) . toHaveBeenNthCalledWith (
155- 5 ,
156- expect . objectContaining ( { url : "https://example.com" } ) ,
157- ) ;
158- expect ( navigationGuardMocks . assertBrowserNavigationResultAllowed ) . toHaveBeenNthCalledWith (
159- 6 ,
160- expect . objectContaining ( { url : "https://example.com" } ) ,
161- ) ;
126+ expectNavigationProbeUrls ( Array . from ( { length : 6 } , ( ) => "https://example.com" ) ) ;
162127 } ) ;
163128
164129 it ( "rechecks the page url after delayed navigation-triggering interactions" , async ( ) => {
@@ -172,18 +137,11 @@ describe("existing-session interaction navigation guard", () => {
172137
173138 expect ( response . statusCode ) . toBe ( 200 ) ;
174139 expect ( chromeMcpMocks . evaluateChromeMcpScript ) . toHaveBeenCalledTimes ( 4 ) ;
175- expect ( navigationGuardMocks . assertBrowserNavigationResultAllowed ) . toHaveBeenNthCalledWith (
176- 1 ,
177- expect . objectContaining ( { url : "https://example.com" } ) ,
178- ) ;
179- expect ( navigationGuardMocks . assertBrowserNavigationResultAllowed ) . toHaveBeenNthCalledWith (
180- 2 ,
181- expect . objectContaining ( { url : "http://169.254.169.254/latest/meta-data/" } ) ,
182- ) ;
183- expect ( navigationGuardMocks . assertBrowserNavigationResultAllowed ) . toHaveBeenNthCalledWith (
184- 3 ,
185- expect . objectContaining ( { url : "http://169.254.169.254/latest/meta-data/" } ) ,
186- ) ;
140+ expectNavigationProbeUrls ( [
141+ "https://example.com" ,
142+ "http://169.254.169.254/latest/meta-data/" ,
143+ "http://169.254.169.254/latest/meta-data/" ,
144+ ] ) ;
187145 } ) ;
188146
189147 it ( "fails closed when location probes never return a usable url" , async ( ) => {
@@ -193,20 +151,7 @@ describe("existing-session interaction navigation guard", () => {
193151 . mockResolvedValueOnce ( null as never )
194152 . mockResolvedValueOnce ( " " as never ) ;
195153
196- const handler = getActPostHandler ( ) ;
197- const response = createBrowserRouteResponse ( ) ;
198- const pending =
199- handler ?.(
200- { params : { } , query : { } , body : { kind : "evaluate" , fn : "() => 1" } } ,
201- response . res ,
202- ) ?? Promise . resolve ( ) ;
203- void pending . catch ( ( ) => { } ) ;
204- const completion = ( async ( ) => {
205- await vi . runAllTimersAsync ( ) ;
206- await pending ;
207- } ) ( ) ;
208-
209- await expect ( completion ) . rejects . toThrow ( "Unable to verify stable post-interaction navigation" ) ;
154+ await expectActionToReject ( { kind : "evaluate" , fn : "() => 1" } ) ;
210155 expect ( navigationGuardMocks . assertBrowserNavigationResultAllowed ) . not . toHaveBeenCalled ( ) ;
211156 } ) ;
212157
@@ -218,24 +163,8 @@ describe("existing-session interaction navigation guard", () => {
218163 . mockResolvedValueOnce ( undefined as never ) // location probe 3 - unreadable
219164 . mockResolvedValueOnce ( undefined as never ) ; // follow-up probe - still unreadable
220165
221- const handler = getActPostHandler ( ) ;
222- const response = createBrowserRouteResponse ( ) ;
223- const pending =
224- handler ?.(
225- { params : { } , query : { } , body : { kind : "evaluate" , fn : "() => 1" } } ,
226- response . res ,
227- ) ?? Promise . resolve ( ) ;
228- void pending . catch ( ( ) => { } ) ;
229- const completion = ( async ( ) => {
230- await vi . runAllTimersAsync ( ) ;
231- await pending ;
232- } ) ( ) ;
233-
234- await expect ( completion ) . rejects . toThrow ( "Unable to verify stable post-interaction navigation" ) ;
235- expect ( navigationGuardMocks . assertBrowserNavigationResultAllowed ) . toHaveBeenCalledOnce ( ) ;
236- expect ( navigationGuardMocks . assertBrowserNavigationResultAllowed ) . toHaveBeenCalledWith (
237- expect . objectContaining ( { url : "https://example.com" } ) ,
238- ) ;
166+ await expectActionToReject ( { kind : "evaluate" , fn : "() => 1" } ) ;
167+ expectNavigationProbeUrls ( [ "https://example.com" ] ) ;
239168 } ) ;
240169
241170 it ( "confirms stability via follow-up probe when URL changes on the last loop iteration" , async ( ) => {
@@ -256,19 +185,11 @@ describe("existing-session interaction navigation guard", () => {
256185 expect ( response . statusCode ) . toBe ( 200 ) ;
257186 // 1 action call + 5 location probes (3 in loop + 1 failed + 1 follow-up)
258187 expect ( chromeMcpMocks . evaluateChromeMcpScript ) . toHaveBeenCalledTimes ( 5 ) ;
259- expect ( navigationGuardMocks . assertBrowserNavigationResultAllowed ) . toHaveBeenCalledTimes ( 3 ) ;
260- expect ( navigationGuardMocks . assertBrowserNavigationResultAllowed ) . toHaveBeenNthCalledWith (
261- 1 ,
262- expect . objectContaining ( { url : "https://example.com" } ) ,
263- ) ;
264- expect ( navigationGuardMocks . assertBrowserNavigationResultAllowed ) . toHaveBeenNthCalledWith (
265- 2 ,
266- expect . objectContaining ( { url : "https://safe-redirect.com" } ) ,
267- ) ;
268- expect ( navigationGuardMocks . assertBrowserNavigationResultAllowed ) . toHaveBeenNthCalledWith (
269- 3 ,
270- expect . objectContaining ( { url : "https://safe-redirect.com" } ) ,
271- ) ;
188+ expectNavigationProbeUrls ( [
189+ "https://example.com" ,
190+ "https://safe-redirect.com" ,
191+ "https://safe-redirect.com" ,
192+ ] ) ;
272193 } ) ;
273194
274195 it ( "keeps probing through the full window before declaring navigation stable" , async ( ) => {
@@ -283,23 +204,12 @@ describe("existing-session interaction navigation guard", () => {
283204
284205 expect ( response . statusCode ) . toBe ( 200 ) ;
285206 expect ( chromeMcpMocks . evaluateChromeMcpScript ) . toHaveBeenCalledTimes ( 5 ) ;
286- expect ( navigationGuardMocks . assertBrowserNavigationResultAllowed ) . toHaveBeenCalledTimes ( 4 ) ;
287- expect ( navigationGuardMocks . assertBrowserNavigationResultAllowed ) . toHaveBeenNthCalledWith (
288- 1 ,
289- expect . objectContaining ( { url : "https://example.com" } ) ,
290- ) ;
291- expect ( navigationGuardMocks . assertBrowserNavigationResultAllowed ) . toHaveBeenNthCalledWith (
292- 2 ,
293- expect . objectContaining ( { url : "https://example.com" } ) ,
294- ) ;
295- expect ( navigationGuardMocks . assertBrowserNavigationResultAllowed ) . toHaveBeenNthCalledWith (
296- 3 ,
297- expect . objectContaining ( { url : "https://safe-redirect.com" } ) ,
298- ) ;
299- expect ( navigationGuardMocks . assertBrowserNavigationResultAllowed ) . toHaveBeenNthCalledWith (
300- 4 ,
301- expect . objectContaining ( { url : "https://safe-redirect.com" } ) ,
302- ) ;
207+ expectNavigationProbeUrls ( [
208+ "https://example.com" ,
209+ "https://example.com" ,
210+ "https://safe-redirect.com" ,
211+ "https://safe-redirect.com" ,
212+ ] ) ;
303213 } ) ;
304214
305215 it ( "fails closed when follow-up probe sees yet another URL change" , async ( ) => {
@@ -310,20 +220,7 @@ describe("existing-session interaction navigation guard", () => {
310220 . mockResolvedValueOnce ( "https://c.com" as never ) // location probe 3: changed again
311221 . mockResolvedValueOnce ( "https://d.com" as never ) ; // follow-up: still changing
312222
313- const handler = getActPostHandler ( ) ;
314- const response = createBrowserRouteResponse ( ) ;
315- const pending =
316- handler ?.(
317- { params : { } , query : { } , body : { kind : "evaluate" , fn : "() => 1" } } ,
318- response . res ,
319- ) ?? Promise . resolve ( ) ;
320- void pending . catch ( ( ) => { } ) ;
321- const completion = ( async ( ) => {
322- await vi . runAllTimersAsync ( ) ;
323- await pending ;
324- } ) ( ) ;
325-
326- await expect ( completion ) . rejects . toThrow ( "Unable to verify stable post-interaction navigation" ) ;
223+ await expectActionToReject ( { kind : "evaluate" , fn : "() => 1" } ) ;
327224 } ) ;
328225
329226 it ( "fails closed when a probe error follows two stable reads" , async ( ) => {
@@ -336,21 +233,8 @@ describe("existing-session interaction navigation guard", () => {
336233 . mockRejectedValueOnce ( new Error ( "context destroyed" ) as never ) // location probe 3 → error
337234 . mockRejectedValueOnce ( new Error ( "context destroyed" ) as never ) ; // follow-up → still errored
338235
339- const handler = getActPostHandler ( ) ;
340- const response = createBrowserRouteResponse ( ) ;
341- const pending =
342- handler ?.(
343- { params : { } , query : { } , body : { kind : "evaluate" , fn : "() => 1" } } ,
344- response . res ,
345- ) ?? Promise . resolve ( ) ;
346- void pending . catch ( ( ) => { } ) ;
347- const completion = ( async ( ) => {
348- await vi . runAllTimersAsync ( ) ;
349- await pending ;
350- } ) ( ) ;
351-
352- await expect ( completion ) . rejects . toThrow ( "Unable to verify stable post-interaction navigation" ) ;
353- expect ( navigationGuardMocks . assertBrowserNavigationResultAllowed ) . toHaveBeenCalledTimes ( 2 ) ;
236+ await expectActionToReject ( { kind : "evaluate" , fn : "() => 1" } ) ;
237+ expectNavigationProbeUrls ( [ "https://example.com" , "https://example.com" ] ) ;
354238 } ) ;
355239
356240 it ( "skips the guard when no SSRF policy is configured" , async ( ) => {
0 commit comments