| 1 | import { assert, expect, it } from "vite-plus/test"; |
| 2 | import { DEFAULT_CLIENT_SETTINGS, type DesktopSnapShotState } from "@t3tools/contracts"; |
| 3 | |
| 4 | import { |
| 5 | createRecordingRequestTracker, |
| 6 | snapShotStatus, |
| 7 | snapShotShortcutStatus, |
| 8 | snapShotUnavailableMessage, |
| 9 | snapShotSoundPatch, |
| 10 | snapShotFeedbackUnavailableMessage, |
| 11 | snapShotSetupSummary, |
| 12 | snapShotSetupButtonLabel, |
| 13 | snapShotSetupComplete, |
| 14 | snapShotDescription, |
| 15 | snapShotAccessibilityUnavailableMessage, |
| 16 | } from "./SnapShotSettings.logic"; |
| 17 | |
| 18 | it.each([ |
| 19 | ["off", { snapShotPlaySound: false }], |
| 20 | ["soft-pop", { snapShotPlaySound: true, snapShotSound: "soft-pop" }], |
| 21 | ["camera-shutter", { snapShotPlaySound: true, snapShotSound: "camera-shutter" }], |
| 22 | ] as const)("maps %s to compatible capture settings", (sound, patch) => { |
| 23 | expect(snapShotSoundPatch(sound)).toEqual(patch); |
| 24 | }); |
| 25 | |
| 26 | it("offers effects only with a capable GNOME extension, explaining how to upgrade v1", () => { |
| 27 | const state: DesktopSnapShotState = { |
| 28 | mode: "portal", |
| 29 | linuxBackend: "gnome-extension", |
| 30 | shortcut: DEFAULT_CLIENT_SETTINGS.snapShotShortcut, |
| 31 | shortcutRegistered: true, |
| 32 | shortcutMessage: null, |
| 33 | message: null, |
| 34 | }; |
| 35 | expect(snapShotFeedbackUnavailableMessage(state)).toContain("Update"); |
| 36 | expect( |
| 37 | snapShotFeedbackUnavailableMessage({ ...state, linuxFeedbackAvailable: true }), |
| 38 | ).toBeUndefined(); |
| 39 | expect(snapShotFeedbackUnavailableMessage({ ...state, linuxBackend: "picker" })).toContain( |
| 40 | "aren't available", |
| 41 | ); |
| 42 | expect(snapShotFeedbackUnavailableMessage({ ...state, mode: "direct" })).toBeUndefined(); |
| 43 | }); |
| 44 | |
| 45 | it("ignores a stale request after a newer request starts", () => { |
| 46 | const requests = createRecordingRequestTracker(); |
| 47 | const firstRequest = requests.tryBegin(); |
| 48 | assert(firstRequest); |
| 49 | |
| 50 | requests.clear(); |
| 51 | const secondRequest = requests.tryBegin(); |
| 52 | assert(secondRequest); |
| 53 | |
| 54 | expect(requests.owns(firstRequest)).toBe(false); |
| 55 | expect(requests.owns(secondRequest)).toBe(true); |
| 56 | expect(requests.tryBegin()).toBeNull(); |
| 57 | }); |
| 58 | |
| 59 | it("reports unavailable capture support without browser globals", () => { |
| 60 | expect(snapShotUnavailableMessage(false)).toBe("Only available in the desktop app."); |
| 61 | }); |
| 62 | |
| 63 | it("describes Niri setup without claiming a global shortcut is registered", () => { |
| 64 | const state: DesktopSnapShotState = { |
| 65 | mode: "portal", |
| 66 | linuxBackend: "niri", |
| 67 | shortcut: DEFAULT_CLIENT_SETTINGS.snapShotShortcut, |
| 68 | shortcutRegistered: false, |
| 69 | shortcutMessage: "Managed by Niri", |
| 70 | message: null, |
| 71 | }; |
| 72 | expect(snapShotStatus(state, true)).toBe("Finish shortcut setup"); |
| 73 | expect(snapShotStatus(state, true)).not.toContain("could not be registered"); |
| 74 | expect(snapShotFeedbackUnavailableMessage(state)).toContain("aren't available on Niri"); |
| 75 | }); |
| 76 | |
| 77 | it("distinguishes Hyprland helper setup, action registration, and verified shortcut delivery", () => { |
| 78 | const state: DesktopSnapShotState = { |
| 79 | mode: "portal", |
| 80 | linuxBackend: "hyprland", |
| 81 | linuxDesktop: "hyprland", |
| 82 | shortcut: DEFAULT_CLIENT_SETTINGS.snapShotShortcut, |
| 83 | shortcutRegistered: false, |
| 84 | shortcutMessage: "Connecting to Hyprland shortcuts…", |
| 85 | message: null, |
| 86 | hyprlandHelper: { status: "not-installed", message: "Install helper" }, |
| 87 | }; |
| 88 | expect(snapShotSetupButtonLabel(state)).toBe("Set up Hyprland capture"); |
| 89 | expect(snapShotStatus(state, false)).toBe("Turn this on to set up snapshots."); |
| 90 | expect(snapShotStatus(state, true)).toContain("Install the capture helper"); |
| 91 | expect(snapShotFeedbackUnavailableMessage(state)).toContain("Install or update"); |
| 92 | const ready = { |
| 93 | ...state, |
| 94 | hyprlandHelper: { status: "ready" as const, message: "Ready" }, |
| 95 | linuxFeedbackAvailable: true, |
| 96 | }; |
| 97 | expect(snapShotSetupButtonLabel(ready)).toBe("Manage capture"); |
| 98 | expect(snapShotShortcutStatus({ ...ready, shortcutPending: true })).not.toContain("permission"); |
| 99 | expect(snapShotStatus({ ...ready, shortcutActionRegistered: true }, true)).toBe( |
| 100 | "Use your shortcut from another app", |
| 101 | ); |
| 102 | expect(snapShotStatus({ ...ready, shortcutVerified: true }, true)).toBe("Ready to capture"); |
| 103 | expect(snapShotFeedbackUnavailableMessage(ready)).toBeUndefined(); |
| 104 | expect(snapShotAccessibilityUnavailableMessage(ready)).toBeUndefined(); |
| 105 | }); |
| 106 | |
| 107 | it("keeps unavailable capture distinct from the opt-in setup prompt", () => { |
| 108 | const state: DesktopSnapShotState = { |
| 109 | mode: "unavailable", |
| 110 | shortcut: DEFAULT_CLIENT_SETTINGS.snapShotShortcut, |
| 111 | shortcutRegistered: false, |
| 112 | shortcutMessage: null, |
| 113 | message: "Wayland is required.", |
| 114 | }; |
| 115 | |
| 116 | expect(snapShotStatus(state, false)).toBe("Wayland is required."); |
| 117 | }); |
| 118 | |
| 119 | it.each(["gnome-extension", "niri", "screenshot-portal", "picker"] as const)( |
| 120 | "waits for opt-in before presenting %s setup requirements", |
| 121 | (linuxBackend) => { |
| 122 | const state: DesktopSnapShotState = { |
| 123 | mode: "portal", |
| 124 | linuxBackend, |
| 125 | shortcut: DEFAULT_CLIENT_SETTINGS.snapShotShortcut, |
| 126 | shortcutRegistered: false, |
| 127 | shortcutMessage: "Shortcut permission needed", |
| 128 | message: "Capture needs attention", |
| 129 | gnomeExtension: { status: "not-installed", message: "Install the extension" }, |
| 130 | }; |
| 131 | |
| 132 | expect(DEFAULT_CLIENT_SETTINGS.snapShotEnabled).toBe(false); |
| 133 | expect(snapShotStatus(state, false)).toBe("Turn this on to set up snapshots."); |
| 134 | expect(snapShotStatus(state, true)).toBe("Capture needs attention"); |
| 135 | }, |
| 136 | ); |
| 137 | |
| 138 | it("distinguishes saved shortcuts from observed delivery without making users repeat setup", () => { |
| 139 | const state: DesktopSnapShotState = { |
| 140 | mode: "portal", |
| 141 | linuxBackend: "gnome-extension", |
| 142 | shortcut: DEFAULT_CLIENT_SETTINGS.snapShotShortcut, |
| 143 | shortcutRegistered: true, |
| 144 | shortcutMessage: "Requested", |
| 145 | message: null, |
| 146 | gnomeExtension: { status: "enabled", message: "Running" }, |
| 147 | }; |
| 148 | expect(snapShotSetupSummary(state, true)).toBe("Shortcut saved"); |
| 149 | expect(snapShotSetupButtonLabel(state)).toBe("Manage capture"); |
| 150 | expect(snapShotSetupSummary({ ...state, shortcutVerified: true }, true)).toBe("Ready to capture"); |
| 151 | expect(snapShotSetupButtonLabel({ ...state, shortcutVerified: true })).toBe("Manage capture"); |
| 152 | expect(snapShotSetupButtonLabel({ ...state, shortcutRegistered: false })).toBe("Manage capture"); |
| 153 | expect( |
| 154 | snapShotSetupButtonLabel({ |
| 155 | ...state, |
| 156 | gnomeExtension: { status: "disabled", message: "Enable the extension" }, |
| 157 | }), |
| 158 | ).toBe("Set up GNOME capture"); |
| 159 | expect(snapShotSetupSummary({ ...state, shortcutVerified: true }, false)).toContain( |
| 160 | "Enable capture", |
| 161 | ); |
| 162 | expect( |
| 163 | snapShotSetupSummary( |
| 164 | { |
| 165 | ...state, |
| 166 | gnomeExtension: { status: "restart-required", message: "Sign out" }, |
| 167 | shortcutVerified: true, |
| 168 | }, |
| 169 | true, |
| 170 | ), |
| 171 | ).toBe("Set up active-window snapshots"); |
| 172 | expect( |
| 173 | snapShotSetupSummary( |
| 174 | { ...state, linuxBackend: "niri", gnomeExtension: undefined, shortcutRegistered: false }, |
| 175 | true, |
| 176 | ), |
| 177 | ).toBe("Finish shortcut setup"); |
| 178 | expect( |
| 179 | snapShotSetupSummary( |
| 180 | { ...state, linuxBackend: "picker", gnomeExtension: undefined, shortcutVerified: true }, |
| 181 | true, |
| 182 | ), |
| 183 | ).toBe("Manual capture only — you'll choose a window each time"); |
| 184 | expect( |
| 185 | snapShotSetupSummary( |
| 186 | { |
| 187 | ...state, |
| 188 | linuxBackend: "screenshot-portal", |
| 189 | gnomeExtension: { status: "not-installed", message: "Optional extension" }, |
| 190 | shortcutVerified: true, |
| 191 | }, |
| 192 | true, |
| 193 | ), |
| 194 | ).toBe("Ready to capture"); |
| 195 | }); |
| 196 | |
| 197 | it.each([ |
| 198 | ["gnome", "GNOME"], |
| 199 | ["kde", "KDE Plasma"], |
| 200 | ["niri", "Niri"], |
| 201 | ] as const)( |
| 202 | "names %s when setup cannot yet determine the capture backend", |
| 203 | (linuxDesktop, name) => { |
| 204 | const state: DesktopSnapShotState = { |
| 205 | mode: "portal", |
| 206 | linuxDesktop, |
| 207 | shortcut: DEFAULT_CLIENT_SETTINGS.snapShotShortcut, |
| 208 | shortcutRegistered: false, |
| 209 | shortcutMessage: null, |
| 210 | message: "Capability check failed", |
| 211 | }; |
| 212 | expect(snapShotSetupButtonLabel(state)).toBe(`Set up ${name} capture`); |
| 213 | }, |
| 214 | ); |
| 215 | |
| 216 | it("keeps picker limitations visible after shortcut verification without recommending a GNOME extension", () => { |
| 217 | const state: DesktopSnapShotState = { |
| 218 | mode: "portal", |
| 219 | linuxBackend: "picker", |
| 220 | shortcut: DEFAULT_CLIENT_SETTINGS.snapShotShortcut, |
| 221 | shortcutRegistered: true, |
| 222 | shortcutMessage: null, |
| 223 | message: null, |
| 224 | shortcutVerified: true, |
| 225 | }; |
| 226 | expect(snapShotStatus(state, true)).toContain("Manual capture only"); |
| 227 | expect(snapShotDescription(state)).toContain("Automatic capture isn't available"); |
| 228 | expect(snapShotFeedbackUnavailableMessage(state)).not.toContain("GNOME"); |
| 229 | expect(snapShotAccessibilityUnavailableMessage(state)).toContain("only provides a screenshot"); |
| 230 | expect( |
| 231 | snapShotAccessibilityUnavailableMessage({ ...state, linuxBackend: "screenshot-portal" }), |
| 232 | ).toContain("only provides a screenshot"); |
| 233 | expect( |
| 234 | snapShotAccessibilityUnavailableMessage({ ...state, linuxBackend: "kde" }), |
| 235 | ).toBeUndefined(); |
| 236 | expect(snapShotFeedbackUnavailableMessage({ ...state, linuxBackend: "kde" })).toContain( |
| 237 | "capture helper", |
| 238 | ); |
| 239 | expect( |
| 240 | snapShotFeedbackUnavailableMessage({ |
| 241 | ...state, |
| 242 | linuxBackend: "kde", |
| 243 | linuxFeedbackAvailable: true, |
| 244 | kdeHelper: { status: "ready", message: "Ready", feedbackAvailable: true }, |
| 245 | }), |
| 246 | ).toBeUndefined(); |
| 247 | expect( |
| 248 | snapShotStatus( |
| 249 | { ...state, linuxBackend: "kde", kdeHelper: { status: "not-installed", message: "Install" } }, |
| 250 | true, |
| 251 | ), |
| 252 | ).toContain("Install the capture helper"); |
| 253 | }); |
| 254 | |
| 255 | it("does not ask Niri users to repeat setup when its capture endpoint is available", () => { |
| 256 | const state: DesktopSnapShotState = { |
| 257 | mode: "portal", |
| 258 | linuxBackend: "niri", |
| 259 | shortcut: DEFAULT_CLIENT_SETTINGS.snapShotShortcut, |
| 260 | shortcutRegistered: false, |
| 261 | shortcutBinding: "Ctrl+Shift+2 { spawn ...; }", |
| 262 | shortcutMessage: null, |
| 263 | message: null, |
| 264 | }; |
| 265 | expect(snapShotStatus(state, true)).toBe("Use your shortcut from another app"); |
| 266 | expect(snapShotSetupButtonLabel(state)).toBe("Manage capture"); |
| 267 | }); |
| 268 | |
| 269 | it("reports pending, denied, and assigned shortcuts without inferring consent from saved keys", () => { |
| 270 | const state: DesktopSnapShotState = { |
| 271 | mode: "portal", |
| 272 | linuxBackend: "screenshot-portal", |
| 273 | shortcut: DEFAULT_CLIENT_SETTINGS.snapShotShortcut, |
| 274 | shortcutRegistered: false, |
| 275 | shortcutPending: true, |
| 276 | shortcutMessage: null, |
| 277 | message: null, |
| 278 | }; |
| 279 | expect(snapShotStatus(state, true)).toContain("Waiting for shortcut permission"); |
| 280 | expect(snapShotShortcutStatus(state)).toContain("Approve the shortcut permission prompt"); |
| 281 | const denied = { ...state, shortcutPending: false, shortcutMessage: "Permission wasn't granted" }; |
| 282 | expect(snapShotShortcutStatus(denied)).toBe("Permission wasn't granted"); |
| 283 | const approved = { |
| 284 | ...denied, |
| 285 | shortcutRegistered: true, |
| 286 | shortcutLabel: "Press <Shift><Control>2", |
| 287 | shortcutMessage: "Desktop shortcut: Press <Shift><Control>2", |
| 288 | }; |
| 289 | expect(snapShotStatus(approved, true)).toBe("Ready to capture"); |
| 290 | expect(snapShotShortcutStatus(approved)).toBeNull(); |
| 291 | expect(snapShotShortcutStatus({ ...approved, shortcutPending: true })).toContain( |
| 292 | "Approve the shortcut permission prompt", |
| 293 | ); |
| 294 | expect( |
| 295 | snapShotShortcutStatus({ |
| 296 | ...approved, |
| 297 | shortcutRegistered: false, |
| 298 | shortcutMessage: "Permission wasn't granted", |
| 299 | }), |
| 300 | ).toBe("Permission wasn't granted"); |
| 301 | }); |
| 302 | |
| 303 | it("hides macOS setup only while permissions and the shortcut are all in place", () => { |
| 304 | const ready: DesktopSnapShotState = { |
| 305 | mode: "direct", |
| 306 | shortcut: DEFAULT_CLIENT_SETTINGS.snapShotShortcut, |
| 307 | shortcutRegistered: true, |
| 308 | shortcutMessage: null, |
| 309 | message: null, |
| 310 | macPermissions: { screenRecording: true, accessibility: true }, |
| 311 | }; |
| 312 | expect(snapShotSetupComplete(ready, true)).toBe(true); |
| 313 | expect(snapShotSetupComplete({ ...ready, macPermissions: undefined }, true)).toBe(false); |
| 314 | expect(snapShotSetupComplete({ ...ready, shortcutRegistered: false }, true)).toBe(false); |
| 315 | const revoked = { |
| 316 | ...ready, |
| 317 | macPermissions: { screenRecording: true, accessibility: false }, |
| 318 | message: "Allow Accessibility in System Settings, then restart T3 Code.", |
| 319 | }; |
| 320 | expect(snapShotSetupComplete(revoked, true)).toBe(false); |
| 321 | expect(snapShotStatus(revoked, true)).toBe("Capture needs attention"); |
| 322 | expect(snapShotSetupButtonLabel(revoked)).toBe("Continue setup"); |
| 323 | expect(snapShotSetupComplete({ ...revoked, message: null }, false)).toBe(true); |
| 324 | expect( |
| 325 | snapShotSetupComplete( |
| 326 | { ...ready, windows: true, macPermissions: undefined, shortcutRegistered: false }, |
| 327 | true, |
| 328 | ), |
| 329 | ).toBe(true); |
| 330 | }); |