* fix(app): prevent and recover workspace layout crash loops * fix(desktop): capture resized browser tabs without hanging A resident guest can paint fresh pixels after resize while capturePage's native surface-copy request remains pending. The same failed guest returns a fresh frame through frame subscription, so waiting for resize or another animation frame does not correct the failing copy request. Capture one subscribed frame within the existing five-second budget and release the subscription on completion or cancellation. Preserve temporary frame production and its original throttling policy. Remove viewport copy retries and E2E screenshot polling; assert the resized image dimensions. Refs #4866. Three full real-Electron browser journeys passed under Xvfb, including physical clicks and fresh screenshots while the window is hidden. * test(app): choose Agent before exercising the runtime catalog The newly merged catalog journey assumed an empty workspace opened an agent draft tab. This branch opens the New launcher, so create the agent draft tab explicitly before checking its composer and model choices. * fix: settle closed browser captures and validate persisted layout fixtures Destroying a guest during frame subscription left its screenshot pending until the capture deadline and blocked queued captures. Reject at destruction, remove the listener on every exit, and return browser_tab_closed. The regression proves immediate settlement and queue release. Reuse the unchanged persisted layout schema from a UI-independent module so the recovery fixture validates storage before editing it. * test(app): encapsulate Explorer layout setup and cleanup Keep persisted layout schemas and corrupted-state construction inside the seedCorruptedWorkspaceLayout helper. Use the existing withWorkspace fixture for cleanup in all four Explorer journeys so test bodies follow a straight setup, action and assertion flow. * test(app): target the selected workspace in layout recovery The corruption helper selected the first persisted entry, so a multi-workspace journey could leave the intended workspace untouched. Resolve its exact persistence key from the requested workspace ID and fail setup when absent. The failing-first journey now preserves an earlier workspace with an agent draft tab while recovering the selected workspace.
6.3 KiB
Browser Capture Harness
The desktop capture harness is the real-Electron verification path for browser screenshots. It validates the compositor behavior that unit tests cannot see:
- the resident automation
<webview>starts in the production parking state; - the parked guest remains paintable and has a copyable viewport frame;
- a never-presented resident webview guest defaults to 1280x800 logical pixels;
- multiple resident webviews are parked as an overlapping stack without per-capture stacking changes;
- a newly attached resident webview whose first useful frame is delayed can be captured by retrying until the frame appears;
- both viewport
capturePageand full-page CDP screenshots return real pixels from the permanent production parking state; - parked guests remain capturable with Chromium background throttling enabled;
- the real-Electron host-composer sentinel proves guest Enter cannot submit a focused host composer;
- the automation group loads the compiled production keyboard boundary and guest preload, then proves that initial page window handlers get first refusal, unhandled shortcuts synchronously suppress editable browser defaults before crossing the host boundary, shortcuts marked unavailable in editable targets retain the browser field's native behavior, handlers registered after preload still get first refusal, focused iframes share the same boundary, digit wildcard shortcuts cross, and background automation stays in the guest.
Run it with the repo Electron:
npm run capture-harness --workspace=@getpaseo/desktop
Build the desktop main process before the automation group so its production guest preload is available:
npm run build:main --workspace=@getpaseo/desktop
PASEO_CAPTURE_HARNESS_GROUP=automation npm run capture-harness --workspace=@getpaseo/desktop
Run the shared browser profile fixture with:
PASEO_CAPTURE_HARNESS_GROUP=browser-profile npm run capture-harness --workspace=@getpaseo/desktop
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
still present after the first Electron process exits and the second starts.
The automation group uses a real guest webview to verify the page-side ref contract:
ARIA-like snapshot text includes headings, static text, and controls; refs survive
pushState when the element still matches; same-URL rerenders stale old refs; and a
file-input ref can be resolved to a CDP backend node id for upload. It also verifies
page-context evaluation, including passing a resolved ref element as the function argument.
Keyboard containment runs last because the host-composer sentinel intentionally leaves
native focus in the host. It reuses an existing fixture button: adding a test-only control
changes the inline fixture geometry exercised by the earlier actionability checks.
On macOS the harness process must set app.setActivationPolicy("accessory") and
hide the Dock icon before creating any window. showInactive() only prevents window
focus; a normal Electron app launch can still activate the app and steal focus.
Harness windows are then created hidden, positioned in a screen corner, skipped from
the taskbar where Electron supports it, and revealed with showInactive() from
ready-to-show. Do not replace this with show(), focus(), or app.focus():
the compositor only needs visible inactive windows, and harness runs must not steal
focus from the person using the machine.
The harness writes PNG evidence and results.json to:
packages/desktop/capture-harness/out/
A passing run prints PASS lines for the production P1 default-throttling parking state,
including fresh, settled, 75-second soak, multi-tab, viewport, and full-page checks. The
PNG sizes may be device-pixel scaled; on a Retina display the 1280x800 logical viewport
is usually saved as 2560x1600.
The existing npm run test:e2e:browser-tabs --workspace=@getpaseo/desktop journey
verifies that a hidden window stops guest animation, captures fresh viewport pixels,
and resumes animation after restoring the window. Its artifacts include the screenshot
and animation measurements. Full-page content correctness remains separately tracked in
the full-page repetition bug.
Mechanism
Electron captures copy from the guest web contents' compositor surface. A resident
webview parked with display:none, offscreen coordinates, or opacity:0 can lose its
copyable surface. Each production webview keeps one permanent body-level surface. Presenting
or parking changes that surface's geometry without reparenting the webview. The parking state
uses left:0, top:0, width:1px, height:1px, overflow:hidden, opacity:1, and
pointer-events:none. The webview stays at its resolved logical viewport, defaulting to
1280x800 before first presentation, with display:inline-flex at left:0, top:0.
Presentation resolves responsive guests to the pane's exact pixel dimensions after the surface
has visible bounds. Do not apply percentage guest sizing against the parked surface: Electron
exposes the 1x1 parking geometry as a real guest resize before expanding it again.
The permanent browser host and overlay-root are explicit sibling paint planes. The browser
plane stays below the overlay plane regardless of body insertion order; menus keep their relative
layering inside overlay-root. Activating a presented browser also focuses its registered guest
WebContents in main so macOS assigns keyboard first-responder ownership to the page.
There is no renderer prep/restore handshake or lifetime background-throttling override.
Screenshot capture temporarily enables frame production inside the shared serialized queue,
restores the previous throttling policy on success or failure, and retains a 5-second capture budget.
The browser tool takes one viewport frame through Electron's frame subscription and releases the
subscription on completion or cancellation. A resized resident guest can produce fresh pixels
while capturePage() leaves its surface-copy request pending; waiting for animation frames or
retrying the copy does not repair that state. Full-page screenshots retain the CDP path with layout
metrics and screenshot clip.