Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions scripts/test-window-adjust-center80.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env node
// Regression test for the `center-80` ("Almost Maximize") window preset.
//
// The native helper `dist/native/window-adjust` is a macOS Swift binary built by
// `npm run build:native`. It normally applies a frame to the focused window and
// only reports `{ok,error}`. To make the preset *sizing* observable we added an
// additive `--print-frame` dry-run (see src/native/window-adjust.swift) that
// computes the target frame for a supplied screen area and prints it as JSON,
// without touching Accessibility or any window.
//
// CI runs `npm test` on Linux without building native binaries, so this test
// skips gracefully off-macOS / when the binary is absent. On the macOS worker
// gate (`npm run build && npm test && npm run check:i18n`) the binary IS built
// and this test exercises the real Swift center-80 arithmetic end-to-end.

import test from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const binary = path.join(root, 'dist', 'native', 'window-adjust');

const isMacOS = process.platform === 'darwin';
const binaryPresent = existsSync(binary);
const skipReason =
!isMacOS
? 'native window-adjust is a macOS-only Swift binary'
: !binaryPresent
? 'dist/native/window-adjust not built (run `npm run build:native`)'
: undefined;

function runPreset(action, area) {
const stdout = execFileSync(
binary,
[
action,
'--print-frame',
'--area-x', String(area.x),
'--area-y', String(area.y),
'--area-width', String(area.width),
'--area-height', String(area.height),
],
{ encoding: 'utf-8', timeout: 5000 },
);
const line = String(stdout || '')
.split(/\r?\n/)
.map((s) => s.trim())
.filter(Boolean)
.reverse()
.find((s) => s.startsWith('{') && s.endsWith('}'));
assert.ok(line, `expected a JSON frame line on stdout, got: ${stdout || ''}`);
const payload = JSON.parse(line);
// Guard against a STALE binary that predates --print-frame support: it ignores
// the unknown flag, falls through to the production path, and emits its normal
// `{"ok":...}` envelope (and, on an Accessibility-trusted dev box, would also
// resize the focused window before this assertion runs). Reject that envelope
// with a clear, actionable message instead of a confusing `frame.width === undefined`.
assert.ok(
!('ok' in payload) && ['x', 'y', 'width', 'height'].every((k) => typeof payload[k] === 'number'),
`window-adjust at ${binary} did not emit a frame via --print-frame (stale binary?). ` +
`Rebuild native with \`npm run build:native\`. stdout was: ${stdout || ''}`,
);
return payload;
}

test('center-80 sizes the window to ~80% of a known 1440x900 screen', { skip: skipReason }, () => {
const screen = { x: 0, y: 0, width: 1440, height: 900 };
const frame = runPreset('center-80', screen);

// 80% of 1440x900 = 1152 x 720, centered at (144, 90). On the pre-fix 0.9 code
// this would have been 1296 x 810 at (72, 45), so the exact assertions below
// fail loudly on a regression to 90%.
assert.equal(frame.width, 1152, `width should be 1152 (80% of 1440), got ${frame.width}`);
assert.equal(frame.height, 720, `height should be 720 (80% of 900), got ${frame.height}`);
assert.equal(frame.x, 144, `x should be 144 (centered), got ${frame.x}`);
assert.equal(frame.y, 90, `y should be 90 (centered), got ${frame.y}`);

// Ratio guards (robust to unrelated area changes): must be ~80%, never ~90%.
const widthPct = frame.width / screen.width;
const heightPct = frame.height / screen.height;
assert.ok(widthPct >= 0.795 && widthPct <= 0.805, `width ratio should be ~0.80, got ${widthPct.toFixed(4)}`);
assert.ok(heightPct >= 0.795 && heightPct <= 0.805, `height ratio should be ~0.80, got ${heightPct.toFixed(4)}`);
});

test('center-80 stays ~80% on a different screen size (2560x1440)', { skip: skipReason }, () => {
const screen = { x: 0, y: 0, width: 2560, height: 1440 };
const frame = runPreset('center-80', screen);

const widthPct = frame.width / screen.width;
const heightPct = frame.height / screen.height;
assert.ok(widthPct >= 0.795 && widthPct <= 0.805, `width ratio ~0.80, got ${widthPct.toFixed(4)}`);
assert.ok(heightPct >= 0.795 && heightPct <= 0.805, `height ratio ~0.80, got ${heightPct.toFixed(4)}`);

// Centered: equal margins on each axis (±1px rounding).
assert.ok(Math.abs(frame.x - (screen.width - frame.width) / 2) <= 1, `horizontally centered, x=${frame.x}`);
assert.ok(Math.abs(frame.y - (screen.height - frame.height) / 2) <= 1, `vertically centered, y=${frame.y}`);
});

// Distinct coverage: center-80 ("Almost Maximize", 0.8) must produce a STRICTLY
// larger frame than `center` (0.6) on the same screen. The per-preset exact-value
// tests above cannot, on their own, catch a regression where two presets collapse
// to the same size (e.g. a copy/pasted branch or wrong multiplier); this one can.
test('center-80 is strictly larger than center (0.8 > 0.6) on the same screen', { skip: skipReason }, () => {
const screen = { x: 0, y: 0, width: 1440, height: 900 };
const big = runPreset('center-80', screen);
const small = runPreset('center', screen);

// Baseline sanity: `center` is 0.6 -> 864 x 540. If this baseline ever drifts
// the comparison below is meaningless, so assert it explicitly.
assert.equal(small.width, 864, `center baseline should be 864 (0.6 * 1440), got ${small.width}`);
assert.equal(small.height, 540, `center baseline should be 540 (0.6 * 900), got ${small.height}`);

assert.ok(
big.width > small.width && big.height > small.height,
`center-80 (${big.width}x${big.height}) should be strictly larger than center (${small.width}x${small.height})`,
);
});
75 changes: 69 additions & 6 deletions src/native/window-adjust.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ private struct OutputPayload: Encodable {
let error: String?
}

