import assert from 'node:assert/strict'; import type { PathLike } from 'node:fs'; import test from 'node:test'; import { isCompiledMacOSHelperCurrent, MacOSWindowTracker, parseMacOSHelperOutput, } from './macos-tracker'; test('parseMacOSHelperOutput parses minimized state', () => { assert.deepEqual(parseMacOSHelperOutput('minimized'), { geometry: null, focused: false, minimized: true, }); }); test('parseMacOSHelperOutput parses active focused state without geometry', () => { assert.deepEqual(parseMacOSHelperOutput('active'), { geometry: null, focused: true, active: true, }); }); test('parseMacOSHelperOutput parses inactive state without geometry', () => { assert.deepEqual(parseMacOSHelperOutput('inactive'), { geometry: null, focused: false, inactive: true, }); }); test('isCompiledMacOSHelperCurrent rejects binaries older than the Swift source', () => { const binaryPath = '/tmp/get-mpv-window-macos'; const sourcePath = '/tmp/get-mpv-window-macos.swift'; const statSync = (binaryMtimeMs: number, sourceMtimeMs: number) => (targetPath: PathLike) => ({ mtimeMs: String(targetPath) === binaryPath ? binaryMtimeMs : sourceMtimeMs, }) as never; const helperFs = { existsSync: () => true, statSync: statSync(1_000, 2_000), }; assert.equal(isCompiledMacOSHelperCurrent(binaryPath, sourcePath, helperFs), false); helperFs.statSync = statSync(2_000, 1_000); assert.equal(isCompiledMacOSHelperCurrent(binaryPath, sourcePath, helperFs), true); }); test('MacOSWindowTracker slows polling while focused target is stable', async () => { const scheduledDelays: number[] = []; let callIndex = 0; const tracker = new MacOSWindowTracker('/tmp/mpv.sock', { resolveHelper: () => ({ helperPath: 'helper', helperType: 'binary', }), runHelper: async () => { callIndex += 1; return { stdout: '10,20,1280,720,1', stderr: '' }; }, fastPollIntervalMs: 250, stablePollIntervalMs: 1_000, setPollTimeout: ((_callback: () => void, delayMs: number) => { scheduledDelays.push(delayMs); return {} as ReturnType; }) as never, clearPollTimeout: (() => {}) as never, } as never); tracker.start(); await new Promise((resolve) => setTimeout(resolve, 0)); tracker.stop(); assert.equal(callIndex, 1); assert.deepEqual(scheduledDelays, [1_000]); }); test('MacOSWindowTracker keeps fast polling while target is not focused', async () => { const scheduledDelays: number[] = []; const tracker = new MacOSWindowTracker('/tmp/mpv.sock', { resolveHelper: () => ({ helperPath: 'helper', helperType: 'binary', }), runHelper: async () => ({ stdout: '10,20,1280,720,0', stderr: '' }), fastPollIntervalMs: 250, stablePollIntervalMs: 1_000, setPollTimeout: ((_callback: () => void, delayMs: number) => { scheduledDelays.push(delayMs); return {} as ReturnType; }) as never, clearPollTimeout: (() => {}) as never, } as never); tracker.start(); await new Promise((resolve) => setTimeout(resolve, 0)); tracker.stop(); assert.deepEqual(scheduledDelays, [250]); }); test('MacOSWindowTracker keeps the last geometry through a single helper miss', async () => { let callIndex = 0; const outputs = [ { stdout: '10,20,1280,720,1', stderr: '' }, { stdout: 'not-found', stderr: '' }, { stdout: '10,20,1280,720,1', stderr: '' }, ]; const tracker = new MacOSWindowTracker('/tmp/mpv.sock', { resolveHelper: () => ({ helperPath: 'helper.swift', helperType: 'swift', }), runHelper: async () => outputs[callIndex++] ?? outputs.at(-1)!, trackingLossGraceMs: 0, }); (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); assert.deepEqual(tracker.getGeometry(), { x: 10, y: 20, width: 1280, height: 720, }); (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); assert.deepEqual(tracker.getGeometry(), { x: 10, y: 20, width: 1280, height: 720, }); (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); assert.deepEqual(tracker.getGeometry(), { x: 10, y: 20, width: 1280, height: 720, }); }); test('MacOSWindowTracker preserves target focus on helper not-found while retaining geometry', async () => { let callIndex = 0; const focusChanges: boolean[] = []; const outputs = [ { stdout: '10,20,1280,720,1', stderr: '' }, { stdout: 'not-found', stderr: '' }, ]; const tracker = new MacOSWindowTracker('/tmp/mpv.sock', { resolveHelper: () => ({ helperPath: 'helper.swift', helperType: 'swift', }), runHelper: async () => outputs[callIndex++] ?? outputs.at(-1)!, trackingLossGraceMs: 1_500, }); tracker.onWindowFocusChange = (focused) => { focusChanges.push(focused); }; (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(tracker.isTargetWindowFocused(), true); (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(tracker.isTracking(), true); assert.deepEqual(tracker.getGeometry(), { x: 10, y: 20, width: 1280, height: 720, }); assert.equal(tracker.isTargetWindowFocused(), true); assert.deepEqual(focusChanges, [true]); }); test('MacOSWindowTracker keeps focused fullscreen target through active helper misses after grace', async () => { let callIndex = 0; let now = 1_000; const outputs = [ { stdout: '10,20,1280,720,1', stderr: '' }, { stdout: 'active', stderr: '' }, { stdout: 'active', stderr: '' }, ]; const tracker = new MacOSWindowTracker('/tmp/mpv.sock', { resolveHelper: () => ({ helperPath: 'helper.swift', helperType: 'swift', }), runHelper: async () => outputs[callIndex++] ?? outputs.at(-1)!, now: () => now, trackingLossGraceMs: 500, }); (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(tracker.isTracking(), true); assert.equal(tracker.isTargetWindowFocused(), true); now += 1_000; (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(tracker.isTracking(), true); 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(), { x: 10, y: 20, width: 1280, height: 720, }); }); test('MacOSWindowTracker drops previously focused target after repeated not-found misses exceed grace', async () => { let callIndex = 0; let now = 1_000; const focusChanges: boolean[] = []; const outputs = [ { stdout: '10,20,1280,720,1', stderr: '' }, { stdout: 'not-found', stderr: '' }, { stdout: 'not-found', stderr: '' }, ]; const tracker = new MacOSWindowTracker('/tmp/mpv.sock', { resolveHelper: () => ({ helperPath: 'helper.swift', helperType: 'swift', }), runHelper: async () => outputs[callIndex++] ?? outputs.at(-1)!, now: () => now, trackingLossGraceMs: 500, }); tracker.onWindowFocusChange = (focused) => { focusChanges.push(focused); }; (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(tracker.isTracking(), true); assert.equal(tracker.isTargetWindowFocused(), true); 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(), { x: 10, y: 20, width: 1280, 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 drops previously focused target after repeated helper execution failures exceed grace', async () => { let callIndex = 0; let now = 1_000; const focusChanges: boolean[] = []; const tracker = new MacOSWindowTracker('/tmp/mpv.sock', { resolveHelper: () => ({ helperPath: 'helper.swift', helperType: 'swift', }), runHelper: async () => { callIndex += 1; if (callIndex === 1) { return { stdout: '10,20,1280,720,1', stderr: '' }; } throw Object.assign(new Error('helper timed out'), { stderr: 'timeout' }); }, now: () => now, trackingLossGraceMs: 500, }); tracker.onWindowFocusChange = (focused) => { focusChanges.push(focused); }; (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(), 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 () => { let callIndex = 0; const focusChanges: boolean[] = []; const outputs = [ { stdout: '10,20,1280,720,1', stderr: '' }, { stdout: 'inactive', stderr: '' }, ]; const tracker = new MacOSWindowTracker('/tmp/mpv.sock', { resolveHelper: () => ({ helperPath: 'helper.swift', helperType: 'swift', }), runHelper: async () => outputs[callIndex++] ?? outputs.at(-1)!, trackingLossGraceMs: 1_500, }); tracker.onWindowFocusChange = (focused) => { focusChanges.push(focused); }; (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(tracker.isTracking(), true); assert.deepEqual(tracker.getGeometry(), { x: 10, y: 20, width: 1280, height: 720, }); assert.equal(tracker.isTargetWindowFocused(), false); assert.deepEqual(focusChanges, [true, false]); }); test('MacOSWindowTracker refreshNow immediately samples frontmost mpv state', async () => { let callIndex = 0; const outputs = [ { stdout: '10,20,1280,720,0', stderr: '' }, { stdout: 'active', stderr: '' }, ]; const tracker = new MacOSWindowTracker('/tmp/mpv.sock', { resolveHelper: () => ({ helperPath: 'helper.swift', helperType: 'swift', }), runHelper: async () => outputs[callIndex++] ?? outputs.at(-1)!, }); await (tracker as unknown as { refreshNow: () => Promise }).refreshNow(); assert.equal(tracker.isTargetWindowFocused(), false); await (tracker as unknown as { refreshNow: () => Promise }).refreshNow(); assert.equal(tracker.isTracking(), true); assert.equal(tracker.isTargetWindowFocused(), true); assert.deepEqual(tracker.getGeometry(), { x: 10, y: 20, width: 1280, height: 720, }); }); test('MacOSWindowTracker drops tracking after consecutive helper misses', async () => { let callIndex = 0; const outputs = [ { stdout: '10,20,1280,720,0', stderr: '' }, { stdout: 'not-found', stderr: '' }, { stdout: 'not-found', stderr: '' }, ]; const tracker = new MacOSWindowTracker('/tmp/mpv.sock', { resolveHelper: () => ({ helperPath: 'helper.swift', helperType: 'swift', }), runHelper: async () => outputs[callIndex++] ?? outputs.at(-1)!, trackingLossGraceMs: 0, }); (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(tracker.isTracking(), true); assert.equal(tracker.isTargetWindowFocused(), false); (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(tracker.isTracking(), true); (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(tracker.isTracking(), false); assert.equal(tracker.getGeometry(), null); assert.equal(tracker.isTargetWindowFocused(), false); }); test('MacOSWindowTracker keeps tracking through repeated helper misses inside grace window', async () => { let callIndex = 0; let now = 1_000; const outputs = [ { stdout: '10,20,1280,720,1', stderr: '' }, { stdout: 'not-found', stderr: '' }, { stdout: 'not-found', stderr: '' }, { stdout: 'not-found', stderr: '' }, { stdout: 'not-found', stderr: '' }, ]; const tracker = new MacOSWindowTracker('/tmp/mpv.sock', { resolveHelper: () => ({ helperPath: 'helper.swift', helperType: 'swift', }), runHelper: async () => outputs[callIndex++] ?? outputs.at(-1)!, now: () => now, trackingLossGraceMs: 1_500, }); (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(tracker.isTracking(), true); now += 250; (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(tracker.isTracking(), true); now += 250; (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(tracker.isTracking(), true); now += 250; (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(tracker.isTracking(), true); assert.deepEqual(tracker.getGeometry(), { x: 10, y: 20, width: 1280, height: 720, }); }); test('MacOSWindowTracker drops tracking after grace window expires', async () => { let callIndex = 0; let now = 1_000; const outputs = [ { stdout: '10,20,1280,720,0', stderr: '' }, { stdout: 'not-found', stderr: '' }, { stdout: 'not-found', stderr: '' }, { stdout: 'not-found', stderr: '' }, ]; const tracker = new MacOSWindowTracker('/tmp/mpv.sock', { resolveHelper: () => ({ helperPath: 'helper.swift', helperType: 'swift', }), runHelper: async () => outputs[callIndex++] ?? outputs.at(-1)!, now: () => now, trackingLossGraceMs: 500, }); (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(tracker.isTracking(), true); assert.equal(tracker.isTargetWindowFocused(), false); now += 250; (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(tracker.isTracking(), true); now += 250; (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(tracker.isTracking(), true); now += 250; (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(tracker.isTracking(), true); now += 250; (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(tracker.isTracking(), false); assert.equal(tracker.getGeometry(), null); }); test('MacOSWindowTracker reports minimized target when helper reports minimized', async () => { let callIndex = 0; let now = 1_000; const outputs = [ { stdout: '10,20,1280,720,1', stderr: '' }, { stdout: 'minimized', stderr: '' }, { stdout: 'minimized', stderr: '' }, ]; const tracker = new MacOSWindowTracker('/tmp/mpv.sock', { resolveHelper: () => ({ helperPath: 'helper.swift', helperType: 'swift', }), runHelper: async () => outputs[callIndex++] ?? outputs.at(-1)!, now: () => now, minimizedTrackingLossGraceMs: 200, }); (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(tracker.isTracking(), true); assert.equal(tracker.isTargetWindowMinimized(), false); now += 250; (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(tracker.isTargetWindowMinimized(), true); assert.equal(tracker.isTracking(), true); now += 250; (tracker as unknown as { pollGeometry: () => void }).pollGeometry(); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(tracker.isTargetWindowMinimized(), true); assert.equal(tracker.isTracking(), false); });