diff --git a/docs/browser-capture-harness.md b/docs/browser-capture-harness.md index 594aacf5f6..f760fafa67 100644 --- a/docs/browser-capture-harness.md +++ b/docs/browser-capture-harness.md @@ -43,6 +43,18 @@ Run the shared browser profile fixture with: PASEO_CAPTURE_HARNESS_GROUP=browser-profile npm run capture-harness --workspace=@getpaseo/desktop ``` +On macOS, test native titlebar dragging after scrolling with: + +```bash +PASEO_CAPTURE_HARNESS_GROUP=titlebar-drag npm run capture-harness --workspace=@getpaseo/desktop +``` + +This group needs Xcode Command Line Tools and Accessibility permission for the +terminal running it. It sends real mouse input to its test window and checks +window movement, including button clicks and a portal over the titlebar. Leave +the mouse idle during this group. It reads the app's production CSS and writes +screenshots and window bounds to the output directory. + The browser profile group runs two Electron processes in sequence. It verifies that each renderer-side `did-attach` identity maps to the correct main-process guest, that two live tabs share cookies and local storage through one persistent session, and that the data is diff --git a/packages/app/public/index.html b/packages/app/public/index.html index f7116fc86b..a95e7b8c9e 100644 --- a/packages/app/public/index.html +++ b/packages/app/public/index.html @@ -70,6 +70,15 @@ -webkit-app-region: no-drag !important; } + /* Electron does not clip app-region rectangles to scroll viewports. + Content-only viewports emit no window regions, including from their + controls. Visible controls here use normal DOM hit testing. + Use initial: Blink parses the explicit value none as no-drag. */ + [data-window-content], + [data-window-content] * { + -webkit-app-region: initial !important; + } + /* Suppress the browser's default focus outline — it appears on mouse clicks and looks out of place against our themed surfaces. Keep a themed ring for keyboard users via :focus-visible. */ diff --git a/packages/app/src/agent-stream/strategy-web.tsx b/packages/app/src/agent-stream/strategy-web.tsx index 25e7a313d2..d0b7eda0df 100644 --- a/packages/app/src/agent-stream/strategy-web.tsx +++ b/packages/app/src/agent-stream/strategy-web.tsx @@ -1241,7 +1241,7 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool !liveAuxiliary; return ( -
+
+#import +#import +#import +#import + +static void post(CGEventType type, CGPoint p, useconds_t delay) { + CGEventRef event = CGEventCreateMouseEvent(NULL, type, p, kCGMouseButtonLeft); + if (!event) { + fprintf(stderr, "mouse: CGEventCreateMouseEvent failed\n"); + exit(3); + } + CGEventSetFlags(event, (CGEventFlags)0); + CGEventPost(kCGHIDEventTap, event); + CFRelease(event); + usleep(delay); +} + +int main(int argc, char **argv) { + if (!AXIsProcessTrusted()) { + fprintf(stderr, "mouse: not accessibility-trusted\n"); + return 2; + } + if (argc == 6 && strcmp(argv[1], "drag") == 0) { + CGPoint a = {atof(argv[2]), atof(argv[3])}; + CGPoint b = {atof(argv[4]), atof(argv[5])}; + post(kCGEventMouseMoved, a, 150000); + post(kCGEventLeftMouseDown, a, 150000); + for (int i = 1; i <= 20; i++) { + CGPoint step = {a.x + (b.x - a.x) * i / 20, a.y + (b.y - a.y) * i / 20}; + post(kCGEventLeftMouseDragged, step, 25000); + } + post(kCGEventLeftMouseUp, b, 25000); + return 0; + } + if (argc == 4 && strcmp(argv[1], "click") == 0) { + CGPoint p = {atof(argv[2]), atof(argv[3])}; + post(kCGEventMouseMoved, p, 150000); + post(kCGEventLeftMouseDown, p, 150000); + post(kCGEventLeftMouseUp, p, 25000); + return 0; + } + fprintf(stderr, "usage: %s drag x1 y1 x2 y2 | click x y\n", argv[0]); + return 1; +} diff --git a/packages/desktop/capture-harness/titlebar-drag.js b/packages/desktop/capture-harness/titlebar-drag.js new file mode 100644 index 0000000000..fe4a35d06c --- /dev/null +++ b/packages/desktop/capture-harness/titlebar-drag.js @@ -0,0 +1,272 @@ +// Native mouse input is required: CDP mouse events do not move Electron windows. +// Load production CSS so restoring the global no-drag rule reproduces the failure. +const fs = require("node:fs"); +const fsp = require("node:fs/promises"); +const path = require("node:path"); +const { execFile } = require("node:child_process"); +const { promisify } = require("node:util"); +const { BrowserWindow } = require("electron"); + +const execFileAsync = promisify(execFile); +const PACKAGE_ROOT = path.join(__dirname, "..", ".."); +const INDEX_HTML = path.join(PACKAGE_ROOT, "app", "public", "index.html"); +const MOUSE_SOURCE = path.join(__dirname, "titlebar-drag-mouse.m"); +const BASE = { x: 100, y: 150 }; +const DRAG_DELTA = { x: 80, y: 50 }; +const DRAG_POINTS = [ + { check: "header-overlay-blank", x: 625, y: 18 }, + { check: "tab-row-overlay-blank", x: 625, y: 50 }, + { check: "sidebar-overlay-blank", x: 125, y: 18 }, + { check: "direct-rail-blank", x: 125, y: 50 }, +]; +const CLICK_POINTS = [ + { check: "header-button-click", id: "header-btn", x: 955, y: 18 }, + { check: "window-owned-toggle-click", id: "window-toggle", x: 40, y: 18 }, + { check: "window-owned-focusable-click", id: "focusable", x: 320, y: 18 }, + { check: "window-owned-negative-tabindex-button-click", id: "negative-button", x: 400, y: 18 }, + { check: "sidebar-tab-click", id: "sidebar-btn", x: 205, y: 18 }, + { check: "rail-tab-click", id: "rail-btn", x: 205, y: 50 }, + { check: "portal-button-click", id: "portal-btn", x: 490, y: 18 }, + { check: "floating-panel-button-click", id: "floating-btn", x: 785, y: 18 }, +]; + +/** Static overlays and a sibling window control mirror the app shell ownership. */ +function buildFixtureHtml(appStyles) { + const dragOverlay = + 'style="position:absolute;top:0;left:0;display:block;width:100%;height:100%;-webkit-app-region:drag"'; + const button = (id, style) => + ``; + return ` + + + +
+
+
+ +${button("window-toggle", "position:absolute;left:10px;top:6px;width:60px;height:24px;z-index:2")} +
Focusable
+
Button
+
+
+
+ Chat link + ${button("chat-code", "position:absolute;left:250px;top:320px;width:180px;height:24px")} +
file.ts
+
+
+
+
+ ${button("portal-btn", "position:fixed;left:460px;top:6px;width:60px;height:24px;pointer-events:auto")} +
+
+ ${button("floating-btn", "position:fixed;left:755px;top:6px;width:60px;height:24px;pointer-events:auto")} +
+`; +} + +function readRealAppStyles() { + const html = fs.readFileSync(INDEX_HTML, "utf8"); + const styles = [...html.matchAll(/]*>([\s\S]*?)<\/style>/g)].map((m) => m[1]).join("\n"); + if (!styles.trim()) throw new Error(`no