Skip to content
Open
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
6 changes: 6 additions & 0 deletions apps/web/src/components/preview/PreviewMoreMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ interface Props {
nativePictureInPicture: boolean;
/** Toggles the optional native always-on-top preview window. */
onNativePictureInPicture: () => void;
/** Opens the current page in the OS default browser. */
onOpenInBrowser: () => void;
/** Environment the tab belongs to; scopes storage clearing to its partitions. */
environmentId: EnvironmentId;
/** Profile the tab was opened under, if the server recorded one. */
Expand Down Expand Up @@ -79,6 +81,7 @@ export function PreviewMoreMenu({
onToggleDeviceToolbar,
nativePictureInPicture,
onNativePictureInPicture,
onOpenInBrowser,
environmentId,
profileId,
profileName,
Expand Down Expand Up @@ -120,6 +123,9 @@ export function PreviewMoreMenu({
? "Close separate preview window"
: "Open separate preview window"}
</MenuItem>
<MenuItem onClick={onOpenInBrowser} disabled={tabDisabled}>
Open in system browser
</MenuItem>
<MenuItem onClick={onToggleDeviceToolbar} disabled={tabDisabled}>
{deviceToolbarVisible ? "Hide device toolbar" : "Show device toolbar"}
</MenuItem>
Expand Down
40 changes: 38 additions & 2 deletions apps/web/src/components/preview/PreviewView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ import { act, Profiler } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { beforeEach, describe, expect, it, vi } from "vite-plus/test";

// PreviewView resolves the local API once at module scope behind a `typeof
// window` guard. The guard has to see a window before the import further down
// runs, or every shell call in this suite is a silent no-op.
vi.hoisted(() => {
globalThis.window ??= {
addEventListener() {},
removeEventListener() {},
} as unknown as Window & typeof globalThis;
});

const mocks = vi.hoisted(() => ({
navigate: vi.fn(async (_tabId: string, _url: string): Promise<void> => undefined),
rememberPreviewUrl: vi.fn(),
Expand All @@ -19,6 +29,8 @@ const mocks = vi.hoisted(() => ({
emptyStateUrl: null as ((url: string) => void) | null,
togglePictureInPicture: null as (() => void) | null,
toggleNativePictureInPicture: null as (() => void) | null,
openInBrowser: null as (() => void) | null,
openExternal: vi.fn(async (_url: string): Promise<void> => undefined),
pictureInPicturePressed: false,
miniPlayerTabId: null as string | null,
openMiniPlayer: vi.fn(),
Expand Down Expand Up @@ -95,7 +107,7 @@ vi.mock("~/lib/previewAnnotation", () => ({
}));

vi.mock("~/localApi", () => ({
ensureLocalApi: vi.fn(),
ensureLocalApi: vi.fn(() => ({ shell: { openExternal: mocks.openExternal } })),
}));

vi.mock("~/previewStateStore", () => ({
Expand Down Expand Up @@ -221,14 +233,15 @@ vi.mock("./PreviewChromeRow", () => ({
onPictureInPicture?: () => void;
pictureInPicture?: boolean;
trailingActions?: {
props: { onNativePictureInPicture?: () => void };
props: { onNativePictureInPicture?: () => void; onOpenInBrowser?: () => void };
};
}) => {
mocks.submittedUrl = props.onSubmit;
mocks.toggleAnnotation = props.onPickElement ?? null;
mocks.togglePictureInPicture = props.onPictureInPicture ?? null;
mocks.toggleNativePictureInPicture =
props.trailingActions?.props.onNativePictureInPicture ?? null;
mocks.openInBrowser = props.trailingActions?.props.onOpenInBrowser ?? null;
mocks.pictureInPicturePressed = props.pictureInPicture ?? false;
return null;
},
Expand Down Expand Up @@ -333,6 +346,8 @@ describe("PreviewView navigation", () => {
mocks.emptyStateUrl = null;
mocks.togglePictureInPicture = null;
mocks.toggleNativePictureInPicture = null;
mocks.openInBrowser = null;
mocks.openExternal.mockClear();
mocks.pictureInPicturePressed = false;
mocks.miniPlayerTabId = null;
mocks.openMiniPlayer.mockClear();
Expand Down Expand Up @@ -519,6 +534,27 @@ describe("PreviewView navigation", () => {
);
});

it("hands the page the tab is showing to the system browser", async () => {
const props = {
threadRef: TEST_THREAD_REF,
tabId: "tab-1",
visible: true,
} as const;

renderToStaticMarkup(<PreviewView {...props} />);
mocks.openInBrowser?.();
await vi.waitFor(() => expect(mocks.openExternal).toHaveBeenCalledWith("http://example.com/"));

// An empty tab has no address to hand over; the menu item stays clickable,
// so the handler is the only thing keeping it from opening about:blank.
mocks.showEmptyState = true;
mocks.openExternal.mockClear();
renderToStaticMarkup(<PreviewView {...props} />);
expect(mocks.openInBrowser).not.toBeNull();
mocks.openInBrowser?.();
expect(mocks.openExternal).not.toHaveBeenCalled();
});

it("forwards Cmd/Ctrl+Enter annotations to the composer send path", async () => {
const annotation = {
id: "annotation-1",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/preview/PreviewView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,7 @@ export function PreviewView({
onToggleDeviceToolbar={handleToggleDeviceToolbar}
nativePictureInPicture={desktopOverlay?.pictureInPicture ?? false}
onNativePictureInPicture={handleNativePictureInPicture}
onOpenInBrowser={handleOpenInBrowser}
/>
) : null
}
Expand Down
Loading