Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
9 changes: 8 additions & 1 deletion apps/desktop/src/backend/DesktopBackendPool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as DesktopObservability from "../app/DesktopObservability.ts";
import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";
import * as DesktopTelemetryPublisher from "../telemetry/DesktopTelemetryPublisher.ts";
import * as ElectronDialog from "../electron/ElectronDialog.ts";
import * as MacApplicationIcon from "../electron/MacApplicationIcon.ts";
import * as DesktopWindow from "../window/DesktopWindow.ts";
import * as DesktopBackendConfiguration from "./DesktopBackendConfiguration.ts";
import * as DesktopBackendPool from "./DesktopBackendPool.ts";
Expand Down Expand Up @@ -79,7 +80,13 @@ function makePoolLayer(
resolveWsl: () => Effect.die("unexpected WSL config resolve"),
} satisfies DesktopBackendConfiguration.DesktopBackendConfiguration["Service"]),
DesktopAppSettings.layerTest(),
ElectronDialog.layer,
ElectronDialog.layer.pipe(
Layer.provide(
Layer.succeed(MacApplicationIcon.MacApplicationIcon, {
resolveDataUrl: () => Effect.die("unexpected application icon resolution"),
} satisfies MacApplicationIcon.MacApplicationIcon["Service"]),
),
),
Layer.succeed(DesktopWindow.DesktopWindow, {
createMain: Effect.die("unexpected window create"),
ensureMain: Effect.die("unexpected window ensure"),
Expand Down
95 changes: 83 additions & 12 deletions apps/desktop/src/electron/ElectronDialog.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import { assert, describe, it } from "@effect/vitest";
import * as Cause from "effect/Cause";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import type { BrowserWindow } from "electron";
import { beforeEach, vi } from "vite-plus/test";

import * as ElectronDialog from "./ElectronDialog.ts";
import * as MacApplicationIcon from "./MacApplicationIcon.ts";

const { showMessageBoxMock, showOpenDialogMock, showErrorBoxMock } = vi.hoisted(() => ({
showMessageBoxMock: vi.fn(),
showOpenDialogMock: vi.fn(),
showErrorBoxMock: vi.fn(),
}));
const { showMessageBoxMock, showOpenDialogMock, showErrorBoxMock, resolveDataUrlMock } = vi.hoisted(
() => ({
showMessageBoxMock: vi.fn(),
showOpenDialogMock: vi.fn(),
showErrorBoxMock: vi.fn(),
resolveDataUrlMock: vi.fn(),
}),
);

vi.mock("electron", () => ({
dialog: {
Expand All @@ -21,13 +26,79 @@ vi.mock("electron", () => ({
},
}));

const applicationIconLayer = Layer.succeed(MacApplicationIcon.MacApplicationIcon, {
resolveDataUrl: (applicationPath) =>
Effect.tryPromise({
try: () => resolveDataUrlMock(applicationPath),
catch: (cause) =>
new MacApplicationIcon.MacApplicationIconResolutionError({ applicationPath, cause }),
}),
} satisfies MacApplicationIcon.MacApplicationIcon["Service"]);
const dialogLayer = ElectronDialog.layer.pipe(Layer.provide(applicationIconLayer));

describe("ElectronDialog", () => {
beforeEach(() => {
showMessageBoxMock.mockReset();
showOpenDialogMock.mockReset();
showErrorBoxMock.mockReset();
resolveDataUrlMock.mockReset();
});

it.effect("selects a macOS application and resolves its system icon", () =>
Effect.gen(function* () {
const owner = { id: 12 } as BrowserWindow;
showOpenDialogMock.mockResolvedValue({
canceled: false,
filePaths: ["/Applications/Terminal.app"],
});
resolveDataUrlMock.mockResolvedValue("data:image/png;base64,icon");
const dialog = yield* ElectronDialog.ElectronDialog;

const result = yield* dialog.pickApplication({ owner: Option.some(owner) });

assert.deepEqual(Option.getOrNull(result), {
applicationPath: "/Applications/Terminal.app",
suggestedName: "Terminal",
iconDataUrl: "data:image/png;base64,icon",
});
assert.deepEqual(showOpenDialogMock.mock.calls[0], [
owner,
{
defaultPath: "/Applications",
properties: ["openFile"],
filters: [{ name: "Applications", extensions: ["app"] }],
},
]);
assert.deepEqual(resolveDataUrlMock.mock.calls[0], ["/Applications/Terminal.app"]);
}).pipe(Effect.provide(dialogLayer)),
);

it.effect("returns none when application selection is cancelled", () =>
Effect.gen(function* () {
showOpenDialogMock.mockResolvedValue({ canceled: true, filePaths: [] });
const dialog = yield* ElectronDialog.ElectronDialog;
assert.isTrue(Option.isNone(yield* dialog.pickApplication({ owner: Option.none() })));
assert.equal(resolveDataUrlMock.mock.calls.length, 0);
}).pipe(Effect.provide(dialogLayer)),
);

it.effect("keeps a valid selection when icon extraction fails", () =>
Effect.gen(function* () {
showOpenDialogMock.mockResolvedValue({
canceled: false,
filePaths: ["/Users/test/Applications/My Tool.app"],
});
resolveDataUrlMock.mockRejectedValue(new Error("icon unavailable"));
const dialog = yield* ElectronDialog.ElectronDialog;

assert.deepEqual(Option.getOrNull(yield* dialog.pickApplication({ owner: Option.none() })), {
applicationPath: "/Users/test/Applications/My Tool.app",
suggestedName: "My Tool",
iconDataUrl: null,
});
}).pipe(Effect.provide(dialogLayer)),
);

it.effect("returns false without opening a confirm dialog for empty messages", () =>
Effect.gen(function* () {
const dialog = yield* ElectronDialog.ElectronDialog;
Expand All @@ -39,7 +110,7 @@ describe("ElectronDialog", () => {

assert.isFalse(result);
assert.equal(showMessageBoxMock.mock.calls.length, 0);
}).pipe(Effect.provide(ElectronDialog.layer)),
}).pipe(Effect.provide(dialogLayer)),
);

it.effect("opens a confirm dialog for the owner window", () =>
Expand All @@ -65,7 +136,7 @@ describe("ElectronDialog", () => {
message: "Delete worktree?",
},
]);
}).pipe(Effect.provide(ElectronDialog.layer)),
}).pipe(Effect.provide(dialogLayer)),
);

it.effect("opens an app-level confirm dialog when there is no owner window", () =>
Expand All @@ -89,7 +160,7 @@ describe("ElectronDialog", () => {
message: "Delete worktree?",
},
]);
}).pipe(Effect.provide(ElectronDialog.layer)),
}).pipe(Effect.provide(dialogLayer)),
);

