diff --git a/packages/desktop/src/features/browser-capture.test.ts b/packages/desktop/src/features/browser-capture.test.ts index 5c7d16a77a..ea377f51c5 100644 --- a/packages/desktop/src/features/browser-capture.test.ts +++ b/packages/desktop/src/features/browser-capture.test.ts @@ -29,7 +29,7 @@ function harness(guest: BrowserCaptureGuest | null = null) { describe("browser capture service", () => { it("validates and rounds guest-relative bounds before capture", async () => { const capturePage = vi.fn(async () => image()); - const { service } = harness({ isDestroyed: () => false, capturePage }); + const { service } = harness({ isDestroyed: () => false, getZoomFactor: () => 1, capturePage }); await expect( service.capture({ @@ -41,9 +41,25 @@ describe("browser capture service", () => { expect(capturePage).toHaveBeenCalledWith({ x: 0, y: 9, width: 20, height: 11 }); }); + it("captures the selected element when the page is zoomed", async () => { + const capturePage = vi.fn(async () => image()); + const { service } = harness({ + isDestroyed: () => false, + getZoomFactor: () => 1.2, + capturePage, + }); + + await service.capture({ + browserId: "browser-1", + hostWebContentsId: 42, + rect: { x: 300, y: 200, width: 120, height: 60 }, + }); + expect(capturePage).toHaveBeenCalledWith({ x: 360, y: 240, width: 144, height: 72 }); + }); + it("rejects invalid or unavailable captures without touching the guest", async () => { const capturePage = vi.fn(async () => image()); - const { service } = harness({ isDestroyed: () => false, capturePage }); + const { service } = harness({ isDestroyed: () => false, getZoomFactor: () => 1, capturePage }); await expect( service.capture({ browserId: "browser-1", hostWebContentsId: 42, rect: { width: 0 } }), diff --git a/packages/desktop/src/features/browser-capture.ts b/packages/desktop/src/features/browser-capture.ts index c2c7326ce7..199fde27e7 100644 --- a/packages/desktop/src/features/browser-capture.ts +++ b/packages/desktop/src/features/browser-capture.ts @@ -5,6 +5,7 @@ export interface BrowserCaptureImage { export interface BrowserCaptureGuest { isDestroyed(): boolean; + getZoomFactor(): number; capturePage(rect: BrowserCaptureRect): Promise; } @@ -40,7 +41,9 @@ export interface BrowserCaptureService { copy(payload: unknown): Promise; } -function captureRect(value: unknown): BrowserCaptureRect | null { +// The renderer measures the element in page CSS pixels, while capturePage +// takes device-independent pixels, so the page zoom scales the region. +function captureRect(value: unknown, zoomFactor: number): BrowserCaptureRect | null { if (!value || typeof value !== "object") return null; const record = value as Record; const coordinates = [record.x, record.y, record.width, record.height]; @@ -51,7 +54,9 @@ function captureRect(value: unknown): BrowserCaptureRect | null { ) { return null; } - const [x, y, width, height] = coordinates as number[]; + const [x, y, width, height] = (coordinates as number[]).map( + (coordinate) => coordinate * zoomFactor, + ); if (width <= 0 || height <= 0) return null; return { x: Math.max(0, Math.round(x)), @@ -80,8 +85,9 @@ export function createBrowserCaptureService( async capture({ browserId, hostWebContentsId, rect }) { if (typeof browserId !== "string" || browserId.trim().length === 0) return null; const guest = dependencies.findGuest(browserId, hostWebContentsId); - const bounds = captureRect(rect); - if (!guest || guest.isDestroyed() || !bounds) return null; + if (!guest || guest.isDestroyed()) return null; + const bounds = captureRect(rect, guest.getZoomFactor()); + if (!bounds) return null; try { const image = await guest.capturePage(bounds); return image.isEmpty() ? null : image.toDataURL();