private struct FramePayload: Encodable {
let x: CGFloat
let y: CGFloat
let width: CGFloat
let height: CGFloat
}

private struct TargetHint {
var bundleId: String?
var appPath: String?
Expand All @@ -86,6 +93,20 @@ private func emit(ok: Bool, error: String? = nil) {
FileHandle.standardOutput.write(bytes)
}

// Dry-run output used only by `--print-frame` (self-test). Emits the computed
// target frame as JSON without touching Accessibility or any window.
private func emitFrame(_ frame: WindowFrame) {
let encoder = JSONEncoder()
let payload = FramePayload(x: frame.x, y: frame.y, width: frame.width, height: frame.height)
guard let data = try? encoder.encode(payload),
let text = String(data: data, encoding: .utf8),
let bytes = (text + "\n").data(using: .utf8) else {
fputs("{\"x\":0,\"y\":0,\"width\":0,\"height\":0}\n", stdout)
return
}
FileHandle.standardOutput.write(bytes)
}

private func clamp(_ value: CGFloat, _ minValue: CGFloat, _ maxValue: CGFloat) -> CGFloat {
if !value.isFinite { return minValue }
if maxValue <= minValue { return minValue }
Expand Down Expand Up @@ -513,10 +534,19 @@ private func visibleArea(forWindowId windowId: Int) -> CGRect? {
return visibleBounds(for: windowBounds)
}

private func adjustedFrame(_ base: WindowFrame, action: AdjustAction, forcedArea: CGRect?, preferredWindowId: Int?) -> WindowFrame {
let area = (preferredWindowId != nil ? visibleArea(forWindowId: preferredWindowId!) : nil)
?? screenVisibleArea(for: base)
?? forcedArea
private func adjustedFrame(_ base: WindowFrame, action: AdjustAction, forcedArea: CGRect?, preferredWindowId: Int?, forceUseForcedArea: Bool = false) -> WindowFrame {
// When forceUseForcedArea is set (only by the `--print-frame` self-test), skip
// live screen detection so the frame is computed deterministically against the
// supplied area. The production call path never sets this (default false), so
// runtime behavior is unchanged.
let area: CGRect?
if forceUseForcedArea {
area = forcedArea
} else {
area = (preferredWindowId != nil ? visibleArea(forWindowId: preferredWindowId!) : nil)
?? screenVisibleArea(for: base)
?? forcedArea
}
let stepX = max(1, round(base.width * adjustRatio))
let stepY = max(1, round(base.height * adjustRatio))
var next = base
Expand Down Expand Up @@ -602,8 +632,10 @@ private func adjustedFrame(_ base: WindowFrame, action: AdjustAction, forcedArea
}
case .center80:
if let area {
let width = max(minWidth, round(area.width * 0.9))
let height = max(minHeight, round(area.height * 0.9))
// "Almost Maximize" (preset id `center-80`): size to 80% of the usable
// screen, then center — matching the preset name and its `80%` keyword.
let width = max(minWidth, round(area.width * 0.8))
let height = max(minHeight, round(area.height * 0.8))
next = WindowFrame(
x: area.origin.x + round((area.width - width) / 2),
y: area.origin.y + round((area.height - height) / 2),
Expand Down Expand Up @@ -980,8 +1012,14 @@ private func run() {
var areaY: CGFloat?
var areaWidth: CGFloat?
var areaHeight: CGFloat?
var printFrame = false
while index < args.count {
let key = args[index]
if key == "--print-frame" {
printFrame = true
index += 1
continue
}
if key == "--bundle-id", index + 1 < args.count {
let value = args[index + 1].trimmingCharacters(in: .whitespacesAndNewlines)
targetHint.bundleId = value.isEmpty ? nil : value
Expand Down Expand Up @@ -1032,6 +1070,31 @@ private func run() {
targetHint.workArea = CGRect(x: x, y: y, width: width, height: height)
}

// Self-test / dry-run mode: compute and print the target frame for the given
// action against the supplied area, then exit BEFORE the Accessibility gate so
// it runs headlessly. Used by scripts/test-window-adjust-center80.mjs to
// assert preset sizing (e.g. center-80 == ~80%). The production app never
// passes --print-frame, so this path is never hit at runtime.
if printFrame {
let dryArea = targetHint.workArea
?? CGRect(x: 0, y: 0, width: NSScreen.main?.frame.width ?? 1440, height: NSScreen.main?.frame.height ?? 900)
let dryBase = WindowFrame(
x: dryArea.origin.x,
y: dryArea.origin.y,
width: dryArea.width,
height: dryArea.height
)
let frame = adjustedFrame(
dryBase,
action: action,
forcedArea: dryArea,
preferredWindowId: nil,
forceUseForcedArea: true
)
emitFrame(frame)
return
}

guard AXIsProcessTrusted() else {
emit(ok: false, error: "accessibility_not_trusted")
return
Expand Down