529 lines
16 KiB
JavaScript
529 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",
|
|
});
|
|
|
|
/**
|
|
* 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.hasAttribute("customizing")) {
|
|
return true;
|
|
}
|
|
let afterCustomizationPromise = BrowserTestUtils.waitForEvent(
|
|
aWindow.gNavToolbox,
|
|
"aftercustomization"
|
|
);
|
|
aWindow.gCustomizeMode.exit();
|
|
return afterCustomizationPromise;
|
|
}
|
|
|
|
function startCustomizing(aWindow = window) {
|
|
if (aWindow.document.documentElement.hasAttribute("customizing")) {
|
|
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) {
|
|
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() {
|
|
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() {
|
|
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() {
|
|
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() {
|
|
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 => {
|
|
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;
|
|
}
|