1// @effect-diagnostics nodeBuiltinImport:off -- Isolated installation/config fixtures; no user's desktop state.
2import * as NodeFSP from "node:fs/promises";
3import * as NodeOS from "node:os";
4import * as NodePath from "node:path";
5import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
6const execute = vi.hoisted(() => vi.fn());
7const startEffects = vi.hoisted(() => vi.fn());
8vi.mock("node:child_process", () => ({ execFile: execute }));
9vi.mock("./NativeCaptureFeedback.ts", () => ({ startNativeCaptureFeedback: startEffects }));
10vi.mock("electron", () => ({
11 nativeImage: {
12 createFromBuffer: () => ({ isEmpty: () => false, getSize: () => ({ width: 2, height: 1 }) }),
13 },
14}));
15import {
16 HyprlandCaptureSetup,
17 captureHyprlandWindow,
18 hyprlandCaptureExecutable,
19 hyprlandCaptureShortcut,
20 isHyprlandCaptureSession,
21} from "./HyprlandSnapShot.ts";
22
23let directory: string;
24let paths: { bundle: string; dataHome: string };
25let setup: HyprlandCaptureSetup;
26let denial: boolean;
27let metadata: typeof window | null;
28const png = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10, 0]);
29const window = {
30 title: "Notes",
31 appName: "editor",
32 appIdentifier: "editor",
33 processId: 123,
34 bounds: { x: -1920, y: 20, width: 800, height: 600 },
35 clientBounds: { x: -1920, y: 20, width: 800, height: 600 },
36};
37beforeEach(async () => {
38 directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-hypr-test-"));
39 paths = {
40 bundle: NodePath.join(directory, "bundle"),
41 dataHome: NodePath.join(directory, "user data"),
42 };
43 await NodeFSP.writeFile(paths.bundle, "bundled executable");
44 setup = new HyprlandCaptureSetup(paths);
45 denial = false;
46 metadata = window;
47 startEffects.mockReset().mockResolvedValue(undefined);
48 execute
49 .mockReset()
50 .mockImplementation(
51 (
52 _file: string,
53 args: string[],
54 _options: unknown,
55 callback: (error: Error | null, stdout: string, stderr: string) => void,
56 ) => {
57 if (args[0] === "capture") {
58 if (denial) callback(new Error("Denied"), "", "Check screen-sharing permission.");
59 else
60 void NodeFSP.writeFile(NodePath.join(args[1]!, "capture.png"), png).then(() =>
61 callback(null, JSON.stringify({ window: metadata }), ""),
62 );
63 } else callback(null, '{"feedbackAvailable":true}', "");
64 },
65 );
66});
67afterEach(async () => {
68 await NodeFSP.rm(directory, { recursive: true, force: true });
69});
70
71it("does not select Hyprland from a stale socket environment or sandbox", () => {
72 expect(isHyprlandCaptureSession({ XDG_CURRENT_DESKTOP: "Hyprland" })).toBe(true);
73 expect(
74 isHyprlandCaptureSession({ XDG_CURRENT_DESKTOP: "KDE", HYPRLAND_INSTANCE_SIGNATURE: "old" }),
75 ).toBe(false);
76 expect(isHyprlandCaptureSession({ XDG_CURRENT_DESKTOP: "Hyprland", FLATPAK_ID: "app" })).toBe(
77 false,
78 );
79 expect(isHyprlandCaptureSession({ XDG_CURRENT_DESKTOP: "Hyprland", SNAP: "app" })).toBe(false);
80});
81it("discovery neither installs a helper nor requests a screenshot", async () => {
82 expect((await setup.state()).status).toBe("not-installed");
83 expect(execute).not.toHaveBeenCalled();
84 await expect(NodeFSP.stat(paths.dataHome)).rejects.toMatchObject({ code: "ENOENT" });
85});
86it("installs offline at a stable executable path and probes only capabilities", async () => {
87 await setup.perform("install-hyprland-helper");
88 expect((await NodeFSP.stat(hyprlandCaptureExecutable(paths))).mode & 0o777).toBe(0o755);
89 expect(await setup.state()).toMatchObject({ status: "ready", feedbackAvailable: true });
90 expect(execute.mock.calls.map(([file, args]) => [file, args])).toEqual([
91 [hyprlandCaptureExecutable(paths), ["check"]],
92 ]);
93});
94it("updates explicitly and removes only its helper", async () => {
95 await setup.perform("install-hyprland-helper");
96 const unrelated = NodePath.join(paths.dataHome, "keep.txt");
97 await NodeFSP.writeFile(unrelated, "keep");
98 await NodeFSP.writeFile(paths.bundle, "update");
99 expect((await setup.state()).status).toBe("update-required");
100 await setup.perform("install-hyprland-helper");
101 expect((await setup.state()).status).toBe("ready");
102 await setup.perform("remove-hyprland-helper");
103 expect((await setup.state()).status).toBe("not-installed");
104 expect(await NodeFSP.readFile(unrelated, "utf8")).toBe("keep");
105});
106it("does not overwrite a linked executable", async () => {
107 const executable = hyprlandCaptureExecutable(paths);
108 await NodeFSP.mkdir(NodePath.dirname(executable), { recursive: true });
109 await NodeFSP.symlink(paths.bundle, executable);
110 await expect(setup.perform("install-hyprland-helper")).rejects.toThrow("not a link");
111 expect(await NodeFSP.readFile(paths.bundle, "utf8")).toBe("bundled executable");
112});
113it.each([true, false])("uses the active config syntax and user bindings (Lua: %s)", async (lua) => {
114 const config = NodePath.join(directory, "hypr");
115 await NodeFSP.mkdir(config);
116 await NodeFSP.writeFile(
117 NodePath.join(config, lua ? "hyprland.lua" : "hyprland.conf"),
118 "custom config",
119 );
120 await NodeFSP.writeFile(
121 NodePath.join(config, lua ? "bindings.lua" : "bindings.conf"),
122 "custom bindings",
123 );
124 const result = await hyprlandCaptureShortcut("com.t3tools.T3Code", "/home/test", directory);
125 expect(result.shortcutConfigPath).toBe(
126 NodePath.join(config, lua ? "bindings.lua" : "bindings.conf"),
127 );
128 expect(result.shortcutBinding).toBe(
129 lua
130 ? 'hl.bind("CTRL + SHIFT + 2", hl.dsp.global("com.t3tools.T3Code:capture-window"))'
131 : "bind = CTRL SHIFT, 2, global, com.t3tools.T3Code:capture-window",
132 );
133 expect(await NodeFSP.readFile(result.shortcutConfigPath, "utf8")).toBe("custom bindings");
134});
135it("captures exact-window metadata for accessibility and focuses only through the installed helper", async () => {
136 await setup.perform("install-hyprland-helper");
137 const result = await captureHyprlandWindow(paths);
138 expect(result.png).toEqual(png);
139 expect(result.window).toEqual(window);
140 await result.feedback?.activate("T3 destination");
141 expect(execute.mock.calls.find(([, args]) => args[0] === "activate")?.slice(0, 2)).toEqual([
142 hyprlandCaptureExecutable(paths),
143 ["activate", String(process.pid), "T3 destination"],
144 ]);
145 const captureDirectory = execute.mock.calls.find(([, args]) => args[0] === "capture")![1][1];
146 await expect(NodeFSP.stat(captureDirectory)).rejects.toMatchObject({ code: "ENOENT" });
147 result.feedback?.close();
148});
149it("does not guess accessibility metadata when the captured window changed", async () => {
150 await setup.perform("install-hyprland-helper");
151 metadata = null;
152 const result = await captureHyprlandWindow(paths, { flash: true, animate: true });
153 expect(result.window).toBeUndefined();
154 expect(startEffects).not.toHaveBeenCalled();
155 result.feedback?.close();
156});
157it("surfaces a capture denial without a picker fallback and removes temporary output", async () => {
158 await setup.perform("install-hyprland-helper");
159 denial = true;
160 await expect(captureHyprlandWindow(paths)).rejects.toThrow("screen-sharing permission");
161 const path = execute.mock.calls.find(([, args]) => args[0] === "capture")![1][1];
162 await expect(NodeFSP.stat(path)).rejects.toMatchObject({ code: "ENOENT" });
163});
164it("keeps capture working when an overlay cannot start", async () => {
165 await setup.perform("install-hyprland-helper");
166 startEffects.mockRejectedValue(new Error("No layer shell"));
167 const result = await captureHyprlandWindow(paths, { flash: true, animate: true });
168 expect(result.png).toEqual(png);
169 expect(result.feedback?.animationStarted).toBe(false);
170 result.feedback?.close();
171});