blob: 78722b1b5c2c3d448fe2d7f5e53c1f66177eac57 [file]
#!/usr/bin/env node
// Real-window smoke for the picture-in-picture mirror's interaction.
//
// The unit tests drive a fake window, so they prove the physics and the state
// machine and nothing about Electron. This runs the real thing: a real parent
// window, a real child panel, the real preload and renderer, and real input
// events delivered to the page. Everything it asserts is something that was
// either measured on this machine or read out of Codex's binary.
//
// Run: npm --workspace @maka/desktop run build:main && npm --workspace @maka/desktop run build:overlay
// && npx electron scripts/pip-interaction-smoke.mjs
import { app, BrowserWindow, screen } from 'electron';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const here = dirname(fileURLToPath(import.meta.url));
const dist = join(here, '..', 'apps', 'desktop', 'dist');
const { createComputerUsePipController } = await import(
join(dist, 'main', 'computer-use', 'pip-window.js')
);
const PNG_1X1 =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==';
let failures = 0;
const check = (label, pass, detail) => {
if (!pass) failures += 1;
console.log(`[${pass ? 'PASS' : 'FAIL'}] ${label}${detail ? ` ${detail}` : ''}`);
};
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
/**
* Poll until the mirror stops moving, so assertions never race the spring.
*
* Requires several identical samples rather than two: the settle is
* overdamped, so its last stretch moves less than a point per frame and two
* consecutive reads can round to the same value while it is still travelling.
* Two was enough to report the mirror had arrived a point short of the anchor.
*/
async function settled(win, timeoutMs = 4000) {
let last = null;
let same = 0;
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const bounds = win.getBounds();
if (last && bounds.x === last.x && bounds.y === last.y) {
if (++same >= 6) return bounds;
} else {
same = 0;
}
last = bounds;
await sleep(60);
}
return win.getBounds();
}
app.whenReady().then(async () => {
const parent = new BrowserWindow({ x: 120, y: 120, width: 1000, height: 700, show: true });
await parent.loadURL('data:text/html,<body style="background:%23202028"></body>');
await sleep(300);
// The pointer the drag reads. Driven from here so the gesture is exact
// rather than dependent on whatever the real mouse happens to be doing.
let pointer = { x: 0, y: 0 };
let pip = null;
const created = [];
pip = createComputerUsePipController({
resolveAnchorRect: () => parent.getBounds(),
subscribeAnchorChanges: (cb) => {
parent.on('move', cb);
parent.on('resize', cb);
return () => {
parent.off('move', cb);
parent.off('resize', cb);
};
},
resolveParentWindow: () => parent,
cursorPoint: () => pointer,
workAreaFor: (rect) => screen.getDisplayMatching(rect).workArea,
preloadPath: join(dist, 'overlay', 'pip-preload.cjs'),
htmlPath: join(dist, 'overlay', 'pip.html'),
});
pip.present({
sessionId: 'smoke',
base64: PNG_1X1,
mimeType: 'image/png',
widthPx: 1000,
heightPx: 800,
});
await sleep(900);
const mirror = BrowserWindow.getAllWindows().find((w) => w !== parent);
if (!mirror) {
console.log('the mirror never opened; stopping.');
app.exit(2);
return;
}
created.push(mirror);
// ── the two properties the child-window fix bought ────────────────────────
check('the mirror does not float above other apps', mirror.isAlwaysOnTop() === false);
// `parent.isFocused()` is about the whole app being frontmost, which a
// headless-ish run cannot promise. What the mirror owes is narrower: it must
// never be the focused window itself.
check('the mirror never took focus', mirror.isFocused() === false);
check('the mirror is the app window’s child', mirror.getParentWindow() === parent);
const seated = mirror.getBounds();
const app0 = parent.getBounds();
check(
'it is seated on the app window’s bottom-right, inset 24pt',
seated.x === app0.x + app0.width - seated.width - 24 &&
seated.y === app0.y + app0.height - seated.height - 24,
`${JSON.stringify(seated)} against ${JSON.stringify(app0)}`,
);
// ── a pure move is carried by the window server, not by us ────────────────
parent.setBounds({ ...app0, x: app0.x + 160 });
await sleep(400);
check(
'moving the app carries the mirror with it',
mirror.getBounds().x === seated.x + 160,
`${seated.x} → ${mirror.getBounds().x}`,
);
// ── click-through until pointed at ────────────────────────────────────────
const contents = mirror.webContents;
const inMirror = (dx, dy) => ({ x: dx, y: dy });
// Hover is decided from the real pointer on the main side, so the test moves
// the pointer rather than injecting a page event.
const mid = mirror.getBounds();
pointer = { x: mid.x + 40, y: mid.y + 40 };
await sleep(300);
const hovered = await contents.executeJavaScript(
"document.getElementById('controls').dataset.visible",
);
check('the controls appear on hover', hovered === '1', `data-visible=${hovered}`);
// ── throw it at the opposite corner ───────────────────────────────────────
const before = mirror.getBounds();
// Press in the middle of the tile, clear of both the controls (top-right,
// 20x20 inset 8) and the resize grip (20x20 inset 8, on whichever corner is
// opposite the anchor). This used to press at (20, 20), which is inside the
// grip's 8..28 box: the grip stops the event propagating, so no drag ever
// started, the moves ran as a resize instead, and the five checks after it
// failed on the cascade. This half of the script had never completed.
const GRAB = { x: Math.round(before.width / 2), y: Math.round(before.height / 2) };
pointer = { x: before.x + GRAB.x, y: before.y + GRAB.y };
contents.sendInputEvent({
type: 'mouseDown',
...inMirror(GRAB.x, GRAB.y),
button: 'left',
clickCount: 1,
});
await sleep(60);
const appNow = parent.getBounds();
// Three samples so the tracker sees real motion, ending fast enough that the
// release reads as a throw rather than a place.
for (const [fx, fy] of [
[0.7, 0.7],
[0.4, 0.4],
[0.22, 0.18],
]) {
pointer = { x: appNow.x + appNow.width * fx, y: appNow.y + appNow.height * fy };
contents.sendInputEvent({ type: 'mouseMove', ...inMirror(GRAB.x, GRAB.y) });
await sleep(30);
}
contents.sendInputEvent({
type: 'mouseUp',
...inMirror(GRAB.x, GRAB.y),
button: 'left',
clickCount: 1,
});
const landed = await settled(mirror);
check(
'thrown up and left, it settles on the top-left anchor',
landed.x === appNow.x + 24 && landed.y === appNow.y + 24,
`${JSON.stringify(landed)} against app ${JSON.stringify(appNow)}`,
);
check(
'and the controller agrees which corner that is',
pip.currentAlignment() === 'top-left',
pip.currentAlignment(),
);
// ── a resize returns it to the corner the user chose ──────────────────────
parent.setBounds({ ...appNow, width: appNow.width - 200, height: appNow.height - 120 });
await sleep(500);
const afterResize = mirror.getBounds();
const appResized = parent.getBounds();
check(
'a resize keeps it on the chosen corner, not the default one',
afterResize.x === appResized.x + 24 && afterResize.y === appResized.y + 24,
JSON.stringify(afterResize),
);
// ── the grip resizes it, away from whichever corner it rests on ──────────
const beforeGrip = mirror.getBounds();
const gripAt = await contents.executeJavaScript(
"(() => { const r = document.getElementById('resize').getBoundingClientRect(); return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) }; })()",
);
// The tile is anchored top-left, so its top-left corner is the one held
// still and the grip has to be on the bottom-right — the corner that moves.
// Pinning the grip to the tile's top-left, which is what the CSS did before
// main started sending the alignment down, put the handle on the corner the
// gesture holds: the pointer left it on the first pixel of the drag.
check(
'the grip sits on the corner opposite the anchor',
gripAt.x > beforeGrip.width / 2 && gripAt.y > beforeGrip.height / 2,
`grip at ${JSON.stringify(gripAt)} in a ${beforeGrip.width}x${beforeGrip.height} tile ` +
`anchored ${pip.currentAlignment()}`,
);
pointer = { x: beforeGrip.x + gripAt.x, y: beforeGrip.y + gripAt.y };
contents.sendInputEvent({ type: 'mouseMove', ...gripAt });
await sleep(150);
contents.sendInputEvent({ type: 'mouseDown', ...gripAt, button: 'left', clickCount: 1 });
await sleep(60);
// Anchored top-left, so the grip is below the anchor and dragging down grows.
for (const dy of [30, 60, 90]) {
pointer = { x: pointer.x, y: beforeGrip.y + gripAt.y + dy };
contents.sendInputEvent({ type: 'mouseMove', ...gripAt });
await sleep(60);
}
contents.sendInputEvent({ type: 'mouseUp', ...gripAt, button: 'left', clickCount: 1 });
await sleep(300);
const afterGrip = mirror.getBounds();
const grew =
Math.max(afterGrip.width, afterGrip.height) > Math.max(beforeGrip.width, beforeGrip.height);
check(
'dragging the grip away from the anchor grows the mirror',
grew,
`${beforeGrip.width}x${beforeGrip.height} → ${afterGrip.width}x${afterGrip.height}`,
);
check(
'and it stays inside Codex’s [100, 400] clamp',
Math.max(afterGrip.width, afterGrip.height) <= 400 &&
Math.min(afterGrip.width, afterGrip.height) >= 1,
`${afterGrip.width}x${afterGrip.height}`,
);
check(
'and it is still on the corner it was resting on',
afterGrip.x === appResized.x + 24 && afterGrip.y === appResized.y + 24,
JSON.stringify(afterGrip),
);
// ── hide dismisses it ─────────────────────────────────────────────────────
//
// The controls are click-through until the *main-side* pointer says the
// pointer is on the mirror — that is the whole point of the hover poll — so
// the fixture has to move its pointer as well as send the page event. It
// did not, and the grip drag happened to leave the pointer one point past
// the tile's bottom edge, so the window was still ignoring mouse events and
// the controls still had `pointer-events: none` when the click arrived.
const tile = mirror.getBounds();
pointer = { x: tile.x + tile.width / 2, y: tile.y + tile.height / 2 };
await sleep(200);
const visibleNow = await contents.executeJavaScript(
"document.getElementById('controls').dataset.visible",
);
check('the controls are on before the click', visibleNow === '1', `data-visible=${visibleNow}`);
const hideAt = await contents.executeJavaScript(
'(() => { const b = document.querySelector(\'[data-control="hide"]\'); const r = b.getBoundingClientRect(); return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) }; })()',
);
pointer = { x: tile.x + hideAt.x, y: tile.y + hideAt.y };
contents.sendInputEvent({ type: 'mouseMove', ...hideAt });
await sleep(120);
contents.sendInputEvent({ type: 'mouseDown', ...hideAt, button: 'left', clickCount: 1 });
contents.sendInputEvent({ type: 'mouseUp', ...hideAt, button: 'left', clickCount: 1 });
await sleep(400);
check('the hide control dismisses the mirror', pip.isVisible() === false);
pip.destroyAll();
for (const w of created) if (!w.isDestroyed()) w.destroy();
parent.destroy();
console.log(failures === 0 ? '\nPIP INTERACTION OK' : `\n${failures} check(s) failed`);
app.exit(failures === 0 ? 0 : 1);
});