1import { assert, describe, it } from "@effect/vitest";
2import * as Cause from "effect/Cause";
3import * as Effect from "effect/Effect";
4import * as Option from "effect/Option";
5import type { BrowserWindow } from "electron";
6import { beforeEach, vi } from "vite-plus/test";
7
8import * as ElectronDialog from "./ElectronDialog.ts";
9
10const { showMessageBoxMock, showOpenDialogMock, showErrorBoxMock } = vi.hoisted(() => ({
11 showMessageBoxMock: vi.fn(),
12 showOpenDialogMock: vi.fn(),
13 showErrorBoxMock: vi.fn(),
14}));
15
16vi.mock("electron", () => ({
17 dialog: {
18 showMessageBox: showMessageBoxMock,
19 showOpenDialog: showOpenDialogMock,
20 showErrorBox: showErrorBoxMock,
21 },
22}));
23
24describe("ElectronDialog", () => {
25 beforeEach(() => {
26 showMessageBoxMock.mockReset();
27 showOpenDialogMock.mockReset();
28 showErrorBoxMock.mockReset();
29 });
30
31 it.effect("preserves folder picker request context and cause", () =>
32 Effect.gen(function* () {
33 const cause = new Error("folder picker failed");
34 const owner = { id: 7 } as BrowserWindow;
35 showOpenDialogMock.mockRejectedValue(cause);
36 const dialog = yield* ElectronDialog.ElectronDialog;
37
38 const error = yield* Effect.flip(
39 dialog.pickFolder({
40 owner: Option.some(owner),
41 defaultPath: Option.some("/workspace"),
42 }),
43 );
44
45 assert.instanceOf(error, ElectronDialog.ElectronDialogPickFolderError);
46 assert.strictEqual(error.ownerWindowId, 7);
47 assert.strictEqual(error.defaultPath, "/workspace");
48 assert.strictEqual(error.cause, cause);
49 assert.include(error.message, "window 7");
50 assert.include(error.message, "/workspace");
51 assert.notInclude(error.message, cause.message);
52 }).pipe(Effect.provide(ElectronDialog.layer)),
53 );
54
55 it.effect("opens a single-file picker when multiple selections are disabled", () =>
56 Effect.gen(function* () {
57 showOpenDialogMock.mockResolvedValue({
58 canceled: false,
59 filePaths: ["/pictures/icon.png"],
60 });
61 const dialog = yield* ElectronDialog.ElectronDialog;
62
63 const paths = yield* dialog.pickFiles({
64 owner: Option.none(),
65 defaultPath: Option.some("/project"),
66 filters: [{ name: "Images", extensions: ["png"] }],
67 multiple: false,
68 });
69
70 assert.deepEqual(paths, ["/pictures/icon.png"]);
71 assert.deepEqual(showOpenDialogMock.mock.calls, [
72 [
73 {
74 defaultPath: "/project",
75 filters: [{ name: "Images", extensions: ["png"] }],
76 properties: ["openFile"],
77 },
78 ],
79 ]);
80 }).pipe(Effect.provide(ElectronDialog.layer)),
81 );
82
83 it.effect("preserves message box request context and cause", () =>
84 Effect.gen(function* () {
85 const cause = new Error("message box failed");
86 showMessageBoxMock.mockRejectedValue(cause);
87 const dialog = yield* ElectronDialog.ElectronDialog;
88
89 const error = yield* Effect.flip(
90 dialog.showMessageBox({
91 type: "warning",
92 title: "Unsaved changes",
93 message: "Discard changes?",
94 detail: "This cannot be undone.",
95 buttons: ["Cancel", "Discard"],
96 }),
97 );
98
99 assert.instanceOf(error, ElectronDialog.ElectronDialogShowMessageBoxError);
100 assert.strictEqual(error.type, "warning");
101 assert.strictEqual(error.titleLength, "Unsaved changes".length);
102 assert.strictEqual(error.messageLength, "Discard changes?".length);
103 assert.strictEqual(error.detailLength, "This cannot be undone.".length);
104 assert.strictEqual(error.buttonCount, 2);
105 assert.notProperty(error, "title");
106 assert.notProperty(error, "dialogMessage");
107 assert.notProperty(error, "dialogDetail");
108 assert.notProperty(error, "buttons");
109 assert.strictEqual(error.cause, cause);
110 assert.include(error.message, "warning");
111 assert.notInclude(error.message, "Unsaved changes");
112 assert.notInclude(error.message, "Discard changes?");
113 assert.notInclude(error.message, "This cannot be undone.");
114 assert.notInclude(error.message, "Cancel");
115 assert.notInclude(error.message, "Discard");
116 assert.notInclude(error.message, cause.message);
117 }).pipe(Effect.provide(ElectronDialog.layer)),
118 );
119
120 it.effect("preserves error box request context and cause in the defect", () =>
121 Effect.gen(function* () {
122 const cause = new Error("error box failed");
123 showErrorBoxMock.mockImplementation(() => {
124 throw cause;
125 });
126 const dialog = yield* ElectronDialog.ElectronDialog;
127
128 const exit = yield* Effect.exit(dialog.showErrorBox("Startup failed", "Could not start."));
129
130 assert.isTrue(exit._tag === "Failure");
131 if (exit._tag === "Success") return;
132 const error = Cause.squash(exit.cause);
133 assert.instanceOf(error, ElectronDialog.ElectronDialogShowErrorBoxError);
134 assert.strictEqual(error.titleLength, "Startup failed".length);
135 assert.strictEqual(error.contentLength, "Could not start.".length);
136 assert.notProperty(error, "title");
137 assert.notProperty(error, "content");
138 assert.strictEqual(error.cause, cause);
139 assert.notInclude(error.message, "Startup failed");
140 assert.notInclude(error.message, "Could not start.");
141 assert.notInclude(error.message, cause.message);
142 }).pipe(Effect.provide(ElectronDialog.layer)),
143 );
144});