From 0fa6c2ac271f3506a0c023af1b9c780df7017752 Mon Sep 17 00:00:00 2001 From: CodeGhost21 <164498022+CodeGhost21@users.noreply.github.com> Date: Tue, 7 Jul 2026 23:19:00 +0530 Subject: [PATCH] fix(webview-accounts): remount provider WebviewHost on account switch to stop cross-account bleed (#4421) (#4642) --- app/src/App.tsx | 8 ++ app/src/__tests__/App.webviewOverlay.test.tsx | 78 ++++++++++++++++--- 2 files changed, 77 insertions(+), 9 deletions(-) diff --git a/app/src/App.tsx b/app/src/App.tsx index 1572a0e17..a7a715580 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -295,7 +295,15 @@ export function AppShellDesktop() { {activeProviderAccount && !accountsOverlayOpen && (
+ {/* key on the account id so switching provider accounts fully + unmounts the previous host (running its cleanup → hideWebviewAccount) + and mounts a fresh one, instead of React reusing one instance with + new props. Guarantees deterministic hide-old-before-show-new + ordering and stops a deselected provider's CEF view from bleeding + into the newly-selected account's slot on rapid rail switches + (#4421). */} diff --git a/app/src/__tests__/App.webviewOverlay.test.tsx b/app/src/__tests__/App.webviewOverlay.test.tsx index 16181c51a..17fd5908f 100644 --- a/app/src/__tests__/App.webviewOverlay.test.tsx +++ b/app/src/__tests__/App.webviewOverlay.test.tsx @@ -5,10 +5,17 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { AppShellDesktop } from '../App'; -const { hideWebviewAccountMock, mockDispatch } = vi.hoisted(() => ({ - hideWebviewAccountMock: vi.fn().mockResolvedValue(undefined), - mockDispatch: vi.fn(), -})); +const { hideWebviewAccountMock, mockDispatch, webviewMountSpy, webviewUnmountSpy } = vi.hoisted( + () => ({ + hideWebviewAccountMock: vi.fn().mockResolvedValue(undefined), + mockDispatch: vi.fn(), + // Fired from the mocked WebviewHost's mount/unmount only (empty-deps + // effect) so a real remount is observable and distinguishable from a + // same-instance prop update. See the #4421 regression test below. + webviewMountSpy: vi.fn(), + webviewUnmountSpy: vi.fn(), + }) +); const baseState = { accounts: { @@ -71,11 +78,21 @@ vi.mock('../store/hooks', () => ({ useAppDispatch: () => mockDispatch, useAppSelector: (selector: (state: typeof baseState) => unknown) => selector(mockState), })); -vi.mock('../components/accounts/WebviewHost', () => ({ - default: ({ accountId }: { accountId: string }) => ( -
{accountId}
- ), -})); +vi.mock('../components/accounts/WebviewHost', () => { + // Empty deps: the spies fire only on a genuine mount/unmount, not on a prop + // change. If App reuses one host instance across account switches (no `key`), + // switching accounts is a prop update and neither spy fires for the new + // account — which is exactly the desync #4421 guards against. + const MockWebviewHost = ({ accountId }: { accountId: string }) => { + useEffect(() => { + webviewMountSpy(accountId); + return () => webviewUnmountSpy(accountId); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return
{accountId}
; + }; + return { default: MockWebviewHost }; +}); vi.mock('../AppRoutes', () => ({ default: () =>
})); vi.mock('../components/AppBackground', () => ({ default: () => null })); vi.mock('../components/layout/shell/AppSidebar', () => ({ default: () =>