fix(desktop): keep browser surfaces interactive
This commit is contained in:
@@ -199,6 +199,8 @@ async function startTargetPage() {
|
||||
<head><title>Desktop browser target</title></head>
|
||||
<body>
|
||||
<button id="bridge-target" onclick="this.textContent = 'Clicked'">Bridge target</button>
|
||||
<label for="typing-target">Typing target</label>
|
||||
<input id="typing-target" />
|
||||
</body>
|
||||
</html>`);
|
||||
});
|
||||
@@ -323,6 +325,55 @@ async function readViewport(client, browserId) {
|
||||
return JSON.parse(evaluated.resultJson);
|
||||
}
|
||||
|
||||
async function clickGuestElement(page, client, browserId, selector) {
|
||||
const evaluated = await callBrowserTool(client, "browser_evaluate", {
|
||||
browserId,
|
||||
function: `() => {
|
||||
const element = document.querySelector(${JSON.stringify(selector)});
|
||||
if (!(element instanceof HTMLElement)) return null;
|
||||
const rect = element.getBoundingClientRect();
|
||||
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
||||
}`,
|
||||
});
|
||||
const elementRect = JSON.parse(evaluated.resultJson);
|
||||
assert(elementRect, `Guest element ${selector} was unavailable`);
|
||||
const webviewRect = await page.evaluate((id) => {
|
||||
const webview = document.querySelector(`[data-paseo-browser-id="${id}"]`);
|
||||
if (!(webview instanceof HTMLElement)) return null;
|
||||
const rect = webview.getBoundingClientRect();
|
||||
return { x: rect.x, y: rect.y };
|
||||
}, browserId);
|
||||
assert(webviewRect, `Browser webview ${browserId} was unavailable`);
|
||||
await page.mouse.click(
|
||||
webviewRect.x + elementRect.x + elementRect.width / 2,
|
||||
webviewRect.y + elementRect.y + elementRect.height / 2,
|
||||
);
|
||||
}
|
||||
|
||||
async function selectDeviceSize(page, label) {
|
||||
await page.locator('[aria-label="Device size"]').click();
|
||||
const item = page.getByText(label, { exact: true });
|
||||
await item.waitFor({ state: "visible", timeout: timeoutMs });
|
||||
const rect = await item.boundingBox();
|
||||
assert(rect, `Device size menu item ${label} had no bounds`);
|
||||
const clip = {
|
||||
x: Math.max(0, rect.x),
|
||||
y: Math.max(0, rect.y),
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
};
|
||||
const openPixels = await page.screenshot({ clip });
|
||||
await page.keyboard.press("Escape");
|
||||
await item.waitFor({ state: "hidden", timeout: timeoutMs });
|
||||
const closedPixels = await page.screenshot({ clip });
|
||||
|
||||
await page.locator('[aria-label="Device size"]').click();
|
||||
await item.waitFor({ state: "visible", timeout: timeoutMs });
|
||||
await page.mouse.click(rect.x + rect.width / 2, rect.y + rect.height / 2);
|
||||
await page.keyboard.press("Escape");
|
||||
return !openPixels.equals(closedPixels);
|
||||
}
|
||||
|
||||
function recordViewportMismatch(failures, label, actual, expected) {
|
||||
if (actual.width === expected.width && actual.height === expected.height) {
|
||||
return;
|
||||
@@ -341,6 +392,16 @@ async function runRegression({ page, client, serverId, targetUrl, callerAgentId
|
||||
await originalWorkspaceRow.waitFor({ state: "visible", timeout: timeoutMs });
|
||||
await originalWorkspaceRow.click();
|
||||
|
||||
await page.evaluate(() => {
|
||||
if (document.getElementById("overlay-root")) return;
|
||||
const overlayRoot = document.createElement("div");
|
||||
overlayRoot.id = "overlay-root";
|
||||
overlayRoot.style.position = "fixed";
|
||||
overlayRoot.style.inset = "0";
|
||||
overlayRoot.style.pointerEvents = "none";
|
||||
document.body.appendChild(overlayRoot);
|
||||
});
|
||||
|
||||
const created = await callBrowserTool(client, "browser_new_tab", { url: targetUrl });
|
||||
const browserId = created.browserId;
|
||||
assert(typeof browserId === "string", "browser_new_tab returned no browserId");
|
||||
@@ -364,6 +425,30 @@ async function runRegression({ page, client, serverId, targetUrl, callerAgentId
|
||||
{ width: firstGuest.width, height: firstGuest.height },
|
||||
);
|
||||
|
||||
await clickGuestElement(page, client, browserId, "#typing-target");
|
||||
const activeGuestElement = await callBrowserTool(client, "browser_evaluate", {
|
||||
browserId,
|
||||
function: "() => document.activeElement?.id ?? null",
|
||||
});
|
||||
assert(
|
||||
JSON.parse(activeGuestElement.resultJson) === "typing-target",
|
||||
"Physical browser click did not focus the guest input",
|
||||
);
|
||||
const focusedGuest = await page.evaluate(
|
||||
(id) => window.paseoDesktop?.browser?.focus?.(id),
|
||||
browserId,
|
||||
);
|
||||
assert(focusedGuest === true, "Electron did not focus the registered browser guest");
|
||||
|
||||
const deviceSizeMenuPainted = await selectDeviceSize(page, "iPhone SE · 375×667");
|
||||
assert(deviceSizeMenuPainted, "Device size menu did not paint above the browser surface");
|
||||
recordViewportMismatch(
|
||||
failures,
|
||||
"device size menu paints and receives input above the browser surface",
|
||||
await readViewport(client, browserId),
|
||||
{ width: 375, height: 667 },
|
||||
);
|
||||
|
||||
await callBrowserTool(client, "browser_wait", {
|
||||
browserId,
|
||||
text: "Bridge target",
|
||||
@@ -426,6 +511,9 @@ async function runRegression({ page, client, serverId, targetUrl, callerAgentId
|
||||
}
|
||||
await callBrowserTool(client, "browser_resize", { browserId, ...requestedViewport });
|
||||
|
||||
await selectDeviceSize(page, "Responsive");
|
||||
const responsiveViewport = await readViewport(client, browserId);
|
||||
|
||||
await originalDeck.getByTestId(`workspace-tab-agent_${callerAgentId}`).click();
|
||||
await page.waitForTimeout(500);
|
||||
try {
|
||||
@@ -437,13 +525,40 @@ async function runRegression({ page, client, serverId, targetUrl, callerAgentId
|
||||
failures,
|
||||
"inactive browser preserves the shared viewport",
|
||||
await readViewport(client, browserId),
|
||||
requestedViewport,
|
||||
responsiveViewport,
|
||||
);
|
||||
const focusContinuitySentinel = "preserve-browser-document-across-focus";
|
||||
await callBrowserTool(client, "browser_evaluate", {
|
||||
browserId,
|
||||
function: `() => { globalThis.__paseoFocusContinuity = ${JSON.stringify(focusContinuitySentinel)}; return globalThis.__paseoFocusContinuity; }`,
|
||||
function: `() => {
|
||||
globalThis.__paseoFocusContinuity = ${JSON.stringify(focusContinuitySentinel)};
|
||||
globalThis.__paseoViewportTransitions = [{ width: innerWidth, height: innerHeight }];
|
||||
addEventListener('resize', () => {
|
||||
globalThis.__paseoViewportTransitions.push({ width: innerWidth, height: innerHeight });
|
||||
});
|
||||
return globalThis.__paseoFocusContinuity;
|
||||
}`,
|
||||
});
|
||||
await page.evaluate((id) => {
|
||||
const webview = document.querySelector(`[data-paseo-browser-id="${id}"]`);
|
||||
if (!webview) throw new Error(`Browser webview ${id} was unavailable`);
|
||||
const events = [];
|
||||
globalThis.__paseoBrowserReactivationEvents = events;
|
||||
for (const name of [
|
||||
"did-start-loading",
|
||||
"did-navigate-in-page",
|
||||
"load-commit",
|
||||
"did-stop-loading",
|
||||
]) {
|
||||
webview.addEventListener(name, (event) => {
|
||||
events.push({
|
||||
name,
|
||||
url: event.url ?? null,
|
||||
isMainFrame: event.isMainFrame ?? null,
|
||||
});
|
||||
});
|
||||
}
|
||||
}, browserId);
|
||||
|
||||
await originalDeck.getByTestId(`workspace-tab-browser_${browserId}`).click();
|
||||
await page.waitForFunction(
|
||||
@@ -458,6 +573,29 @@ async function runRegression({ page, client, serverId, targetUrl, callerAgentId
|
||||
{ id: browserId, webContentsId: firstGuest.webContentsId },
|
||||
{ timeout: timeoutMs },
|
||||
);
|
||||
await page.waitForTimeout(1_500);
|
||||
const reactivationEvents = await page.evaluate(
|
||||
() => globalThis.__paseoBrowserReactivationEvents ?? [],
|
||||
);
|
||||
const unexpectedReactivationCommits = reactivationEvents.filter(
|
||||
(event) => event.name === "did-navigate-in-page" || event.name === "load-commit",
|
||||
);
|
||||
assert(
|
||||
unexpectedReactivationCommits.length === 0,
|
||||
`responsive browser reactivation committed the current document: ${JSON.stringify(unexpectedReactivationCommits)}`,
|
||||
);
|
||||
const viewportTransitionsResult = await callBrowserTool(client, "browser_evaluate", {
|
||||
browserId,
|
||||
function: "() => globalThis.__paseoViewportTransitions ?? []",
|
||||
});
|
||||
const viewportTransitions = JSON.parse(viewportTransitionsResult.resultJson);
|
||||
const collapsedViewport = viewportTransitions.find(
|
||||
(viewport) => viewport.width <= 1 || viewport.height <= 1,
|
||||
);
|
||||
assert(
|
||||
collapsedViewport === undefined,
|
||||
`responsive browser reactivation collapsed the guest viewport: ${JSON.stringify(viewportTransitions)}`,
|
||||
);
|
||||
const continuityResult = await callBrowserTool(client, "browser_evaluate", {
|
||||
browserId,
|
||||
function: "() => globalThis.__paseoFocusContinuity ?? null",
|
||||
@@ -531,7 +669,7 @@ async function runRegression({ page, client, serverId, targetUrl, callerAgentId
|
||||
failures,
|
||||
"browser viewport survives workspace eviction and reattachment",
|
||||
await readViewport(client, browserId),
|
||||
requestedViewport,
|
||||
responsiveViewport,
|
||||
);
|
||||
|
||||
await originalDeck.getByRole("button", { name: "Annotate element" }).click();
|
||||
@@ -553,6 +691,8 @@ async function runRegression({ page, client, serverId, targetUrl, callerAgentId
|
||||
originalWebContentsId: firstGuest.webContentsId,
|
||||
finalWebContentsId: parkedGuest.webContentsId,
|
||||
viewport: "passed",
|
||||
guestFocus: "passed",
|
||||
overlayPlane: "passed",
|
||||
inactiveCapture: "passed",
|
||||
list: "passed",
|
||||
snapshot: "passed",
|
||||
|
||||
@@ -463,6 +463,18 @@ ipcMain.handle("paseo:browser:set-workspace-active-browser", (event, rawInput: u
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("paseo:browser:focus", (event, browserId: unknown): boolean => {
|
||||
if (typeof browserId !== "string" || browserId.trim().length === 0) {
|
||||
return false;
|
||||
}
|
||||
const contents = getPaseoBrowserWebContentsForHostWindow(browserId, event.sender.id);
|
||||
if (!contents) {
|
||||
return false;
|
||||
}
|
||||
contents.focus();
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle("paseo:browser:open-devtools", (event, browserId: unknown) => {
|
||||
if (typeof browserId !== "string" || browserId.trim().length === 0) {
|
||||
const result = {
|
||||
|
||||
@@ -111,6 +111,7 @@ contextBridge.exposeInMainWorld("paseoDesktop", {
|
||||
ipcRenderer.invoke("paseo:browser:unregister-workspace-browser", browserId),
|
||||
setWorkspaceActiveBrowser: (input: { workspaceId: string; browserId: string | null }) =>
|
||||
ipcRenderer.invoke("paseo:browser:set-workspace-active-browser", input),
|
||||
focus: (browserId: string) => ipcRenderer.invoke("paseo:browser:focus", browserId),
|
||||
openDevTools: (browserId: string) =>
|
||||
ipcRenderer.invoke("paseo:browser:open-devtools", browserId),
|
||||
clearProfile: (legacyBrowserIds: string[]) =>
|
||||
|
||||
Reference in New Issue
Block a user