| 1 | import { assert, describe, it } from "@effect/vitest"; |
| 2 | import { vi } from "vite-plus/test"; |
| 3 | |
| 4 | import { |
| 5 | activateWindowsForegroundWithApi, |
| 6 | isWindowsShellHostedForegroundWithApi, |
| 7 | type WindowsForegroundApi, |
| 8 | } from "./WindowsForeground.ts"; |
| 9 | |
| 10 | function nativeHandle(value: bigint, bytes = 8): Buffer { |
| 11 | const handle = Buffer.alloc(bytes); |
| 12 | if (bytes === 8) handle.writeBigUInt64LE(value); |
| 13 | else handle.writeUInt32LE(Number(value)); |
| 14 | return handle; |
| 15 | } |
| 16 | |
| 17 | function makeApi(input: { |
| 18 | readonly foregroundWindow?: bigint; |
| 19 | readonly currentThreadId?: number; |
| 20 | readonly foregroundThreadId?: number; |
| 21 | readonly attached?: boolean; |
| 22 | readonly activated?: boolean; |
| 23 | }) { |
| 24 | return { |
| 25 | getCurrentThreadId: vi.fn(() => input.currentThreadId ?? 10), |
| 26 | getForegroundWindow: vi.fn(() => input.foregroundWindow ?? 99n), |
| 27 | getWindowClassName: vi.fn(() => "Chrome_WidgetWin_1"), |
| 28 | getWindowThreadId: vi.fn(() => input.foregroundThreadId ?? 20), |
| 29 | getWindowThreadAndProcessId: vi.fn(() => ({ |
| 30 | threadId: input.foregroundThreadId ?? 20, |
| 31 | processId: 500, |
| 32 | })), |
| 33 | getWindowText: vi.fn(() => ""), |
| 34 | getWindowRect: vi.fn(() => undefined), |
| 35 | getProcessImagePath: vi.fn(() => ""), |
| 36 | isKeyDown: vi.fn(() => false), |
| 37 | attachThreadInput: vi.fn((_source, _target, attach) => |
| 38 | attach ? (input.attached ?? true) : true, |
| 39 | ), |
| 40 | setForegroundWindow: vi.fn(() => input.activated ?? true), |
| 41 | } satisfies WindowsForegroundApi; |
| 42 | } |
| 43 | |
| 44 | describe("Windows foreground activation", () => { |
| 45 | it.each([4, 8])("activates a %i-byte native window handle", (bytes) => { |
| 46 | const api = makeApi({}); |
| 47 | const handle = nativeHandle(41n, bytes); |
| 48 | |
| 49 | assert.isTrue(activateWindowsForegroundWithApi(handle, api)); |
| 50 | |
| 51 | assert.deepEqual(api.attachThreadInput.mock.calls, [ |
| 52 | [10, 20, true], |
| 53 | [10, 20, false], |
| 54 | ]); |
| 55 | assert.deepEqual(api.setForegroundWindow.mock.calls, [[41n]]); |
| 56 | }); |
| 57 | |
| 58 | it("does not disturb input queues when T3 is already foreground", () => { |
| 59 | const api = makeApi({ foregroundWindow: 41n }); |
| 60 | |
| 61 | assert.isTrue(activateWindowsForegroundWithApi(nativeHandle(41n), api)); |
| 62 | |
| 63 | assert.lengthOf(api.getCurrentThreadId.mock.calls, 0); |
| 64 | assert.lengthOf(api.attachThreadInput.mock.calls, 0); |
| 65 | assert.lengthOf(api.setForegroundWindow.mock.calls, 0); |
| 66 | }); |
| 67 | |
| 68 | it("does not attach a thread to itself", () => { |
| 69 | const api = makeApi({ currentThreadId: 20, foregroundThreadId: 20 }); |
| 70 | |
| 71 | assert.isTrue(activateWindowsForegroundWithApi(nativeHandle(41n), api)); |
| 72 | |
| 73 | assert.lengthOf(api.attachThreadInput.mock.calls, 0); |
| 74 | assert.deepEqual(api.setForegroundWindow.mock.calls, [[41n]]); |
| 75 | }); |
| 76 | |
| 77 | it("uses SetForegroundWindow's result as the activation receipt", () => { |
| 78 | const api = makeApi({ activated: false }); |
| 79 | |
| 80 | assert.isFalse(activateWindowsForegroundWithApi(nativeHandle(41n), api)); |
| 81 | |
| 82 | assert.deepEqual(api.attachThreadInput.mock.calls, [ |
| 83 | [10, 20, true], |
| 84 | [10, 20, false], |
| 85 | ]); |
| 86 | }); |
| 87 | |
| 88 | it("still asks Windows directly when the input queues cannot be attached", () => { |
| 89 | const api = makeApi({ attached: false }); |
| 90 | |
| 91 | assert.isTrue(activateWindowsForegroundWithApi(nativeHandle(41n), api)); |
| 92 | |
| 93 | assert.deepEqual(api.attachThreadInput.mock.calls, [[10, 20, true]]); |
| 94 | assert.deepEqual(api.setForegroundWindow.mock.calls, [[41n]]); |
| 95 | }); |
| 96 | |
| 97 | it("rejects malformed native handles", () => { |
| 98 | const api = makeApi({}); |
| 99 | |
| 100 | assert.throws(() => activateWindowsForegroundWithApi(Buffer.alloc(6), api)); |
| 101 | assert.lengthOf(api.getForegroundWindow.mock.calls, 0); |
| 102 | }); |
| 103 | |
| 104 | it("recognizes a shell-hosted foreground window", () => { |
| 105 | const api = makeApi({}); |
| 106 | api.getWindowClassName.mockReturnValue("ApplicationFrameWindow"); |
| 107 | |
| 108 | assert.isTrue(isWindowsShellHostedForegroundWithApi(api)); |
| 109 | assert.deepEqual(api.getWindowClassName.mock.calls, [[99n]]); |
| 110 | }); |
| 111 | |
| 112 | it("does not classify ordinary or missing foreground windows as shell-hosted", () => { |
| 113 | const ordinary = makeApi({}); |
| 114 | const missing = makeApi({ foregroundWindow: 0n }); |
| 115 | |
| 116 | assert.isFalse(isWindowsShellHostedForegroundWithApi(ordinary)); |
| 117 | assert.isFalse(isWindowsShellHostedForegroundWithApi(missing)); |
| 118 | assert.lengthOf(missing.getWindowClassName.mock.calls, 0); |
| 119 | }); |
| 120 | }); |