Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions apps/web/src/browser/HostedBrowserWebview.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
} from "~/hooks/useSettings";
import { useBrowserSurfaceStore } from "./browserSurfaceStore";
import * as desktopTabLifetime from "./desktopTabLifetime";
import { isGuestFocusPointerDown } from "./guestFocusPointer";
import { HostedBrowserWebview } from "./HostedBrowserWebview";

let renderer: ReactTestRenderer | undefined;
Expand Down Expand Up @@ -99,6 +100,74 @@ afterEach(async () => {
});

describe("HostedBrowserWebview settings hydration", () => {
it("dismisses host popups on guest focus and removes the listener on unmount", async () => {
mocks.getClientSettings.mockResolvedValue(DEFAULT_CLIENT_SETTINGS);
await act(async () => ensureClientSettingsHydrated());
class TestPointerEvent extends Event {
readonly pointerType: string;

constructor(type: string, init: EventInit & { pointerType: string }) {
super(type, init);
this.pointerType = init.pointerType;
}
}
vi.stubGlobal("PointerEvent", TestPointerEvent);
const guest = Object.assign(new EventTarget(), { getWebContentsId: () => 41 });
const hostDocument = new EventTarget();
const onHostOutsidePointer = vi.fn();
hostDocument.addEventListener("pointerdown", onHostOutsidePointer);
const dispatchGuestEvent = guest.dispatchEvent.bind(guest);
vi.spyOn(guest, "dispatchEvent").mockImplementation((event) => {
const delivered = dispatchGuestEvent(event);
if (event.bubbles) hostDocument.dispatchEvent(event);
return delivered;
});
const onPointerDown = vi.fn();
guest.addEventListener("pointerdown", onPointerDown);

await act(() => {
renderer = create(
<HostedBrowserWebview
threadRef={{
environmentId: EnvironmentId.make("guest-focus"),
threadId: ThreadId.make("thread-guest-focus"),
}}
tabId="server-tab"
runtimeTabId="guest-focus-tab"
initialUrl="https://example.com"
viewport={FILL_PREVIEW_VIEWPORT}
pictureInPicture={false}
profileId={undefined}
zoomFactor={1}
/>,
{
createNodeMock: (element) =>
element.type === "webview"
? guest
: { scrollLeft: 0, scrollTop: 0, scrollTo: () => undefined },
},
);
});

guest.dispatchEvent(new Event("focus"));
expect(onPointerDown).toHaveBeenCalledOnce();
expect(onHostOutsidePointer).toHaveBeenCalledOnce();
expect(isGuestFocusPointerDown(onHostOutsidePointer.mock.calls[0]?.[0])).toBe(true);
expect(
isGuestFocusPointerDown(new TestPointerEvent("pointerdown", { pointerType: "mouse" })),
).toBe(false);
expect(onPointerDown.mock.calls[0]?.[0]).toMatchObject({
type: "pointerdown",
bubbles: true,
pointerType: "mouse",
});

await act(() => renderer?.unmount());
guest.dispatchEvent(new Event("focus"));
expect(onPointerDown).toHaveBeenCalledOnce();
expect(onHostOutsidePointer).toHaveBeenCalledOnce();
});

it("starts a retained background tab only after a settings read succeeds on retry", async () => {
const firstRead = deferred<ClientSettings | null>();
const retryRead = deferred<ClientSettings | null>();
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/browser/HostedBrowserWebview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
import { BrowserDeviceToolbar } from "./BrowserDeviceToolbar";
import { BrowserViewportResizeHandles } from "./BrowserViewportResizeHandles";
import { acquireDesktopTab, type AcquiredDesktopTab } from "./desktopTabLifetime";
import { dispatchGuestFocusPointerDown } from "./guestFocusPointer";
import { resolveHostedBrowserWebviewWrapperStyle } from "./hostedBrowserWebviewStyle";
import { usePreviewWebviewConfig } from "./previewWebviewConfigState";
import { useBrowserViewportResize } from "./useBrowserViewportResize";
Expand Down Expand Up @@ -180,7 +181,13 @@ export function HostedBrowserWebview(props: {
dispatchSnapShotComposerFocus();
}
};
// Guest presses do not reach this document, so host popovers miss their
// outside-pointer dismissal when focus moves into the webview.
const dismissHostPopups = () => {
dispatchGuestFocusPointerDown(webview);
};
webview.addEventListener("focus", handleFocus);
webview.addEventListener("focus", dismissHostPopups);
webview.addEventListener("did-attach", register);
webview.addEventListener("dom-ready", register);
webview.addEventListener("render-process-gone", recoverGuest);
Expand All @@ -189,6 +196,7 @@ export function HostedBrowserWebview(props: {
disposed = true;
if (recoveryTimeout !== null) clearTimeout(recoveryTimeout);
webview.removeEventListener("focus", handleFocus);
webview.removeEventListener("focus", dismissHostPopups);
webview.removeEventListener("did-attach", register);
webview.removeEventListener("dom-ready", register);
webview.removeEventListener("render-process-gone", recoverGuest);
Expand Down
13 changes: 13 additions & 0 deletions apps/web/src/browser/guestFocusPointer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// A guest press only surfaces in the host as webview focus. Mark the synthetic
// outside pointer so the theme inspector leaves it to host popup listeners.
const guestFocusPointers = new WeakSet<Event>();

export function dispatchGuestFocusPointerDown(webview: HTMLElement): void {
const event = new PointerEvent("pointerdown", { bubbles: true, pointerType: "mouse" });
guestFocusPointers.add(event);
webview.dispatchEvent(event);
}

export function isGuestFocusPointerDown(event: Event): boolean {
return guestFocusPointers.has(event);
}
2 changes: 2 additions & 0 deletions apps/web/src/components/settings/ThemeEditorPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
type ThemeDefinition,
} from "../../themePalette";
import { cn } from "../../lib/utils";
import { isGuestFocusPointerDown } from "../../browser/guestFocusPointer";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { Switch } from "../ui/switch";
Expand Down Expand Up @@ -682,6 +683,7 @@ export function ThemeEditorPanel({
if (event.relatedTarget === null) clearHover();
};
const handlePointerDown = (event: PointerEvent) => {
if (isGuestFocusPointerDown(event)) return;
const target = event.target;
if (!(target instanceof Element) || target.closest("[data-theme-editor-panel]")) return;
event.preventDefault();
Expand Down
Loading