Conversation
* Add sidebar mode support - Route signing flows (grant access, sign tx, sign blob, sign auth entry) through the sidebar when it is open - Add SIDEBAR_REGISTER/UNREGISTER handlers to track the active sidebar window - Add SidebarSigningListener component so the sidebar can receive and navigate to signing requests - Add "Sidebar mode" button in AccountHeader menu to open the sidebar manually - Add openSidebar() helper for Chrome (sidePanel API) and Firefox (sidebarAction) - Fix isFullscreenMode to exclude sidebar mode - Add "Open sidebar mode by default" toggle in Settings > Preferences, persisted to local storage and applied on background startup via chrome.sidePanel.setPanelBehavior Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix sidebar mode bugs: signing window, cross-browser compat, promise timeouts, feature detection, and popup close behavior - Fix setAllowedStatus bypassing openSigningWindow, causing signing windows to not appear when sidebar mode is disabled - Replace chrome.storage.session with in-memory variable for cross-browser (Firefox) compatibility - Add error handling for setPanelBehavior() calls - Add 5-minute timeout to reject hanging Promises when sidebar closes mid-signing - Replace Arc browser UA sniffing with feature detection (typeof globalThis.chrome?.sidePanel?.open) - Fix popup not closing when opening sidebar mode - Add missing isOpenSidebarByDefault to SignTransaction test mocks Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix signing popup not appearing after sidebar is closed Replace unreliable beforeunload-based SIDEBAR_UNREGISTER with a long-lived port connection. Background now clears sidebarWindowId via onDisconnect when the sidebar closes, preventing stale IDs from routing signing requests through a closed sidebar instead of opening a popup. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix flaky e2e test: restore test.slow() for View failed transaction The test.slow() call was removed during the 5.38.0 refactoring, reducing the timeout from 45s to 15s. This causes intermittent CI failures when network stubs take longer to respond under sequential execution (IS_INTEGRATION_MODE=true with workers=1). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix e2e account history stubs to use context.route() instead of page.route() Chrome extension service workers make fetch requests outside the page context, so page.route() does not intercept them. Switching to context.route() ensures the account history stubs are applied at the browser context level, which covers both page and service worker requests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix View failed transaction test: use addInitScript to mock fetch Playwright's context.route()/page.route() do not reliably intercept fetch requests from Chrome extension popup pages in CI headless mode. Instead, use page.addInitScript() to patch window.fetch directly in the extension page's JavaScript environment. This approach injects code before any page scripts run and is guaranteed to intercept the account-history fetch calls regardless of the network interception mechanism. Keep context.route() as a network-level fallback for environments where route interception does work. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Piyal Basu <pbasu235@gmail.com>
There was a problem hiding this comment.
Pull request overview
Adds “sidebar mode” support across the extension, including routing signing flows to an open sidebar, a user-facing toggle to open the sidebar by default (Chromium sidePanel behavior), and several cross-browser / test-stability fixes (notably around Playwright request interception in extension contexts).
Changes:
- Introduces sidebar-mode runtime + navigation plumbing (sidebar registration, sidebar listener, and signing-flow routing).
- Adds the “Open sidebar mode by default” preference persisted to storage and applied on background startup (Chromium).
- Stabilizes e2e account-history stubs and fixes a flaky account history test via context-level routing and fetch patching.
Reviewed changes
Copilot reviewed 32 out of 32 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| extension/src/popup/views/Preferences/index.tsx | Adds new “Open sidebar mode by default” toggle in Preferences UI. |
| extension/src/popup/views/IntegrationTest.tsx | Updates integration test settings payload to include new setting. |
| extension/src/popup/views/tests/Swap.test.tsx | Updates mocked settings to include new setting. |
| extension/src/popup/views/tests/SignTransaction.test.tsx | Updates mocked settings to include new setting. |
| extension/src/popup/views/tests/ManageAssets.test.tsx | Updates mocked settings to include new setting. |
| extension/src/popup/views/tests/GrantAccess.test.tsx | Updates mocked settings to include new setting. |
| extension/src/popup/views/tests/AddFunds.test.tsx | Updates mocked settings to include new setting. |
| extension/src/popup/views/tests/AccountHistory.test.tsx | Updates mocked settings/state to include new setting. |
| extension/src/popup/views/tests/AccountCreator.test.tsx | Updates mocked settings to include new setting. |
| extension/src/popup/views/tests/Account.test.tsx | Updates mocked settings to include new setting. |
| extension/src/popup/Router.tsx | Mounts SidebarSigningListener only in sidebar mode. |
| extension/src/popup/helpers/navigate.ts | Adds openSidebar() helper (Chrome sidePanel + Firefox sidebarAction). |
| extension/src/popup/helpers/isSidebarMode.ts | Adds helper to detect sidebar mode via URL query param. |
| extension/src/popup/helpers/isFullscreenMode.ts | Excludes sidebar mode from fullscreen detection. |
| extension/src/popup/ducks/settings.ts | Adds new setting to state, thunk payloads, and selector. |
| extension/src/popup/components/SidebarSigningListener/index.tsx | New component: port connection + message-driven navigation for sidebar signing flows. |
| extension/src/popup/components/account/AccountHeader/index.tsx | Adds “Sidebar mode” menu action to open sidebar. |
| extension/src/constants/localStorageTypes.ts | Adds local storage key for open-by-default sidebar behavior. |
| extension/src/background/messageListener/popupMessageListener.ts | Adds sidebar window ID tracking and OPEN_SIDEBAR / register/unregister handling. |
| extension/src/background/messageListener/handlers/saveSettings.ts | Persists new setting and applies Chromium panel behavior immediately. |
| extension/src/background/messageListener/handlers/loadSettings.ts | Loads new setting from storage. |
| extension/src/background/messageListener/freighterApiMessageListener.ts | Routes signing windows to sidebar when registered; adds TTL fallback behavior. |
| extension/src/background/index.ts | Initializes sidebar behavior and connection listener on background startup. |
| extension/public/static/manifest/v3.json | Adds sidePanel permission and default side_panel path. |
| extension/public/static/manifest/v2.json | Adds sidebar_action configuration for Firefox (manifest v2). |
| extension/public/background.ts | Wires new background initializers. |
| extension/e2e-tests/helpers/stubs.ts | Switches account-history stubs to BrowserContext routing. |
| extension/e2e-tests/accountHistory.test.ts | Stabilizes account-history interception; restores slow timeout and adds fetch patching. |
| @shared/constants/services.ts | Adds service types for sidebar operations. |
| @shared/api/types/types.ts | Extends Settings/Response types with isOpenSidebarByDefault. |
| @shared/api/types/message-request.ts | Extends SaveSettingsMessage and adds sidebar message types. |
| @shared/api/internal.ts | Extends internal saveSettings API to include new setting. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export const openSidebar = async () => { | ||
| try { | ||
| if ((browser as any).sidebarAction) { | ||
| // Firefox | ||
| await (browser as any).sidebarAction.open(); | ||
| } else { | ||
| // Chrome — must be called in user gesture context before closing popup | ||
| const win = await chrome.windows.getCurrent(); | ||
| await chrome.sidePanel.setOptions({ | ||
| path: "index.html?mode=sidebar", | ||
| enabled: true, | ||
| }); | ||
| await chrome.sidePanel.open({ windowId: win.id! }); | ||
| } | ||
| } catch (e) { | ||
| console.error("Failed to open sidebar:", e); | ||
| } | ||
| window.close(); | ||
| }; |
There was a problem hiding this comment.
openSidebar() always calls window.close() even if opening the sidebar fails (caught error). This can leave the user with no UI if sidePanel/sidebarAction is unavailable or the call rejects. Only close the popup after a successful open (or after confirming the API exists and the request was dispatched successfully).
| {typeof globalThis.chrome?.sidePanel?.open === "function" && ( | ||
| <div | ||
| className="AccountHeader__options__item" | ||
| onClick={() => openSidebar()} | ||
| > | ||
| <Text as="div" size="sm" weight="medium"> | ||
| {t("Sidebar mode")} | ||
| </Text> | ||
| <div className="AccountHeader__options__item__icon"> | ||
| <Icon.LayoutRight /> | ||
| </div> | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
The sidebar UI entry is gated on globalThis.chrome?.sidePanel?.open, so Firefox (which uses browser.sidebarAction.open()) will never show the “Sidebar mode” menu item even though openSidebar() supports Firefox. Use feature detection that matches openSidebar() (e.g., check for sidebarAction.open OR sidePanel.open).
| {typeof globalThis.chrome?.sidePanel?.open === "function" && ( | ||
| <div className="Preferences--section"> | ||
| <div className="Preferences--section--title"> | ||
| <span>{t("Open sidebar mode by default")} </span> | ||
| <div className="Preferences--toggle"> | ||
| <Toggle | ||
| fieldSize="sm" | ||
| checked={initialValues.isOpenSidebarByDefaultValue} | ||
| customInput={<Field />} | ||
| id="isOpenSidebarByDefaultValue" | ||
| /> | ||
| </div> | ||
| </div> | ||
| <span className="Preferences--section--subtitle"> | ||
| {t( | ||
| "Open Freighter in sidebar instead of popup when clicking the extension icon", | ||
| )} | ||
| </span> | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
The “Open sidebar mode by default” preference is currently only rendered when chrome.sidePanel.open exists, which hides the setting on Firefox even though the extension declares a sidebar_action. If the intention is Chromium-only behavior, consider clarifying in UI text; otherwise, gate on Firefox sidebarAction support as well so the option is visible where sidebar mode is supported.
| const openSigningWindow = async (hashRoute: string, width?: number) => { | ||
| const sidebarWindowId = getSidebarWindowId(); | ||
| if (sidebarWindowId !== null) { | ||
| browser.runtime.sendMessage({ type: SIDEBAR_NAVIGATE, route: hashRoute }); | ||
| try { | ||
| if ((browser as any).sidebarAction) { | ||
| // Firefox | ||
| await (browser as any).sidebarAction.open(); | ||
| } else { | ||
| // Chrome and other Chromium browsers | ||
| await chrome.sidePanel.open({ windowId: sidebarWindowId }); | ||
| } | ||
| } catch (_) { | ||
| // ignore if unavailable | ||
| } | ||
| return null; | ||
| } |
There was a problem hiding this comment.
openSigningWindow(): if a sidebarWindowId is set but opening the side panel/sidebar fails, the function still returns null (sidebar mode) and callers will wait for the TTL timeout instead of falling back to opening a popup. Consider catching failures and falling back to browser.windows.create (and/or clearing sidebarWindowId) so signing requests still appear immediately when sidebar cannot be opened.
| ...WINDOW_SETTINGS, | ||
| width: 400, | ||
| }); | ||
| const popup = await openSigningWindow(`/grant-access?${encodeOrigin}`); |
There was a problem hiding this comment.
requestAccess(): grant-access popups previously used a width of 400, but this call no longer passes the width argument to openSigningWindow, which changes the popup dimensions. Pass the same explicit width (400) here to preserve the existing UI layout.
| const popup = await openSigningWindow(`/grant-access?${encodeOrigin}`); | |
| const popup = await openSigningWindow(`/grant-access?${encodeOrigin}`, 400); |
| setSidebarWindowId, | ||
| } from "./messageListener/popupMessageListener"; | ||
| import { freighterApiMessageListener } from "./messageListener/freighterApiMessageListener"; | ||
| import { SIDEBAR_PORT_NAME } from "popup/components/SidebarSigningListener"; |
There was a problem hiding this comment.
background/index.ts imports SIDEBAR_PORT_NAME from a popup React component module. This can pull react/react-router-dom (and potentially DOM-dependent code) into the background bundle, risking runtime issues and bloating the service worker. Move SIDEBAR_PORT_NAME to a shared constants module (e.g., @shared/constants/sidebar) and import it from both background and popup.
| import { SIDEBAR_PORT_NAME } from "popup/components/SidebarSigningListener"; | |
| import { SIDEBAR_PORT_NAME } from "@shared/constants/sidebar"; |
| export const stubAccountHistory = async (context: BrowserContext) => { | ||
| await context.route("**/account-history/**", async (route) => { | ||
| const json = [ |
There was a problem hiding this comment.
stubAccountHistory() now accepts a BrowserContext, but there are still many call sites in e2e tests passing a Page (e.g., addAsset.test.ts, hideCollectible.test.ts, freighterApiIntegration.test.ts). This will cause TypeScript compile/runtime errors. Update all callers to pass the test BrowserContext instead of the Page.
| error: "", | ||
| isDataSharingAllowed: false, | ||
| isMemoValidationEnabled: false, | ||
| isHideDustEnabled: true, | ||
| isOpenSidebarByDefault: false, | ||
| settingsState: SettingsState.SUCCESS, | ||
| isSorobanPublicEnabled: false, |
There was a problem hiding this comment.
Several newly added isOpenSidebarByDefault properties are mis-indented (e.g., this line). With eslint-plugin-prettier enabled, this is likely to fail lint/format checks. Run Prettier (or fix indentation) for these added lines throughout the file.
| networkDetails: TESTNET_NETWORK_DETAILS, | ||
| networksList: DEFAULT_NETWORKS, | ||
| isHideDustEnabled: true, | ||
| isOpenSidebarByDefault: false, |
There was a problem hiding this comment.
The newly added isOpenSidebarByDefault field is mis-indented here; with prettier-as-eslint this can fail CI formatting checks. Align indentation with surrounding object literal properties.
| isOpenSidebarByDefault: false, | |
| isOpenSidebarByDefault: false, |
| isDataSharingAllowed: false, | ||
| isMemoValidationEnabled: false, | ||
| isHideDustEnabled: true, | ||
| isOpenSidebarByDefault: false, |
There was a problem hiding this comment.
The added isOpenSidebarByDefault property is mis-indented here (and likely in other added mocks in this file). Please run Prettier / fix formatting to match surrounding properties to avoid lint failures.
| isOpenSidebarByDefault: false, | |
| isOpenSidebarByDefault: false, |
* feat: harden sidebar mode with Firefox support, security fixes, and reliability improvements Port all sidebar changes from sidebar-mode-audit: add Firefox sidebar_action support, harden trust boundaries (sender validation, port origin checks, isFromExtensionPage guard on OPEN_SIDEBAR), replace broadcast messaging with direct port communication, add safeResolve to prevent double-resolving promises, fix boolean coercion for isOpenSidebarByDefault, reject pending signing requests on sidebar close, remove dead SIDEBAR_REGISTER/UNREGISTER code, and add extension protocol guard to isSidebarMode. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * security: add interstitial gate and route allowlist for sidebar signing flow Prevent rapid-fire signing phishing in sidebar mode by showing an interstitial screen when a new signing request arrives while the user is already reviewing one. Also hardens the SidebarSigningListener with a route allowlist (only known signing routes are navigable) and runtime type guards on port messages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add missing Portuguese translations for sidebar mode strings Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove Firefox "open sidebar by default" — not supported by platform Firefox requires sidebarAction.open() in a synchronous user gesture handler and has no equivalent to Chrome's setPanelBehavior. Remove the broken setPopup/onClicked approach, hide the preference toggle on Firefox, add open_at_install: false to prevent auto-opening, and fix boolean storage reads for isOpenSidebarByDefault. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update loadSaveSettings tests to mock booleans instead of strings browser.storage.local preserves types, so getItem returns boolean true/false, not string "true"/"false". Update test mocks to match. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: use named onRemoved handler checking popup.id and self-removing after safeResolve Agent-Logs-Url: https://github.com/stellar/freighter/sessions/33844927-58ec-481b-9846-80c0e28a34b4 Co-authored-by: piyalbasu <6789586+piyalbasu@users.noreply.github.com> * redesign ConfirmSidebarRequest to match updated Figma spec Left-align layout, use smaller square icon, pill-shaped buttons, updated button variants and text per new design. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Update extension/src/popup/locales/pt/translation.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update extension/src/popup/views/ConfirmSidebarRequest/index.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: track sidebar UUIDs separately and guard port/windowId clear on disconnect Agent-Logs-Url: https://github.com/stellar/freighter/sessions/7e15d7bf-0539-4ceb-80a3-eb9ae1ce7a9b Co-authored-by: piyalbasu <6789586+piyalbasu@users.noreply.github.com> * fix: address code review issues for sidebar signing flow - Add sidebarQueueUuids tracking to setAllowedStatus handler so sidebar disconnect properly rejects the pending promise instead of hanging - ConfirmSidebarRequest "Reject" now extracts UUID from the next route and calls rejectAccess so the dapp promise resolves immediately - Ensure window.close() runs in the catch block of openSidebar() so the popup is closed even when the side panel API throws Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: defer sidebar disconnect cleanup to avoid rejecting during page reload chrome.sidePanel.open() can reload the sidebar page, causing a brief port disconnect/reconnect. The disconnect handler was immediately rejecting all in-flight signing requests before the new port could connect. Use a cancellable deferred cleanup: schedule rejection on disconnect, but cancel it if a new sidebar port connects before it fires. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Update extension/src/popup/constants/metricsNames.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: add REJECT_SIGNING_REQUEST handler to clean all queues on ConfirmSidebarRequest reject Agent-Logs-Url: https://github.com/stellar/freighter/sessions/da099665-1448-4e91-b2eb-4daa952a09bb Co-authored-by: piyalbasu <6789586+piyalbasu@users.noreply.github.com> * refactor: unify sidebarQueueUuids lifecycle with activeQueueUuids Move sidebarQueueUuids population from freighterApiMessageListener (eager, on openSigningWindow return) into the MARK_QUEUE_ACTIVE handler in popupMessageListener (on signing view mount/unmount), matching how activeQueueUuids is managed. Extract duplicated onWindowRemoved logic into a shared rejectOnWindowClose helper. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add coverage for sidebar disconnect cleanup and MARK_QUEUE_ACTIVE 16 tests covering deferred disconnect cleanup (cancellation on reconnect, queue rejection, stale port handling) and sidebarQueueUuids management via MARK_QUEUE_ACTIVE. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: move sidebar port helpers to background/helpers/sidebarPort Extract setSidebarPort, clearSidebarPort, getSidebarPort from freighterApiMessageListener into their own module to reduce coupling. Update all imports across source and test files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: rejectSigningRequest returns consistent empty object instead of void Agent-Logs-Url: https://github.com/stellar/freighter/sessions/2d47d7af-dc71-4412-8251-1486f2c3b5df Co-authored-by: piyalbasu <6789586+piyalbasu@users.noreply.github.com> * docs: add sidebar mode implementation spec Add CLAUDE.md pointing to specs/ directory and a comprehensive sidebar mode spec covering architecture, signing flow, Firefox limitations, and E2E testing constraints. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: track sidebar-routed UUIDs at routing time and guard disconnect cleanup Two fixes to sidebar queue management: 1. Move sidebarQueueUuids tracking from MARK_QUEUE_ACTIVE (view mount time) to openSigningWindow (routing time). This ensures requests behind the ConfirmSidebarRequest interstitial are properly cleaned up if the sidebar disconnects before the signing view mounts. 2. Only schedule deferred disconnect cleanup when the disconnecting port is the currently active sidebar port. Previously, a stale port disconnecting after a newer port connected would still schedule cleanup, potentially rejecting requests on the live sidebar. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: update sidebar spec to reflect queue tracking changes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add sidebarMode property to signing view metrics Includes isSidebarMode() in the view metrics for signTransaction, signAuthEntry, signMessage, grantAccess, and addToken so we can distinguish sidebar vs popup signing requests in analytics. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: harden sidebar message handling per security review - M1: Make sender required in popupMessageListener so omitting it fails-closed instead of bypassing the extension-page guard - M2: Add sender.id check to sidebar port validation for defense-in-depth alongside the existing tab and URL checks - M3: Add isFromExtensionPage guard to REJECT_SIGNING_REQUEST handler - M4: Add runtime windowId type validation in OPEN_SIDEBAR handler Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address all findings from Cassio's code review Important fixes: - I2: Extract shared removeUuidFromAllQueues helper to eliminate duplicated queue cleanup logic between disconnect handler and rejectSigningRequest - I3: Replace raw HTML <h1>/<div> with <Heading>/<Text> from @stellar/design-system - I4: Add rejectOnWindowClose to requestAccess and setAllowedStatus so popup close rejects the dapp promise immediately instead of hanging until TTL - I5: Add sidebar TTL fallback to submitToken for consistency with other handlers Code quality: - C1: Move misplaced DataStorageAccess import to top of file - C2: Type safeResolve as unknown instead of any - C3: Add comment explaining why SIDEBAR_NAVIGATE is a plain constant - C4: Extract makeSafeResolve helper to replace 6 duplicated inline closures Suggestions: - S1: Derive SIGNING_ROUTE_PREFIXES from ALLOWED_NAV_PREFIXES - S2: Extract SIDEBAR_DISCONNECT_DEBOUNCE_MS named constant - S3: Guard ConfirmSidebarRequest route with SidebarOnlyRoute redirect - S4: Use finally for window.close() in openSidebar Nits: - Replace hardcoded font-weight values with CSS custom properties - Remove hardcoded font-family (inherited from design system) Tests: - Add rejectSigningRequest handler tests (6 cases) - Add isValidNextRoute open-redirect validation tests (10 cases) - Add SidebarSigningListener route allowlist tests (23 cases) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: piyalbasu <6789586+piyalbasu@users.noreply.github.com>
Replace unreliable beforeunload-based SIDEBAR_UNREGISTER with a long-lived port connection. Background now clears sidebarWindowId via onDisconnect when the sidebar closes, preventing stale IDs from routing signing requests through a closed sidebar instead of opening a popup.
The test.slow() call was removed during the 5.38.0 refactoring, reducing the timeout from 45s to 15s. This causes intermittent CI failures when network stubs take longer to respond under sequential execution (IS_INTEGRATION_MODE=true with workers=1).
Chrome extension service workers make fetch requests outside the page context, so page.route() does not intercept them. Switching to context.route() ensures the account history stubs are applied at the browser context level, which covers both page and service worker requests.
Playwright's context.route()/page.route() do not reliably intercept fetch requests from Chrome extension popup pages in CI headless mode. Instead, use page.addInitScript() to patch window.fetch directly in the extension page's JavaScript environment. This approach injects code before any page scripts run and is guaranteed to intercept the account-history fetch calls regardless of the network interception mechanism.
Keep context.route() as a network-level fallback for environments where route interception does work.