mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-28 04:49:49 -07:00
Fix macOS overlay foreground handling and character-dictionary cache reuse (#68)
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, rmSync, utimesSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { MacOSWindowTracker, parseMacOSHelperOutput } from './macos-tracker';
|
||||
import {
|
||||
isCompiledMacOSHelperCurrent,
|
||||
MacOSWindowTracker,
|
||||
parseMacOSHelperOutput,
|
||||
} from './macos-tracker';
|
||||
|
||||
test('parseMacOSHelperOutput parses minimized state', () => {
|
||||
assert.deepEqual(parseMacOSHelperOutput('minimized'), {
|
||||
@@ -10,6 +17,99 @@ test('parseMacOSHelperOutput parses minimized state', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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 tempDir = mkdtempSync(join(tmpdir(), 'subminer-macos-helper-'));
|
||||
try {
|
||||
const binaryPath = join(tempDir, 'get-mpv-window-macos');
|
||||
const sourcePath = join(tempDir, 'get-mpv-window-macos.swift');
|
||||
writeFileSync(binaryPath, 'binary');
|
||||
writeFileSync(sourcePath, 'source');
|
||||
|
||||
const older = new Date('2026-01-01T00:00:00Z');
|
||||
const newer = new Date('2026-01-01T00:00:05Z');
|
||||
utimesSync(binaryPath, older, older);
|
||||
utimesSync(sourcePath, newer, newer);
|
||||
|
||||
assert.equal(isCompiledMacOSHelperCurrent(binaryPath, sourcePath), false);
|
||||
|
||||
utimesSync(binaryPath, newer, newer);
|
||||
utimesSync(sourcePath, older, older);
|
||||
|
||||
assert.equal(isCompiledMacOSHelperCurrent(binaryPath, sourcePath), true);
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: 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<typeof setTimeout>;
|
||||
}) 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<typeof setTimeout>;
|
||||
}) 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 = [
|
||||
@@ -55,10 +155,221 @@ test('MacOSWindowTracker keeps the last geometry through a single helper miss',
|
||||
});
|
||||
});
|
||||
|
||||
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 drops tracking after consecutive helper misses', async () => {
|
||||
let callIndex = 0;
|
||||
const outputs = [
|
||||
{ stdout: '10,20,1280,720,1', stderr: '' },
|
||||
{ stdout: '10,20,1280,720,0', stderr: '' },
|
||||
{ stdout: 'not-found', stderr: '' },
|
||||
{ stdout: 'not-found', stderr: '' },
|
||||
];
|
||||
@@ -75,6 +386,7 @@ test('MacOSWindowTracker drops tracking after consecutive helper misses', async
|
||||
(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));
|
||||
@@ -84,6 +396,7 @@ test('MacOSWindowTracker drops tracking after consecutive helper misses', async
|
||||
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 () => {
|
||||
@@ -137,7 +450,7 @@ test('MacOSWindowTracker drops tracking after grace window expires', async () =>
|
||||
let callIndex = 0;
|
||||
let now = 1_000;
|
||||
const outputs = [
|
||||
{ stdout: '10,20,1280,720,1', stderr: '' },
|
||||
{ stdout: '10,20,1280,720,0', stderr: '' },
|
||||
{ stdout: 'not-found', stderr: '' },
|
||||
{ stdout: 'not-found', stderr: '' },
|
||||
{ stdout: 'not-found', stderr: '' },
|
||||
@@ -156,6 +469,7 @@ test('MacOSWindowTracker drops tracking after grace window expires', async () =>
|
||||
(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();
|
||||
|
||||
Reference in New Issue
Block a user