diff --git a/apps/browser/src/platform/browser/browser-popup-utils.spec.ts b/apps/browser/src/platform/browser/browser-popup-utils.spec.ts index 89459523843..9cd7790aef4 100644 --- a/apps/browser/src/platform/browser/browser-popup-utils.spec.ts +++ b/apps/browser/src/platform/browser/browser-popup-utils.spec.ts @@ -149,6 +149,9 @@ describe("BrowserPopupUtils", () => { incognito: false, width: PopupWidthOptions.default, }); + jest.spyOn(BrowserApi, "getPlatformInfo").mockResolvedValue({ + os: "win", + } as chrome.runtime.PlatformInfo); jest.spyOn(BrowserApi, "createWindow").mockImplementation(); }); @@ -267,6 +270,54 @@ describe("BrowserPopupUtils", () => { url: `chrome-extension://id/${url}?uilocation=popout&singleActionPopout=123`, }); }); + + it("omits position when on Linux with Wayland-like coordinates (left=0, top=0)", async () => { + const url = "popup/index.html"; + jest.spyOn(BrowserApi, "getWindow").mockReset().mockResolvedValueOnce({ + id: 2, + left: 0, + top: 0, + focused: false, + alwaysOnTop: false, + incognito: false, + width: PopupWidthOptions.default, + }); + jest.spyOn(BrowserApi, "getPlatformInfo").mockResolvedValue({ + os: "linux", + } as chrome.runtime.PlatformInfo); + jest.spyOn(BrowserPopupUtils as any, "isSingleActionPopoutOpen").mockResolvedValueOnce(false); + + await BrowserPopupUtils.openPopout(url); + + expect(BrowserApi.createWindow).toHaveBeenCalledWith({ + type: "popup", + focused: true, + width: PopupWidthOptions.default, + height: 630, + url: `chrome-extension://id/${url}?uilocation=popout`, + }); + }); + + it("includes position when on Linux with non-zero window coordinates", async () => { + const url = "popup/index.html"; + // Uses the beforeEach window (left: 100, top: 100) — non-zero, so not Wayland-like. + jest.spyOn(BrowserApi, "getPlatformInfo").mockResolvedValue({ + os: "linux", + } as chrome.runtime.PlatformInfo); + jest.spyOn(BrowserPopupUtils as any, "isSingleActionPopoutOpen").mockResolvedValueOnce(false); + + await BrowserPopupUtils.openPopout(url); + + expect(BrowserApi.createWindow).toHaveBeenCalledWith({ + type: "popup", + focused: true, + width: PopupWidthOptions.default, + height: 630, + left: 85, + top: 190, + url: `chrome-extension://id/${url}?uilocation=popout`, + }); + }); }); describe("openCurrentPagePopout", () => { diff --git a/apps/browser/src/platform/browser/browser-popup-utils.ts b/apps/browser/src/platform/browser/browser-popup-utils.ts index 7333023d178..ae528621b63 100644 --- a/apps/browser/src/platform/browser/browser-popup-utils.ts +++ b/apps/browser/src/platform/browser/browser-popup-utils.ts @@ -150,10 +150,25 @@ export default class BrowserPopupUtils { const offsetTop = 90; const popupWidth = defaultPopoutWindowOptions.width; const senderWindow = await BrowserApi.getWindow(senderWindowId); + + // On Wayland, browser window coordinates are not being precisely reported. This is + // particularly troublesome for multi-monitor configurations, where the popup can be placed + // far enough outside the visible area that the browser refuses to create the window and emits + // an error: Invalid value for bounds. Bounds must be at least 50% within visible screen space. + // It is acceptable that this heuristic may fire for X11 sessions. + const operatingSystemIsLinux = (await BrowserApi.getPlatformInfo()).os === "linux"; + const coordsMaybeNotPrecise = senderWindow.left === 0 && senderWindow.top === 0; + const canPositionWindow = !(operatingSystemIsLinux && coordsMaybeNotPrecise); + const positionOptions = canPositionWindow + ? { + left: senderWindow.left + senderWindow.width - popupWidth - offsetRight, + top: senderWindow.top + offsetTop, + } + : {}; + const popoutWindowOptions = { - left: senderWindow.left + senderWindow.width - popupWidth - offsetRight, - top: senderWindow.top + offsetTop, ...defaultPopoutWindowOptions, + ...positionOptions, ...windowOptions, url: BrowserPopupUtils.buildPopoutUrl(extensionUrlPath, singleActionKey), };