1import { it as effectIt } from "@effect/vitest";
2import {
3 DEFAULT_BROWSER_PROFILE_ID,
4 INCOGNITO_BROWSER_PROFILE_ID,
5 PreviewAutomationStatus,
6} from "@t3tools/contracts";
7import * as Cause from "effect/Cause";
8import * as Effect from "effect/Effect";
9import * as Exit from "effect/Exit";
10import * as Option from "effect/Option";
11import * as Schema from "effect/Schema";
12import { beforeEach, describe, expect, it, vi } from "vite-plus/test";
13
14import * as PreviewManager from "../../preview/Manager.ts";
15import * as BrowserImport from "../../preview/BrowserImport/BrowserImport.ts";
16import * as PreviewIpc from "./preview.ts";
17
18const { fromPartition } = vi.hoisted(() => ({
19 fromPartition: vi.fn(() => {
20 throw new Error("Session can only be received when app is ready");
21 }),
22}));
23
24vi.mock("electron", () => ({
25 BrowserWindow: {
26 getAllWindows: vi.fn(() => []),
27 },
28 session: {
29 fromPartition,
30 },
31 webContents: {
32 fromId: vi.fn(() => null),
33 },
34}));
35
36describe("preview IPC methods", () => {
37 beforeEach(() => {
38 fromPartition.mockClear();
39 });
40
41 it("does not access the Electron session while the module loads", async () => {
42 await expect(import("./preview.ts")).resolves.toBeDefined();
43 expect(fromPartition).not.toHaveBeenCalled();
44 });
45
46 it("derives distinct partition scopes when identifiers contain the delimiter", () => {
47 const first = PreviewIpc.resolvePartitionScope("a", "b::c");
48 const second = PreviewIpc.resolvePartitionScope("a::b", "c");
49
50 expect(first).toEqual({ scope: '["a","b::c"]', persistent: true, namespace: "profile" });
51 expect(second).toEqual({ scope: '["a::b","c"]', persistent: true, namespace: "profile" });
52 expect(first.scope).not.toBe(second.scope);
53 });
54
55 it("preserves lone surrogates without collapsing them to replacement characters", () => {
56 const highSurrogate = PreviewIpc.resolvePartitionScope("environment", "profile-\ud800");
57 const lowSurrogate = PreviewIpc.resolvePartitionScope("environment", "profile-\udc00");
58 const replacement = PreviewIpc.resolvePartitionScope("environment", "profile-�");
59
60 expect(highSurrogate.scope).toBe('["environment","profile-\\ud800"]');
61 expect(lowSurrogate.scope).toBe('["environment","profile-\\udc00"]');
62 expect(highSurrogate.scope).not.toBe(lowSurrogate.scope);
63 expect(highSurrogate.scope).not.toBe(replacement.scope);
64 expect(lowSurrogate.scope).not.toBe(replacement.scope);
65 });
66
67 it("keeps the legacy default partition scope and incognito persistence", () => {
68 expect(PreviewIpc.resolvePartitionScope("environment::legacy", undefined)).toEqual({
69 scope: "environment::legacy",
70 persistent: true,
71 });
72 expect(
73 PreviewIpc.resolvePartitionScope("environment::legacy", DEFAULT_BROWSER_PROFILE_ID),
74 ).toEqual({ scope: "environment::legacy", persistent: true });
75 expect(
76 PreviewIpc.resolvePartitionScope("environment::legacy", INCOGNITO_BROWSER_PROFILE_ID),
77 ).toEqual({
78 scope: '["environment::legacy","incognito"]',
79 persistent: false,
80 namespace: "profile",
81 });
82 });
83
84 effectIt.effect("targets imports at the same partition tuple as the renderer", () => {
85 const received: Array<Parameters<BrowserImport.BrowserImport["Service"]["importCookies"]>[0]> =
86 [];
87 const browserImport = BrowserImport.BrowserImport.of({
88 listSources: Effect.succeed([]),
89 importCookies: (input) =>
90 Effect.sync(() => {
91 received.push(input);
92 return { imported: 0, skipped: 0, skippedDomains: [] };
93 }),
94 });
95 const request = (environmentId: string, targetProfileId: string) =>
96 PreviewIpc.importBrowserCookies.handler({
97 environmentId,
98 sourceId: "helium",
99 sourceProfileDirectory: "Default",
100 targetProfileId,
101 });
102
103 return Effect.gen(function* () {
104 yield* request("a", "b");
105 yield* request("a::b", DEFAULT_BROWSER_PROFILE_ID);
106
107 expect(received[0]).toMatchObject(PreviewIpc.resolvePartitionScope("a", "b"));
108 expect(received[1]).toMatchObject(
109 PreviewIpc.resolvePartitionScope("a::b", DEFAULT_BROWSER_PROFILE_ID),
110 );
111 expect(received[0]?.namespace).toBe("profile");
112 expect(received[1]?.namespace).toBeUndefined();
113 }).pipe(Effect.provideService(BrowserImport.BrowserImport, browserImport));
114 });
115
116 effectIt.effect("rejects invalid webContents ids before resolving the preview service", () =>
117 Effect.map(
118 PreviewIpc.registerWebview
119 .handler({ tabId: "tab-1", webContentsId: 0 })
120 .pipe(Effect.provideService(PreviewManager.PreviewManager, null as never), Effect.exit),
121 (exit) => {
122 expect(Exit.isFailure(exit)).toBe(true);
123 if (Exit.isSuccess(exit)) return;
124 const error = Cause.findErrorOption(exit.cause);
125 expect(Option.isSome(error) && Schema.isSchemaError(error.value)).toBe(true);
126 expect(fromPartition).not.toHaveBeenCalled();
127 },
128 ),
129 );
130
131 it("keeps the public automation status tab id limit", () => {
132 const encode = Schema.encodeUnknownSync(PreviewAutomationStatus);
133 const tabId = "t".repeat(129);
134
135 expect(() =>
136 encode({
137 available: false,
138 visible: true,
139 tabId,
140 url: null,
141 title: null,
142 loading: false,
143 }),
144 ).toThrow();
145 });
146});