Fix multi-webview browser capture prep

This commit is contained in:
Mohamed Boudra
2026-07-03 07:00:06 +02:00
parent 33ffeb4d42
commit f22865df62
7 changed files with 392 additions and 82 deletions
@@ -28,10 +28,18 @@ describe("resident browser webviews", () => {
expect(webview.style.display).toBe("inline-flex");
expect(webview.style.width).toBe("1280px");
expect(webview.style.height).toBe("800px");
expect(webview.style.position).toBe("absolute");
expect(webview.style.left).toBe("0px");
expect(webview.style.top).toBe("0px");
expect(webview.style.zIndex).toBe("0");
const reused = takeResidentBrowserWebview("browser-a");
expect(reused).toBe(webview);
expect(webview.style.position).toBe("");
expect(webview.style.left).toBe("");
expect(webview.style.top).toBe("");
expect(webview.style.zIndex).toBe("");
expect(takeResidentBrowserWebview("browser-a")).toBeNull();
});
@@ -87,6 +95,10 @@ describe("resident browser webviews", () => {
expect(webview.style.display).toBe("inline-flex");
expect(webview.style.width).toBe("1280px");
expect(webview.style.height).toBe("800px");
expect(webview.style.position).toBe("absolute");
expect(webview.style.left).toBe("0px");
expect(webview.style.top).toBe("0px");
expect(webview.style.zIndex).toBe("2");
await restoreResidentBrowserWebviewAfterPixelCapture(preparation);
@@ -94,6 +106,42 @@ describe("resident browser webviews", () => {
expect(host?.style.width).toBe("1280px");
expect(host?.style.height).toBe("800px");
expect(host?.style.opacity).toBe("0");
expect(webview.style.zIndex).toBe("0");
});
it("parks resident webviews as an overlapping stack and raises the capture target", async () => {
const firstWebview = ensureResidentBrowserWebview({
browserId: "browser-first",
url: "https://example.com/first",
});
const secondWebview = ensureResidentBrowserWebview({
browserId: "browser-second",
url: "https://example.com/second",
});
if (!firstWebview || !secondWebview) {
throw new Error("Expected resident browser webviews");
}
expect(firstWebview.style.position).toBe("absolute");
expect(firstWebview.style.left).toBe("0px");
expect(firstWebview.style.top).toBe("0px");
expect(firstWebview.style.zIndex).toBe("0");
expect(secondWebview.style.position).toBe("absolute");
expect(secondWebview.style.left).toBe("0px");
expect(secondWebview.style.top).toBe("0px");
expect(secondWebview.style.zIndex).toBe("0");
const preparation = await prepareResidentBrowserWebviewForPixelCapture({
browserId: "browser-second",
});
expect(firstWebview.style.zIndex).toBe("0");
expect(secondWebview.style.zIndex).toBe("2");
await restoreResidentBrowserWebviewAfterPixelCapture(preparation);
expect(firstWebview.style.zIndex).toBe("0");
expect(secondWebview.style.zIndex).toBe("0");
});
it("keeps the resident host paintable until every capture token is restored", async () => {
@@ -20,6 +20,7 @@ interface ActiveCapturePreparation {
browserId: string;
requestId?: string;
preparesResidentHost: boolean;
webview: HTMLElement;
}
function trimNonEmpty(value: string | null | undefined): string | null {
@@ -98,15 +99,37 @@ function applyResidentWebviewStyle(webview: HTMLElement): void {
webview.style.height = `${RESIDENT_VIEWPORT_HEIGHT}px`;
webview.style.border = "0";
webview.style.background = "transparent";
webview.style.position = "absolute";
webview.style.left = "0";
webview.style.top = "0";
webview.style.marginTop = "0";
webview.style.zIndex = "0";
}
function hasActiveResidentHostPreparation(): boolean {
for (const preparation of activeCapturePreparations.values()) {
if (preparation.preparesResidentHost) {
return true;
function clearResidentWebviewParkingStyle(webview: HTMLElement): void {
webview.style.position = "";
webview.style.left = "";
webview.style.top = "";
webview.style.marginTop = "";
webview.style.zIndex = "";
}
function residentWebviewChildren(host: HTMLElement): HTMLElement[] {
const webviews: HTMLElement[] = [];
for (const element of host.querySelectorAll(`[${BROWSER_ID_ATTRIBUTE}]`)) {
if (element instanceof HTMLElement) {
webviews.push(element);
}
}
return false;
return webviews;
}
function raiseResidentWebviewForCapture(host: HTMLElement, target: HTMLElement): void {
for (const webview of residentWebviewChildren(host)) {
applyResidentWebviewStyle(webview);
}
applyResidentWebviewStyle(target);
target.style.zIndex = "2";
}
function nextAnimationFrame(): Promise<void> {
@@ -143,13 +166,32 @@ function activeCaptureTokenFor(input: { token?: string; requestId?: string }): s
return null;
}
function parkResidentHostIfIdle(): void {
if (hasActiveResidentHostPreparation()) {
function latestActiveResidentHostPreparation(): ActiveCapturePreparation | null {
let latest: ActiveCapturePreparation | null = null;
for (const preparation of activeCapturePreparations.values()) {
if (preparation.preparesResidentHost) {
latest = preparation;
}
}
return latest;
}
function syncResidentHostCaptureState(): void {
const host = readDocument()?.getElementById(RESIDENT_BROWSER_HOST_ID);
if (!(host instanceof HTMLElement)) {
return;
}
const host = readDocument()?.getElementById(RESIDENT_BROWSER_HOST_ID);
if (host instanceof HTMLElement) {
applyResidentHostParkingStyle(host);
const latest = latestActiveResidentHostPreparation();
if (latest) {
applyResidentHostCaptureStyle(host);
raiseResidentWebviewForCapture(host, latest.webview);
return;
}
applyResidentHostParkingStyle(host);
for (const webview of residentWebviewChildren(host)) {
applyResidentWebviewStyle(webview);
}
}
@@ -160,7 +202,8 @@ function releaseCapturePreparationToken(token: string): void {
}
activeCapturePreparations.delete(token);
if (preparation.preparesResidentHost) {
parkResidentHostIfIdle();
applyResidentWebviewStyle(preparation.webview);
syncResidentHostCaptureState();
}
}
@@ -170,12 +213,12 @@ function releaseCapturePreparationsForBrowser(browserId: string): void {
activeCapturePreparations.delete(token);
}
}
parkResidentHostIfIdle();
syncResidentHostCaptureState();
}
function releaseAllCapturePreparations(): void {
activeCapturePreparations.clear();
parkResidentHostIfIdle();
syncResidentHostCaptureState();
}
export function prepareBrowserWebview(
@@ -233,6 +276,7 @@ export function takeResidentBrowserWebview(browserId: string): HTMLElement | nul
}
residentWebviewsByBrowserId.delete(normalizedBrowserId);
clearResidentWebviewParkingStyle(webview);
return webview;
}
@@ -278,11 +322,12 @@ export async function prepareResidentBrowserWebviewForPixelCapture(input: {
browserId,
...(requestId ? { requestId } : {}),
preparesResidentHost,
webview,
});
try {
if (preparesResidentHost) {
applyResidentHostCaptureStyle(host);
applyResidentWebviewStyle(webview);
raiseResidentWebviewForCapture(host, webview);
}
await waitForCapturePaint(webview);
if (!activeCapturePreparations.has(token)) {