diff --git a/src/zen/tests/moz.build b/src/zen/tests/moz.build
index fbbf132a8..3e3969fdf 100644
--- a/src/zen/tests/moz.build
+++ b/src/zen/tests/moz.build
@@ -20,6 +20,7 @@ BROWSER_CHROME_MANIFESTS += [
"ub-actions/browser.toml",
"urlbar/browser.toml",
"welcome/browser.toml",
+ "window_drag/browser.toml",
"window_sync/browser.toml",
]
diff --git a/src/zen/tests/window_drag/browser.toml b/src/zen/tests/window_drag/browser.toml
new file mode 100644
index 000000000..6e8dac13c
--- /dev/null
+++ b/src/zen/tests/window_drag/browser.toml
@@ -0,0 +1,10 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[DEFAULT]
+support-files = [
+ "head.js",
+]
+
+["browser_window_drag_basic.js"]
diff --git a/src/zen/tests/window_drag/browser_window_drag_basic.js b/src/zen/tests/window_drag/browser_window_drag_basic.js
new file mode 100644
index 000000000..7887fb68b
--- /dev/null
+++ b/src/zen/tests/window_drag/browser_window_drag_basic.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let gDragStartCount = 0;
+
+add_setup(async function () {
+ const observer = () => gDragStartCount++;
+ Services.obs.addObserver(observer, WINDOW_DRAG_TOPIC);
+ registerCleanupFunction(() => {
+ Services.obs.removeObserver(observer, WINDOW_DRAG_TOPIC);
+ });
+});
+
+add_task(async function test_drag_from_empty_top_area() {
+ await BrowserTestUtils.withNewTab(WINDOW_DRAG_TEST_PAGE, async browser => {
+ await synthesizeContentDrag(browser, 50, 50);
+ await TestUtils.waitForCondition(
+ () => gDragStartCount === 1,
+ "Dragging an empty area in the top region should start a window drag"
+ );
+ is(gDragStartCount, 1, "Exactly one window drag started");
+ });
+});
+
+add_task(async function test_no_drag_on_interactive_target() {
+ await BrowserTestUtils.withNewTab(WINDOW_DRAG_TEST_PAGE, async browser => {
+ // Start the gesture on top of the link. Messages from the same actor
+ // pair are ordered, so the control drag afterwards would arrive later.
+ await BrowserTestUtils.synthesizeMouse(
+ "#link",
+ 5,
+ 5,
+ { type: "mousedown" },
+ browser
+ );
+ await BrowserTestUtils.synthesizeMouse(
+ "#link",
+ 35,
+ 15,
+ { type: "mousemove", buttons: 1 },
+ browser
+ );
+ await BrowserTestUtils.synthesizeMouse(
+ "#link",
+ 45,
+ 20,
+ { type: "mouseup" },
+ browser
+ );
+
+ // Control drag from an eligible area.
+ await synthesizeContentDrag(browser, 50, 50);
+ await TestUtils.waitForCondition(
+ () => gDragStartCount >= 2,
+ "The control drag should start a window drag"
+ );
+ is(
+ gDragStartCount,
+ 2,
+ "Dragging a link must not start a window drag (only the control did)"
+ );
+ });
+});
+
+add_task(async function test_no_drag_below_top_region() {
+ await BrowserTestUtils.withNewTab(WINDOW_DRAG_TEST_PAGE, async browser => {
+ const innerHeight = await getContentInnerHeight(browser);
+ await synthesizeContentDrag(browser, 50, Math.floor(innerHeight * 0.6));
+
+ // Control drag from inside the top region.
+ await synthesizeContentDrag(browser, 50, 50);
+ await TestUtils.waitForCondition(
+ () => gDragStartCount >= 3,
+ "The control drag should start a window drag"
+ );
+ is(
+ gDragStartCount,
+ 3,
+ "Dragging below the top region must not start a window drag"
+ );
+ });
+});
+
+add_task(async function test_pref_disables_window_drag() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["zen.view.drag-window-from-content", false]],
+ });
+ await BrowserTestUtils.withNewTab(WINDOW_DRAG_TEST_PAGE, async browser => {
+ await synthesizeContentDrag(browser, 50, 50);
+ });
+ await SpecialPowers.popPrefEnv();
+
+ // Control drag with the pref back on, in a fresh tab so the actor is
+ // created again.
+ await BrowserTestUtils.withNewTab(WINDOW_DRAG_TEST_PAGE, async browser => {
+ await synthesizeContentDrag(browser, 50, 50);
+ await TestUtils.waitForCondition(
+ () => gDragStartCount >= 4,
+ "The control drag should start a window drag"
+ );
+ is(
+ gDragStartCount,
+ 4,
+ "Dragging with the pref disabled must not start a window drag"
+ );
+ });
+});
diff --git a/src/zen/tests/window_drag/head.js b/src/zen/tests/window_drag/head.js
new file mode 100644
index 000000000..cd7e1dccb
--- /dev/null
+++ b/src/zen/tests/window_drag/head.js
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const WINDOW_DRAG_TOPIC = "zen-window-drag-started";
+
+const WINDOW_DRAG_TEST_PAGE = `https://example.com/document-builder.sjs?html=${encodeURIComponent(`
+
+
+ a link
+`)}`;
+
+/**
+ * Synthesizes a primary-button drag gesture inside the content area,
+ * starting at (x, y) and moving well past the drag threshold.
+ */
+async function synthesizeContentDrag(browser, x, y) {
+ await BrowserTestUtils.synthesizeMouse(
+ null,
+ x,
+ y,
+ { type: "mousedown" },
+ browser
+ );
+ for (let i = 1; i <= 3; i++) {
+ await BrowserTestUtils.synthesizeMouse(
+ null,
+ x + i * 10,
+ y + i * 5,
+ { type: "mousemove", buttons: 1 },
+ browser
+ );
+ }
+ await BrowserTestUtils.synthesizeMouse(
+ null,
+ x + 40,
+ y + 20,
+ { type: "mouseup" },
+ browser
+ );
+}
+
+function getContentInnerHeight(browser) {
+ return SpecialPowers.spawn(browser, [], () => content.innerHeight);
+}
diff --git a/src/zen/window-drag/actors/ZenWindowDragChild.sys.mjs b/src/zen/window-drag/actors/ZenWindowDragChild.sys.mjs
index 95aeecf4b..1ef721739 100644
--- a/src/zen/window-drag/actors/ZenWindowDragChild.sys.mjs
+++ b/src/zen/window-drag/actors/ZenWindowDragChild.sys.mjs
@@ -103,6 +103,10 @@ export class ZenWindowDragChild extends JSWindowActorChild {
#startScreenY = 0;
handleEvent(event) {
+ // Never let pages spoof the gesture with synthetic events.
+ if (!event.isTrusted) {
+ return;
+ }
switch (event.type) {
case "mousedown":
this.#onMouseDown(event);
@@ -321,7 +325,9 @@ export class ZenWindowDragChild extends JSWindowActorChild {
if (!style) {
return false;
}
- const cursor = style.cursor.split(",").pop().trim();
+ // The keyword is always the last component of the computed value.
+ // Don't split on "," as url() cursor images may contain commas.
+ const cursor = style.cursor.match(/[a-z-]+$/)?.[0];
return kInteractiveCursors.has(cursor);
}
}
diff --git a/src/zen/window-drag/actors/ZenWindowDragParent.sys.mjs b/src/zen/window-drag/actors/ZenWindowDragParent.sys.mjs
index fa6607a0a..3d92ab614 100644
--- a/src/zen/window-drag/actors/ZenWindowDragParent.sys.mjs
+++ b/src/zen/window-drag/actors/ZenWindowDragParent.sys.mjs
@@ -22,6 +22,12 @@ export class ZenWindowDragParent extends JSWindowActorParent {
if (!win || win.closed || win.windowState === win.STATE_FULLSCREEN) {
return;
}
+ if (Cu.isInAutomation) {
+ // Tests can't exercise a real OS drag session; let them observe the
+ // decision instead.
+ Services.obs.notifyObservers(win, "zen-window-drag-started");
+ return;
+ }
lazy.zenDragAndDropService.beginNativeWindowMove(win);
}
}