1// @effect-diagnostics nodeBuiltinImport:off - Stands in for an Electron webContents.
2import { assert, describe, it } from "@effect/vitest";
3import * as NodeEvents from "node:events";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Option from "effect/Option";
7
8import * as ElectronWindow from "../electron/ElectronWindow.ts";
9import { WEB_LINK_OPEN_CHANNEL } from "../ipc/channels.ts";
10import * as DesktopWebLinks from "./DesktopWebLinks.ts";
11
12const makeWindow = () => {
13 const sent: Array<string> = [];
14 const revealed: Array<unknown> = [];
15 const webContents = Object.assign(new NodeEvents.EventEmitter(), {
16 isDestroyed: () => false,
17 send: (channel: string, url: string) => {
18 if (channel === WEB_LINK_OPEN_CHANNEL) sent.push(url);
19 },
20 });
21 const window = { webContents } as unknown as Electron.BrowserWindow;
22 /** The web app starts loading a new page, as a reload does. */
23 const reload = () =>
24 webContents.emit("did-start-navigation", { isMainFrame: true, isSameDocument: false });
25 const layer = DesktopWebLinks.layer.pipe(
26 Layer.provide(
27 Layer.succeed(
28 ElectronWindow.ElectronWindow,
29 ElectronWindow.ElectronWindow.of({
30 currentMainOrFirst: Effect.succeed(Option.some(window)),
31 reveal: (target: Electron.BrowserWindow) => Effect.sync(() => void revealed.push(target)),
32 } as unknown as ElectronWindow.ElectronWindow["Service"]),
33 ),
34 ),
35 );
36 return { sent, revealed, layer, reload };
37};
38
39describe("DesktopWebLinks", () => {
40 it("treats only http and https as web links", () => {
41 assert.isTrue(DesktopWebLinks.isWebLink("https://example.com/"));
42 assert.isTrue(DesktopWebLinks.isWebLink("http://localhost:3000/a?b=1"));
43 assert.isFalse(DesktopWebLinks.isWebLink("t3code://app/welcome"));
44 assert.isFalse(DesktopWebLinks.isWebLink("mailto:hello@example.com"));
45 assert.isFalse(DesktopWebLinks.isWebLink("not a url"));
46 });
47
48 it("opens HTML files the way a browser does, and nothing else by path", () => {
49 assert.isTrue(DesktopWebLinks.isWebPageFile("/Users/me/report.html"));
50 assert.isTrue(DesktopWebLinks.isWebPageFile("/tmp/page.HTM"));
51 assert.isTrue(DesktopWebLinks.isWebPageFile("/tmp/page.xhtml"));
52 assert.isFalse(DesktopWebLinks.isWebPageFile("/tmp/notes.md"));
53 assert.isFalse(DesktopWebLinks.isWebPageFile("/tmp/html"));
54 });
55
56 it.effect("holds links until the renderer listens, then delivers them in order", () => {
57 const { sent, revealed, layer } = makeWindow();
58 return Effect.gen(function* () {
59 const webLinks = yield* DesktopWebLinks.DesktopWebLinks;
60 // The link that launched the app arrives before the web app has loaded.
61 yield* webLinks.receive("https://example.com/first");
62 yield* webLinks.receive("https://example.com/second");
63 assert.deepStrictEqual(sent, []);
64
65 yield* webLinks.setRendererReady(true);
66 assert.deepStrictEqual(sent, ["https://example.com/first", "https://example.com/second"]);
67 assert.strictEqual(revealed.length, 1);
68
69 yield* webLinks.receive("https://example.com/third");
70 assert.deepStrictEqual(sent.at(-1), "https://example.com/third");
71
72 // A reloading web app stops listening; links wait for it again.
73 yield* webLinks.setRendererReady(false);
74 yield* webLinks.receive("https://example.com/fourth");
75 assert.strictEqual(sent.length, 3);
76 }).pipe(Effect.provide(layer));
77 });
78
79 it.effect("holds links that arrive while the web app reloads until it listens again", () => {
80 const { sent, layer, reload } = makeWindow();
81 return Effect.gen(function* () {
82 const webLinks = yield* DesktopWebLinks.DesktopWebLinks;
83 yield* webLinks.setRendererReady(true);
84 // The old page never says it stopped; the reload itself ends its listening.
85 reload();
86 yield* webLinks.receive("https://example.com/during-reload");
87 assert.deepStrictEqual(sent, []);
88
89 yield* webLinks.setRendererReady(true);
90 assert.deepStrictEqual(sent, ["https://example.com/during-reload"]);
91 }).pipe(Effect.provide(layer));
92 });
93});