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} from "./HyprlandSnapShot.ts";
21import { CaptureSetupRequiredError } from "./linuxCaptureSession.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});
70it("installs offline at a stable executable path and probes only capabilities", async () => {
71 await setup.perform("install-hyprland-helper");
72 expect((await NodeFSP.stat(hyprlandCaptureExecutable(paths))).mode & 0o777).toBe(0o755);
73 expect(await setup.state()).toMatchObject({ status: "ready", feedbackAvailable: true });
74 expect(execute.mock.calls.map(([file, args]) => [file, args])).toEqual([
75 [hyprlandCaptureExecutable(paths), ["check"]],
76 ]);
77});
78it("updates explicitly and removes only its helper", async () => {
79 await setup.perform("install-hyprland-helper");
80 const unrelated = NodePath.join(paths.dataHome, "keep.txt");
81 await NodeFSP.writeFile(unrelated, "keep");
82 await NodeFSP.writeFile(paths.bundle, "update");
83 expect((await setup.state()).status).toBe("update-required");
84 await setup.perform("install-hyprland-helper");
85 expect((await setup.state()).status).toBe("ready");
86 await setup.perform("remove-hyprland-helper");
87 expect((await setup.state()).status).toBe("not-installed");
88 expect(await NodeFSP.readFile(unrelated, "utf8")).toBe("keep");
89});
90it("updates a helper left stale by an app update before capturing", async () => {
91 await setup.perform("install-hyprland-helper");
92 await NodeFSP.writeFile(paths.bundle, "update");
93 const result = await captureHyprlandWindow(paths);
94 expect(result.png).toEqual(png);
95 expect(await NodeFSP.readFile(hyprlandCaptureExecutable(paths), "utf8")).toBe("update");
96 result.feedback?.close();
97});
98it("leaves first-time installation to explicit setup", async () => {
99 await expect(captureHyprlandWindow(paths)).rejects.toBeInstanceOf(CaptureSetupRequiredError);
100 await expect(NodeFSP.stat(hyprlandCaptureExecutable(paths))).rejects.toMatchObject({
101 code: "ENOENT",
102 });
103});
104it("does not overwrite a linked executable", async () => {
105 const executable = hyprlandCaptureExecutable(paths);
106 await NodeFSP.mkdir(NodePath.dirname(executable), { recursive: true });
107 await NodeFSP.symlink(paths.bundle, executable);
108 await expect(setup.perform("install-hyprland-helper")).rejects.toThrow("not a link");
109 expect(await NodeFSP.readFile(paths.bundle, "utf8")).toBe("bundled executable");
110});
111it.each([true, false])("uses the active config syntax and user bindings (Lua: %s)", async (lua) => {
112 const config = NodePath.join(directory, "hypr");
113 await NodeFSP.mkdir(config);
114 await NodeFSP.writeFile(
115 NodePath.join(config, lua ? "hyprland.lua" : "hyprland.conf"),
116 "custom config",
117 );
118 await NodeFSP.writeFile(
119 NodePath.join(config, lua ? "bindings.lua" : "bindings.conf"),
120 "custom bindings",
121 );
122 const result = await hyprlandCaptureShortcut("com.t3tools.T3Code", "/home/test", directory);
123 expect(result.shortcutConfigPath).toBe(
124 NodePath.join(config, lua ? "bindings.lua" : "bindings.conf"),
125 );
126 expect(result.shortcutBinding).toBe(
127 lua
128 ? 'hl.bind("CTRL + SHIFT + 2", hl.dsp.global("com.t3tools.T3Code:capture-window"))'
129 : "bind = CTRL SHIFT, 2, global, com.t3tools.T3Code:capture-window",
130 );
131 expect(await NodeFSP.readFile(result.shortcutConfigPath, "utf8")).toBe("custom bindings");
132});
133it("captures exact-window metadata for accessibility and focuses only through the installed helper", async () => {
134 await setup.perform("install-hyprland-helper");
135 const result = await captureHyprlandWindow(paths);
136 expect(result.png).toEqual(png);
137 expect(result.window).toEqual(window);
138 await result.feedback?.activate("T3 destination");
139 expect(execute.mock.calls.find(([, args]) => args[0] === "activate")?.slice(0, 2)).toEqual([
140 hyprlandCaptureExecutable(paths),
141 ["activate", String(process.pid), "T3 destination"],
142 ]);
143 const captureDirectory = execute.mock.calls.find(([, args]) => args[0] === "capture")![1][1];
144 await expect(NodeFSP.stat(captureDirectory)).rejects.toMatchObject({ code: "ENOENT" });
145 result.feedback?.close();
146});
147it("does not guess accessibility metadata when the captured window changed", async () => {
148 await setup.perform("install-hyprland-helper");
149 metadata = null;
150 const result = await captureHyprlandWindow(paths, { flash: true, animate: true });
151 expect(result.window).toBeUndefined();
152 expect(startEffects).not.toHaveBeenCalled();
153 result.feedback?.close();
154});
155it("surfaces a capture denial without a picker fallback and removes temporary output", async () => {
156 await setup.perform("install-hyprland-helper");
157 denial = true;
158 await expect(captureHyprlandWindow(paths)).rejects.toThrow("screen-sharing permission");
159 const path = execute.mock.calls.find(([, args]) => args[0] === "capture")![1][1];
160 await expect(NodeFSP.stat(path)).rejects.toMatchObject({ code: "ENOENT" });
161});
162it("keeps capture working when an overlay cannot start", async () => {
163 await setup.perform("install-hyprland-helper");
164 startEffects.mockRejectedValue(new Error("No layer shell"));
165 const result = await captureHyprlandWindow(paths, { flash: true, animate: true });
166 expect(result.png).toEqual(png);
167 expect(result.feedback?.animationStarted).toBe(false);
168 result.feedback?.close();
169});