mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-10 16:19:24 -07:00
Fix Windows overlay tracking, z-order, and startup visibility
- switch Windows overlay tracking to native win32 polling with native owner and z-order helpers - keep the visible overlay and stats overlay aligned across focus handoff, transient tracker misses, and minimize/restore cycles - start the visible overlay click-through and hide the initial opaque startup frame until the tracked transparent state settles - add a backlog task for the inconsistent mpv y-t overlay toggle after menu toggles
This commit is contained in:
@@ -1,56 +1,62 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { WindowsWindowTracker } from './windows-tracker';
|
||||
import type { MpvPollResult } from './win32';
|
||||
|
||||
test('WindowsWindowTracker skips overlapping polls while helper is in flight', async () => {
|
||||
let helperCalls = 0;
|
||||
let release: (() => void) | undefined;
|
||||
const gate = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
function mpvVisible(
|
||||
overrides: Partial<MpvPollResult & { x?: number; y?: number; width?: number; height?: number; focused?: boolean }> = {},
|
||||
): MpvPollResult {
|
||||
return {
|
||||
matches: [
|
||||
{
|
||||
hwnd: 12345,
|
||||
bounds: {
|
||||
x: overrides.x ?? 0,
|
||||
y: overrides.y ?? 0,
|
||||
width: overrides.width ?? 1280,
|
||||
height: overrides.height ?? 720,
|
||||
},
|
||||
area: (overrides.width ?? 1280) * (overrides.height ?? 720),
|
||||
isForeground: overrides.focused ?? true,
|
||||
},
|
||||
],
|
||||
focusState: overrides.focused ?? true,
|
||||
windowState: 'visible',
|
||||
};
|
||||
}
|
||||
|
||||
const mpvNotFound: MpvPollResult = {
|
||||
matches: [],
|
||||
focusState: false,
|
||||
windowState: 'not-found',
|
||||
};
|
||||
|
||||
const mpvMinimized: MpvPollResult = {
|
||||
matches: [],
|
||||
focusState: false,
|
||||
windowState: 'minimized',
|
||||
};
|
||||
|
||||
test('WindowsWindowTracker skips overlapping polls while poll is in flight', () => {
|
||||
let pollCalls = 0;
|
||||
const tracker = new WindowsWindowTracker(undefined, {
|
||||
resolveHelper: () => ({
|
||||
kind: 'powershell',
|
||||
command: 'powershell.exe',
|
||||
args: ['-File', 'helper.ps1'],
|
||||
helperPath: 'helper.ps1',
|
||||
}),
|
||||
runHelper: async () => {
|
||||
helperCalls += 1;
|
||||
await gate;
|
||||
return {
|
||||
stdout: '0,0,640,360',
|
||||
stderr: 'focus=focused',
|
||||
};
|
||||
pollMpvWindows: () => {
|
||||
pollCalls += 1;
|
||||
return mpvVisible();
|
||||
},
|
||||
});
|
||||
|
||||
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
|
||||
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
|
||||
assert.equal(helperCalls, 1);
|
||||
|
||||
assert.ok(release);
|
||||
release();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.equal(pollCalls, 2);
|
||||
});
|
||||
|
||||
test('WindowsWindowTracker updates geometry from helper output', async () => {
|
||||
test('WindowsWindowTracker updates geometry from poll output', () => {
|
||||
const tracker = new WindowsWindowTracker(undefined, {
|
||||
resolveHelper: () => ({
|
||||
kind: 'powershell',
|
||||
command: 'powershell.exe',
|
||||
args: ['-File', 'helper.ps1'],
|
||||
helperPath: 'helper.ps1',
|
||||
}),
|
||||
runHelper: async () => ({
|
||||
stdout: '10,20,1280,720',
|
||||
stderr: 'focus=focused',
|
||||
}),
|
||||
pollMpvWindows: () => mpvVisible({ 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,
|
||||
@@ -61,59 +67,180 @@ test('WindowsWindowTracker updates geometry from helper output', async () => {
|
||||
assert.equal(tracker.isTargetWindowFocused(), true);
|
||||
});
|
||||
|
||||
test('WindowsWindowTracker clears geometry for helper misses', async () => {
|
||||
test('WindowsWindowTracker clears geometry for poll misses', () => {
|
||||
const tracker = new WindowsWindowTracker(undefined, {
|
||||
resolveHelper: () => ({
|
||||
kind: 'powershell',
|
||||
command: 'powershell.exe',
|
||||
args: ['-File', 'helper.ps1'],
|
||||
helperPath: 'helper.ps1',
|
||||
}),
|
||||
runHelper: async () => ({
|
||||
stdout: 'not-found',
|
||||
stderr: 'focus=not-focused',
|
||||
}),
|
||||
pollMpvWindows: () => mpvNotFound,
|
||||
trackingLossGraceMs: 0,
|
||||
});
|
||||
|
||||
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.equal(tracker.getGeometry(), null);
|
||||
assert.equal(tracker.isTargetWindowFocused(), false);
|
||||
});
|
||||
|
||||
test('WindowsWindowTracker retries without socket filter when filtered helper lookup misses', async () => {
|
||||
const helperCalls: Array<string | null> = [];
|
||||
const tracker = new WindowsWindowTracker('\\\\.\\pipe\\subminer-socket', {
|
||||
resolveHelper: () => ({
|
||||
kind: 'powershell',
|
||||
command: 'powershell.exe',
|
||||
args: ['-File', 'helper.ps1'],
|
||||
helperPath: 'helper.ps1',
|
||||
}),
|
||||
runHelper: async (_spec, _mode, targetMpvSocketPath) => {
|
||||
helperCalls.push(targetMpvSocketPath);
|
||||
if (targetMpvSocketPath) {
|
||||
return {
|
||||
stdout: 'not-found',
|
||||
stderr: 'focus=not-focused',
|
||||
};
|
||||
}
|
||||
return {
|
||||
stdout: '25,30,1440,810',
|
||||
stderr: 'focus=focused',
|
||||
};
|
||||
},
|
||||
test('WindowsWindowTracker keeps the last geometry through a single poll miss', () => {
|
||||
let callIndex = 0;
|
||||
const outputs = [
|
||||
mpvVisible({ x: 10, y: 20, width: 1280, height: 720 }),
|
||||
mpvNotFound,
|
||||
mpvVisible({ x: 10, y: 20, width: 1280, height: 720 }),
|
||||
];
|
||||
|
||||
const tracker = new WindowsWindowTracker(undefined, {
|
||||
pollMpvWindows: () => 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 });
|
||||
|
||||
assert.deepEqual(helperCalls, ['\\\\.\\pipe\\subminer-socket', null]);
|
||||
assert.deepEqual(tracker.getGeometry(), {
|
||||
x: 25,
|
||||
y: 30,
|
||||
width: 1440,
|
||||
height: 810,
|
||||
});
|
||||
assert.equal(tracker.isTargetWindowFocused(), true);
|
||||
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
|
||||
assert.deepEqual(tracker.getGeometry(), { x: 10, y: 20, width: 1280, height: 720 });
|
||||
|
||||
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
|
||||
assert.deepEqual(tracker.getGeometry(), { x: 10, y: 20, width: 1280, height: 720 });
|
||||
});
|
||||
|
||||
test('WindowsWindowTracker drops tracking after grace window expires', () => {
|
||||
let callIndex = 0;
|
||||
let now = 1_000;
|
||||
const outputs = [
|
||||
mpvVisible({ x: 10, y: 20, width: 1280, height: 720 }),
|
||||
mpvNotFound,
|
||||
mpvNotFound,
|
||||
mpvNotFound,
|
||||
mpvNotFound,
|
||||
];
|
||||
|
||||
const tracker = new WindowsWindowTracker(undefined, {
|
||||
pollMpvWindows: () => outputs[callIndex++] ?? outputs.at(-1)!,
|
||||
now: () => now,
|
||||
trackingLossGraceMs: 500,
|
||||
});
|
||||
|
||||
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
|
||||
assert.equal(tracker.isTracking(), true);
|
||||
|
||||
now += 250;
|
||||
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
|
||||
assert.equal(tracker.isTracking(), true);
|
||||
|
||||
now += 250;
|
||||
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
|
||||
assert.equal(tracker.isTracking(), true);
|
||||
|
||||
now += 250;
|
||||
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
|
||||
assert.equal(tracker.isTracking(), true);
|
||||
|
||||
now += 250;
|
||||
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
|
||||
assert.equal(tracker.isTracking(), false);
|
||||
assert.equal(tracker.getGeometry(), null);
|
||||
});
|
||||
|
||||
test('WindowsWindowTracker keeps tracking through repeated poll misses inside grace window', () => {
|
||||
let callIndex = 0;
|
||||
let now = 1_000;
|
||||
const outputs = [
|
||||
mpvVisible({ x: 10, y: 20, width: 1280, height: 720 }),
|
||||
mpvNotFound,
|
||||
mpvNotFound,
|
||||
mpvNotFound,
|
||||
mpvVisible({ x: 10, y: 20, width: 1280, height: 720 }),
|
||||
];
|
||||
|
||||
const tracker = new WindowsWindowTracker(undefined, {
|
||||
pollMpvWindows: () => outputs[callIndex++] ?? outputs.at(-1)!,
|
||||
now: () => now,
|
||||
trackingLossGraceMs: 1_500,
|
||||
});
|
||||
|
||||
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
|
||||
assert.equal(tracker.isTracking(), true);
|
||||
|
||||
now += 250;
|
||||
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
|
||||
assert.equal(tracker.isTracking(), true);
|
||||
|
||||
now += 250;
|
||||
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
|
||||
assert.equal(tracker.isTracking(), true);
|
||||
|
||||
now += 250;
|
||||
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
|
||||
assert.equal(tracker.isTracking(), true);
|
||||
assert.deepEqual(tracker.getGeometry(), { x: 10, y: 20, width: 1280, height: 720 });
|
||||
|
||||
now += 250;
|
||||
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
|
||||
assert.equal(tracker.isTracking(), true);
|
||||
assert.deepEqual(tracker.getGeometry(), { x: 10, y: 20, width: 1280, height: 720 });
|
||||
});
|
||||
|
||||
test('WindowsWindowTracker keeps tracking through a transient minimized report inside minimized grace window', () => {
|
||||
let callIndex = 0;
|
||||
let now = 1_000;
|
||||
const outputs: MpvPollResult[] = [
|
||||
mpvVisible({ x: 10, y: 20, width: 1280, height: 720 }),
|
||||
mpvMinimized,
|
||||
mpvVisible({ x: 10, y: 20, width: 1280, height: 720 }),
|
||||
];
|
||||
|
||||
const tracker = new WindowsWindowTracker(undefined, {
|
||||
pollMpvWindows: () => outputs[callIndex++] ?? outputs.at(-1)!,
|
||||
now: () => now,
|
||||
minimizedTrackingLossGraceMs: 200,
|
||||
});
|
||||
|
||||
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
|
||||
assert.equal(tracker.isTracking(), true);
|
||||
|
||||
now += 100;
|
||||
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
|
||||
assert.equal(tracker.isTracking(), true);
|
||||
assert.deepEqual(tracker.getGeometry(), { x: 10, y: 20, width: 1280, height: 720 });
|
||||
|
||||
now += 100;
|
||||
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
|
||||
assert.equal(tracker.isTracking(), true);
|
||||
assert.deepEqual(tracker.getGeometry(), { x: 10, y: 20, width: 1280, height: 720 });
|
||||
});
|
||||
|
||||
test('WindowsWindowTracker keeps tracking through repeated transient minimized reports inside minimized grace window', () => {
|
||||
let callIndex = 0;
|
||||
let now = 1_000;
|
||||
const outputs: MpvPollResult[] = [
|
||||
mpvVisible({ x: 10, y: 20, width: 1280, height: 720 }),
|
||||
mpvMinimized,
|
||||
mpvMinimized,
|
||||
mpvVisible({ x: 10, y: 20, width: 1280, height: 720 }),
|
||||
];
|
||||
|
||||
const tracker = new WindowsWindowTracker(undefined, {
|
||||
pollMpvWindows: () => outputs[callIndex++] ?? outputs.at(-1)!,
|
||||
now: () => now,
|
||||
minimizedTrackingLossGraceMs: 500,
|
||||
});
|
||||
|
||||
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
|
||||
assert.equal(tracker.isTracking(), true);
|
||||
|
||||
now += 250;
|
||||
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
|
||||
assert.equal(tracker.isTracking(), true);
|
||||
assert.equal(tracker.isTargetWindowMinimized(), true);
|
||||
assert.deepEqual(tracker.getGeometry(), { x: 10, y: 20, width: 1280, height: 720 });
|
||||
|
||||
now += 250;
|
||||
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
|
||||
assert.equal(tracker.isTracking(), true);
|
||||
assert.equal(tracker.isTargetWindowMinimized(), true);
|
||||
assert.deepEqual(tracker.getGeometry(), { x: 10, y: 20, width: 1280, height: 720 });
|
||||
|
||||
now += 250;
|
||||
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
|
||||
assert.equal(tracker.isTracking(), true);
|
||||
assert.equal(tracker.isTargetWindowMinimized(), false);
|
||||
assert.deepEqual(tracker.getGeometry(), { x: 10, y: 20, width: 1280, height: 720 });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user