Backed out changeset 5714b7239b70 (bug 1849021)
Backed out changeset 91e5d7de3484 (bug 1849021)
Backed out changeset cc3142b909f6 (bug 1854660)
Backed out changeset 76d2a8ae9c19 (bug 1854646)
Backed out changeset 36460a147684 (bug 1854636)
Backed out changeset c7529a962a8c (bug 1854625)
Backed out changeset 791f9b03ea96 (bug 1854538)
Backed out changeset bd3f3cb434a5 (bug 1854536)
Backed out changeset 16f4f5bdcaa7 (bug 1855491)
Backed out changeset 6258bab20196 (bug 1854533)
Backed out changeset 29dca16eee2e (bug 1854531)
Backed out changeset 3a69d2c3f562 (bug 1854528)
Backed out changeset 0c63a9ea8b34 (bug 1854525)
Backed out changeset 5795bb6187a5 (bug 1854522)
Backed out changeset 7d4a123a432f (bug 1854520)
Backed out changeset ec95287bd702 (bug 1854517)
Backed out changeset 9914c1481f0d (bug 1854514)
Backed out changeset 25d85e656905 (bug 1854512)
Backed out changeset 828657f2ab73 (bug 1854509)
Backed out changeset 9d8b2e14c375 (bug 1854506)
Backed out changeset 1b3ee1e46e12 (bug 1854502)
Backed out changeset 37c2ec0851dc (bug 1854460)
Backed out changeset c2a60a8fc2d3 (bug 1854452)
Backed out changeset 744a163f2bd1 (bug 1854447)
Backed out changeset 1a9629dae7d5 (bug 1854443)
Backed out changeset c3d5c1ba2a84 (bug 1854228)
Backed out changeset dcd05518b004 (bug 1692110)
Backed out changeset 1c65a4cb618c (bug 1692110)
Backed out changeset 603b891147bd (bug 1692110)
Backed out changeset 53e2fac79d84 (bug 1692110)
537 lines
16 KiB
JavaScript
537 lines
16 KiB
JavaScript
/* 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";
|
|
|
|
ChromeUtils.defineESModuleGetters(this, {
|
|
CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs",
|
|
CustomizableUITestUtils:
|
|
"resource://testing-common/CustomizableUITestUtils.sys.mjs",
|
|
});
|
|
|
|
var EventUtils = {};
|
|
Services.scriptloader.loadSubScript(
|
|
"chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
|
|
EventUtils
|
|
);
|
|
|
|
/**
|
|
* Instance of CustomizableUITestUtils for the current browser window.
|
|
*/
|
|
var gCUITestUtils = new CustomizableUITestUtils(window);
|
|
|
|
Services.prefs.setBoolPref("browser.uiCustomization.skipSourceNodeCheck", true);
|
|
registerCleanupFunction(() =>
|
|
Services.prefs.clearUserPref("browser.uiCustomization.skipSourceNodeCheck")
|
|
);
|
|
|
|
var { synthesizeDrop, synthesizeMouseAtCenter } = EventUtils;
|
|
|
|
const kForceOverflowWidthPx = 450;
|
|
|
|
function createDummyXULButton(id, label, win = window) {
|
|
let btn = win.document.createXULElement("toolbarbutton");
|
|
btn.id = id;
|
|
btn.setAttribute("label", label || id);
|
|
btn.className = "toolbarbutton-1 chromeclass-toolbar-additional";
|
|
win.gNavToolbox.palette.appendChild(btn);
|
|
return btn;
|
|
}
|
|
|
|
var gAddedToolbars = new Set();
|
|
|
|
function createToolbarWithPlacements(id, placements = [], properties = {}) {
|
|
gAddedToolbars.add(id);
|
|
let tb = document.createXULElement("toolbar");
|
|
tb.id = id;
|
|
tb.setAttribute("customizable", "true");
|
|
|
|
properties.type = CustomizableUI.TYPE_TOOLBAR;
|
|
properties.defaultPlacements = placements;
|
|
CustomizableUI.registerArea(id, properties);
|
|
gNavToolbox.appendChild(tb);
|
|
CustomizableUI.registerToolbarNode(tb);
|
|
return tb;
|
|
}
|
|
|
|
function createOverflowableToolbarWithPlacements(id, placements) {
|
|
gAddedToolbars.add(id);
|
|
|
|
let tb = document.createXULElement("toolbar");
|
|
tb.id = id;
|
|
tb.setAttribute("customizationtarget", id + "-target");
|
|
|
|
let customizationtarget = document.createXULElement("hbox");
|
|
customizationtarget.id = id + "-target";
|
|
customizationtarget.setAttribute("flex", "1");
|
|
tb.appendChild(customizationtarget);
|
|
|
|
let overflowPanel = document.createXULElement("panel");
|
|
overflowPanel.id = id + "-overflow";
|
|
document.getElementById("mainPopupSet").appendChild(overflowPanel);
|
|
|
|
let overflowList = document.createXULElement("vbox");
|
|
overflowList.id = id + "-overflow-list";
|
|
overflowPanel.appendChild(overflowList);
|
|
|
|
let chevron = document.createXULElement("toolbarbutton");
|
|
chevron.id = id + "-chevron";
|
|
tb.appendChild(chevron);
|
|
|
|
CustomizableUI.registerArea(id, {
|
|
type: CustomizableUI.TYPE_TOOLBAR,
|
|
defaultPlacements: placements,
|
|
overflowable: true,
|
|
});
|
|
|
|
tb.setAttribute("customizable", "true");
|
|
tb.setAttribute("overflowable", "true");
|
|
tb.setAttribute("default-overflowpanel", overflowPanel.id);
|
|
tb.setAttribute("default-overflowtarget", overflowList.id);
|
|
tb.setAttribute("default-overflowbutton", chevron.id);
|
|
tb.setAttribute("addon-webext-overflowbutton", "unified-extensions-button");
|
|
tb.setAttribute("addon-webext-overflowtarget", "overflowed-extensions-list");
|
|
|
|
gNavToolbox.appendChild(tb);
|
|
CustomizableUI.registerToolbarNode(tb);
|
|
return tb;
|
|
}
|
|
|
|
function removeCustomToolbars() {
|
|
CustomizableUI.reset();
|
|
for (let toolbarId of gAddedToolbars) {
|
|
CustomizableUI.unregisterArea(toolbarId, true);
|
|
let tb = document.getElementById(toolbarId);
|
|
if (tb.hasAttribute("overflowpanel")) {
|
|
let panel = document.getElementById(tb.getAttribute("overflowpanel"));
|
|
if (panel) {
|
|
panel.remove();
|
|
}
|
|
}
|
|
tb.remove();
|
|
}
|
|
gAddedToolbars.clear();
|
|
}
|
|
|
|
function resetCustomization() {
|
|
return CustomizableUI.reset();
|
|
}
|
|
|
|
function isInDevEdition() {
|
|
return AppConstants.MOZ_DEV_EDITION;
|
|
}
|
|
|
|
function removeNonReleaseButtons(areaPanelPlacements) {
|
|
if (isInDevEdition() && areaPanelPlacements.includes("developer-button")) {
|
|
areaPanelPlacements.splice(
|
|
areaPanelPlacements.indexOf("developer-button"),
|
|
1
|
|
);
|
|
}
|
|
}
|
|
|
|
function removeNonOriginalButtons() {
|
|
CustomizableUI.removeWidgetFromArea("sync-button");
|
|
}
|
|
|
|
function assertAreaPlacements(areaId, expectedPlacements) {
|
|
let actualPlacements = getAreaWidgetIds(areaId);
|
|
placementArraysEqual(areaId, actualPlacements, expectedPlacements);
|
|
}
|
|
|
|
function placementArraysEqual(areaId, actualPlacements, expectedPlacements) {
|
|
info("Actual placements: " + actualPlacements.join(", "));
|
|
info("Expected placements: " + expectedPlacements.join(", "));
|
|
is(
|
|
actualPlacements.length,
|
|
expectedPlacements.length,
|
|
"Area " + areaId + " should have " + expectedPlacements.length + " items."
|
|
);
|
|
let minItems = Math.min(expectedPlacements.length, actualPlacements.length);
|
|
for (let i = 0; i < minItems; i++) {
|
|
if (typeof expectedPlacements[i] == "string") {
|
|
is(
|
|
actualPlacements[i],
|
|
expectedPlacements[i],
|
|
"Item " + i + " in " + areaId + " should match expectations."
|
|
);
|
|
} else if (expectedPlacements[i] instanceof RegExp) {
|
|
ok(
|
|
expectedPlacements[i].test(actualPlacements[i]),
|
|
"Item " +
|
|
i +
|
|
" (" +
|
|
actualPlacements[i] +
|
|
") in " +
|
|
areaId +
|
|
" should match " +
|
|
expectedPlacements[i]
|
|
);
|
|
} else {
|
|
ok(
|
|
false,
|
|
"Unknown type of expected placement passed to " +
|
|
" assertAreaPlacements. Is your test broken?"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function todoAssertAreaPlacements(areaId, expectedPlacements) {
|
|
let actualPlacements = getAreaWidgetIds(areaId);
|
|
let isPassing = actualPlacements.length == expectedPlacements.length;
|
|
let minItems = Math.min(expectedPlacements.length, actualPlacements.length);
|
|
for (let i = 0; i < minItems; i++) {
|
|
if (typeof expectedPlacements[i] == "string") {
|
|
isPassing = isPassing && actualPlacements[i] == expectedPlacements[i];
|
|
} else if (expectedPlacements[i] instanceof RegExp) {
|
|
isPassing = isPassing && expectedPlacements[i].test(actualPlacements[i]);
|
|
} else {
|
|
ok(
|
|
false,
|
|
"Unknown type of expected placement passed to " +
|
|
" assertAreaPlacements. Is your test broken?"
|
|
);
|
|
}
|
|
}
|
|
todo(
|
|
isPassing,
|
|
"The area placements for " +
|
|
areaId +
|
|
" should equal the expected placements."
|
|
);
|
|
}
|
|
|
|
function getAreaWidgetIds(areaId) {
|
|
return CustomizableUI.getWidgetIdsInArea(areaId);
|
|
}
|
|
|
|
function simulateItemDrag(aToDrag, aTarget, aEvent = {}, aOffset = 2) {
|
|
let ev = aEvent;
|
|
if (ev == "end" || ev == "start") {
|
|
let win = aTarget.ownerGlobal;
|
|
const dwu = win.windowUtils;
|
|
let bounds = dwu.getBoundsWithoutFlushing(aTarget);
|
|
if (ev == "end") {
|
|
ev = {
|
|
clientX: bounds.right - aOffset,
|
|
clientY: bounds.bottom - aOffset,
|
|
};
|
|
} else {
|
|
ev = { clientX: bounds.left + aOffset, clientY: bounds.top + aOffset };
|
|
}
|
|
}
|
|
ev._domDispatchOnly = true;
|
|
synthesizeDrop(
|
|
aToDrag.parentNode,
|
|
aTarget,
|
|
null,
|
|
null,
|
|
aToDrag.ownerGlobal,
|
|
aTarget.ownerGlobal,
|
|
ev
|
|
);
|
|
// Ensure dnd suppression is cleared.
|
|
synthesizeMouseAtCenter(aTarget, { type: "mouseup" }, aTarget.ownerGlobal);
|
|
}
|
|
|
|
function endCustomizing(aWindow = window) {
|
|
if (aWindow.document.documentElement.getAttribute("customizing") != "true") {
|
|
return true;
|
|
}
|
|
let afterCustomizationPromise = BrowserTestUtils.waitForEvent(
|
|
aWindow.gNavToolbox,
|
|
"aftercustomization"
|
|
);
|
|
aWindow.gCustomizeMode.exit();
|
|
return afterCustomizationPromise;
|
|
}
|
|
|
|
function startCustomizing(aWindow = window) {
|
|
if (aWindow.document.documentElement.getAttribute("customizing") == "true") {
|
|
return null;
|
|
}
|
|
let customizationReadyPromise = BrowserTestUtils.waitForEvent(
|
|
aWindow.gNavToolbox,
|
|
"customizationready"
|
|
);
|
|
aWindow.gCustomizeMode.enter();
|
|
return customizationReadyPromise;
|
|
}
|
|
|
|
function promiseObserverNotified(aTopic) {
|
|
return new Promise(resolve => {
|
|
Services.obs.addObserver(function onNotification(subject, topic, data) {
|
|
Services.obs.removeObserver(onNotification, topic);
|
|
resolve({ subject, data });
|
|
}, aTopic);
|
|
});
|
|
}
|
|
|
|
function openAndLoadWindow(aOptions, aWaitForDelayedStartup = false) {
|
|
return new Promise(resolve => {
|
|
let win = OpenBrowserWindow(aOptions);
|
|
if (aWaitForDelayedStartup) {
|
|
Services.obs.addObserver(function onDS(aSubject, aTopic, aData) {
|
|
if (aSubject != win) {
|
|
return;
|
|
}
|
|
Services.obs.removeObserver(onDS, "browser-delayed-startup-finished");
|
|
resolve(win);
|
|
}, "browser-delayed-startup-finished");
|
|
} else {
|
|
win.addEventListener(
|
|
"load",
|
|
function () {
|
|
resolve(win);
|
|
},
|
|
{ once: true }
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
function promiseWindowClosed(win) {
|
|
return new Promise(resolve => {
|
|
win.addEventListener(
|
|
"unload",
|
|
function () {
|
|
resolve();
|
|
},
|
|
{ once: true }
|
|
);
|
|
win.close();
|
|
});
|
|
}
|
|
|
|
function promiseOverflowShown(win) {
|
|
let panelEl = win.document.getElementById("widget-overflow");
|
|
return promisePanelElementShown(win, panelEl);
|
|
}
|
|
|
|
function promisePanelElementShown(win, aPanel) {
|
|
return new Promise((resolve, reject) => {
|
|
let timeoutId = win.setTimeout(() => {
|
|
reject("Panel did not show within 20 seconds.");
|
|
}, 20000);
|
|
function onPanelOpen(e) {
|
|
aPanel.removeEventListener("popupshown", onPanelOpen);
|
|
win.clearTimeout(timeoutId);
|
|
resolve();
|
|
}
|
|
aPanel.addEventListener("popupshown", onPanelOpen);
|
|
});
|
|
}
|
|
|
|
function promiseOverflowHidden(win) {
|
|
let panelEl = win.PanelUI.overflowPanel;
|
|
return promisePanelElementHidden(win, panelEl);
|
|
}
|
|
|
|
function promisePanelElementHidden(win, aPanel) {
|
|
return new Promise((resolve, reject) => {
|
|
let timeoutId = win.setTimeout(() => {
|
|
reject("Panel did not hide within 20 seconds.");
|
|
}, 20000);
|
|
function onPanelClose(e) {
|
|
aPanel.removeEventListener("popuphidden", onPanelClose);
|
|
win.clearTimeout(timeoutId);
|
|
executeSoon(resolve);
|
|
}
|
|
aPanel.addEventListener("popuphidden", onPanelClose);
|
|
});
|
|
}
|
|
|
|
function isPanelUIOpen() {
|
|
return PanelUI.panel.state == "open" || PanelUI.panel.state == "showing";
|
|
}
|
|
|
|
function isOverflowOpen() {
|
|
let panel = document.getElementById("widget-overflow");
|
|
return panel.state == "open" || panel.state == "showing";
|
|
}
|
|
|
|
function subviewShown(aSubview) {
|
|
return new Promise((resolve, reject) => {
|
|
let win = aSubview.ownerGlobal;
|
|
let timeoutId = win.setTimeout(() => {
|
|
reject("Subview (" + aSubview.id + ") did not show within 20 seconds.");
|
|
}, 20000);
|
|
function onViewShown(e) {
|
|
aSubview.removeEventListener("ViewShown", onViewShown);
|
|
win.clearTimeout(timeoutId);
|
|
resolve();
|
|
}
|
|
aSubview.addEventListener("ViewShown", onViewShown);
|
|
});
|
|
}
|
|
|
|
function subviewHidden(aSubview) {
|
|
return new Promise((resolve, reject) => {
|
|
let win = aSubview.ownerGlobal;
|
|
let timeoutId = win.setTimeout(() => {
|
|
reject("Subview (" + aSubview.id + ") did not hide within 20 seconds.");
|
|
}, 20000);
|
|
function onViewHiding(e) {
|
|
aSubview.removeEventListener("ViewHiding", onViewHiding);
|
|
win.clearTimeout(timeoutId);
|
|
resolve();
|
|
}
|
|
aSubview.addEventListener("ViewHiding", onViewHiding);
|
|
});
|
|
}
|
|
|
|
function waitFor(aTimeout = 100) {
|
|
return new Promise(resolve => {
|
|
setTimeout(() => resolve(), aTimeout);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Starts a load in an existing tab and waits for it to finish (via some event).
|
|
*
|
|
* @param aTab The tab to load into.
|
|
* @param aUrl The url to load.
|
|
* @param aEventType The load event type to wait for. Defaults to "load".
|
|
* @return {Promise} resolved when the event is handled.
|
|
*/
|
|
function promiseTabLoadEvent(aTab, aURL) {
|
|
let browser = aTab.linkedBrowser;
|
|
|
|
BrowserTestUtils.startLoadingURIString(browser, aURL);
|
|
return BrowserTestUtils.browserLoaded(browser);
|
|
}
|
|
|
|
/**
|
|
* Wait for an attribute on a node to change
|
|
*
|
|
* @param aNode Node on which the mutation is expected
|
|
* @param aAttribute The attribute we're interested in
|
|
* @param aFilterFn A function to check if the new value is what we want.
|
|
* @return {Promise} resolved when the requisite mutation shows up.
|
|
*/
|
|
function promiseAttributeMutation(aNode, aAttribute, aFilterFn) {
|
|
return new Promise((resolve, reject) => {
|
|
info("waiting for mutation of attribute '" + aAttribute + "'.");
|
|
let obs = new MutationObserver(mutations => {
|
|
for (let mut of mutations) {
|
|
let attr = mut.attributeName;
|
|
let newValue = mut.target.getAttribute(attr);
|
|
if (aFilterFn(newValue)) {
|
|
ok(
|
|
true,
|
|
"mutation occurred: attribute '" +
|
|
attr +
|
|
"' changed to '" +
|
|
newValue +
|
|
"' from '" +
|
|
mut.oldValue +
|
|
"'."
|
|
);
|
|
obs.disconnect();
|
|
resolve();
|
|
} else {
|
|
info(
|
|
"Ignoring mutation that produced value " +
|
|
newValue +
|
|
" because of filter."
|
|
);
|
|
}
|
|
}
|
|
});
|
|
obs.observe(aNode, { attributeFilter: [aAttribute] });
|
|
});
|
|
}
|
|
|
|
function popupShown(aPopup) {
|
|
return BrowserTestUtils.waitForPopupEvent(aPopup, "shown");
|
|
}
|
|
|
|
function popupHidden(aPopup) {
|
|
return BrowserTestUtils.waitForPopupEvent(aPopup, "hidden");
|
|
}
|
|
|
|
// This is a simpler version of the context menu check that
|
|
// exists in contextmenu_common.js.
|
|
function checkContextMenu(aContextMenu, aExpectedEntries, aWindow = window) {
|
|
let children = [...aContextMenu.children];
|
|
// Ignore hidden nodes:
|
|
children = children.filter(n => !n.hidden);
|
|
|
|
for (let i = 0; i < children.length; i++) {
|
|
let menuitem = children[i];
|
|
try {
|
|
if (aExpectedEntries[i][0] == "---") {
|
|
is(menuitem.localName, "menuseparator", "menuseparator expected");
|
|
continue;
|
|
}
|
|
|
|
let selector = aExpectedEntries[i][0];
|
|
ok(
|
|
menuitem.matches(selector),
|
|
"menuitem should match " + selector + " selector"
|
|
);
|
|
let commandValue = menuitem.getAttribute("command");
|
|
let relatedCommand = commandValue
|
|
? aWindow.document.getElementById(commandValue)
|
|
: null;
|
|
let menuItemDisabled = relatedCommand
|
|
? relatedCommand.getAttribute("disabled") == "true"
|
|
: menuitem.getAttribute("disabled") == "true";
|
|
is(
|
|
menuItemDisabled,
|
|
!aExpectedEntries[i][1],
|
|
"disabled state for " + selector
|
|
);
|
|
} catch (e) {
|
|
ok(false, "Exception when checking context menu: " + e);
|
|
}
|
|
}
|
|
}
|
|
|
|
function waitForOverflowButtonShown(win = window) {
|
|
info("Waiting for overflow button to show");
|
|
let ov = win.document.getElementById("nav-bar-overflow-button");
|
|
return waitForElementShown(ov.icon);
|
|
}
|
|
function waitForElementShown(element) {
|
|
return BrowserTestUtils.waitForCondition(() => {
|
|
info("Checking if element has non-0 size");
|
|
// We intentionally flush layout to ensure the element is actually shown.
|
|
let rect = element.getBoundingClientRect();
|
|
return rect.width > 0 && rect.height > 0;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Opens the history panel through the history toolbarbutton in the
|
|
* navbar and returns a promise that resolves as soon as the panel is open
|
|
* is showing.
|
|
*/
|
|
async function openHistoryPanel(doc = document) {
|
|
await waitForOverflowButtonShown();
|
|
await doc.getElementById("nav-bar").overflowable.show();
|
|
info("Menu panel was opened");
|
|
|
|
let historyButton = doc.getElementById("history-panelmenu");
|
|
Assert.ok(historyButton, "History button appears in Panel Menu");
|
|
|
|
historyButton.click();
|
|
|
|
let historyPanel = doc.getElementById("PanelUI-history");
|
|
return BrowserTestUtils.waitForEvent(historyPanel, "ViewShown");
|
|
}
|
|
|
|
/**
|
|
* Closes the history panel and returns a promise that resolves as sooon
|
|
* as the panel is closed.
|
|
*/
|
|
async function hideHistoryPanel(doc = document) {
|
|
let historyView = doc.getElementById("PanelUI-history");
|
|
let historyPanel = historyView.closest("panel");
|
|
let promise = BrowserTestUtils.waitForEvent(historyPanel, "popuphidden");
|
|
historyPanel.hidePopup();
|
|
return promise;
|
|
}
|