From f044877c830dd4b4cf0b630c4d5c2b79320faafb Mon Sep 17 00:00:00 2001 From: sudacode Date: Sat, 16 May 2026 19:04:16 -0700 Subject: [PATCH] fix(macos): drop target after grace period on repeated tracking misses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - registerTrackingMiss was resetting grace state on every miss, so focus was never released; now starts timer on first miss and drops after grace elapses - update two tests to assert focus is dropped (not preserved) once grace expires - add IPC test for setIgnoreMouseEvents → onOverlayMouseInteractionChanged mapping --- src/core/services/ipc.test.ts | 22 ++++++++++++++ src/window-trackers/macos-tracker.test.ts | 35 ++++++++++------------- src/window-trackers/macos-tracker.ts | 9 ++++-- 3 files changed, 44 insertions(+), 22 deletions(-) diff --git a/src/core/services/ipc.test.ts b/src/core/services/ipc.test.ts index de6b8367..8b812225 100644 --- a/src/core/services/ipc.test.ts +++ b/src/core/services/ipc.test.ts @@ -311,6 +311,28 @@ test('createIpcDepsRuntime wires AniList handlers', async () => { assert.equal(deps.getPlaybackPaused(), true); }); +test('registerIpcHandlers maps setIgnoreMouseEvents to overlay interaction active state', () => { + const { registrar, handlers } = createFakeIpcRegistrar(); + const calls: string[] = []; + + registerIpcHandlers( + createRegisterIpcDeps({ + onOverlayMouseInteractionChanged: (active) => { + calls.push(`overlay-interaction:${active}`); + }, + }), + registrar, + ); + + const handler = handlers.on.get(IPC_CHANNELS.command.setIgnoreMouseEvents); + assert.equal(typeof handler, 'function'); + + handler?.({}, true, { forward: true }); + handler?.({}, false, {}); + + assert.deepEqual(calls, ['overlay-interaction:false', 'overlay-interaction:true']); +}); + test('registerIpcHandlers runs AniList update after manual mark watched succeeds', async () => { const { registrar, handlers } = createFakeIpcRegistrar(); const calls: string[] = []; diff --git a/src/window-trackers/macos-tracker.test.ts b/src/window-trackers/macos-tracker.test.ts index 8e8553b2..4dcedaa6 100644 --- a/src/window-trackers/macos-tracker.test.ts +++ b/src/window-trackers/macos-tracker.test.ts @@ -236,7 +236,7 @@ test('MacOSWindowTracker keeps focused fullscreen target through active helper m }); }); -test('MacOSWindowTracker keeps previously focused target through repeated not-found misses after grace', async () => { +test('MacOSWindowTracker drops previously focused target after repeated not-found misses exceed grace', async () => { let callIndex = 0; let now = 1_000; const focusChanges: boolean[] = []; @@ -244,7 +244,6 @@ test('MacOSWindowTracker keeps previously focused target through repeated not-fo { stdout: '10,20,1280,720,1', stderr: '' }, { stdout: 'not-found', stderr: '' }, { stdout: 'not-found', stderr: '' }, - { stdout: 'not-found', stderr: '' }, ]; const tracker = new MacOSWindowTracker('/tmp/mpv.sock', { @@ -269,14 +268,6 @@ test('MacOSWindowTracker keeps previously focused target through repeated not-fo (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); - now += 1_000; - (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); - await new Promise((resolve) => setTimeout(resolve, 0)); - - now += 1_000; - (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); - await new Promise((resolve) => setTimeout(resolve, 0)); - assert.equal(tracker.isTracking(), true); assert.equal(tracker.isTargetWindowFocused(), true); assert.deepEqual(tracker.getGeometry(), { @@ -286,9 +277,18 @@ test('MacOSWindowTracker keeps previously focused target through repeated not-fo height: 720, }); assert.deepEqual(focusChanges, [true]); + + now += 1_000; + (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); + await new Promise((resolve) => setTimeout(resolve, 0)); + + assert.equal(tracker.isTracking(), false); + assert.equal(tracker.isTargetWindowFocused(), false); + assert.equal(tracker.getGeometry(), null); + assert.deepEqual(focusChanges, [true, false]); }); -test('MacOSWindowTracker keeps previously focused target through repeated helper execution failures', async () => { +test('MacOSWindowTracker drops previously focused target after repeated helper execution failures exceed grace', async () => { let callIndex = 0; let now = 1_000; const focusChanges: boolean[] = []; @@ -323,15 +323,10 @@ test('MacOSWindowTracker keeps previously focused target through repeated helper (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); - assert.equal(tracker.isTracking(), true); - assert.equal(tracker.isTargetWindowFocused(), true); - assert.deepEqual(tracker.getGeometry(), { - x: 10, - y: 20, - width: 1280, - height: 720, - }); - assert.deepEqual(focusChanges, [true]); + assert.equal(tracker.isTracking(), false); + assert.equal(tracker.isTargetWindowFocused(), false); + assert.equal(tracker.getGeometry(), null); + assert.deepEqual(focusChanges, [true, false]); }); test('MacOSWindowTracker marks target unfocused on explicit inactive helper signal', async () => { diff --git a/src/window-trackers/macos-tracker.ts b/src/window-trackers/macos-tracker.ts index 33885bea..761f1500 100644 --- a/src/window-trackers/macos-tracker.ts +++ b/src/window-trackers/macos-tracker.ts @@ -394,8 +394,13 @@ export class MacOSWindowTracker extends BaseWindowTracker { private registerTrackingMiss(graceMs = this.trackingLossGraceMs): void { if (this.shouldPreserveFocusedTargetOnMiss()) { - this.resetTrackingLossState(); - return; + if (this.trackingLossStartedAtMs === null) { + this.trackingLossStartedAtMs = this.now(); + return; + } + if (this.now() - this.trackingLossStartedAtMs <= graceMs) { + return; + } } this.consecutiveMisses += 1;