it.effect("preserves folder picker request context and cause", () =>
Expand All @@ -114,7 +185,7 @@ describe("ElectronDialog", () => {
assert.include(error.message, "window 7");
assert.include(error.message, "/workspace");
assert.notInclude(error.message, cause.message);
}).pipe(Effect.provide(ElectronDialog.layer)),
}).pipe(Effect.provide(dialogLayer)),
);

it.effect("preserves confirmation request context and cause", () =>
Expand All @@ -139,7 +210,7 @@ describe("ElectronDialog", () => {
assert.include(error.message, "window 9");
assert.notInclude(error.message, "Confirm removal?");
assert.notInclude(error.message, cause.message);
}).pipe(Effect.provide(ElectronDialog.layer)),
}).pipe(Effect.provide(dialogLayer)),
);

it.effect("preserves message box request context and cause", () =>
Expand Down Expand Up @@ -176,7 +247,7 @@ describe("ElectronDialog", () => {
assert.notInclude(error.message, "Cancel");
assert.notInclude(error.message, "Discard");
assert.notInclude(error.message, cause.message);
}).pipe(Effect.provide(ElectronDialog.layer)),
}).pipe(Effect.provide(dialogLayer)),
);

it.effect("preserves error box request context and cause in the defect", () =>
Expand All @@ -201,6 +272,6 @@ describe("ElectronDialog", () => {
assert.notInclude(error.message, "Startup failed");
assert.notInclude(error.message, "Could not start.");
assert.notInclude(error.message, cause.message);
}).pipe(Effect.provide(ElectronDialog.layer)),
}).pipe(Effect.provide(dialogLayer)),
);
});
Loading
Loading