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
124 changes: 124 additions & 0 deletions apps/code/src/main/platform-adapters/electron-dialog.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

const mockGetFocusedWindow = vi.hoisted(() => vi.fn());
const mockGetAllWindows = vi.hoisted(() => vi.fn());
const mockShowOpenDialog = vi.hoisted(() => vi.fn());
const mockShowMessageBox = vi.hoisted(() => vi.fn());

vi.mock("inversify", () => ({
injectable: () => (target: unknown) => target,
}));

vi.mock("electron", () => ({
BrowserWindow: {
getFocusedWindow: mockGetFocusedWindow,
getAllWindows: mockGetAllWindows,
},
dialog: {
showOpenDialog: mockShowOpenDialog,
showMessageBox: mockShowMessageBox,
},
}));

import { ElectronDialog } from "./electron-dialog";

function makeWindow(overrides: { visible?: boolean } = {}) {
return {
isVisible: vi.fn(() => overrides.visible ?? true),
focus: vi.fn(),
};
}

describe("ElectronDialog.pickFile", () => {
beforeEach(() => {
vi.clearAllMocks();
mockShowOpenDialog.mockResolvedValue({
canceled: false,
filePaths: ["/repo"],
});
});

it("parents the picker to the focused window when one exists", async () => {
const focused = makeWindow();
mockGetFocusedWindow.mockReturnValue(focused);

const result = await new ElectronDialog().pickFile({ directories: true });

expect(result).toEqual(["/repo"]);
expect(mockShowOpenDialog).toHaveBeenCalledWith(
focused,
expect.any(Object),
);
// A focused window is already on top, so we never reach the fallback windows.
expect(mockGetAllWindows).not.toHaveBeenCalled();
});

it("falls back to a visible window and focuses it when nothing is focused", async () => {
mockGetFocusedWindow.mockReturnValue(null);
const hidden = makeWindow({ visible: false });
const visible = makeWindow({ visible: true });
mockGetAllWindows.mockReturnValue([hidden, visible]);

await new ElectronDialog().pickFile({ directories: true });

expect(visible.focus).toHaveBeenCalledTimes(1);
// The dialog must be parented to the visible window, not opened unparented
// (which is what previously left it stuck behind/off-screen).
expect(mockShowOpenDialog).toHaveBeenCalledWith(
visible,
expect.any(Object),
);
});

it("opens unparented only when there are no windows at all", async () => {
mockGetFocusedWindow.mockReturnValue(null);
mockGetAllWindows.mockReturnValue([]);

await new ElectronDialog().pickFile({ directories: true });

expect(mockShowOpenDialog).toHaveBeenCalledWith(expect.any(Object));
expect(mockShowOpenDialog.mock.calls[0]).toHaveLength(1);
});
Comment on lines +41 to +81
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Prefer parameterised tests

The three cases here test the same function entry point (pickFile) with varying window configurations and differing showOpenDialog/focus assertions — a good fit for it.each. The team's convention prefers parameterised tests for exactly this shape of variation.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/main/platform-adapters/electron-dialog.test.ts
Line: 41-81

Comment:
**Prefer parameterised tests**

The three cases here test the same function entry point (`pickFile`) with varying window configurations and differing `showOpenDialog`/`focus` assertions — a good fit for `it.each`. The team's convention prefers parameterised tests for exactly this shape of variation.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

});
Comment on lines +32 to +82
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Tests only cover pickFile, not confirm()

The PR description explicitly calls out applying the same resolveDialogParent() fix to confirm(), but the test file has no coverage for that path. If the wiring in confirm() is later accidentally removed or regressed, nothing would catch it. A minimal test mirroring the focused-window case for confirm() (checking that showMessageBox receives the parent window) would close this gap.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/main/platform-adapters/electron-dialog.test.ts
Line: 32-82

Comment:
**Tests only cover `pickFile`, not `confirm()`**

The PR description explicitly calls out applying the same `resolveDialogParent()` fix to `confirm()`, but the test file has no coverage for that path. If the wiring in `confirm()` is later accidentally removed or regressed, nothing would catch it. A minimal test mirroring the focused-window case for `confirm()` (checking that `showMessageBox` receives the parent window) would close this gap.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


describe("ElectronDialog.confirm", () => {
beforeEach(() => {
vi.clearAllMocks();
mockShowMessageBox.mockResolvedValue({ response: 0 });
});

it("parents the message box to the focused window when one exists", async () => {
const focused = makeWindow();
mockGetFocusedWindow.mockReturnValue(focused);

await new ElectronDialog().confirm({
title: "Discard?",
message: "Discard changes?",
options: ["Cancel", "Discard"],
});

expect(mockShowMessageBox).toHaveBeenCalledWith(
focused,
expect.any(Object),
);
});

it("falls back to a visible window and focuses it when nothing is focused", async () => {
mockGetFocusedWindow.mockReturnValue(null);
const hidden = makeWindow({ visible: false });
const visible = makeWindow({ visible: true });
mockGetAllWindows.mockReturnValue([hidden, visible]);

await new ElectronDialog().confirm({
title: "Discard?",
message: "Discard changes?",
options: ["Cancel", "Discard"],
});

expect(visible.focus).toHaveBeenCalledTimes(1);
expect(mockShowMessageBox).toHaveBeenCalledWith(
visible,
expect.any(Object),
);
});
});
23 changes: 21 additions & 2 deletions apps/code/src/main/platform-adapters/electron-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,25 @@ function severityToType(severity?: DialogSeverity): MessageBoxOptions["type"] {
return severity ?? "none";
}

/**
* Resolve the window to parent a native dialog to.
*
* `BrowserWindow.getFocusedWindow()` returns `null` whenever the app has no
* focused window — which happens during onboarding flows that hand focus to
* external windows (OS auth, the GitHub device-login browser tab, etc.). An
* unparented dialog can then open behind the app window or off-screen, looking
* completely unresponsive to the user. Fall back to a visible window and focus
* it so the dialog is always parented to and rendered on top of a real window.
*/
function resolveDialogParent(): BrowserWindow | null {
const focused = BrowserWindow.getFocusedWindow();
if (focused) return focused;
const windows = BrowserWindow.getAllWindows();
const parent = windows.find((w) => w.isVisible()) ?? windows[0] ?? null;
parent?.focus();
return parent;
}

function buildProperties(options: PickFileOptions): OpenDialogProperty[] {
const properties: OpenDialogProperty[] = ["treatPackageAsDirectory"];
if (options.filesAndDirectories) {
Expand All @@ -37,7 +56,7 @@ function buildProperties(options: PickFileOptions): OpenDialogProperty[] {
@injectable()
export class ElectronDialog implements IDialog {
public async confirm(options: ConfirmOptions): Promise<number> {
const parent = BrowserWindow.getFocusedWindow();
const parent = resolveDialogParent();
const electronOptions: MessageBoxOptions = {
type: severityToType(options.severity),
title: options.title,
Expand All @@ -54,7 +73,7 @@ export class ElectronDialog implements IDialog {
}

public async pickFile(options: PickFileOptions): Promise<string[]> {
const parent = BrowserWindow.getFocusedWindow();
const parent = resolveDialogParent();
const electronOptions: OpenDialogOptions = {
title: options.title,
properties: buildProperties(options),
Expand Down
Loading