fune/browser/components/customizableui/test/head.js
James Teh f606e110b6 Bug 1692110 part 3: Don't run accessibility checks for click events generated by drag/drop. r=places-reviewers,ayeddi,Gijs,Standard8
Drag/drop generates click events, but these aren't actually a click event intended to activate a control.
Therefore, we don't want to run accessibility checks for the target.
To achieve this, EventUtils.synthesizeDropAfterDragOver instructs AccessibilityUtils to ignore click events while the event is being sent.
This requires that EventUtils has the reference to AccessibilityUtils set by the browser test harness.
As such, some tests had to be adjusted so that they use EventUtils from the browser test scope, rather than loading their own copy of EventUtils.

Depends on D190004

Differential Revision: https://phabricator.services.mozilla.com/D185776
2023-10-09 18:20:12 +00:00

530 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.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;
}