| 1 | import { DEFAULT_CLIENT_SETTINGS, type DesktopSnapShotState } from "@t3tools/contracts"; |
| 2 | import { expect, it } from "vite-plus/test"; |
| 3 | import { |
| 4 | captureSetupAccessReady, |
| 5 | captureSetupBackend, |
| 6 | captureSetupCheckMessage, |
| 7 | captureSetupDesktopName, |
| 8 | captureSetupInitialStep, |
| 9 | captureSetupMacPermissionsReady, |
| 10 | captureSetupShortcutReady, |
| 11 | captureSetupShouldDisableOnClose, |
| 12 | } from "./SnapShotSetupDialog.logic"; |
| 13 | |
| 14 | const gnome: DesktopSnapShotState = { |
| 15 | mode: "portal", |
| 16 | linuxBackend: "gnome-extension", |
| 17 | shortcut: DEFAULT_CLIENT_SETTINGS.snapShotShortcut, |
| 18 | shortcutRegistered: true, |
| 19 | shortcutMessage: "Requested", |
| 20 | shortcutVerified: false, |
| 21 | message: null, |
| 22 | gnomeExtension: { status: "enabled", message: "Running" }, |
| 23 | }; |
| 24 | |
| 25 | it("derives Hyprland setup from its own helper and registered action, not old GNOME approval", () => { |
| 26 | const state: DesktopSnapShotState = { |
| 27 | ...gnome, |
| 28 | linuxBackend: "hyprland", |
| 29 | linuxDesktop: "hyprland", |
| 30 | shortcutRegistered: false, |
| 31 | hyprlandHelper: { status: "not-installed", message: "Install" }, |
| 32 | }; |
| 33 | expect(captureSetupBackend(state)).toBe("hyprland"); |
| 34 | expect(captureSetupDesktopName(state)).toBe("Hyprland"); |
| 35 | expect(captureSetupAccessReady(state)).toBe(false); |
| 36 | expect(captureSetupInitialStep(state)).toBe("access"); |
| 37 | const installed = { ...state, hyprlandHelper: { status: "ready" as const, message: "Ready" } }; |
| 38 | expect(captureSetupInitialStep(installed)).toBe("shortcut"); |
| 39 | expect(captureSetupShortcutReady(installed, false)).toBe(false); |
| 40 | expect(captureSetupShortcutReady({ ...installed, shortcutPending: true }, false)).toBe(false); |
| 41 | expect(captureSetupShortcutReady({ ...installed, shortcutActionRegistered: true }, false)).toBe( |
| 42 | true, |
| 43 | ); |
| 44 | expect(captureSetupAccessReady({ ...installed, message: "Check access" })).toBe(false); |
| 45 | }); |
| 46 | |
| 47 | it.each([ |
| 48 | "not-installed", |
| 49 | "disabled", |
| 50 | "restart-required", |
| 51 | "update-required", |
| 52 | "extensions-disabled", |
| 53 | "error", |
| 54 | ] as const)( |
| 55 | "keeps %s GNOME setup on the access step, even with a previously verified shortcut", |
| 56 | (status) => { |
| 57 | const state = { |
| 58 | ...gnome, |
| 59 | shortcutVerified: true, |
| 60 | gnomeExtension: { status, message: "Needs setup" }, |
| 61 | }; |
| 62 | expect(captureSetupInitialStep(state)).toBe("access"); |
| 63 | expect(captureSetupInitialStep(state, "shortcut")).toBe("access"); |
| 64 | expect(captureSetupAccessReady(state)).toBe(false); |
| 65 | expect(captureSetupShortcutReady(state, false)).toBe(false); |
| 66 | }, |
| 67 | ); |
| 68 | |
| 69 | it("does not declare access ready before the enabled extension's capture endpoint is available", () => { |
| 70 | expect(captureSetupAccessReady({ ...gnome, linuxBackend: "picker" })).toBe(false); |
| 71 | expect(captureSetupAccessReady(gnome)).toBe(true); |
| 72 | }); |
| 73 | |
| 74 | it("resumes after login from real extension state without an onboarding-completed flag", () => { |
| 75 | expect( |
| 76 | captureSetupInitialStep({ |
| 77 | ...gnome, |
| 78 | gnomeExtension: { status: "disabled", message: "Enable it" }, |
| 79 | }), |
| 80 | ).toBe("access"); |
| 81 | expect(captureSetupInitialStep({ ...gnome, shortcutRegistered: false })).toBe("shortcut"); |
| 82 | expect(captureSetupInitialStep(gnome)).toBe("shortcut"); |
| 83 | }); |
| 84 | |
| 85 | it("finishes setup with a saved shortcut without requiring a separate delivery test", () => { |
| 86 | expect(captureSetupInitialStep({ ...gnome, shortcutVerified: false })).toBe("shortcut"); |
| 87 | expect(captureSetupShortcutReady(gnome, false)).toBe(true); |
| 88 | }); |
| 89 | |
| 90 | it("still allows revisiting capture access and editing a saved shortcut", () => { |
| 91 | expect(captureSetupInitialStep(gnome, "access")).toBe("access"); |
| 92 | expect(captureSetupInitialStep(gnome, "shortcut")).toBe("shortcut"); |
| 93 | }); |
| 94 | |
| 95 | it("does not skip native permission setup when capture has not been enabled", () => { |
| 96 | expect( |
| 97 | captureSetupInitialStep({ |
| 98 | ...gnome, |
| 99 | mode: "direct", |
| 100 | linuxBackend: undefined, |
| 101 | gnomeExtension: undefined, |
| 102 | shortcutRegistered: false, |
| 103 | }), |
| 104 | ).toBe("access"); |
| 105 | }); |
| 106 | |
| 107 | it("keeps the picker explanation when automatic capture is unavailable", () => { |
| 108 | expect( |
| 109 | captureSetupInitialStep({ ...gnome, linuxBackend: "picker", gnomeExtension: undefined }), |
| 110 | ).toBe("access"); |
| 111 | }); |
| 112 | |
| 113 | it("offers manual capture on an unsupported GNOME version instead of trapping setup", () => { |
| 114 | const state = { |
| 115 | ...gnome, |
| 116 | linuxBackend: "picker" as const, |
| 117 | gnomeExtension: { status: "unsupported" as const, message: "New GNOME version" }, |
| 118 | }; |
| 119 | expect(captureSetupBackend(state)).toBe("picker"); |
| 120 | expect(captureSetupAccessReady(state)).toBe(true); |
| 121 | expect(captureSetupInitialStep(state)).toBe("access"); |
| 122 | expect(captureSetupCheckMessage(state)).toContain("choose a window each time"); |
| 123 | }); |
| 124 | |
| 125 | it.each(["not-installed", "update-required", "error"] as const)( |
| 126 | "requires KDE helper access for %s even with a saved shortcut", |
| 127 | (status) => { |
| 128 | const state = { |
| 129 | ...gnome, |
| 130 | linuxBackend: "kde" as const, |
| 131 | gnomeExtension: undefined, |
| 132 | kdeHelper: { status, message: "Needs setup" }, |
| 133 | shortcutVerified: true, |
| 134 | }; |
| 135 | expect(captureSetupBackend(state)).toBe("kde"); |
| 136 | expect(captureSetupAccessReady(state)).toBe(false); |
| 137 | expect(captureSetupInitialStep(state, "shortcut")).toBe("access"); |
| 138 | expect(captureSetupShortcutReady(state, false)).toBe(false); |
| 139 | }, |
| 140 | ); |
| 141 | |
| 142 | it("resumes KDE setup after the helper passes its real permission check", () => { |
| 143 | const state = { |
| 144 | ...gnome, |
| 145 | linuxBackend: "kde" as const, |
| 146 | gnomeExtension: undefined, |
| 147 | kdeHelper: { status: "ready" as const, message: "Ready" }, |
| 148 | }; |
| 149 | expect(captureSetupAccessReady(state)).toBe(true); |
| 150 | expect(captureSetupInitialStep(state)).toBe("shortcut"); |
| 151 | expect(captureSetupInitialStep({ ...state, shortcutRegistered: false })).toBe("shortcut"); |
| 152 | }); |
| 153 | |
| 154 | it("uses the current desktop's access requirements while preserving setup for the others", () => { |
| 155 | const kde: DesktopSnapShotState = { |
| 156 | ...gnome, |
| 157 | linuxDesktop: "kde", |
| 158 | linuxBackend: "kde", |
| 159 | kdeHelper: { status: "not-installed", message: "Install the helper" }, |
| 160 | shortcutVerified: false, |
| 161 | }; |
| 162 | expect(captureSetupDesktopName(kde)).toBe("KDE Plasma"); |
| 163 | expect(captureSetupInitialStep(kde)).toBe("access"); |
| 164 | expect( |
| 165 | captureSetupInitialStep({ |
| 166 | ...kde, |
| 167 | kdeHelper: { status: "ready", message: "Ready" }, |
| 168 | shortcutRegistered: false, |
| 169 | }), |
| 170 | ).toBe("shortcut"); |
| 171 | |
| 172 | const niri: DesktopSnapShotState = { |
| 173 | ...gnome, |
| 174 | linuxDesktop: "niri", |
| 175 | linuxBackend: "niri", |
| 176 | shortcutRegistered: false, |
| 177 | }; |
| 178 | expect(captureSetupDesktopName(niri)).toBe("Niri"); |
| 179 | expect(captureSetupInitialStep(niri)).toBe("shortcut"); |
| 180 | |
| 181 | expect(captureSetupDesktopName(gnome)).toBe("GNOME"); |
| 182 | expect(captureSetupInitialStep(gnome)).toBe("shortcut"); |
| 183 | expect(captureSetupShortcutReady(gnome, false)).toBe(true); |
| 184 | }); |
| 185 | |
| 186 | it("does not infer a Linux desktop for native capture or an unidentified portal", () => { |
| 187 | expect(captureSetupDesktopName({ ...gnome, mode: "direct" })).toBeUndefined(); |
| 188 | expect( |
| 189 | captureSetupDesktopName({ |
| 190 | ...gnome, |
| 191 | linuxBackend: "screenshot-portal", |
| 192 | gnomeExtension: undefined, |
| 193 | }), |
| 194 | ).toBeUndefined(); |
| 195 | }); |
| 196 | |
| 197 | it("acknowledges an unchanged recheck while GNOME still needs a sign-out", () => { |
| 198 | const state = { |
| 199 | ...gnome, |
| 200 | gnomeExtension: { status: "restart-required" as const, message: "Sign out" }, |
| 201 | }; |
| 202 | expect(captureSetupCheckMessage(state)).toBe("Still waiting for you to sign out and back in."); |
| 203 | expect(captureSetupAccessReady(state)).toBe(false); |
| 204 | }); |
| 205 | |
| 206 | it("only confirms capture access when the rechecked extension is running and reachable", () => { |
| 207 | expect(captureSetupCheckMessage(gnome)).toBe("Ready. Continue to choose your shortcut."); |
| 208 | expect(captureSetupCheckMessage({ ...gnome, linuxBackend: "picker" })).toBe( |
| 209 | "Not ready yet. Finish the step above.", |
| 210 | ); |
| 211 | expect( |
| 212 | captureSetupCheckMessage({ |
| 213 | ...gnome, |
| 214 | gnomeExtension: { status: "disabled", message: "Enable it" }, |
| 215 | }), |
| 216 | ).toBe("Not ready yet. Finish the step above."); |
| 217 | }); |
| 218 | |
| 219 | it("does not report a successful check when capture support could not be read", () => { |
| 220 | expect(captureSetupCheckMessage({ ...gnome, message: "Desktop disconnected" })).toContain( |
| 221 | "Still unable to check access", |
| 222 | ); |
| 223 | expect( |
| 224 | captureSetupCheckMessage({ |
| 225 | ...gnome, |
| 226 | gnomeExtension: { status: "error", message: "Could not read extension state" }, |
| 227 | }), |
| 228 | ).toContain("Still unable to check access"); |
| 229 | }); |
| 230 | |
| 231 | it("uses a capable portal without requiring the optional GNOME extension", () => { |
| 232 | const state = { |
| 233 | ...gnome, |
| 234 | linuxBackend: "screenshot-portal" as const, |
| 235 | gnomeExtension: { status: "not-installed" as const, message: "Not installed" }, |
| 236 | }; |
| 237 | expect(captureSetupBackend(state)).toBe("portal"); |
| 238 | expect(captureSetupAccessReady(state)).toBe(true); |
| 239 | expect(captureSetupInitialStep(state)).toBe("shortcut"); |
| 240 | expect( |
| 241 | captureSetupCheckMessage({ |
| 242 | ...state, |
| 243 | gnomeExtension: { status: "error", message: "Optional extension failed" }, |
| 244 | }), |
| 245 | ).toBe("Ready. Continue to choose your shortcut."); |
| 246 | }); |
| 247 | |
| 248 | it("lets Niri setup finish with configuration instructions without claiming the binding was verified", () => { |
| 249 | const niri = { |
| 250 | ...gnome, |
| 251 | linuxBackend: "niri" as const, |
| 252 | gnomeExtension: undefined, |
| 253 | shortcutRegistered: false, |
| 254 | }; |
| 255 | expect(captureSetupBackend(niri)).toBe("niri"); |
| 256 | expect(captureSetupAccessReady(niri)).toBe(true); |
| 257 | expect(captureSetupShortcutReady(niri, false)).toBe(false); |
| 258 | const withEndpoint = { ...niri, shortcutBinding: "Ctrl+Shift+2 { spawn ...; }" }; |
| 259 | expect(captureSetupInitialStep(withEndpoint)).toBe("shortcut"); |
| 260 | expect(captureSetupShortcutReady(withEndpoint, false)).toBe(true); |
| 261 | expect(withEndpoint.shortcutRegistered).toBe(false); |
| 262 | expect(withEndpoint.shortcutVerified).toBe(false); |
| 263 | }); |
| 264 | |
| 265 | it("requires saving a changed chord before finishing setup", () => { |
| 266 | expect(captureSetupShortcutReady(gnome, true)).toBe(false); |
| 267 | expect(captureSetupShortcutReady(gnome, false)).toBe(true); |
| 268 | expect(captureSetupShortcutReady({ ...gnome, shortcutRegistered: false }, false)).toBe(false); |
| 269 | }); |
| 270 | |
| 271 | it("allows leaving setup while the desktop prompt is pending but never after an explicit denial", () => { |
| 272 | const pending = { ...gnome, shortcutRegistered: false, shortcutPending: true }; |
| 273 | expect(captureSetupShortcutReady(pending, false)).toBe(true); |
| 274 | expect(captureSetupShortcutReady(pending, true)).toBe(false); |
| 275 | expect(captureSetupShortcutReady({ ...pending, shortcutPending: false }, false)).toBe(false); |
| 276 | }); |
| 277 | |
| 278 | it.each([false, true])( |
| 279 | "does not require a previously observed shortcut activation (%s)", |
| 280 | (shortcutVerified) => { |
| 281 | const state = { ...gnome, shortcutVerified }; |
| 282 | expect(captureSetupShortcutReady(state, false)).toBe(true); |
| 283 | expect(captureSetupInitialStep(state)).toBe("shortcut"); |
| 284 | }, |
| 285 | ); |
| 286 | |
| 287 | it("blocks finishing if desktop access is lost during the wizard", () => { |
| 288 | expect( |
| 289 | captureSetupShortcutReady( |
| 290 | { |
| 291 | ...gnome, |
| 292 | shortcutVerified: true, |
| 293 | message: "Desktop disconnected", |
| 294 | }, |
| 295 | false, |
| 296 | ), |
| 297 | ).toBe(false); |
| 298 | expect(captureSetupAccessReady({ ...gnome, mode: "unavailable" })).toBe(false); |
| 299 | }); |
| 300 | |
| 301 | it.each([ |
| 302 | [false, false, true], |
| 303 | [false, true, false], |
| 304 | [true, false, false], |
| 305 | [true, true, false], |
| 306 | ] as const)( |
| 307 | "closing setup (previously enabled=%s, completed=%s) disables only an unfinished first opt-in", |
| 308 | (wasEnabled, completed, disable) => { |
| 309 | expect(captureSetupShouldDisableOnClose(wasEnabled, completed)).toBe(disable); |
| 310 | }, |
| 311 | ); |
| 312 | |
| 313 | it("gates Continue on macOS permissions, requiring accessibility only when app text is on", () => { |
| 314 | const mac: DesktopSnapShotState = { |
| 315 | ...gnome, |
| 316 | mode: "direct", |
| 317 | linuxBackend: undefined, |
| 318 | gnomeExtension: undefined, |
| 319 | macPermissions: { screenRecording: true, accessibility: false }, |
| 320 | }; |
| 321 | expect(captureSetupMacPermissionsReady(mac, true)).toBe(false); |
| 322 | expect(captureSetupMacPermissionsReady(mac, false)).toBe(true); |
| 323 | expect( |
| 324 | captureSetupMacPermissionsReady( |
| 325 | { ...mac, macPermissions: { screenRecording: false, accessibility: true } }, |
| 326 | false, |
| 327 | ), |
| 328 | ).toBe(false); |
| 329 | expect(captureSetupMacPermissionsReady({ ...mac, macPermissions: undefined }, true)).toBe(true); |
| 330 | }); |
| 331 | |
| 332 | it.each(["kde", "hyprland"] as const)("ignores errors from inactive helpers on %s", (backend) => { |
| 333 | const state: DesktopSnapShotState = { |
| 334 | ...gnome, |
| 335 | linuxBackend: backend, |
| 336 | kdeHelper: { status: backend === "kde" ? "ready" : "error", message: "KDE" }, |
| 337 | hyprlandHelper: { status: backend === "hyprland" ? "ready" : "error", message: "Hyprland" }, |
| 338 | }; |
| 339 | expect(captureSetupAccessReady(state)).toBe(true); |
| 340 | expect(captureSetupCheckMessage(state)).toBe("Ready. Continue to choose your shortcut."); |
| 341 | }); |