From b93fc7e8f6b1300f58728ee365d2646dfaedfcce Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 2 Aug 2026 16:32:33 -0700 Subject: [PATCH] test(browser): consolidate navigation security fixtures (#118240) --- .../pw-tools-core.browser-ssrf-guard.test.ts | 917 +++++++----------- ...core.interactions.navigation-guard.test.ts | 152 +-- 2 files changed, 407 insertions(+), 662 deletions(-) diff --git a/extensions/browser/src/browser/pw-tools-core.browser-ssrf-guard.test.ts b/extensions/browser/src/browser/pw-tools-core.browser-ssrf-guard.test.ts index c415a808df02..e7db5ee7f9ee 100644 --- a/extensions/browser/src/browser/pw-tools-core.browser-ssrf-guard.test.ts +++ b/extensions/browser/src/browser/pw-tools-core.browser-ssrf-guard.test.ts @@ -11,6 +11,17 @@ const pageState = vi.hoisted(() => ({ locator: null as Record | null, })); +type NavigationGuardCall = { + action: (url: string) => Promise; + onPolicyCheckStarted?: (check: Promise) => void; + onPolicyDenied?: (event: { + state: "detected" | "handled"; + error: unknown; + sourcePreserved?: boolean; + }) => void; + page: { url: () => string }; +}; + const sessionMocks = vi.hoisted(() => ({ assertPageNavigationCompletedSafely: vi.fn(async () => {}), closeBlockedNavigationTarget: vi.fn(async () => {}), @@ -37,13 +48,7 @@ const sessionMocks = vi.hoisted(() => ({ storeRoleRefsForTarget: vi.fn(() => {}), wasBrowserNavigationSourcePreservedAfterPolicyDenial: vi.fn((_err: unknown) => false), withPageNavigationRequestGuard: vi.fn( - async ({ - action, - page, - }: { - action: (url: string) => Promise; - page: { url: () => string }; - }) => await action(page.url()), + async ({ action, page }: NavigationGuardCall) => await action(page.url()), ), })); @@ -61,6 +66,57 @@ vi.mock("./pw-session.page-cdp.js", () => pageCdpMocks); const interactions = await import("./pw-tools-core.interactions.js"); const snapshots = await import("./pw-tools-core.snapshot.js"); +const strictNavigationOptions = () => + ({ + cdpUrl: "http://127.0.0.1:18792", + targetId: "tab-1", + ssrfPolicy: { allowPrivateNetwork: false }, + }) as const; + +const proxiedNavigationOptions = () => + ({ + ...strictNavigationOptions(), + browserProxyMode: "explicit-browser-proxy", + }) as const; + +function completedNavigationExpectation(proxied = false) { + return { + ...strictNavigationOptions(), + page: pageState.page, + response: null, + ...(proxied ? { browserProxyMode: "explicit-browser-proxy" as const } : {}), + }; +} + +function installInteractionPage( + page: Record, + locator: Record, +): void { + pageState.page = page; + pageState.locator = locator; +} + +function mockNavigationGuardOnce( + implementation: (args: NavigationGuardCall) => Promise, +): void { + sessionMocks.withPageNavigationRequestGuard.mockImplementationOnce(implementation); +} + +function createDeferred() { + let resolve!: (value: T | PromiseLike) => void; + let reject!: (reason?: unknown) => void; + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise; + reject = rejectPromise; + }); + return { promise, reject, resolve }; +} + +async function withFakeTimers(run: () => Promise): Promise { + vi.useFakeTimers(); + await run().finally(() => vi.useRealTimers()); +} + function createSnapshotPage(overrides: Record) { const mainFrame = {}; return { @@ -85,27 +141,23 @@ describe("pw-tools-core browser SSRF guards", () => { it("re-checks click-triggered navigations with the session safety helper", async () => { let currentUrl = "https://example.com"; - pageState.page = { url: vi.fn(() => currentUrl) }; - pageState.locator = { - click: vi.fn(async () => { - currentUrl = "https://target.example"; - }), - }; + installInteractionPage( + { url: vi.fn(() => currentUrl) }, + { + click: vi.fn(async () => { + currentUrl = "https://target.example"; + }), + }, + ); await interactions.clickViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...strictNavigationOptions(), ref: "1", - ssrfPolicy: { allowPrivateNetwork: false }, }); - expect(sessionMocks.assertPageNavigationCompletedSafely).toHaveBeenCalledWith({ - cdpUrl: "http://127.0.0.1:18792", - page: pageState.page, - response: null, - ssrfPolicy: { allowPrivateNetwork: false }, - targetId: "tab-1", - }); + expect(sessionMocks.assertPageNavigationCompletedSafely).toHaveBeenCalledWith( + completedNavigationExpectation(), + ); }); it.each([ @@ -114,11 +166,8 @@ describe("pw-tools-core browser SSRF guards", () => { method: "hover", run: async () => await interactions.hoverViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...proxiedNavigationOptions(), ref: "1", - ssrfPolicy: { allowPrivateNetwork: false }, - browserProxyMode: "explicit-browser-proxy", }), }, { @@ -126,12 +175,9 @@ describe("pw-tools-core browser SSRF guards", () => { method: "dragTo", run: async () => await interactions.dragViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...proxiedNavigationOptions(), startRef: "1", endRef: "2", - ssrfPolicy: { allowPrivateNetwork: false }, - browserProxyMode: "explicit-browser-proxy", }), }, { @@ -139,23 +185,22 @@ describe("pw-tools-core browser SSRF guards", () => { method: "scrollIntoViewIfNeeded", run: async () => await interactions.scrollIntoViewViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...proxiedNavigationOptions(), ref: "1", - ssrfPolicy: { allowPrivateNetwork: false }, - browserProxyMode: "explicit-browser-proxy", }), }, ])( "guards $name document requests and runs the canonical post-check", async ({ method, run }) => { let currentUrl = "https://example.com"; - pageState.page = { url: vi.fn(() => currentUrl) }; - pageState.locator = { - [method]: vi.fn(async () => { - currentUrl = "https://93.184.216.34/target"; - }), - }; + installInteractionPage( + { url: vi.fn(() => currentUrl) }, + { + [method]: vi.fn(async () => { + currentUrl = "https://93.184.216.34/target"; + }), + }, + ); await run(); @@ -167,14 +212,9 @@ describe("pw-tools-core browser SSRF guards", () => { ssrfPolicy: { allowPrivateNetwork: false }, browserProxyMode: "explicit-browser-proxy", }); - expect(sessionMocks.assertPageNavigationCompletedSafely).toHaveBeenCalledWith({ - cdpUrl: "http://127.0.0.1:18792", - page: pageState.page, - response: null, - ssrfPolicy: { allowPrivateNetwork: false }, - browserProxyMode: "explicit-browser-proxy", - targetId: "tab-1", - }); + expect(sessionMocks.assertPageNavigationCompletedSafely).toHaveBeenCalledWith( + completedNavigationExpectation(true), + ); }, ); @@ -183,93 +223,69 @@ describe("pw-tools-core browser SSRF guards", () => { name: "click", run: async () => await interactions.clickViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...proxiedNavigationOptions(), ref: "1", - ssrfPolicy: { allowPrivateNetwork: false }, - browserProxyMode: "explicit-browser-proxy", }), }, { name: "type", run: async () => await interactions.typeViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...proxiedNavigationOptions(), ref: "1", text: "value", - ssrfPolicy: { allowPrivateNetwork: false }, - browserProxyMode: "explicit-browser-proxy", }), }, { name: "type-submit", run: async () => await interactions.typeViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...proxiedNavigationOptions(), ref: "1", text: "value", submit: true, - ssrfPolicy: { allowPrivateNetwork: false }, - browserProxyMode: "explicit-browser-proxy", }), }, { name: "press", run: async () => await interactions.pressKeyViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...proxiedNavigationOptions(), key: "Enter", - ssrfPolicy: { allowPrivateNetwork: false }, - browserProxyMode: "explicit-browser-proxy", }), }, { name: "select", run: async () => await interactions.selectOptionViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...proxiedNavigationOptions(), ref: "1", values: ["one"], - ssrfPolicy: { allowPrivateNetwork: false }, - browserProxyMode: "explicit-browser-proxy", }), }, { name: "fill", run: async () => await interactions.fillFormViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...proxiedNavigationOptions(), fields: [{ ref: "1", type: "text", value: "value" }], - ssrfPolicy: { allowPrivateNetwork: false }, - browserProxyMode: "explicit-browser-proxy", }), }, { name: "evaluate", run: async () => await interactions.evaluateViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...proxiedNavigationOptions(), fn: "() => true", - ssrfPolicy: { allowPrivateNetwork: false }, - browserProxyMode: "explicit-browser-proxy", }), }, { name: "evaluate-ref", run: async () => await interactions.evaluateViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...proxiedNavigationOptions(), ref: "1", fn: "(el) => Boolean(el)", - ssrfPolicy: { allowPrivateNetwork: false }, - browserProxyMode: "explicit-browser-proxy", }), }, ])("guards $name document requests and preserves proxy policy", async ({ run }) => { @@ -277,22 +293,24 @@ describe("pw-tools-core browser SSRF guards", () => { const navigate = vi.fn(async () => { currentUrl = "https://93.184.216.34/target"; }); - pageState.page = { - url: vi.fn(() => currentUrl), - mouse: { click: navigate }, - keyboard: { press: navigate }, - evaluate: navigate, - evaluateHandle: vi.fn(async () => ({ dispose: vi.fn(async () => {}) })), - waitForFunction: navigate, - }; - pageState.locator = { - click: navigate, - fill: navigate, - press: navigate, - selectOption: navigate, - setChecked: navigate, - evaluate: navigate, - }; + installInteractionPage( + { + url: vi.fn(() => currentUrl), + mouse: { click: navigate }, + keyboard: { press: navigate }, + evaluate: navigate, + evaluateHandle: vi.fn(async () => ({ dispose: vi.fn(async () => {}) })), + waitForFunction: navigate, + }, + { + click: navigate, + fill: navigate, + press: navigate, + selectOption: navigate, + setChecked: navigate, + evaluate: navigate, + }, + ); await run(); @@ -304,31 +322,18 @@ describe("pw-tools-core browser SSRF guards", () => { ssrfPolicy: { allowPrivateNetwork: false }, browserProxyMode: "explicit-browser-proxy", }); - expect(sessionMocks.assertPageNavigationCompletedSafely).toHaveBeenLastCalledWith({ - cdpUrl: "http://127.0.0.1:18792", - page: pageState.page, - response: null, - ssrfPolicy: { allowPrivateNetwork: false }, - browserProxyMode: "explicit-browser-proxy", - targetId: "tab-1", - }); + expect(sessionMocks.assertPageNavigationCompletedSafely).toHaveBeenLastCalledWith( + completedNavigationExpectation(true), + ); }); it("guards executable wait predicates and preserves proxy policy", async () => { let currentUrl = "https://example.com"; const order: string[] = []; - sessionMocks.withPageNavigationRequestGuard.mockImplementationOnce( - async ({ - action, - page, - }: { - action: (url: string) => Promise; - page: { url: () => string }; - }) => { - order.push("guard"); - return await action(page.url()); - }, - ); + mockNavigationGuardOnce(async ({ action, page }) => { + order.push("guard"); + return await action(page.url()); + }); const documentHandle = { dispose: vi.fn(async () => {}) }; const waitForFunction = vi.fn( async ( @@ -350,12 +355,9 @@ describe("pw-tools-core browser SSRF guards", () => { }; await interactions.waitForViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...proxiedNavigationOptions(), timeMs: 1, fn: "() => true", - ssrfPolicy: { allowPrivateNetwork: false }, - browserProxyMode: "explicit-browser-proxy", }); expect(waitForFunction).toHaveBeenCalledWith( @@ -372,14 +374,9 @@ describe("pw-tools-core browser SSRF guards", () => { ssrfPolicy: { allowPrivateNetwork: false }, browserProxyMode: "explicit-browser-proxy", }); - expect(sessionMocks.assertPageNavigationCompletedSafely).toHaveBeenLastCalledWith({ - cdpUrl: "http://127.0.0.1:18792", - page: pageState.page, - response: null, - ssrfPolicy: { allowPrivateNetwork: false }, - browserProxyMode: "explicit-browser-proxy", - targetId: "tab-1", - }); + expect(sessionMocks.assertPageNavigationCompletedSafely).toHaveBeenLastCalledWith( + completedNavigationExpectation(true), + ); }); it("preserves declared async wait predicates", async () => { @@ -392,10 +389,8 @@ describe("pw-tools-core browser SSRF guards", () => { }; await interactions.waitForViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...strictNavigationOptions(), fn: "async () => true", - ssrfPolicy: { allowPrivateNetwork: false }, }); expect(waitForFunction).toHaveBeenCalledOnce(); @@ -421,10 +416,8 @@ describe("pw-tools-core browser SSRF guards", () => { }; await interactions.waitForViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...strictNavigationOptions(), fn: "() => Promise.resolve(true)", - ssrfPolicy: { allowPrivateNetwork: false }, }); expect(sessionMocks.closeBlockedNavigationTarget).not.toHaveBeenCalled(); @@ -446,10 +439,8 @@ describe("pw-tools-core browser SSRF guards", () => { await expect( interactions.waitForViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...strictNavigationOptions(), fn: "() => document.cookie", - ssrfPolicy: { allowPrivateNetwork: false }, }), ).rejects.toThrow("Wait predicate document changed"); @@ -470,12 +461,10 @@ describe("pw-tools-core browser SSRF guards", () => { await expect( interactions.waitForViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...strictNavigationOptions(), timeMs: 1, fn: "() => true", signal: ctrl.signal, - ssrfPolicy: { allowPrivateNetwork: false }, }), ).rejects.toThrow("aborted during passive wait"); await Promise.resolve(); @@ -500,11 +489,9 @@ describe("pw-tools-core browser SSRF guards", () => { await expect( interactions.waitForViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...strictNavigationOptions(), fn: "() => true", signal: ctrl.signal, - ssrfPolicy: { allowPrivateNetwork: false }, }), ).rejects.toThrow("aborted during document capture"); @@ -514,114 +501,79 @@ describe("pw-tools-core browser SSRF guards", () => { it("keeps the request guard alive until an aborted hover actually settles", async () => { const ctrl = new AbortController(); - let hoverStarted!: () => void; - let releaseHover!: () => void; - const started = new Promise((resolve) => { - hoverStarted = resolve; - }); - const pendingHover = new Promise((resolve) => { - releaseHover = resolve; - }); + const started = createDeferred(); + const hover = createDeferred(); let guardSettled = false; - pageState.page = { url: vi.fn(() => "https://example.com") }; - pageState.locator = { - hover: vi.fn(() => { - hoverStarted(); - return pendingHover; - }), - }; - sessionMocks.withPageNavigationRequestGuard.mockImplementationOnce( - async ({ - action, - page, - }: { - action: (url: string) => Promise; - page: { url: () => string }; - }) => { - try { - return await action(page.url()); - } finally { - guardSettled = true; - } + installInteractionPage( + { url: vi.fn(() => "https://example.com") }, + { + hover: vi.fn(() => { + started.resolve(); + return hover.promise; + }), }, ); + mockNavigationGuardOnce(async ({ action, page }) => { + try { + return await action(page.url()); + } finally { + guardSettled = true; + } + }); const task = interactions.hoverViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...strictNavigationOptions(), ref: "1", - ssrfPolicy: { allowPrivateNetwork: false }, signal: ctrl.signal, }); - await started; + await started.promise; ctrl.abort(new Error("aborted by test")); await expect(task).rejects.toThrow("aborted by test"); expect(guardSettled).toBe(false); - releaseHover(); + hover.resolve(); await vi.waitFor(() => expect(guardSettled).toBe(true)); }); it("lets a request-policy denial observed before abort win", async () => { const ctrl = new AbortController(); - let releaseHover!: () => void; - const pendingHover = new Promise((resolve) => { - releaseHover = resolve; - }); - let policyObserved!: () => void; - const observed = new Promise((resolve) => { - policyObserved = resolve; - }); - let releaseFulfill!: () => void; - const pendingFulfill = new Promise((resolve) => { - releaseFulfill = resolve; - }); + const hover = createDeferred(); + const observed = createDeferred(); + const fulfill = createDeferred(); const blocked = new Error("browser navigation blocked by policy"); blocked.name = "SsrFBlockedError"; let guardSettled = false; - pageState.page = { url: vi.fn(() => "about:blank") }; - pageState.locator = { hover: vi.fn(() => pendingHover) }; + installInteractionPage( + { url: vi.fn(() => "about:blank") }, + { + hover: vi.fn(() => hover.promise), + }, + ); sessionMocks.isPolicyDenyNavigationError.mockImplementationOnce( (err: unknown) => err === blocked, ); sessionMocks.wasBrowserNavigationSourcePreservedAfterPolicyDenial.mockReturnValueOnce(true); - sessionMocks.withPageNavigationRequestGuard.mockImplementationOnce( - async ({ - action, - onPolicyDenied, - page, - }: { - action: (url: string) => Promise; - onPolicyDenied?: (event: { - state: "detected" | "handled"; - error: unknown; - sourcePreserved?: boolean; - }) => void; - page: { url: () => string }; - }) => { - const actionTask = action(page.url()); - onPolicyDenied?.({ state: "detected", error: blocked }); - policyObserved(); - await pendingFulfill; - onPolicyDenied?.({ state: "handled", error: blocked, sourcePreserved: true }); - try { - await actionTask; - throw blocked; - } finally { - guardSettled = true; - } - }, - ); + mockNavigationGuardOnce(async ({ action, onPolicyDenied, page }) => { + const actionTask = action(page.url()); + onPolicyDenied?.({ state: "detected", error: blocked }); + observed.resolve(); + await fulfill.promise; + onPolicyDenied?.({ state: "handled", error: blocked, sourcePreserved: true }); + try { + await actionTask; + throw blocked; + } finally { + guardSettled = true; + } + }); const task = interactions.hoverViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...strictNavigationOptions(), ref: "1", - ssrfPolicy: { allowPrivateNetwork: false }, signal: ctrl.signal, }); - await observed; + await observed.promise; ctrl.abort(new Error("aborted after policy denial")); let settled = false; @@ -632,75 +584,52 @@ describe("pw-tools-core browser SSRF guards", () => { .catch(() => {}); await Promise.resolve(); expect(settled).toBe(false); - releaseFulfill(); + fulfill.resolve(); await Promise.resolve(); expect(settled).toBe(false); expect(guardSettled).toBe(false); - releaseHover(); + hover.resolve(); await expect(task).rejects.toBe(blocked); await vi.waitFor(() => expect(guardSettled).toBe(true)); }); it("waits for an in-flight policy decision before returning abort", async () => { const ctrl = new AbortController(); - let releaseHover!: () => void; - const pendingHover = new Promise((resolve) => { - releaseHover = resolve; - }); - let rejectPolicy!: (err: unknown) => void; - const policyPending = new Promise((_resolve, reject) => { - rejectPolicy = reject; - }); - let policyStarted!: () => void; - const started = new Promise((resolve) => { - policyStarted = resolve; - }); + const hover = createDeferred(); + const policy = createDeferred(); + const started = createDeferred(); const blocked = new Error("browser navigation blocked by policy"); blocked.name = "SsrFBlockedError"; - pageState.page = { url: vi.fn(() => "about:blank") }; - pageState.locator = { hover: vi.fn(() => pendingHover) }; + installInteractionPage( + { url: vi.fn(() => "about:blank") }, + { + hover: vi.fn(() => hover.promise), + }, + ); sessionMocks.isPolicyDenyNavigationError.mockImplementation((err: unknown) => err === blocked); sessionMocks.wasBrowserNavigationSourcePreservedAfterPolicyDenial.mockImplementation( (err: unknown) => err === blocked, ); - sessionMocks.withPageNavigationRequestGuard.mockImplementationOnce( - async ({ - action, - onPolicyCheckStarted, - onPolicyDenied, - page, - }: { - action: (url: string) => Promise; - onPolicyCheckStarted?: (check: Promise) => void; - onPolicyDenied?: (event: { - state: "detected" | "handled"; - error: unknown; - sourcePreserved?: boolean; - }) => void; - page: { url: () => string }; - }) => { - const actionTask = action(page.url()); - onPolicyCheckStarted?.(policyPending); - policyStarted(); - try { - await policyPending; - } catch (err) { - onPolicyDenied?.({ state: "detected", error: err }); - onPolicyDenied?.({ state: "handled", error: err, sourcePreserved: true }); - } - await actionTask; - throw blocked; - }, - ); + mockNavigationGuardOnce(async ({ action, onPolicyCheckStarted, onPolicyDenied, page }) => { + const actionTask = action(page.url()); + onPolicyCheckStarted?.(policy.promise); + started.resolve(); + try { + await policy.promise; + } catch (err) { + onPolicyDenied?.({ state: "detected", error: err }); + onPolicyDenied?.({ state: "handled", error: err, sourcePreserved: true }); + } + await actionTask; + throw blocked; + }); const task = interactions.hoverViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...strictNavigationOptions(), ref: "1", - ssrfPolicy: { allowPrivateNetwork: false }, signal: ctrl.signal, }); - await started; + await started.promise; ctrl.abort(new Error("aborted while policy pending")); let settled = false; void task @@ -711,10 +640,10 @@ describe("pw-tools-core browser SSRF guards", () => { await Promise.resolve(); expect(settled).toBe(false); - rejectPolicy(blocked); + policy.reject(blocked); await Promise.resolve(); expect(settled).toBe(false); - releaseHover(); + hover.resolve(); await expect(task).rejects.toBe(blocked); sessionMocks.isPolicyDenyNavigationError.mockImplementation(() => false); sessionMocks.wasBrowserNavigationSourcePreservedAfterPolicyDenial.mockImplementation( @@ -724,46 +653,29 @@ describe("pw-tools-core browser SSRF guards", () => { it("returns abort once an in-flight policy decision allows the request", async () => { const ctrl = new AbortController(); - let releaseHover!: () => void; - const pendingHover = new Promise((resolve) => { - releaseHover = resolve; - }); - let allowPolicy!: () => void; - const policyPending = new Promise((resolve) => { - allowPolicy = resolve; - }); - let policyStarted!: () => void; - const started = new Promise((resolve) => { - policyStarted = resolve; - }); - pageState.page = { url: vi.fn(() => "about:blank") }; - pageState.locator = { hover: vi.fn(() => pendingHover) }; - sessionMocks.withPageNavigationRequestGuard.mockImplementationOnce( - async ({ - action, - onPolicyCheckStarted, - page, - }: { - action: (url: string) => Promise; - onPolicyCheckStarted?: (check: Promise) => void; - page: { url: () => string }; - }) => { - const actionTask = action(page.url()); - onPolicyCheckStarted?.(policyPending); - policyStarted(); - await policyPending; - return await actionTask; + const hover = createDeferred(); + const policy = createDeferred(); + const started = createDeferred(); + installInteractionPage( + { url: vi.fn(() => "about:blank") }, + { + hover: vi.fn(() => hover.promise), }, ); + mockNavigationGuardOnce(async ({ action, onPolicyCheckStarted, page }) => { + const actionTask = action(page.url()); + onPolicyCheckStarted?.(policy.promise); + started.resolve(); + await policy.promise; + return await actionTask; + }); const task = interactions.hoverViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...strictNavigationOptions(), ref: "1", - ssrfPolicy: { allowPrivateNetwork: false }, signal: ctrl.signal, }); - await started; + await started.promise; ctrl.abort(new Error("aborted while policy pending")); let settled = false; void task @@ -774,68 +686,47 @@ describe("pw-tools-core browser SSRF guards", () => { await Promise.resolve(); expect(settled).toBe(false); - allowPolicy(); + policy.resolve(); await expect(task).rejects.toThrow("aborted while policy pending"); - releaseHover(); + hover.resolve(); }); it("quarantines immediately when a preserved denied source later becomes unsafe", async () => { const ctrl = new AbortController(); - let releaseHover!: () => void; - const pendingHover = new Promise((resolve) => { - releaseHover = resolve; - }); - let reportUnsafe!: () => void; - const unsafeReported = new Promise((resolve) => { - reportUnsafe = resolve; - }); - let policyDetected!: () => void; - const detected = new Promise((resolve) => { - policyDetected = resolve; - }); + const hover = createDeferred(); + const unsafeReported = createDeferred(); + const detected = createDeferred(); const blocked = new Error("browser navigation blocked by policy"); blocked.name = "SsrFBlockedError"; - pageState.page = { url: vi.fn(() => "about:blank") }; - pageState.locator = { hover: vi.fn(() => pendingHover) }; + installInteractionPage( + { url: vi.fn(() => "about:blank") }, + { + hover: vi.fn(() => hover.promise), + }, + ); sessionMocks.isPolicyDenyNavigationError.mockImplementation((err: unknown) => err === blocked); sessionMocks.wasBrowserNavigationSourcePreservedAfterPolicyDenial.mockImplementation( (err: unknown) => err === blocked, ); - sessionMocks.withPageNavigationRequestGuard.mockImplementationOnce( - async ({ - action, - onPolicyDenied, - page, - }: { - action: (url: string) => Promise; - onPolicyDenied?: (event: { - state: "detected" | "handled"; - error: unknown; - sourcePreserved?: boolean; - }) => void; - page: { url: () => string }; - }) => { - const actionTask = action(page.url()); - onPolicyDenied?.({ state: "detected", error: blocked }); - policyDetected(); - onPolicyDenied?.({ state: "handled", error: blocked, sourcePreserved: true }); - await unsafeReported; - onPolicyDenied?.({ state: "handled", error: blocked, sourcePreserved: false }); - await actionTask; - throw blocked; - }, - ); + mockNavigationGuardOnce(async ({ action, onPolicyDenied, page }) => { + const actionTask = action(page.url()); + onPolicyDenied?.({ state: "detected", error: blocked }); + detected.resolve(); + onPolicyDenied?.({ state: "handled", error: blocked, sourcePreserved: true }); + await unsafeReported.promise; + onPolicyDenied?.({ state: "handled", error: blocked, sourcePreserved: false }); + await actionTask; + throw blocked; + }); const task = interactions.hoverViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...strictNavigationOptions(), ref: "1", - ssrfPolicy: { allowPrivateNetwork: false }, signal: ctrl.signal, }); - await detected; + await detected.promise; ctrl.abort(new Error("aborted after policy denial")); - reportUnsafe(); + unsafeReported.resolve(); await vi.waitFor(() => expect(sessionMocks.quarantineBlockedNavigationTarget).toHaveBeenCalledWith({ @@ -844,7 +735,7 @@ describe("pw-tools-core browser SSRF guards", () => { targetId: "tab-1", }), ); - releaseHover(); + hover.resolve(); await expect(task).rejects.toBe(blocked); sessionMocks.isPolicyDenyNavigationError.mockImplementation(() => false); sessionMocks.wasBrowserNavigationSourcePreservedAfterPolicyDenial.mockImplementation( @@ -853,8 +744,7 @@ describe("pw-tools-core browser SSRF guards", () => { }); it("keeps the request guard for the full grace after an early safe post-check", async () => { - vi.useFakeTimers(); - try { + await withFakeTimers(async () => { let currentUrl = "https://example.com"; let guardSettled = false; pageState.page = { url: vi.fn(() => currentUrl) }; @@ -863,27 +753,17 @@ describe("pw-tools-core browser SSRF guards", () => { currentUrl = "https://example.org"; }), }; - sessionMocks.withPageNavigationRequestGuard.mockImplementationOnce( - async ({ - action, - page, - }: { - action: (url: string) => Promise; - page: { url: () => string }; - }) => { - try { - return await action(page.url()); - } finally { - guardSettled = true; - } - }, - ); + mockNavigationGuardOnce(async ({ action, page }) => { + try { + return await action(page.url()); + } finally { + guardSettled = true; + } + }); const task = interactions.hoverViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...strictNavigationOptions(), ref: "1", - ssrfPolicy: { allowPrivateNetwork: false }, }); await vi.advanceTimersByTimeAsync(249); @@ -891,14 +771,11 @@ describe("pw-tools-core browser SSRF guards", () => { await vi.advanceTimersByTimeAsync(1); await task; expect(guardSettled).toBe(true); - } finally { - vi.useRealTimers(); - } + }); }); it("does not add a navigation grace without a policy", async () => { - vi.useFakeTimers(); - try { + await withFakeTimers(async () => { pageState.page = { url: vi.fn(() => "about:blank") }; pageState.locator = { hover: vi.fn(async () => {}) }; let settled = false; @@ -916,58 +793,42 @@ describe("pw-tools-core browser SSRF guards", () => { await vi.advanceTimersByTimeAsync(0); await task; expect(settled).toBe(true); - } finally { - vi.useRealTimers(); - } + }); }); it("quarantines a late unpreserved policy failure after abort already returned", async () => { const ctrl = new AbortController(); - let hoverStarted!: () => void; - let releaseHover!: () => void; - const started = new Promise((resolve) => { - hoverStarted = resolve; - }); - const pendingHover = new Promise((resolve) => { - releaseHover = resolve; - }); + const started = createDeferred(); + const hover = createDeferred(); const blocked = new Error("late browser navigation blocked by policy"); blocked.name = "SsrFBlockedError"; - pageState.page = { url: vi.fn(() => "https://example.com") }; - pageState.locator = { - hover: vi.fn(() => { - hoverStarted(); - return pendingHover; - }), - }; + installInteractionPage( + { url: vi.fn(() => "https://example.com") }, + { + hover: vi.fn(() => { + started.resolve(); + return hover.promise; + }), + }, + ); sessionMocks.isPolicyDenyNavigationError.mockImplementationOnce( (err: unknown) => err instanceof Error && err.name === "SsrFBlockedError", ); - sessionMocks.withPageNavigationRequestGuard.mockImplementationOnce( - async ({ - action, - page, - }: { - action: (url: string) => Promise; - page: { url: () => string }; - }) => { - await action(page.url()); - throw blocked; - }, - ); + mockNavigationGuardOnce(async ({ action, page }) => { + await action(page.url()); + throw blocked; + }); const task = interactions.hoverViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...strictNavigationOptions(), ref: "1", - ssrfPolicy: { allowPrivateNetwork: false }, signal: ctrl.signal, }); - await started; + await started.promise; ctrl.abort(new Error("aborted by test")); await expect(task).rejects.toThrow("aborted by test"); - releaseHover(); + hover.resolve(); await vi.waitFor(() => expect(sessionMocks.quarantineBlockedNavigationTarget).toHaveBeenCalledWith({ cdpUrl: "http://127.0.0.1:18792", @@ -979,17 +840,16 @@ describe("pw-tools-core browser SSRF guards", () => { it("preserves SSRF policy when aborting a pending click", async () => { const ctrl = new AbortController(); - let clickStarted: () => void = () => {}; - const clickStartedPromise = new Promise((resolve) => { - clickStarted = resolve; - }); - pageState.page = { url: vi.fn(() => "https://example.com") }; - pageState.locator = { - click: vi.fn(() => { - clickStarted(); - return new Promise(() => {}); - }), - }; + const clickStarted = createDeferred(); + installInteractionPage( + { url: vi.fn(() => "https://example.com") }, + { + click: vi.fn(() => { + clickStarted.resolve(); + return new Promise(() => {}); + }), + }, + ); const task = interactions.clickViaPlaywright({ cdpUrl: "http://127.0.0.1:18792", @@ -999,7 +859,7 @@ describe("pw-tools-core browser SSRF guards", () => { signal: ctrl.signal, }); - await clickStartedPromise; + await clickStarted.promise; ctrl.abort(new Error("aborted by test")); await expect(task).rejects.toThrow("aborted by test"); @@ -1016,21 +876,15 @@ describe("pw-tools-core browser SSRF guards", () => { { label: "click before slow type", slowly: true, firstMethod: "click" as const }, ])("stops a multi-step type action after aborting $label", async ({ slowly, firstMethod }) => { const ctrl = new AbortController(); - let firstStepStarted!: () => void; - let releaseFirstStep!: () => void; - const started = new Promise((resolve) => { - firstStepStarted = resolve; - }); - const pendingFirstStep = new Promise((resolve) => { - releaseFirstStep = resolve; - }); + const started = createDeferred(); + const firstStepPending = createDeferred(); const click = vi.fn(async () => {}); const fill = vi.fn(async () => {}); const type = vi.fn(async () => {}); const press = vi.fn(async () => {}); const firstStep = vi.fn(() => { - firstStepStarted(); - return pendingFirstStep; + started.resolve(); + return firstStepPending.promise; }); if (firstMethod === "click") { click.mockImplementation(firstStep); @@ -1038,40 +892,37 @@ describe("pw-tools-core browser SSRF guards", () => { fill.mockImplementation(firstStep); } let guardSettled = false; - pageState.page = { url: vi.fn(() => "https://example.com") }; - pageState.locator = { click, fill, type, press }; - sessionMocks.withPageNavigationRequestGuard.mockImplementationOnce( - async ({ - action, - page, - }: { - action: (url: string) => Promise; - page: { url: () => string }; - }) => { - try { - return await action(page.url()); - } finally { - guardSettled = true; - } + installInteractionPage( + { url: vi.fn(() => "https://example.com") }, + { + click, + fill, + type, + press, }, ); + mockNavigationGuardOnce(async ({ action, page }) => { + try { + return await action(page.url()); + } finally { + guardSettled = true; + } + }); const task = interactions.typeViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...strictNavigationOptions(), ref: "1", text: "value", submit: true, slowly, - ssrfPolicy: { allowPrivateNetwork: false }, signal: ctrl.signal, }); - await started; + await started.promise; ctrl.abort(new Error("aborted by test")); await expect(task).rejects.toThrow("aborted by test"); - releaseFirstStep(); + firstStepPending.resolve(); await vi.waitFor(() => expect(guardSettled).toBe(true)); expect(type).not.toHaveBeenCalled(); expect(press).not.toHaveBeenCalled(); @@ -1079,83 +930,64 @@ describe("pw-tools-core browser SSRF guards", () => { it("re-checks select-triggered navigations with the session safety helper", async () => { let currentUrl = "https://example.com"; - pageState.page = { url: vi.fn(() => currentUrl) }; - pageState.locator = { - selectOption: vi.fn(async () => { - currentUrl = "https://target.example"; - }), - }; + installInteractionPage( + { url: vi.fn(() => currentUrl) }, + { + selectOption: vi.fn(async () => { + currentUrl = "https://target.example"; + }), + }, + ); await interactions.selectOptionViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...strictNavigationOptions(), ref: "1", values: ["go"], - ssrfPolicy: { allowPrivateNetwork: false }, }); - expect(sessionMocks.assertPageNavigationCompletedSafely).toHaveBeenCalledWith({ - cdpUrl: "http://127.0.0.1:18792", - page: pageState.page, - response: null, - ssrfPolicy: { allowPrivateNetwork: false }, - targetId: "tab-1", - }); + expect(sessionMocks.assertPageNavigationCompletedSafely).toHaveBeenCalledWith( + completedNavigationExpectation(), + ); }); it("re-checks form fill-triggered navigations with the session safety helper", async () => { let currentUrl = "https://example.com"; - pageState.page = { url: vi.fn(() => currentUrl) }; - pageState.locator = { - fill: vi.fn(async () => { - currentUrl = "https://target.example"; - }), - }; + installInteractionPage( + { url: vi.fn(() => currentUrl) }, + { + fill: vi.fn(async () => { + currentUrl = "https://target.example"; + }), + }, + ); await interactions.fillFormViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...strictNavigationOptions(), fields: [{ ref: "1", type: "text", value: "go" }], - ssrfPolicy: { allowPrivateNetwork: false }, }); - expect(sessionMocks.assertPageNavigationCompletedSafely).toHaveBeenCalledWith({ - cdpUrl: "http://127.0.0.1:18792", - page: pageState.page, - response: null, - ssrfPolicy: { allowPrivateNetwork: false }, - targetId: "tab-1", - }); + expect(sessionMocks.assertPageNavigationCompletedSafely).toHaveBeenCalledWith( + completedNavigationExpectation(), + ); }); it("stops form filling when the first field's request guard denies navigation", async () => { const fill = vi.fn(async () => {}); const blocked = new Error("blocked field navigation"); blocked.name = "SsrFBlockedError"; - pageState.page = { url: vi.fn(() => "https://example.com") }; - pageState.locator = { fill }; - sessionMocks.withPageNavigationRequestGuard.mockImplementationOnce( - async ({ - action, - page, - }: { - action: (url: string) => Promise; - page: { url(): string }; - }) => { - await action(page.url()); - throw blocked; - }, - ); + installInteractionPage({ url: vi.fn(() => "https://example.com") }, { fill }); + mockNavigationGuardOnce(async ({ action, page }) => { + await action(page.url()); + throw blocked; + }); await expect( interactions.fillFormViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...strictNavigationOptions(), fields: [ { ref: "1", type: "text", value: "first" }, { ref: "2", type: "text", value: "second" }, ], - ssrfPolicy: { allowPrivateNetwork: false }, }), ).rejects.toThrow("blocked field navigation"); @@ -1171,10 +1003,8 @@ describe("pw-tools-core browser SSRF guards", () => { }; await interactions.evaluateViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...strictNavigationOptions(), fn: "() => document.body.innerText", - ssrfPolicy: { allowPrivateNetwork: false }, }); expect( @@ -1201,51 +1031,39 @@ describe("pw-tools-core browser SSRF guards", () => { it("re-checks batched click-triggered navigations with the session safety helper", async () => { let currentUrl = "https://example.com"; - pageState.page = { url: vi.fn(() => currentUrl) }; - pageState.locator = { - click: vi.fn(async () => { - currentUrl = "https://target.example"; - }), - }; + installInteractionPage( + { url: vi.fn(() => currentUrl) }, + { + click: vi.fn(async () => { + currentUrl = "https://target.example"; + }), + }, + ); await interactions.batchViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", + ...strictNavigationOptions(), actions: [{ kind: "click", ref: "1" }], - ssrfPolicy: { allowPrivateNetwork: false }, }); - expect(sessionMocks.assertPageNavigationCompletedSafely).toHaveBeenCalledWith({ - cdpUrl: "http://127.0.0.1:18792", - page: pageState.page, - response: null, - ssrfPolicy: { allowPrivateNetwork: false }, - targetId: "tab-1", - }); + expect(sessionMocks.assertPageNavigationCompletedSafely).toHaveBeenCalledWith( + completedNavigationExpectation(), + ); }); it("re-checks current page URL before snapshotting AI content", async () => { const ariaSnapshot = vi.fn(async () => 'button "Save"'); pageState.page = createSnapshotPage({ ariaSnapshot, - on: vi.fn(), - off: vi.fn(), url: vi.fn(() => "https://example.com"), }); await snapshots.snapshotAiViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", - ssrfPolicy: { allowPrivateNetwork: false }, + ...strictNavigationOptions(), }); - expect(sessionMocks.assertPageNavigationCompletedSafely).toHaveBeenCalledWith({ - cdpUrl: "http://127.0.0.1:18792", - page: pageState.page, - response: null, - ssrfPolicy: { allowPrivateNetwork: false }, - targetId: "tab-1", - }); + expect(sessionMocks.assertPageNavigationCompletedSafely).toHaveBeenCalledWith( + completedNavigationExpectation(), + ); expect( requireInvocationOrder( sessionMocks.assertPageNavigationCompletedSafely.mock, @@ -1258,25 +1076,16 @@ describe("pw-tools-core browser SSRF guards", () => { const ariaSnapshot = vi.fn(async () => ""); pageState.page = createSnapshotPage({ locator: vi.fn(() => ({ ariaSnapshot })), - mainFrame: vi.fn(() => ({})), - on: vi.fn(), - off: vi.fn(), url: vi.fn(() => "https://example.com"), }); await snapshots.snapshotRoleViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", - ssrfPolicy: { allowPrivateNetwork: false }, + ...strictNavigationOptions(), }); - expect(sessionMocks.assertPageNavigationCompletedSafely).toHaveBeenCalledWith({ - cdpUrl: "http://127.0.0.1:18792", - page: pageState.page, - response: null, - ssrfPolicy: { allowPrivateNetwork: false }, - targetId: "tab-1", - }); + expect(sessionMocks.assertPageNavigationCompletedSafely).toHaveBeenCalledWith( + completedNavigationExpectation(), + ); expect( requireInvocationOrder( sessionMocks.assertPageNavigationCompletedSafely.mock, @@ -1291,18 +1100,12 @@ describe("pw-tools-core browser SSRF guards", () => { }; await snapshots.snapshotAriaViaPlaywright({ - cdpUrl: "http://127.0.0.1:18792", - targetId: "tab-1", - ssrfPolicy: { allowPrivateNetwork: false }, + ...strictNavigationOptions(), }); - expect(sessionMocks.assertPageNavigationCompletedSafely).toHaveBeenCalledWith({ - cdpUrl: "http://127.0.0.1:18792", - page: pageState.page, - response: null, - ssrfPolicy: { allowPrivateNetwork: false }, - targetId: "tab-1", - }); + expect(sessionMocks.assertPageNavigationCompletedSafely).toHaveBeenCalledWith( + completedNavigationExpectation(), + ); expect( requireInvocationOrder( sessionMocks.assertPageNavigationCompletedSafely.mock, diff --git a/extensions/browser/src/browser/pw-tools-core.interactions.navigation-guard.test.ts b/extensions/browser/src/browser/pw-tools-core.interactions.navigation-guard.test.ts index d012d2329ee7..800fb43acb7b 100644 --- a/extensions/browser/src/browser/pw-tools-core.interactions.navigation-guard.test.ts +++ b/extensions/browser/src/browser/pw-tools-core.interactions.navigation-guard.test.ts @@ -28,9 +28,13 @@ function createMutableFrame(initialUrl: string) { }; } -async function runWithVirtualNavigationGrace(run: () => Promise): Promise { +async function withFakeTimers(run: () => Promise): Promise { vi.useFakeTimers(); - try { + return await run().finally(() => vi.useRealTimers()); +} + +async function runWithVirtualNavigationGrace(run: () => Promise): Promise { + return await withFakeTimers(async () => { // Observe rejection before advancing the production grace timer to avoid a transient // unhandled rejection; timing-specific cases below still advance exact durations. const settled = run().then( @@ -43,9 +47,7 @@ async function runWithVirtualNavigationGrace(run: () => Promise): Promise< throw result.reason; } return result.value; - } finally { - vi.useRealTimers(); - } + }); } const strictNavigationOptions = () => @@ -134,8 +136,7 @@ function mockDownloadCapture(drain: DownloadCapture["drain"], dispose = vi.fn()) describe("pw-tools-core interaction navigation guard", () => { it("waits for the grace window before completing a successful non-navigating click", async () => { - vi.useFakeTimers(); - try { + await withFakeTimers(async () => { const { listeners, page } = createNavigationPage("http://127.0.0.1:9222/json/version"); const click = vi.fn(async () => {}); installInteractionPage(page, { click }); @@ -154,14 +155,11 @@ describe("pw-tools-core interaction navigation guard", () => { await task; expect(completion).toHaveBeenCalledTimes(1); expect(listeners.size).toBe(0); - } finally { - vi.useRealTimers(); - } + }); }); it("runs the post-click navigation guard when navigation starts shortly after the click resolves", async () => { - vi.useFakeTimers(); - try { + await withFakeTimers(async () => { const navigation = createNavigationPage("http://127.0.0.1:9222/json/version"); const click = vi.fn(async () => { setTimeout(() => { @@ -188,14 +186,11 @@ describe("pw-tools-core interaction navigation guard", () => { expect(getPwToolsCoreSessionMocks().assertPageNavigationCompletedSafely).toHaveBeenCalledWith( completedNavigationExpectation(page), ); - } finally { - vi.useRealTimers(); - } + }); }); it("runs the post-select navigation guard when navigation starts shortly after the select resolves", async () => { - vi.useFakeTimers(); - try { + await withFakeTimers(async () => { const navigation = createNavigationPage("https://example.com/form"); const selectOption = vi.fn(async () => { setTimeout(() => { @@ -219,14 +214,11 @@ describe("pw-tools-core interaction navigation guard", () => { expect(getPwToolsCoreSessionMocks().assertPageNavigationCompletedSafely).toHaveBeenCalledWith( completedNavigationExpectation(page), ); - } finally { - vi.useRealTimers(); - } + }); }); it("checks subframe navigations before a later main-frame navigation", async () => { - vi.useFakeTimers(); - try { + await withFakeTimers(async () => { const mainFrame = {}; const subframe = { url: () => "https://example.com/embed" }; const navigation = createNavigationPage("http://127.0.0.1:9222/json/version", { @@ -267,14 +259,11 @@ describe("pw-tools-core interaction navigation guard", () => { expect(getPwToolsCoreSessionMocks().assertPageNavigationCompletedSafely).toHaveBeenCalledWith( completedNavigationExpectation(page), ); - } finally { - vi.useRealTimers(); - } + }); }); it("blocks subframe-only navigation to a private URL during the post-action grace window", async () => { - vi.useFakeTimers(); - try { + await withFakeTimers(async () => { const mainFrame = {}; const subframe = { url: () => "http://169.254.169.254/latest/meta-data/" }; const navigation = createNavigationPage("https://attacker.example.com/page", { mainFrame }); @@ -300,14 +289,11 @@ describe("pw-tools-core interaction navigation guard", () => { expect(getPwToolsCoreSessionMocks().assertPageNavigationCompletedSafely).toHaveBeenCalledWith( completedNavigationExpectation(page), ); - } finally { - vi.useRealTimers(); - } + }); }); it("snapshots delayed subframe URLs before later rewrites make them look safe", async () => { - vi.useFakeTimers(); - try { + await withFakeTimers(async () => { const mainFrame = {}; const subframe = createMutableFrame("http://169.254.169.254/latest/meta-data/"); const navigation = createNavigationPage("https://attacker.example.com/page", { mainFrame }); @@ -334,14 +320,11 @@ describe("pw-tools-core interaction navigation guard", () => { ssrfPolicy: { allowPrivateNetwork: false }, url: "http://169.254.169.254/latest/meta-data/", }); - } finally { - vi.useRealTimers(); - } + }); }); it("still quarantines the main frame when a delayed subframe block fires first", async () => { - vi.useFakeTimers(); - try { + await withFakeTimers(async () => { const mainFrame = {}; const subframe = { url: () => "http://169.254.169.254/latest/meta-data/" }; const navigation = createNavigationPage("https://attacker.example.com/page", { mainFrame }); @@ -375,14 +358,11 @@ describe("pw-tools-core interaction navigation guard", () => { expect(getPwToolsCoreSessionMocks().assertPageNavigationCompletedSafely).toHaveBeenCalledWith( completedNavigationExpectation(page), ); - } finally { - vi.useRealTimers(); - } + }); }); it("does not stop watching for a later main-frame navigation after a harmless subframe hop", async () => { - vi.useFakeTimers(); - try { + await withFakeTimers(async () => { const mainFrame = {}; const subframe = { url: () => "about:blank" }; const navigation = createNavigationPage("http://127.0.0.1:9222/json/version", { @@ -412,14 +392,11 @@ describe("pw-tools-core interaction navigation guard", () => { expect(getPwToolsCoreSessionMocks().assertPageNavigationCompletedSafely).toHaveBeenCalledWith( completedNavigationExpectation(page), ); - } finally { - vi.useRealTimers(); - } + }); }); it("checks delayed subframe navigations in the action-error recovery path", async () => { - vi.useFakeTimers(); - try { + await withFakeTimers(async () => { const mainFrame = {}; const subframe = { url: () => "http://169.254.169.254/latest/meta-data/" }; const navigation = createNavigationPage("https://attacker.example.com/page", { mainFrame }); @@ -457,14 +434,11 @@ describe("pw-tools-core interaction navigation guard", () => { "navigation request guard invocation", ), ).toBeLessThan(requireInvocationOrder(page.evaluate.mock, "page evaluation invocation")); - } finally { - vi.useRealTimers(); - } + }); }); it("snapshots subframe URLs observed during the action before they change", async () => { - vi.useFakeTimers(); - try { + await withFakeTimers(async () => { const mainFrame = {}; const subframe = createMutableFrame("http://169.254.169.254/latest/meta-data/"); const navigation = createNavigationPage("https://attacker.example.com/page", { mainFrame }); @@ -493,14 +467,11 @@ describe("pw-tools-core interaction navigation guard", () => { ssrfPolicy: { allowPrivateNetwork: false }, url: "http://169.254.169.254/latest/meta-data/", }); - } finally { - vi.useRealTimers(); - } + }); }); it("still quarantines the main frame when an in-flight subframe block fires first", async () => { - vi.useFakeTimers(); - try { + await withFakeTimers(async () => { const mainFrame = {}; const subframe = { url: () => "http://169.254.169.254/latest/meta-data/" }; const navigation = createNavigationPage("https://attacker.example.com/page", { mainFrame }); @@ -536,14 +507,11 @@ describe("pw-tools-core interaction navigation guard", () => { expect(getPwToolsCoreSessionMocks().assertPageNavigationCompletedSafely).toHaveBeenCalledWith( completedNavigationExpectation(page), ); - } finally { - vi.useRealTimers(); - } + }); }); it("deduplicates delayed navigation guards across repeated successful interactions", async () => { - vi.useFakeTimers(); - try { + await withFakeTimers(async () => { const navigation = createNavigationPage("http://127.0.0.1:9222/json/version"); const click = vi.fn(async () => {}); const { listeners, page } = navigation; @@ -566,14 +534,11 @@ describe("pw-tools-core interaction navigation guard", () => { getPwToolsCoreSessionMocks().assertPageNavigationCompletedSafely, ).toHaveBeenCalledTimes(3); expect(listeners.size).toBe(0); - } finally { - vi.useRealTimers(); - } + }); }); it("propagates blocked delayed navigation instead of reporting click success", async () => { - vi.useFakeTimers(); - try { + await withFakeTimers(async () => { const navigation = createNavigationPage("http://127.0.0.1:9222/json/version"); const click = vi.fn(async () => { setTimeout(() => { @@ -595,9 +560,7 @@ describe("pw-tools-core interaction navigation guard", () => { await vi.advanceTimersByTimeAsync(250); await rejection; expect(listeners.size).toBe(0); - } finally { - vi.useRealTimers(); - } + }); }); it("runs the post-click navigation guard with the resolved SSRF policy", async () => { @@ -623,8 +586,7 @@ describe("pw-tools-core interaction navigation guard", () => { }); it("skips interaction navigation guards when no explicit SSRF policy is provided", async () => { - vi.useFakeTimers(); - try { + await withFakeTimers(async () => { const mainFrame = {}; const navigation = createNavigationPage("http://127.0.0.1:9222/json/version", { mainFrame, @@ -648,9 +610,7 @@ describe("pw-tools-core interaction navigation guard", () => { expect( getPwToolsCoreSessionMocks().assertPageNavigationCompletedSafely, ).not.toHaveBeenCalled(); - } finally { - vi.useRealTimers(); - } + }); }); it("runs the post-evaluate navigation guard after page evaluation", async () => { @@ -722,8 +682,7 @@ describe("pw-tools-core interaction navigation guard", () => { }); it("runs the post-keypress navigation guard when navigation starts shortly after the keypress resolves", async () => { - vi.useFakeTimers(); - try { + await withFakeTimers(async () => { const navigation = createNavigationPage("http://127.0.0.1:9222/json/version", { extras: { keyboard: { @@ -750,14 +709,11 @@ describe("pw-tools-core interaction navigation guard", () => { expect(getPwToolsCoreSessionMocks().assertPageNavigationCompletedSafely).toHaveBeenCalledWith( completedNavigationExpectation(page), ); - } finally { - vi.useRealTimers(); - } + }); }); it("defaults non-finite keypress delays before calling Playwright", async () => { - vi.useFakeTimers(); - try { + await withFakeTimers(async () => { const press = vi.fn(async () => {}); const page = { keyboard: { press }, @@ -777,14 +733,11 @@ describe("pw-tools-core interaction navigation guard", () => { await task; expect(press).toHaveBeenCalledWith("Enter", { delay: 0 }); - } finally { - vi.useRealTimers(); - } + }); }); it("propagates blocked delayed submit navigation instead of reporting type success", async () => { - vi.useFakeTimers(); - try { + await withFakeTimers(async () => { const navigation = createNavigationPage("https://example.com/form"); const locator = { fill: vi.fn(async () => {}), @@ -814,9 +767,7 @@ describe("pw-tools-core interaction navigation guard", () => { await vi.advanceTimersByTimeAsync(250); await rejection; expect(listeners.size).toBe(0); - } finally { - vi.useRealTimers(); - } + }); }); it("runs the final committed-URL check when a click leaves the URL unchanged", async () => { @@ -914,8 +865,7 @@ describe("pw-tools-core interaction navigation guard", () => { }); it("runs the post-evaluate navigation guard when evaluate rejects after triggering navigation", async () => { - vi.useFakeTimers(); - try { + await withFakeTimers(async () => { const navigation = createNavigationPage("http://127.0.0.1:9222/json/version", { extras: { evaluate: vi.fn(async () => { @@ -947,9 +897,7 @@ describe("pw-tools-core interaction navigation guard", () => { expect(getPwToolsCoreSessionMocks().assertPageNavigationCompletedSafely).toHaveBeenCalledWith( completedNavigationExpectation(page), ); - } finally { - vi.useRealTimers(); - } + }); }); it("returns click downloads without adding a second policy grace", async () => { @@ -1098,8 +1046,7 @@ describe("pw-tools-core interaction navigation guard", () => { }); it("does not add a second download grace after a settled guarded failure", async () => { - vi.useFakeTimers(); - try { + await withFakeTimers(async () => { const page = { url: vi.fn(() => "https://example.com") }; const drain = vi.fn(async () => undefined); mockDownloadCapture(drain); @@ -1118,14 +1065,11 @@ describe("pw-tools-core interaction navigation guard", () => { await expectation; expect(drain).toHaveBeenCalledWith(NO_EXTRA_DOWNLOAD_GRACE); - } finally { - vi.useRealTimers(); - } + }); }); it("blocks a private final URL after an earlier safe navigation", async () => { - vi.useFakeTimers(); - try { + await withFakeTimers(async () => { let currentUrl = "https://example.com"; const blocked = new Error("final browser URL blocked by policy"); blocked.name = "SsrFBlockedError"; @@ -1165,9 +1109,7 @@ describe("pw-tools-core interaction navigation guard", () => { page, targetId: "T1", }); - } finally { - vi.useRealTimers(); - } + }); }); it("stops a permissive batch and quarantines when source preservation fails", async () => {