Bug 1873947 - Fix dragging screenshots tab to new window. r=sfoster,mconley

Differential Revision: https://phabricator.services.mozilla.com/D198470
This commit is contained in:
Niklas Baumgardner 2024-03-07 13:32:00 +00:00
parent 76b1e7a644
commit 3e4d27dae4
6 changed files with 288 additions and 48 deletions

View File

@ -44,6 +44,10 @@ export class ScreenshotsComponentChild extends JSWindowActorChild {
return this.getDocumentTitle();
case "Screenshots:GetMethodsUsed":
return this.getMethodsUsed();
case "Screenshots:RemoveEventListeners":
return this.removeEventListeners();
case "Screenshots:AddEventListeners":
return this.addEventListeners();
}
return null;
}
@ -84,14 +88,6 @@ export class ScreenshotsComponentChild extends JSWindowActorChild {
}
this.#scrollTask.arm();
break;
case "visibilitychange":
if (
event.target.visibilityState === "hidden" &&
this.overlay?.state === "crosshairs"
) {
this.requestCancelScreenshot("navigation");
}
break;
case "Screenshots:Close":
this.requestCancelScreenshot(event.detail.reason);
break;
@ -210,6 +206,13 @@ export class ScreenshotsComponentChild extends JSWindowActorChild {
});
}
addEventListeners() {
this.contentWindow.addEventListener("beforeunload", this);
this.contentWindow.addEventListener("resize", this);
this.contentWindow.addEventListener("scroll", this);
this.addOverlayEventListeners();
}
addOverlayEventListeners() {
let chromeEventHandler = this.docShell.chromeEventHandler;
for (let event of ScreenshotsComponentChild.OVERLAY_EVENTS) {
@ -234,16 +237,19 @@ export class ScreenshotsComponentChild extends JSWindowActorChild {
let overlay =
this.overlay ||
(this.#overlay = new lazy.ScreenshotsOverlay(this.document));
this.document.ownerGlobal.addEventListener("beforeunload", this);
this.contentWindow.addEventListener("resize", this);
this.contentWindow.addEventListener("scroll", this);
this.contentWindow.addEventListener("visibilitychange", this);
this.addOverlayEventListeners();
this.addEventListeners();
overlay.initialize();
return true;
}
removeEventListeners() {
this.contentWindow.removeEventListener("beforeunload", this);
this.contentWindow.removeEventListener("resize", this);
this.contentWindow.removeEventListener("scroll", this);
this.removeOverlayEventListeners();
}
removeOverlayEventListeners() {
let chromeEventHandler = this.docShell.chromeEventHandler;
for (let event of ScreenshotsComponentChild.OVERLAY_EVENTS) {
@ -255,11 +261,7 @@ export class ScreenshotsComponentChild extends JSWindowActorChild {
* Removes event listeners and the screenshots overlay.
*/
endScreenshotsOverlay(options = {}) {
this.document.ownerGlobal.removeEventListener("beforeunload", this);
this.contentWindow.removeEventListener("resize", this);
this.contentWindow.removeEventListener("scroll", this);
this.contentWindow.removeEventListener("visibilitychange", this);
this.removeOverlayEventListeners();
this.removeEventListeners();
this.overlay?.tearDown(options);
this.#resizeTask?.disarm();

View File

@ -65,11 +65,13 @@ export class ScreenshotsComponentParent extends JSWindowActorParent {
// otherwise looks like the UIPhases.CLOSED state.
return;
}
switch (message.name) {
case "Screenshots:CancelScreenshot":
case "Screenshots:CancelScreenshot": {
let { reason } = message.data;
ScreenshotsUtils.cancel(browser, reason);
break;
}
case "Screenshots:CopyScreenshot":
ScreenshotsUtils.closePanel(browser);
({ region } = message.data);
@ -112,11 +114,12 @@ export class ScreenshotsComponentParent extends JSWindowActorParent {
export class ScreenshotsHelperParent extends JSWindowActorParent {
receiveMessage(message) {
switch (message.name) {
case "ScreenshotsHelper:GetElementRectFromPoint":
case "ScreenshotsHelper:GetElementRectFromPoint": {
let cxt = BrowsingContext.get(message.data.bcId);
return cxt.currentWindowGlobal
.getActor("ScreenshotsHelper")
.sendQuery("ScreenshotsHelper:GetElementRectFromPoint", message.data);
}
}
return null;
}
@ -186,10 +189,87 @@ export var ScreenshotsUtils = {
},
handleEvent(event) {
// Escape should cancel and exit
if (event.type === "keydown" && event.key === "Escape") {
let browser = event.view.gBrowser.selectedBrowser;
this.cancel(browser, "escape");
switch (event.type) {
case "keydown":
if (event.key === "Escape") {
// Escape should cancel and exit
let browser = event.view.gBrowser.selectedBrowser;
this.cancel(browser, "escape");
}
break;
case "TabSelect":
this.handleTabSelect(event);
break;
case "SwapDocShells":
this.handleDocShellSwapEvent(event);
break;
case "EndSwapDocShells":
this.handleEndDocShellSwapEvent(event);
break;
}
},
/**
* When we swap docshells for a given screenshots browser, we need to update
* the browserToScreenshotsState WeakMap to the correct browser. If the old
* browser is in a state other than OVERLAYSELECTION, we will close
* screenshots.
*
* @param {Event} event The SwapDocShells event
*/
handleDocShellSwapEvent(event) {
let oldBrowser = event.target;
let newBrowser = event.detail;
const currentUIPhase = this.getUIPhase(oldBrowser);
if (currentUIPhase === UIPhases.OVERLAYSELECTION) {
newBrowser.addEventListener("SwapDocShells", this);
newBrowser.addEventListener("EndSwapDocShells", this);
oldBrowser.removeEventListener("SwapDocShells", this);
let perBrowserState =
this.browserToScreenshotsState.get(oldBrowser) || {};
this.browserToScreenshotsState.set(newBrowser, perBrowserState);
this.browserToScreenshotsState.delete(oldBrowser);
this.getActor(oldBrowser).sendAsyncMessage(
"Screenshots:RemoveEventListeners"
);
} else {
this.cancel(oldBrowser, "navigation");
}
},
/**
* When we swap docshells for a given screenshots browser, we need to add the
* event listeners to the new browser because we removed event listeners in
* handleDocShellSwapEvent.
*
* We attach the overlay event listeners to this.docShell.chromeEventHandler
* in ScreenshotsComponentChild.sys.mjs which is the browser when the page is
* loaded via the parent process (about:config, about:robots, etc) and when
* this is the case, we lose the event listeners on the original browser.
* To fix this, we remove the event listeners on the old browser and add the
* event listeners to the new browser when a SwapDocShells occurs.
*
* @param {Event} event The EndSwapDocShells event
*/
handleEndDocShellSwapEvent(event) {
let browser = event.target;
this.getActor(browser).sendAsyncMessage("Screenshots:AddEventListeners");
browser.removeEventListener("EndSwapDocShells", this);
},
/**
* When we receive a TabSelect event, we will close screenshots in the
* previous tab if the previous tab was in the initial state.
*
* @param {Event} event The TabSelect event
*/
handleTabSelect(event) {
let previousTab = event.detail.previousTab;
if (this.getUIPhase(previousTab.linkedBrowser) === UIPhases.INITIAL) {
this.cancel(previousTab.linkedBrowser, "navigation");
}
},
@ -249,10 +329,14 @@ export var ScreenshotsUtils = {
start(browser, reason = "") {
const uiPhase = this.getUIPhase(browser);
switch (uiPhase) {
case UIPhases.CLOSED:
case UIPhases.CLOSED: {
this.captureFocusedElement(browser, "previousFocusRef");
this.showPanelAndOverlay(browser, reason);
browser.addEventListener("SwapDocShells", this);
let gBrowser = browser.getTabBrowser();
gBrowser.tabContainer.addEventListener("TabSelect", this);
break;
}
case UIPhases.INITIAL:
// nothing to do, panel & overlay are already open
break;
@ -277,6 +361,10 @@ export var ScreenshotsUtils = {
this.resetMethodsUsed();
this.attemptToRestoreFocus(browser);
browser.removeEventListener("SwapDocShells", this);
const gBrowser = browser.getTabBrowser();
gBrowser.tabContainer.removeEventListener("TabSelect", this);
this.browserToScreenshotsState.delete(browser);
if (Cu.isInAutomation) {
Services.obs.notifyObservers(null, "screenshots-exit");
@ -465,21 +553,15 @@ export var ScreenshotsUtils = {
},
/**
* Returns the buttons panel for the given browser
* Returns the buttons panel for the given browser if the panel exists.
* Otherwise creates the buttons panel and returns the buttons panel.
* @param browser The current browser
* @returns The buttons panel
*/
panelForBrowser(browser) {
return browser.ownerDocument.getElementById("screenshotsPagePanel");
},
/**
* Create the buttons container from its template, for this browser
* @param browser The current browser
* @returns The buttons panel
*/
createPanelForBrowser(browser) {
let buttonsPanel = this.panelForBrowser(browser);
let buttonsPanel = browser.ownerDocument.getElementById(
"screenshotsPagePanel"
);
if (!buttonsPanel) {
let doc = browser.ownerDocument;
let template = doc.getElementById("screenshotsPagePanelTemplate");
@ -491,7 +573,10 @@ export var ScreenshotsUtils = {
anchor.appendChild(buttonsPanel);
}
return this.panelForBrowser(browser);
return (
buttonsPanel ??
browser.ownerDocument.getElementById("screenshotsPagePanel")
);
},
/**
@ -533,7 +618,6 @@ export var ScreenshotsUtils = {
async showPanelAndOverlay(browser, data) {
let actor = this.getActor(browser);
actor.sendAsyncMessage("Screenshots:ShowOverlay");
this.createPanelForBrowser(browser);
this.recordTelemetryEvent("started", data, {});
this.openPanel(browser);
},

View File

@ -60,4 +60,6 @@ skip-if = ["!crashreporter"]
["browser_test_element_picker.js"]
["browser_test_moving_tab_to_new_window.js"]
["browser_test_resize.js"]

View File

@ -283,6 +283,23 @@ add_task(async function test_scrollingScreenshotsOpen() {
let { scrollWidth, scrollHeight } =
await helper.getScreenshotsOverlayDimensions();
info(
JSON.stringify(
{
left,
top,
right,
bottom,
width,
height,
scrollWidth,
scrollHeight,
},
null,
2
)
);
is(left, startX, "The box left is 10");
is(top, startY, "The box top is 10");
is(

View File

@ -0,0 +1,123 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(async function test_movingTabToNewWindow() {
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
let originalHelper = new ScreenshotsHelper(tab.linkedBrowser);
originalHelper.triggerUIFromToolbar();
await originalHelper.waitForOverlay();
await originalHelper.dragOverlay(10, 10, 300, 300);
let newWindow = gBrowser.replaceTabWithWindow(tab);
let swapDocShellPromise = BrowserTestUtils.waitForEvent(
tab.linkedBrowser,
"SwapDocShells"
);
await swapDocShellPromise;
let newtab = newWindow.gBrowser.selectedTab;
let newHelper = new ScreenshotsHelper(newtab.linkedBrowser);
let isInitialized = await newHelper.isOverlayInitialized();
ok(isInitialized, "Overlay is initialized after switching windows");
ok(
!ScreenshotsUtils.browserToScreenshotsState.has(tab.linkedBrowser),
"The old browser is no longer in the ScreenshotsUtils weakmap"
);
ok(
ScreenshotsUtils.browserToScreenshotsState.has(newtab.linkedBrowser),
"The new browser is in the ScreenshotsUtils weakmap"
);
await newHelper.clickCancelButton();
await newHelper.assertStateChange("crosshairs");
await newHelper.waitForOverlay();
swapDocShellPromise = BrowserTestUtils.waitForEvent(
newtab.linkedBrowser,
"SwapDocShells"
);
gBrowser.adoptTab(newWindow.gBrowser.selectedTab, 1, true);
await swapDocShellPromise;
tab = gBrowser.selectedTab;
let helper = new ScreenshotsHelper(tab.linkedBrowser);
isInitialized = await helper.isOverlayInitialized();
ok(!isInitialized, "Overlay is not initialized");
ok(
!ScreenshotsUtils.browserToScreenshotsState.has(tab.linkedBrowser),
"The old browser is no longer in the ScreenshotsUtils weakmap"
);
ok(
!ScreenshotsUtils.browserToScreenshotsState.has(newtab.linkedBrowser),
"The new browser is no longer in the ScreenshotsUtils weakmap"
);
await BrowserTestUtils.removeTab(tab);
});
add_task(async function test_movingParentProcessTabToNewWindow() {
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"about:robots"
);
let originalHelper = new ScreenshotsHelper(tab.linkedBrowser);
originalHelper.triggerUIFromToolbar();
await originalHelper.waitForOverlay();
await originalHelper.dragOverlay(10, 10, 300, 300);
let newWindow = gBrowser.replaceTabWithWindow(tab);
let swapDocShellPromise = BrowserTestUtils.waitForEvent(
tab.linkedBrowser,
"SwapDocShells"
);
await swapDocShellPromise;
let newtab = newWindow.gBrowser.selectedTab;
let newHelper = new ScreenshotsHelper(newtab.linkedBrowser);
let isInitialized = await newHelper.isOverlayInitialized();
ok(isInitialized, "Overlay is initialized after switching windows");
ok(
!ScreenshotsUtils.browserToScreenshotsState.has(tab.linkedBrowser),
"The old browser is no longer in the ScreenshotsUtils weakmap"
);
ok(
ScreenshotsUtils.browserToScreenshotsState.has(newtab.linkedBrowser),
"The new browser is in the ScreenshotsUtils weakmap"
);
await newHelper.clickCancelButton();
await newHelper.assertStateChange("crosshairs");
await newHelper.waitForOverlay();
swapDocShellPromise = BrowserTestUtils.waitForEvent(
newtab.linkedBrowser,
"SwapDocShells"
);
gBrowser.adoptTab(newWindow.gBrowser.selectedTab, 1, true);
await swapDocShellPromise;
tab = gBrowser.selectedTab;
let helper = new ScreenshotsHelper(tab.linkedBrowser);
isInitialized = await helper.isOverlayInitialized();
ok(!isInitialized, "Overlay is not initialized");
ok(
!ScreenshotsUtils.browserToScreenshotsState.has(tab.linkedBrowser),
"The old browser is no longer in the ScreenshotsUtils weakmap"
);
ok(
!ScreenshotsUtils.browserToScreenshotsState.has(newtab.linkedBrowser),
"The new browser is no longer in the ScreenshotsUtils weakmap"
);
await BrowserTestUtils.removeTab(tab);
});

View File

@ -38,15 +38,21 @@ const MouseEvents = {
{},
{
get: (target, name) =>
async function (x, y, options = {}) {
async function (x, y, options = {}, browser) {
if (name === "click") {
this.down(x, y, options);
this.up(x, y, options);
this.down(x, y, options, browser);
this.up(x, y, options, browser);
} else {
await safeSynthesizeMouseEventInContentPage(":root", x, y, {
type: "mouse" + name,
...options,
});
await safeSynthesizeMouseEventInContentPage(
":root",
x,
y,
{
type: "mouse" + name,
...options,
},
browser
);
}
},
}
@ -474,7 +480,7 @@ class ScreenshotsHelper {
);
info(`clicking cancel button at ${x}, ${y}`);
mouse.click(x, y);
mouse.click(x, y, {}, this.browser);
}
async clickPreviewCancelButton() {
@ -878,9 +884,15 @@ async function safeSynthesizeMouseEventInContentPage(
selector,
x,
y,
options = {}
options = {},
browser
) {
let context = gBrowser.selectedBrowser.browsingContext;
let context;
if (!browser) {
context = gBrowser.selectedBrowser.browsingContext;
} else {
context = browser.browsingContext;
}
BrowserTestUtils.synthesizeMouse(selector, x, y, options, context);
}