The handles are invisible and paint above the frame, so the north strip ran the full window width across the caption buttons and the north-east square sat on the close button's own top-right corner. Crossing one dropped that button's :hover mid-glow, which reads as the Aero glow flickering or sticking — an artifact with nothing wrong in the CSS at all. Only the bottom-right corner overlaps nothing: it lands in the 6px gutter that .window-body's margin leaves below and right of the content. That is also where Win7 put its own resize gripper, so one grip is the more faithful answer anyway. Bumped to 1rem from 0.75rem since it is now the only way to resize. resizeFrame stays general and the probe still exercises all eight edges: the origin-moving cases are the subtle ones and are worth keeping proven, so restoring a side later is one entry in the component against tested math. Both the component and gotchas.md now spell out which edges are safe to restore (south, east, west) and which are not (north and both top corners). Harness diffs 0; math, hover and menu probes clean. Co-Authored-By: Claude <noreply@anthropic.com>
127 lines
5.3 KiB
TypeScript
127 lines
5.3 KiB
TypeScript
// Programmatic test for the synthesized window resize's frame arithmetic.
|
|
//
|
|
// bun scripts/resize-math-probe.ts
|
|
//
|
|
// Why this exists: the win7 skin resizes itself by dragging handles and moving
|
|
// the native frame (see ui/src/app/skins/7like/window-resize/window-resize.ts —
|
|
// a STOPGAP with a removal checklist). Pointer drags cannot be synthesized
|
|
// headlessly on this machine, so the interaction itself needs a human; but the
|
|
// part most likely to be subtly wrong is pure arithmetic — eight edges, four of
|
|
// which move the window's ORIGIN as well as its size, plus a minimum-size clamp
|
|
// that must not let the anchored edge drift. That is all in `resize-math.ts`,
|
|
// which is deliberately dependency-free so this script can drive it directly.
|
|
//
|
|
// The failure this guards against is not "the window does not resize" (obvious
|
|
// on sight) but "dragging the west edge inward past the minimum walks the whole
|
|
// window across the screen" — correct-looking until you hit the limit.
|
|
//
|
|
// This covers MORE than the UI currently mounts, deliberately: only the
|
|
// bottom-right grip is wired up (an invisible handle over a caption button
|
|
// steals its hover — see the component), but the math stays general so that
|
|
// restoring a side later is one line against already-proven arithmetic.
|
|
import {
|
|
MIN_HEIGHT,
|
|
MIN_WIDTH,
|
|
RESIZE_EDGES,
|
|
type Frame,
|
|
type ResizeEdge,
|
|
resizeFrame,
|
|
} from "../ui/src/app/skins/7like/window-resize/resize-math";
|
|
|
|
const START: Frame = { x: 100, y: 200, width: 1000, height: 800 };
|
|
|
|
interface Case {
|
|
name: string;
|
|
edge: ResizeEdge;
|
|
dx: number;
|
|
dy: number;
|
|
want: Frame;
|
|
}
|
|
|
|
// Every expectation is written out in full rather than computed, so a bug in
|
|
// resizeFrame cannot be mirrored into the expectation by sharing its algebra.
|
|
const CASES: Case[] = [
|
|
// --- edges: each moves only what it owns ---
|
|
{ name: "east grows width only", edge: "e", dx: 50, dy: 999, want: { x: 100, y: 200, width: 1050, height: 800 } },
|
|
{ name: "south grows height only", edge: "s", dx: 999, dy: 40, want: { x: 100, y: 200, width: 1000, height: 840 } },
|
|
{ name: "west moves x AND width", edge: "w", dx: -30, dy: 0, want: { x: 70, y: 200, width: 1030, height: 800 } },
|
|
{ name: "north moves y AND height", edge: "n", dx: 0, dy: -25, want: { x: 100, y: 175, width: 1000, height: 825 } },
|
|
// --- corners: two axes at once ---
|
|
{ name: "south-east", edge: "se", dx: 10, dy: 20, want: { x: 100, y: 200, width: 1010, height: 820 } },
|
|
{ name: "north-west", edge: "nw", dx: -10, dy: -20, want: { x: 90, y: 180, width: 1010, height: 820 } },
|
|
{ name: "north-east", edge: "ne", dx: 10, dy: -20, want: { x: 100, y: 180, width: 1010, height: 820 } },
|
|
{ name: "south-west", edge: "sw", dx: -10, dy: 20, want: { x: 90, y: 200, width: 1010, height: 820 } },
|
|
// --- the anchored edge must not drift once clamped ---
|
|
// west dragged far east: width parks at MIN, and x parks where the EAST edge
|
|
// (100 + 1000 = 1100) minus MIN puts it. If x kept tracking dx, the window
|
|
// would slide away while appearing frozen.
|
|
{
|
|
name: "west clamps, east edge holds",
|
|
edge: "w",
|
|
dx: 5000,
|
|
dy: 0,
|
|
want: { x: 1100 - MIN_WIDTH, y: 200, width: MIN_WIDTH, height: 800 },
|
|
},
|
|
{
|
|
name: "north clamps, south edge holds",
|
|
edge: "n",
|
|
dx: 0,
|
|
dy: 5000,
|
|
want: { x: 100, y: 1000 - MIN_HEIGHT, width: 1000, height: MIN_HEIGHT },
|
|
},
|
|
// east/south clamp the size but never touch the origin
|
|
{ name: "east clamps, origin holds", edge: "e", dx: -5000, dy: 0, want: { x: 100, y: 200, width: MIN_WIDTH, height: 800 } },
|
|
{ name: "south clamps, origin holds", edge: "s", dx: 0, dy: -5000, want: { x: 100, y: 200, width: 1000, height: MIN_HEIGHT } },
|
|
// a corner clamping on BOTH axes at once
|
|
{
|
|
name: "north-west clamps both",
|
|
edge: "nw",
|
|
dx: 5000,
|
|
dy: 5000,
|
|
want: { x: 1100 - MIN_WIDTH, y: 1000 - MIN_HEIGHT, width: MIN_WIDTH, height: MIN_HEIGHT },
|
|
},
|
|
// a zero drag must be a no-op, or a click on a handle would nudge the window
|
|
{ name: "zero drag is identity", edge: "se", dx: 0, dy: 0, want: START },
|
|
];
|
|
|
|
const fmt = (f: Frame) => `${f.x},${f.y} ${f.width}x${f.height}`;
|
|
|
|
console.log("[resize-math] frame arithmetic:\n");
|
|
console.log(" case edge got verdict");
|
|
let failed = 0;
|
|
for (const c of CASES) {
|
|
const got = resizeFrame(c.edge, START, c.dx, c.dy);
|
|
const ok = fmt(got) === fmt(c.want);
|
|
if (!ok) failed++;
|
|
console.log(
|
|
` ${c.name.padEnd(32)} ${c.edge.padEnd(5)} ${fmt(got).padEnd(22)} ${ok ? "ok" : `FAIL: want ${fmt(c.want)}`}`,
|
|
);
|
|
}
|
|
|
|
// Structural check: every edge must be exercised above, so adding a ninth
|
|
// handle without a case cannot slip through.
|
|
const covered = new Set(CASES.map((c) => c.edge));
|
|
const missing = RESIZE_EDGES.filter((e) => !covered.has(e));
|
|
if (missing.length) {
|
|
console.log(`\n[resize-math] no case covers: ${missing.join(", ")}`);
|
|
failed++;
|
|
}
|
|
|
|
// The clamp must never produce a frame smaller than the minimum, whatever the
|
|
// drag — a cheap sweep over every edge and a range of deltas.
|
|
for (const edge of RESIZE_EDGES) {
|
|
for (const d of [-9999, -1000, -1, 0, 1, 1000, 9999]) {
|
|
const got = resizeFrame(edge, START, d, d);
|
|
if (got.width < MIN_WIDTH || got.height < MIN_HEIGHT) {
|
|
console.log(`\n[resize-math] ${edge} at delta ${d} produced ${fmt(got)} — under the minimum`);
|
|
failed++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (failed) {
|
|
console.log(`\n[resize-math] ${failed} failure(s)`);
|
|
process.exit(2);
|
|
}
|
|
console.log("\n[resize-math] clean");
|