diff --git a/src/browser/base/content/browser-box-inc-xhtml.patch b/src/browser/base/content/browser-box-inc-xhtml.patch
index 7dfb9ea6f..c08f89aeb 100644
--- a/src/browser/base/content/browser-box-inc-xhtml.patch
+++ b/src/browser/base/content/browser-box-inc-xhtml.patch
@@ -1,8 +1,8 @@
diff --git a/browser/base/content/browser-box.inc.xhtml b/browser/base/content/browser-box.inc.xhtml
-index afa7f8e7dd74173bf2c696bd96f7e86e8b0126bc..4847c24923f673e91eb7fb65ea6b037f38062405 100644
+index 2faed30e09511c381051bc40910a883d1d7bc10d..6ba2d0d91235ed33e4b4bad281c974b5960beaa2 100644
--- a/browser/base/content/browser-box.inc.xhtml
+++ b/browser/base/content/browser-box.inc.xhtml
-@@ -25,7 +25,15 @@
+@@ -25,7 +25,13 @@
@@ -15,6 +15,10 @@ index afa7f8e7dd74173bf2c696bd96f7e86e8b0126bc..4847c24923f673e91eb7fb65ea6b037f
+#include zen-tabbrowser-elements.inc.xhtml
+
+@@ -34,3 +40,5 @@
+
+
+
+
diff --git a/src/browser/base/content/browser-fullScreenAndPointerLock-js.patch b/src/browser/base/content/browser-fullScreenAndPointerLock-js.patch
index 0a56038db..26368038d 100644
--- a/src/browser/base/content/browser-fullScreenAndPointerLock-js.patch
+++ b/src/browser/base/content/browser-fullScreenAndPointerLock-js.patch
@@ -1,19 +1,13 @@
diff --git a/browser/base/content/browser-fullScreenAndPointerLock.js b/browser/base/content/browser-fullScreenAndPointerLock.js
-index f459f3ca4048c29a32832b46ba87de8552a0ab8a..d317676e6d12d756ddf5dfbffe73050de07c2375 100644
+index 0e1aa344c084abdce40058278b12a042a4510997..50c06aaab823cb8572eee2ad53a7b709b1cfd084 100644
--- a/browser/base/content/browser-fullScreenAndPointerLock.js
+++ b/browser/base/content/browser-fullScreenAndPointerLock.js
-@@ -423,10 +423,10 @@ var FullScreen = {
+@@ -425,8 +425,6 @@ var FullScreen = {
gNavToolbox.classList.toggle("fullscreen-with-menubar", shiftSize > 0);
let transform = shiftSize > 0 ? `translateY(${shiftSize}px)` : "";
- gNavToolbox.style.transform = transform;
-- gURLBar.textbox.style.transform = gURLBar.textbox.hasAttribute("breakout")
-- ? transform
-- : "";
-+ //gNavToolbox.style.transform = transform;
-+ //gURLBar.textbox.style.transform = gURLBar.textbox.hasAttribute("breakout")
-+ // ? transform
-+ // : "";
+- gURLBar.style.transform = gURLBar.hasAttribute("breakout") ? transform : "";
if (shiftSize > 0) {
// If the mouse tracking missed our fullScreenToggler, then the toolbox
// might not have been shown before the menubar is animated down. Make
diff --git a/src/browser/base/content/navigator-toolbox-js.patch b/src/browser/base/content/navigator-toolbox-js.patch
index 19790013b..e29a351e4 100644
--- a/src/browser/base/content/navigator-toolbox-js.patch
+++ b/src/browser/base/content/navigator-toolbox-js.patch
@@ -1,5 +1,5 @@
diff --git a/browser/base/content/navigator-toolbox.js b/browser/base/content/navigator-toolbox.js
-index 7b776b15d52367a008ce6bf53dcfcbbe007b7453..da23f716c753f5a43f17bb5ed7a3d335891168c2 100644
+index 15469e9d9b91c1eaef2578c9e43b6999edac3392..553402b41bc15f7cd99bf87c54416dc66d7c03e7 100644
--- a/browser/base/content/navigator-toolbox.js
+++ b/browser/base/content/navigator-toolbox.js
@@ -6,7 +6,7 @@
@@ -11,9 +11,9 @@ index 7b776b15d52367a008ce6bf53dcfcbbe007b7453..da23f716c753f5a43f17bb5ed7a3d335
const widgetOverflow = document.getElementById("widget-overflow");
function onPopupShowing(event) {
-@@ -185,6 +185,7 @@ document.addEventListener(
+@@ -184,6 +184,7 @@ document.addEventListener(
+ #forward-button,
#reload-button ,
- #urlbar-go-button,
#reader-mode-button,
+ #zen-tabs-wrapper,
#picture-in-picture-button,
@@ -27,15 +27,15 @@ index 7b776b15d52367a008ce6bf53dcfcbbe007b7453..da23f716c753f5a43f17bb5ed7a3d335
gBrowser.handleNewTabMiddleClick(element, event);
break;
-@@ -316,6 +318,7 @@ document.addEventListener(
+@@ -318,6 +320,7 @@ document.addEventListener(
#downloads-button,
#fxa-toolbar-menu-button,
#unified-extensions-button,
+ #zen-site-data-icon-button,
- #library-button
+ #library-button,
+ #split-view-button
`);
- if (!element) {
-@@ -398,6 +401,16 @@ document.addEventListener(
+@@ -401,6 +404,16 @@ document.addEventListener(
gUnifiedExtensions.togglePanel(event);
break;
diff --git a/src/browser/components/aboutlogins/content/components/login-list-css.patch b/src/browser/components/aboutlogins/content/components/login-list-css.patch
deleted file mode 100644
index 66be640df..000000000
--- a/src/browser/components/aboutlogins/content/components/login-list-css.patch
+++ /dev/null
@@ -1,22 +0,0 @@
-diff --git a/browser/components/aboutlogins/content/components/login-list.css b/browser/components/aboutlogins/content/components/login-list.css
-index e8d2077ca425d9a2b924aa6ac6ba8ada36b76615..95a8ab3dcff832f16068b6739526e8e29f0b4e26 100644
---- a/browser/components/aboutlogins/content/components/login-list.css
-+++ b/browser/components/aboutlogins/content/components/login-list.css
-@@ -3,7 +3,7 @@
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
- :host {
-- border-inline-end: 1px solid var(--in-content-border-color);
-+ border: 1px solid var(--in-content-border-color);
- background-color: var(--background-color-box);
- display: flex;
- flex-direction: column;
-@@ -162,3 +162,8 @@ ol {
- font-size: 0.85em;
- color: var(--text-color-deemphasized);
- }
-+
-+:host {
-+ border-radius: 5px;
-+ margin: 10px;
-+}
diff --git a/src/browser/components/preferences/jar-mn.patch b/src/browser/components/preferences/jar-mn.patch
index ebc62b7f3..12b5e864d 100644
--- a/src/browser/components/preferences/jar-mn.patch
+++ b/src/browser/components/preferences/jar-mn.patch
@@ -1,11 +1,10 @@
diff --git a/browser/components/preferences/jar.mn b/browser/components/preferences/jar.mn
-index 7c5f6e9053f119a790f83a68c75ee11a6c172d98..cb0e1386318e8a8df24dfbbf8430d0a09fe07d16 100644
+index 5e53352fdbd25d2aed40d0117c3ad7072b9acf8c..0f400864ba7cc850d212ddeeefb71a06376e845f 100644
--- a/browser/components/preferences/jar.mn
+++ b/browser/components/preferences/jar.mn
-@@ -37,3 +37,5 @@ browser.jar:
- content/browser/preferences/widgets/security-privacy-card.mjs (widgets/security-privacy/security-privacy-card/security-privacy-card.mjs)
- content/browser/preferences/widgets/security-privacy-card.css (widgets/security-privacy/security-privacy-card/security-privacy-card.css)
+@@ -42,3 +42,5 @@ browser.jar:
content/browser/preferences/widgets/sync-device-name.mjs (widgets/sync-device-name/sync-device-name.mjs)
+ content/browser/preferences/widgets/sync-engines-list.mjs (widgets/sync-engine-list/sync-engines-list.mjs)
+ content/browser/preferences/widgets/sync-engines-list.css (widgets/sync-engine-list/sync-engines-list.css)
+
+ content/browser/preferences/zen-settings.js
-\ No newline at end of file
diff --git a/src/browser/components/preferences/preferences-js.patch b/src/browser/components/preferences/preferences-js.patch
index bf1d1d963..a0fc76361 100644
--- a/src/browser/components/preferences/preferences-js.patch
+++ b/src/browser/components/preferences/preferences-js.patch
@@ -1,8 +1,8 @@
diff --git a/browser/components/preferences/preferences.js b/browser/components/preferences/preferences.js
-index a0e16b375f0ad7ef58bf68d7574453ef37d4557f..2d5b5a0715b5ed8ebe592eebea0219a6cde17b52 100644
+index 7d6835b8636f771943b8081b7d56f579812026c2..97227eec4baeffd39b23ed6194ce7fa108c3b862 100644
--- a/browser/components/preferences/preferences.js
+++ b/browser/components/preferences/preferences.js
-@@ -118,6 +118,7 @@ ChromeUtils.defineLazyGetter(this, "gSubDialog", function () {
+@@ -124,6 +124,7 @@ ChromeUtils.defineLazyGetter(this, "gSubDialog", function () {
styleSheets: [
"chrome://browser/skin/preferences/dialog.css",
"chrome://browser/skin/preferences/preferences.css",
@@ -10,7 +10,7 @@ index a0e16b375f0ad7ef58bf68d7574453ef37d4557f..2d5b5a0715b5ed8ebe592eebea0219a6
],
resizeCallback: async ({ title, frame }) => {
// Search within main document and highlight matched keyword.
-@@ -225,6 +226,10 @@ function init_all() {
+@@ -344,6 +345,10 @@ function init_all() {
register_module("paneSearch", gSearchPane);
register_module("panePrivacy", gPrivacyPane);
register_module("paneContainers", gContainersPane);
@@ -19,5 +19,5 @@ index a0e16b375f0ad7ef58bf68d7574453ef37d4557f..2d5b5a0715b5ed8ebe592eebea0219a6
+ register_module("paneZenCKS", gZenCKSSettings);
+ register_module("paneZenMarketplace", gZenMarketplaceManager);
- for (let [subPane, config] of Object.entries(CONFIG_PANES)) {
- subPane = friendlyPrefCategoryNameToInternalName(subPane);
+ for (let [id, config] of Object.entries(CONFIG_PANES)) {
+ SettingPaneManager.registerPane(id, config);
diff --git a/src/browser/components/preferences/preferences-xhtml.patch b/src/browser/components/preferences/preferences-xhtml.patch
index d696a22d5..71f3de067 100644
--- a/src/browser/components/preferences/preferences-xhtml.patch
+++ b/src/browser/components/preferences/preferences-xhtml.patch
@@ -1,9 +1,9 @@
diff --git a/browser/components/preferences/preferences.xhtml b/browser/components/preferences/preferences.xhtml
-index 993554d64efa6ce39fbcc01d4676a69199e158ff..e69aec388162997ef7cb93481cfedb9d102b4fe0 100644
+index a1aec823d6266d69ff02aa8523d2da999a0871b9..559f2ea12e53dd19f24e7953db4388456b97f02c 100644
--- a/browser/components/preferences/preferences.xhtml
+++ b/browser/components/preferences/preferences.xhtml
-@@ -45,6 +45,8 @@
-
+@@ -46,6 +46,8 @@
+
+#include zen-preferences-links.xhtml
@@ -11,7 +11,7 @@ index 993554d64efa6ce39fbcc01d4676a69199e158ff..e69aec388162997ef7cb93481cfedb9d
-@@ -113,6 +115,11 @@
+@@ -115,6 +117,11 @@
@@ -23,7 +23,7 @@ index 993554d64efa6ce39fbcc01d4676a69199e158ff..e69aec388162997ef7cb93481cfedb9d
@@ -74,7 +74,7 @@ index 993554d64efa6ce39fbcc01d4676a69199e158ff..e69aec388162997ef7cb93481cfedb9d
@@ -86,7 +86,7 @@ index 993554d64efa6ce39fbcc01d4676a69199e158ff..e69aec388162997ef7cb93481cfedb9d
-@@ -254,6 +300,10 @@
+@@ -260,6 +306,10 @@
#include sync.inc.xhtml
#include experimental.inc.xhtml
#include moreFromMozilla.inc.xhtml
diff --git a/src/browser/components/sidebar/browser-sidebar-js.patch b/src/browser/components/sidebar/browser-sidebar-js.patch
index 215f98bae..29188602e 100644
--- a/src/browser/components/sidebar/browser-sidebar-js.patch
+++ b/src/browser/components/sidebar/browser-sidebar-js.patch
@@ -1,5 +1,5 @@
diff --git a/browser/components/sidebar/browser-sidebar.js b/browser/components/sidebar/browser-sidebar.js
-index 220c3c83fa09daaec433803a4655a20114a137a6..1ef8adc3ecfa61839bb993f7fc090133bffe77db 100644
+index bd6db85732e6e99ca51ce9c4c1a7d469199cf8cb..6c7859c1014baf4e343714ef797cf558d065e189 100644
--- a/browser/components/sidebar/browser-sidebar.js
+++ b/browser/components/sidebar/browser-sidebar.js
@@ -793,7 +793,7 @@ var SidebarController = {
@@ -8,6 +8,6 @@ index 220c3c83fa09daaec433803a4655a20114a137a6..1ef8adc3ecfa61839bb993f7fc090133
let contentArea = document.getElementById("tabbrowser-tabbox");
- let browser = document.getElementById("browser");
+ let browser = document.getElementById("tabbrowser-tabbox");
- [...browser.children].forEach((node, i) => {
- node.style.order = i + 1;
+ [...browser.children].forEach((node, i, children) => {
+ node.style.order = this._positionStart ? i + 1 : children.length - i;
});
diff --git a/src/browser/components/tabbrowser/content/drag-and-drop-js.patch b/src/browser/components/tabbrowser/content/drag-and-drop-js.patch
index 6b34e716e..7ca6b9dde 100644
--- a/src/browser/components/tabbrowser/content/drag-and-drop-js.patch
+++ b/src/browser/components/tabbrowser/content/drag-and-drop-js.patch
@@ -1,18 +1,18 @@
diff --git a/browser/components/tabbrowser/content/drag-and-drop.js b/browser/components/tabbrowser/content/drag-and-drop.js
-index 97b931c3c7385a52d20204369fcf6d6999053687..bd64a97046d2a62e842cc922764ad129da99905f 100644
+index 57800333445ec7850742145527e04ae8d504b0bb..3a5015c391c594c94aa77a7ff9b08762d36276f6 100644
--- a/browser/components/tabbrowser/content/drag-and-drop.js
+++ b/browser/components/tabbrowser/content/drag-and-drop.js
-@@ -32,6 +32,9 @@
+@@ -35,6 +35,9 @@
* @returns {MozTabbrowserTab|vbox}
*/
const elementToMove = element => {
+ if (element.group?.hasAttribute("split-view-group")) {
+ return element.group;
+ }
- if (isTab(element)) {
+ if (isTab(element) || isSplitViewWrapper(element)) {
return element;
}
-@@ -112,6 +115,9 @@
+@@ -115,6 +118,9 @@
}
let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
@@ -22,7 +22,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bd64a97046d2a62e842cc922764ad129
if (
(dropEffect == "move" || dropEffect == "copy") &&
document == draggedTab.ownerDocument &&
-@@ -130,10 +136,6 @@
+@@ -133,10 +139,6 @@
// Pinned tabs in expanded vertical mode are on a grid format and require
// different logic to drag and drop.
@@ -33,7 +33,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bd64a97046d2a62e842cc922764ad129
this._animateTabMove(event);
return;
}
-@@ -266,6 +268,15 @@
+@@ -269,6 +271,15 @@
this._tabDropIndicator.hidden = true;
event.stopPropagation();
@@ -49,7 +49,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bd64a97046d2a62e842cc922764ad129
if (draggedTab && dropEffect == "copy") {
let duplicatedDraggedTab;
let duplicatedTabs = [];
-@@ -291,8 +302,9 @@
+@@ -294,8 +305,9 @@
let translateOffsetY = oldTranslateY % tabHeight;
let newTranslateX = oldTranslateX - translateOffsetX;
let newTranslateY = oldTranslateY - translateOffsetY;
@@ -61,10 +61,10 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bd64a97046d2a62e842cc922764ad129
if (this._isContainerVerticalPinnedGrid(draggedTab)) {
// Update both translate axis for pinned vertical expanded tabs
-@@ -308,8 +320,8 @@
+@@ -311,8 +323,8 @@
}
} else {
- let tabs = this._tabbrowserTabs.ariaFocusableItems.slice(
+ let tabs = this._tabbrowserTabs.dragAndDropElements.slice(
- isPinned ? 0 : numPinned,
- isPinned ? numPinned : undefined
+ isPinned ? (essential ? 0 : gBrowser._numZenEssentials) : numPinned,
@@ -72,7 +72,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bd64a97046d2a62e842cc922764ad129
);
let size = this._tabbrowserTabs.verticalMode ? "height" : "width";
let screenAxis = this._tabbrowserTabs.verticalMode
-@@ -362,11 +374,13 @@
+@@ -365,11 +377,13 @@
this._dragToPinPromoCard,
];
let shouldPin =
@@ -86,7 +86,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bd64a97046d2a62e842cc922764ad129
isTab(draggedTab) &&
draggedTab.pinned &&
this._tabbrowserTabs.arrowScrollbox.contains(event.target);
-@@ -384,6 +398,7 @@
+@@ -387,6 +401,7 @@
(oldTranslateY && oldTranslateY != newTranslateY);
} else if (this._tabbrowserTabs.verticalMode) {
shouldTranslate &&= oldTranslateY && oldTranslateY != newTranslateY;
@@ -94,7 +94,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bd64a97046d2a62e842cc922764ad129
} else {
shouldTranslate &&= oldTranslateX && oldTranslateX != newTranslateX;
}
-@@ -440,7 +455,7 @@
+@@ -443,7 +458,7 @@
item.removeAttribute("tabdrop-samewindow");
resolve();
};
@@ -103,15 +103,15 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bd64a97046d2a62e842cc922764ad129
postTransitionCleanup();
} else {
let onTransitionEnd = transitionendEvent => {
-@@ -581,6 +596,7 @@
+@@ -584,6 +599,7 @@
- let nextItem = this._tabbrowserTabs.ariaFocusableItems[newIndex];
+ let nextItem = this._tabbrowserTabs.dragAndDropElements[newIndex];
let tabGroup = isTab(nextItem) && nextItem.group;
+ if (gZenViewSplitter.handleTabDrop(event, urls, replace, inBackground)) return;
gBrowser.loadTabs(urls, {
inBackground,
replace,
-@@ -618,7 +634,16 @@
+@@ -621,7 +637,16 @@
this._expandGroupOnDrop(draggedTab);
}
this._resetTabsAfterDrop(draggedTab.ownerDocument);
@@ -129,7 +129,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bd64a97046d2a62e842cc922764ad129
if (
dt.mozUserCancelled ||
dt.dropEffect != "none" ||
-@@ -822,7 +847,10 @@
+@@ -825,7 +850,10 @@
_getDragTarget(event, { ignoreSides = false } = {}) {
let { target } = event;
while (target) {
@@ -141,7 +141,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bd64a97046d2a62e842cc922764ad129
break;
}
target = target.parentNode;
-@@ -839,14 +867,17 @@
+@@ -842,14 +870,17 @@
return null;
}
}
@@ -161,7 +161,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bd64a97046d2a62e842cc922764ad129
!this._tabbrowserTabs.expandOnHover
);
}
-@@ -877,7 +908,8 @@
+@@ -880,7 +911,8 @@
isTabGroupLabel(draggedTab) &&
draggedTab._dragData?.expandGroupOnDrop
) {
@@ -171,7 +171,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bd64a97046d2a62e842cc922764ad129
}
}
-@@ -1055,7 +1087,6 @@
+@@ -1058,7 +1090,6 @@
// using updateDragImage. On Linux, we can use a panel.
if (platform == "win" || platform == "macosx") {
captureListener = function () {
@@ -179,7 +179,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bd64a97046d2a62e842cc922764ad129
};
} else {
// Create a panel to use it in setDragImage
-@@ -1093,7 +1124,6 @@
+@@ -1096,7 +1127,6 @@
);
dragImageOffset = dragImageOffset * scale;
}
@@ -187,7 +187,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bd64a97046d2a62e842cc922764ad129
// _dragData.offsetX/Y give the coordinates that the mouse should be
// positioned relative to the corner of the new window created upon
-@@ -1112,7 +1142,7 @@
+@@ -1115,7 +1145,7 @@
let dropEffect = this.getDropEffectForTabDrag(event);
let isMovingInTabStrip = !fromTabList && dropEffect == "move";
let collapseTabGroupDuringDrag =
@@ -196,7 +196,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bd64a97046d2a62e842cc922764ad129
tab._dragData = {
offsetX: this._tabbrowserTabs.verticalMode
-@@ -1122,7 +1152,7 @@
+@@ -1125,7 +1155,7 @@
? event.screenY - window.screenY - tabOffset
: event.screenY - window.screenY,
scrollPos:
@@ -205,7 +205,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bd64a97046d2a62e842cc922764ad129
? this._tabbrowserTabs.pinnedTabsContainer.scrollPosition
: this._tabbrowserTabs.arrowScrollbox.scrollPosition,
screenX: event.screenX,
-@@ -1149,6 +1179,7 @@
+@@ -1152,6 +1182,7 @@
if (collapseTabGroupDuringDrag) {
tab.group.collapsed = true;
@@ -213,31 +213,31 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bd64a97046d2a62e842cc922764ad129
}
}
}
-@@ -1173,6 +1204,7 @@
+@@ -1176,6 +1207,7 @@
if (tabStripItemElement.hasAttribute("dragtarget")) {
return;
}
+ return;
let isPinned = tab.pinned;
let numPinned = gBrowser.pinnedTabCount;
- let allTabs = this._tabbrowserTabs.ariaFocusableItems;
-@@ -1598,7 +1630,6 @@
+ let dragAndDropElements = this._tabbrowserTabs.dragAndDropElements;
+@@ -1601,7 +1633,6 @@
- for (let item of this._tabbrowserTabs.ariaFocusableItems) {
+ for (let item of this._tabbrowserTabs.dragAndDropElements) {
item = elementToMove(item);
- item.style.transform = "";
item.removeAttribute("multiselected-move-together");
delete item._moveTogetherSelectedTabsData;
}
-@@ -2426,7 +2457,6 @@
- for (let item of this._tabbrowserTabs.ariaFocusableItems) {
+@@ -2429,7 +2460,6 @@
+ for (let item of this._tabbrowserTabs.dragAndDropElements) {
this._resetGroupTarget(item);
item = elementToMove(item);
- item.style.transform = "";
}
this._tabbrowserTabs.removeAttribute("movingtab-group");
this._tabbrowserTabs.removeAttribute("movingtab-ungroup");
-@@ -2457,17 +2487,14 @@
+@@ -2460,17 +2490,14 @@
tab.style.left = "";
tab.style.top = "";
tab.style.maxWidth = "";
diff --git a/src/browser/components/tabbrowser/content/tab-js.patch b/src/browser/components/tabbrowser/content/tab-js.patch
index 4c9a66df9..134cf91e4 100644
--- a/src/browser/components/tabbrowser/content/tab-js.patch
+++ b/src/browser/components/tabbrowser/content/tab-js.patch
@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tab.js b/browser/components/tabbrowser/content/tab.js
-index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..1cc31b04e0165f7ae6ffc377da08f8d83bee7390 100644
+index 6528da245d922e2ccdb59cbf8106055bc34d1e74..1cd3395961a80e3b48582794b5b5365c9b8386b3 100644
--- a/browser/components/tabbrowser/content/tab.js
+++ b/browser/components/tabbrowser/content/tab.js
@@ -21,6 +21,7 @@
@@ -10,18 +10,19 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..1cc31b04e0165f7ae6ffc377da08f8d8
-@@ -37,8 +38,10 @@
+@@ -38,9 +39,11 @@
+
+
+
`;
-@@ -84,7 +87,7 @@
+@@ -86,7 +89,7 @@
".tab-content":
"pinned,selected=visuallyselected,multiselected,titlechanged,attention",
".tab-icon-stack":
@@ -30,7 +31,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..1cc31b04e0165f7ae6ffc377da08f8d8
".tab-throbber":
"fadein,pinned,busy,progress,selected=visuallyselected",
".tab-icon-pending":
-@@ -93,9 +96,9 @@
+@@ -95,9 +98,9 @@
"src=image,requestcontextid,fadein,pinned,selected=visuallyselected,busy,crashed,sharing,pictureinpicture,pending,discarded",
".tab-sharing-icon-overlay": "sharing,selected=visuallyselected,pinned",
".tab-icon-overlay":
@@ -42,7 +43,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..1cc31b04e0165f7ae6ffc377da08f8d8
".tab-label-container":
"pinned,selected=visuallyselected,labeldirection",
".tab-label":
-@@ -186,7 +189,7 @@
+@@ -188,7 +191,7 @@
}
set _visuallySelected(val) {
@@ -51,7 +52,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..1cc31b04e0165f7ae6ffc377da08f8d8
return;
}
-@@ -222,11 +225,21 @@
+@@ -224,11 +227,21 @@
}
get visible() {
@@ -78,7 +79,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..1cc31b04e0165f7ae6ffc377da08f8d8
}
get hidden() {
-@@ -305,7 +318,7 @@
+@@ -307,7 +320,7 @@
return false;
}
@@ -87,7 +88,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..1cc31b04e0165f7ae6ffc377da08f8d8
}
get lastAccessed() {
-@@ -382,7 +395,18 @@
+@@ -384,7 +397,18 @@
}
get group() {
@@ -107,7 +108,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..1cc31b04e0165f7ae6ffc377da08f8d8
}
get splitview() {
-@@ -473,6 +497,8 @@
+@@ -489,6 +513,8 @@
this.style.MozUserFocus = "ignore";
} else if (
event.target.classList.contains("tab-close-button") ||
@@ -116,7 +117,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..1cc31b04e0165f7ae6ffc377da08f8d8
event.target.classList.contains("tab-icon-overlay") ||
event.target.classList.contains("tab-audio-button")
) {
-@@ -527,6 +553,10 @@
+@@ -543,6 +569,10 @@
this.style.MozUserFocus = "";
}
@@ -127,7 +128,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..1cc31b04e0165f7ae6ffc377da08f8d8
on_click(event) {
if (event.button != 0) {
return;
-@@ -587,6 +617,14 @@
+@@ -603,6 +633,14 @@
// (see tabbrowser-tabs 'click' handler).
gBrowser.tabContainer._blockDblClick = true;
}
@@ -142,7 +143,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..1cc31b04e0165f7ae6ffc377da08f8d8
}
on_dblclick(event) {
-@@ -610,6 +648,8 @@
+@@ -626,6 +664,8 @@
animate: true,
triggeringEvent: event,
});
diff --git a/src/zen/tests/mochitests/moz.build b/src/zen/tests/mochitests/moz.build
new file mode 100644
index 000000000..593fada64
--- /dev/null
+++ b/src/zen/tests/mochitests/moz.build
@@ -0,0 +1,14 @@
+
+# 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/.
+
+# This file is autogenerated by scripts/import_external_tests.py
+# Do not edit manually.
+
+BROWSER_CHROME_MANIFESTS += [
+ "reportbrokensite/browser.toml",
+ "safebrowsing/browser.toml",
+ "shell/browser.toml",
+ "tooltiptext/browser.toml",
+]
\ No newline at end of file
diff --git a/src/zen/tests/mochitests/reportbrokensite/browser.toml b/src/zen/tests/mochitests/reportbrokensite/browser.toml
new file mode 100644
index 000000000..4ae01db0e
--- /dev/null
+++ b/src/zen/tests/mochitests/reportbrokensite/browser.toml
@@ -0,0 +1,57 @@
+[DEFAULT]
+tags = "report-broken-site"
+support-files = [
+ "example_report_page.html",
+ "head.js",
+ "sendMoreInfoTestEndpoint.html",
+]
+
+["browser_addon_data_sent.js"]
+disabled="Disabled by import_external_tests.py"
+support-files = [ "send_more_info.js" ]
+skip-if = ["os == 'win' && os_version == '11.26100' && processor == 'x86_64' && opt"] # Bug 1955805
+
+["browser_antitracking_data_sent.js"]
+support-files = [ "send_more_info.js" ]
+
+["browser_back_buttons.js"]
+
+["browser_error_messages.js"]
+
+["browser_experiment_data_sent.js"]
+support-files = [ "send_more_info.js" ]
+
+["browser_keyboard_navigation.js"]
+skip-if = [
+ "os == 'linux' && os_version == '24.04' && processor == 'x86_64' && tsan", # Bug 1867132
+ "os == 'linux' && os_version == '24.04' && processor == 'x86_64' && asan", # Bug 1867132
+ "os == 'linux' && os_version == '24.04' && processor == 'x86_64' && debug", # Bug 1867132
+ "os == 'win' && os_version == '11.26100' && processor == 'x86_64' && asan", # Bug 1867132
+]
+
+["browser_learn_more_link.js"]
+
+["browser_parent_menuitems.js"]
+
+["browser_prefers_contrast.js"]
+
+["browser_reason_dropdown.js"]
+
+["browser_report_send.js"]
+support-files = [ "send.js" ]
+
+["browser_send_more_info.js"]
+support-files = [
+ "send_more_info.js",
+ "../../../../toolkit/components/gfx/content/videotest.mp4",
+]
+
+["browser_tab_key_order.js"]
+
+["browser_tab_switch_handling.js"]
+
+["browser_webcompat.com_fallback.js"]
+support-files = [
+ "send_more_info.js",
+ "../../../../toolkit/components/gfx/content/videotest.mp4",
+]
diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_addon_data_sent.js b/src/zen/tests/mochitests/reportbrokensite/browser_addon_data_sent.js
new file mode 100644
index 000000000..c7fb557d1
--- /dev/null
+++ b/src/zen/tests/mochitests/reportbrokensite/browser_addon_data_sent.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests to ensure that the right data is sent for
+ * private windows and when ETP blocks content.
+ */
+
+/* import-globals-from send.js */
+/* import-globals-from send_more_info.js */
+
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+AddonTestUtils.initMochitest(this);
+
+Services.scriptloader.loadSubScript(
+ getRootDirectory(gTestPath) + "send_more_info.js",
+ this
+);
+
+add_common_setup();
+
+const TEMP_ID = "testtempaddon@tests.mozilla.org";
+const TEMP_NAME = "Temporary Addon";
+const TEMP_VERSION = "0.1.0";
+
+const PERM_ID = "testpermaddon@tests.mozilla.org";
+const PERM_NAME = "Permanent Addon";
+const PERM_VERSION = "0.2.0";
+
+const DISABLED_ID = "testdisabledaddon@tests.mozilla.org";
+const DISABLED_NAME = "Disabled Addon";
+const DISABLED_VERSION = "0.3.0";
+
+const EXPECTED_ADDONS = [
+ { id: PERM_ID, name: PERM_NAME, temporary: false, version: PERM_VERSION },
+ { id: TEMP_ID, name: TEMP_NAME, temporary: true, version: TEMP_VERSION },
+];
+
+function loadAddon(id, name, version, isTemp = false) {
+ return ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id } },
+ name,
+ version,
+ },
+ useAddonManager: isTemp ? "temporary" : "permanent",
+ });
+}
+
+async function installAddons() {
+ const temp = await loadAddon(TEMP_ID, TEMP_NAME, TEMP_VERSION, true);
+ await temp.startup();
+
+ const perm = await loadAddon(PERM_ID, PERM_NAME, PERM_VERSION);
+ await perm.startup();
+
+ const dis = await loadAddon(DISABLED_ID, DISABLED_NAME, DISABLED_VERSION);
+ await dis.startup();
+ await (await AddonManager.getAddonByID(DISABLED_ID)).disable();
+
+ return async () => {
+ await temp.unload();
+ await perm.unload();
+ await dis.unload();
+ };
+}
+
+add_task(async function testSendButton() {
+ ensureReportBrokenSitePreffedOn();
+ ensureReasonOptional();
+ const addonCleanup = await installAddons();
+
+ const tab = await openTab(REPORTABLE_PAGE_URL);
+
+ await testSend(tab, AppMenu(), {
+ addons: EXPECTED_ADDONS,
+ });
+
+ closeTab(tab);
+ await addonCleanup();
+});
+
+add_task(async function testSendingMoreInfo() {
+ ensureReportBrokenSitePreffedOn();
+ ensureSendMoreInfoEnabled();
+ const addonCleanup = await installAddons();
+
+ const tab = await openTab(REPORTABLE_PAGE_URL);
+
+ await testSendMoreInfo(tab, HelpMenu(), {
+ addons: EXPECTED_ADDONS,
+ });
+
+ closeTab(tab);
+ await addonCleanup();
+});
diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_antitracking_data_sent.js b/src/zen/tests/mochitests/reportbrokensite/browser_antitracking_data_sent.js
new file mode 100644
index 000000000..dd0b5ceaf
--- /dev/null
+++ b/src/zen/tests/mochitests/reportbrokensite/browser_antitracking_data_sent.js
@@ -0,0 +1,126 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests to ensure that the right data is sent for
+ * private windows and when ETP blocks content.
+ */
+
+/* import-globals-from send.js */
+/* import-globals-from send_more_info.js */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ getRootDirectory(gTestPath) + "send_more_info.js",
+ this
+);
+
+add_common_setup();
+
+add_task(setupStrictETP);
+
+function getEtpCategory() {
+ return Services.prefs.getStringPref(
+ "browser.contentblocking.category",
+ "standard"
+ );
+}
+
+add_task(async function testSendButton() {
+ ensureReportBrokenSitePreffedOn();
+ ensureReasonOptional();
+
+ const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ const blockedPromise = waitForContentBlockingEvent(3, win);
+ const tab = await openTab(REPORTABLE_PAGE_URL3, win);
+ await blockedPromise;
+
+ await testSend(tab, AppMenu(win), {
+ breakageCategory: "adblocker",
+ description: "another test description",
+ antitracking: {
+ blockList: "strict",
+ blockedOrigins: null,
+ isPrivateBrowsing: true,
+ hasTrackingContentBlocked: true,
+ hasMixedActiveContentBlocked: true,
+ hasMixedDisplayContentBlocked: true,
+ btpHasPurgedSite: false,
+ etpCategory: getEtpCategory(),
+ },
+ frameworks: {
+ fastclick: true,
+ marfeel: true,
+ mobify: true,
+ },
+ });
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function testSendingMoreInfo() {
+ ensureReportBrokenSitePreffedOn();
+ ensureSendMoreInfoEnabled();
+
+ const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ const blockedPromise = waitForContentBlockingEvent(3, win);
+ const tab = await openTab(REPORTABLE_PAGE_URL3, win);
+ await blockedPromise;
+
+ await testSendMoreInfo(tab, HelpMenu(win), {
+ antitracking: {
+ blockList: "strict",
+ blockedOrigins: ["https://trackertest.org"],
+ isPrivateBrowsing: true,
+ hasTrackingContentBlocked: true,
+ hasMixedActiveContentBlocked: true,
+ hasMixedDisplayContentBlocked: true,
+ btpHasPurgedSite: false,
+ etpCategory: getEtpCategory(),
+ },
+ frameworks: { fastclick: true, mobify: true, marfeel: true },
+ consoleLog: [
+ {
+ level: "error",
+ log(actual) {
+ // "Blocked loading mixed display content http://example.com/tests/image/test/mochitest/blue.png"
+ return (
+ Array.isArray(actual) &&
+ actual.length == 1 &&
+ actual[0].includes("blue.png")
+ );
+ },
+ pos: "0:1",
+ uri: REPORTABLE_PAGE_URL3,
+ },
+ {
+ level: "error",
+ log(actual) {
+ // "Blocked loading mixed active content http://tracking.example.org/browser/browser/base/content/test/protectionsUI/benignPage.html",
+ return (
+ Array.isArray(actual) &&
+ actual.length == 1 &&
+ actual[0].includes("benignPage.html")
+ );
+ },
+ pos: "0:1",
+ uri: REPORTABLE_PAGE_URL3,
+ },
+ {
+ level: "warn",
+ log(actual) {
+ // "The resource at https://trackertest.org/ was blocked because content blocking is enabled.",
+ return (
+ Array.isArray(actual) &&
+ actual.length == 1 &&
+ actual[0].includes("trackertest.org")
+ );
+ },
+ pos: "0:1",
+ uri: REPORTABLE_PAGE_URL3,
+ },
+ ],
+ });
+
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_back_buttons.js b/src/zen/tests/mochitests/reportbrokensite/browser_back_buttons.js
new file mode 100644
index 000000000..c004442c2
--- /dev/null
+++ b/src/zen/tests/mochitests/reportbrokensite/browser_back_buttons.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests to ensure that Report Broken Site popups will be
+ * reset to whichever tab the user is on as they change
+ * between windows and tabs. */
+
+"use strict";
+
+add_common_setup();
+
+add_task(async function testBackButtonsAreAdded() {
+ ensureReportBrokenSitePreffedOn();
+
+ await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
+ let rbs = await AppMenu().openReportBrokenSite();
+ rbs.isBackButtonEnabled();
+ await rbs.clickBack();
+ await rbs.close();
+
+ rbs = await HelpMenu().openReportBrokenSite();
+ ok(!rbs.backButton, "Back button is not shown for Help Menu");
+ await rbs.close();
+
+ rbs = await ProtectionsPanel().openReportBrokenSite();
+ rbs.isBackButtonEnabled();
+ await rbs.clickBack();
+ await rbs.close();
+
+ rbs = await HelpMenu().openReportBrokenSite();
+ ok(!rbs.backButton, "Back button is not shown for Help Menu");
+ await rbs.close();
+ });
+});
diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_error_messages.js b/src/zen/tests/mochitests/reportbrokensite/browser_error_messages.js
new file mode 100644
index 000000000..54b93cb2d
--- /dev/null
+++ b/src/zen/tests/mochitests/reportbrokensite/browser_error_messages.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Test that the Report Broken Site errors messages are shown on
+ * the UI if the user enters an invalid URL or clicks the send
+ * button while it is disabled due to not selecting a "reason"
+ */
+
+"use strict";
+
+add_common_setup();
+
+add_task(async function test() {
+ ensureReportBrokenSitePreffedOn();
+ ensureReasonRequired();
+
+ await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
+ for (const menu of [AppMenu(), ProtectionsPanel(), HelpMenu()]) {
+ const rbs = await menu.openReportBrokenSite();
+ const { sendButton, URLInput } = rbs;
+
+ rbs.isURLInvalidMessageHidden();
+ rbs.isReasonNeededMessageHidden();
+
+ rbs.setURL("");
+ window.document.activeElement.blur();
+ rbs.isURLInvalidMessageShown();
+ rbs.isReasonNeededMessageHidden();
+
+ rbs.setURL("https://asdf");
+ window.document.activeElement.blur();
+ rbs.isURLInvalidMessageHidden();
+ rbs.isReasonNeededMessageHidden();
+
+ rbs.setURL("http:/ /asdf");
+ window.document.activeElement.blur();
+ rbs.isURLInvalidMessageShown();
+ rbs.isReasonNeededMessageHidden();
+
+ rbs.setURL("https://asdf");
+ const selectPromise = BrowserTestUtils.waitForSelectPopupShown(window);
+ EventUtils.synthesizeMouseAtCenter(sendButton, {}, window);
+ await selectPromise;
+ rbs.isURLInvalidMessageHidden();
+ rbs.isReasonNeededMessageShown();
+ await rbs.dismissDropdownPopup();
+
+ rbs.chooseReason("slow");
+ rbs.isURLInvalidMessageHidden();
+ rbs.isReasonNeededMessageHidden();
+
+ rbs.setURL("");
+ rbs.chooseReason("choose");
+ window.ownerGlobal.document.activeElement?.blur();
+ const focusPromise = BrowserTestUtils.waitForEvent(URLInput, "focus");
+ EventUtils.synthesizeMouseAtCenter(sendButton, {}, window);
+ await focusPromise;
+ rbs.isURLInvalidMessageShown();
+ rbs.isReasonNeededMessageShown();
+
+ rbs.clickCancel();
+ }
+ });
+});
diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_experiment_data_sent.js b/src/zen/tests/mochitests/reportbrokensite/browser_experiment_data_sent.js
new file mode 100644
index 000000000..3cedcd549
--- /dev/null
+++ b/src/zen/tests/mochitests/reportbrokensite/browser_experiment_data_sent.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests to ensure that the right data is sent for
+ * private windows and when ETP blocks content.
+ */
+
+/* import-globals-from send.js */
+/* import-globals-from send_more_info.js */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ getRootDirectory(gTestPath) + "send_more_info.js",
+ this
+);
+
+const { ExperimentAPI } = ChromeUtils.importESModule(
+ "resource://nimbus/ExperimentAPI.sys.mjs"
+);
+const { NimbusTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/NimbusTestUtils.sys.mjs"
+);
+
+add_common_setup();
+
+const EXPECTED_EXPERIMENTS_IN_REPORT = [
+ { slug: "test-experiment", branch: "branch", kind: "nimbusExperiment" },
+ { slug: "test-experiment-rollout", branch: "branch", kind: "nimbusRollout" },
+];
+
+let EXPERIMENT_CLEANUPS;
+
+add_setup(async function () {
+ await ExperimentAPI.ready();
+ EXPERIMENT_CLEANUPS = [
+ await NimbusTestUtils.enrollWithFeatureConfig(
+ { featureId: "no-feature-firefox-desktop", value: {} },
+ { slug: "test-experiment", branchSlug: "branch" }
+ ),
+ await NimbusTestUtils.enrollWithFeatureConfig(
+ { featureId: "no-feature-firefox-desktop", value: {} },
+ { slug: "test-experiment-rollout", isRollout: true, branchSlug: "branch" }
+ ),
+ async () => {
+ ExperimentAPI.manager.store._deleteForTests("test-experiment-disabled");
+ await NimbusTestUtils.flushStore();
+ },
+ ];
+
+ await NimbusTestUtils.enrollWithFeatureConfig(
+ { featureId: "no-feature-firefox-desktop", value: {} },
+ { slug: "test-experiment-disabled" }
+ );
+ await ExperimentAPI.manager.unenroll("test-experiment-disabled");
+});
+
+add_task(async function testSendButton() {
+ ensureReportBrokenSitePreffedOn();
+ ensureReasonOptional();
+
+ const tab = await openTab(REPORTABLE_PAGE_URL);
+
+ await testSend(tab, AppMenu(), {
+ experiments: EXPECTED_EXPERIMENTS_IN_REPORT,
+ });
+
+ closeTab(tab);
+});
+
+add_task(async function testSendingMoreInfo() {
+ ensureReportBrokenSitePreffedOn();
+ ensureSendMoreInfoEnabled();
+
+ const tab = await openTab(REPORTABLE_PAGE_URL);
+
+ await testSendMoreInfo(tab, HelpMenu(), {
+ experiments: EXPECTED_EXPERIMENTS_IN_REPORT,
+ });
+
+ closeTab(tab);
+});
+
+add_task(async function teardown() {
+ for (const cleanup of EXPERIMENT_CLEANUPS) {
+ await cleanup();
+ }
+});
diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_keyboard_navigation.js b/src/zen/tests/mochitests/reportbrokensite/browser_keyboard_navigation.js
new file mode 100644
index 000000000..3bf9278e4
--- /dev/null
+++ b/src/zen/tests/mochitests/reportbrokensite/browser_keyboard_navigation.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests to ensure that sending or canceling reports with
+ * the Send and Cancel buttons work (as well as the Okay button)
+ */
+
+"use strict";
+
+add_common_setup();
+
+requestLongerTimeout(2);
+
+async function testPressingKey(key, tabToMatch, makePromise, followUp) {
+ await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
+ for (const menu of [AppMenu(), ProtectionsPanel(), HelpMenu()]) {
+ info(
+ `Opening RBS to test pressing ${key} for ${tabToMatch} on ${menu.menuDescription}`
+ );
+ const rbs = await menu.openReportBrokenSite();
+ const promise = makePromise(rbs);
+ if (tabToMatch) {
+ if (await tabTo(tabToMatch)) {
+ await pressKeyAndAwait(promise, key);
+ followUp && (await followUp(rbs));
+ await rbs.close();
+ ok(true, `was able to activate ${tabToMatch} with keyboard`);
+ } else {
+ await rbs.close();
+ ok(false, `could not tab to ${tabToMatch}`);
+ }
+ } else {
+ await pressKeyAndAwait(promise, key);
+ followUp && (await followUp(rbs));
+ await rbs.close();
+ ok(true, `was able to use keyboard`);
+ }
+ }
+ });
+}
+
+add_task(async function testSendMoreInfo() {
+ ensureReportBrokenSitePreffedOn();
+ ensureSendMoreInfoEnabled();
+ await testPressingKey(
+ "KEY_Enter",
+ "#report-broken-site-popup-send-more-info-link",
+ rbs => rbs.waitForSendMoreInfoTab(),
+ () => gBrowser.removeCurrentTab()
+ );
+});
+
+add_task(async function testCancel() {
+ ensureReportBrokenSitePreffedOn();
+ await testPressingKey(
+ "KEY_Enter",
+ "#report-broken-site-popup-cancel-button",
+ rbs => BrowserTestUtils.waitForEvent(rbs.mainView, "ViewHiding")
+ );
+});
+
+add_task(async function testSendAndOkay() {
+ ensureReportBrokenSitePreffedOn();
+ await testPressingKey(
+ "KEY_Enter",
+ "#report-broken-site-popup-send-button",
+ rbs => rbs.awaitReportSentViewOpened(),
+ async rbs => {
+ await tabTo("#report-broken-site-popup-okay-button");
+ const promise = BrowserTestUtils.waitForEvent(rbs.sentView, "ViewHiding");
+ await pressKeyAndAwait(promise, "KEY_Enter");
+ }
+ );
+});
+
+add_task(async function testESCOnMain() {
+ ensureReportBrokenSitePreffedOn();
+ await testPressingKey("KEY_Escape", undefined, rbs =>
+ BrowserTestUtils.waitForEvent(rbs.mainView, "ViewHiding")
+ );
+});
+
+add_task(async function testESCOnSent() {
+ ensureReportBrokenSitePreffedOn();
+ await testPressingKey(
+ "KEY_Enter",
+ "#report-broken-site-popup-send-button",
+ rbs => rbs.awaitReportSentViewOpened(),
+ async rbs => {
+ const promise = BrowserTestUtils.waitForEvent(rbs.sentView, "ViewHiding");
+ await pressKeyAndAwait(promise, "KEY_Escape");
+ }
+ );
+});
+
+add_task(async function testBackButtons() {
+ ensureReportBrokenSitePreffedOn();
+ await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
+ for (const menu of [AppMenu(), ProtectionsPanel()]) {
+ await menu.openReportBrokenSite();
+ await tabTo("#report-broken-site-popup-mainView .subviewbutton-back");
+ const promise = BrowserTestUtils.waitForEvent(menu.popup, "ViewShown");
+ await pressKeyAndAwait(promise, "KEY_Enter");
+ menu.close();
+ }
+ });
+});
diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_learn_more_link.js b/src/zen/tests/mochitests/reportbrokensite/browser_learn_more_link.js
new file mode 100644
index 000000000..4040bef59
--- /dev/null
+++ b/src/zen/tests/mochitests/reportbrokensite/browser_learn_more_link.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests to ensure that the reason dropdown is shown or hidden
+ * based on its pref, and that its optional and required modes affect
+ * the Send button and report appropriately.
+ */
+
+"use strict";
+
+add_common_setup();
+
+async function ensureLearnMoreLinkWorks(menu) {
+ const rbs = await menu.openReportBrokenSite();
+ const { win, mainView, learnMoreLink } = rbs;
+ ok(learnMoreLink, "Found a learn more link");
+
+ const promises = [
+ BrowserTestUtils.waitForEvent(mainView, "ViewHiding"),
+ BrowserTestUtils.waitForNewTab(win.gBrowser, LEARN_MORE_TEST_URL),
+ ];
+ EventUtils.synthesizeMouseAtCenter(learnMoreLink, {}, win);
+ const results = await Promise.all(promises);
+ gBrowser.removeTab(results[1]);
+}
+
+add_task(async function testLearnMoreLink() {
+ ensureReportBrokenSitePreffedOn();
+ await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
+ await ensureLearnMoreLinkWorks(AppMenu());
+ await ensureLearnMoreLinkWorks(HelpMenu());
+ await ensureLearnMoreLinkWorks(ProtectionsPanel());
+ });
+ const telemetry = Glean.webcompatreporting.learnMore.testGetValue();
+ is(telemetry.length, 3, "Got telemetry");
+});
diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_parent_menuitems.js b/src/zen/tests/mochitests/reportbrokensite/browser_parent_menuitems.js
new file mode 100644
index 000000000..36f172789
--- /dev/null
+++ b/src/zen/tests/mochitests/reportbrokensite/browser_parent_menuitems.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Test that the Report Broken Site menu items are disabled
+ * when the active tab is not on a reportable URL, and is hidden
+ * when the feature is disabled via pref. Also ensure that the
+ * Report Broken Site item that is automatically generated in
+ * the app menu's help sub-menu is hidden.
+ */
+
+"use strict";
+
+add_common_setup();
+
+add_task(async function testMenus() {
+ ensureReportBrokenSitePreffedOff();
+
+ const appMenu = AppMenu();
+ const menus = [appMenu, ProtectionsPanel(), HelpMenu()];
+
+ async function forceMenuItemStateUpdate() {
+ ReportBrokenSite.enableOrDisableMenuitems(window);
+
+ // the hidden/disabled state of all of the menuitems may not update until one
+ // is rendered; then the related 's state is propagated to them all.
+ await appMenu.open();
+ await appMenu.close();
+ }
+
+ await BrowserTestUtils.withNewTab("about:blank", async function () {
+ await forceMenuItemStateUpdate();
+ for (const { menuDescription, reportBrokenSite } of menus) {
+ isMenuItemHidden(
+ reportBrokenSite,
+ `${menuDescription} option hidden on invalid page when preffed off`
+ );
+ }
+ });
+
+ await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
+ await forceMenuItemStateUpdate();
+ for (const { menuDescription, reportBrokenSite } of menus) {
+ isMenuItemHidden(
+ reportBrokenSite,
+ `${menuDescription} option hidden on valid page when preffed off`
+ );
+ }
+ });
+
+ ensureReportBrokenSitePreffedOn();
+
+ await BrowserTestUtils.withNewTab("about:blank", async function () {
+ await forceMenuItemStateUpdate();
+ for (const { menuDescription, reportBrokenSite } of menus) {
+ isMenuItemDisabled(
+ reportBrokenSite,
+ `${menuDescription} option disabled on invalid page when preffed on`
+ );
+ }
+ });
+
+ await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
+ await forceMenuItemStateUpdate();
+ for (const { menuDescription, reportBrokenSite } of menus) {
+ isMenuItemEnabled(
+ reportBrokenSite,
+ `${menuDescription} option enabled on valid page when preffed on`
+ );
+ }
+ });
+
+ ensureReportBrokenSitePreffedOff();
+
+ await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
+ await forceMenuItemStateUpdate();
+ for (const { menuDescription, reportBrokenSite } of menus) {
+ isMenuItemHidden(
+ reportBrokenSite,
+ `${menuDescription} option hidden again when pref toggled back off`
+ );
+ }
+ });
+
+ ensureReportBrokenSitePreffedOn();
+ ensureReportBrokenSiteDisabledByPolicy();
+
+ await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
+ await forceMenuItemStateUpdate();
+ for (const { menuDescription, reportBrokenSite } of menus) {
+ isMenuItemHidden(
+ reportBrokenSite,
+ `${menuDescription} option hidden when disabled by DisableFeedbackCommands enterprise policy`
+ );
+ }
+ });
+});
diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_prefers_contrast.js b/src/zen/tests/mochitests/reportbrokensite/browser_prefers_contrast.js
new file mode 100644
index 000000000..b184b5145
--- /dev/null
+++ b/src/zen/tests/mochitests/reportbrokensite/browser_prefers_contrast.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Test that the background color of the "report sent"
+ * view is not green in non-default contrast modes.
+ */
+
+"use strict";
+
+add_common_setup();
+
+const HIGH_CONTRAST_MODE_OFF = [[PREFS.USE_ACCESSIBILITY_THEME, 0]];
+
+const HIGH_CONTRAST_MODE_ON = [[PREFS.USE_ACCESSIBILITY_THEME, 1]];
+
+add_task(async function testReportSentViewBGColor() {
+ ensureReportBrokenSitePreffedOn();
+ ensureReasonDisabled();
+
+ await BrowserTestUtils.withNewTab(
+ REPORTABLE_PAGE_URL,
+ async function (browser) {
+ const { defaultView } = browser.ownerGlobal.document;
+
+ const menu = AppMenu();
+
+ await SpecialPowers.pushPrefEnv({ set: HIGH_CONTRAST_MODE_OFF });
+ const rbs = await menu.openReportBrokenSite();
+ const { mainView, sentView } = rbs;
+ mainView.style.backgroundColor = "var(--background-color-success)";
+ const expectedReportSentBGColor =
+ defaultView.getComputedStyle(mainView).backgroundColor;
+ mainView.style.backgroundColor = "";
+ const expectedPrefersReducedBGColor =
+ defaultView.getComputedStyle(mainView).backgroundColor;
+
+ await rbs.clickSend();
+ is(
+ defaultView.getComputedStyle(sentView).backgroundColor,
+ expectedReportSentBGColor,
+ "Using green bgcolor when not prefers-contrast"
+ );
+ await rbs.clickOkay();
+
+ await SpecialPowers.pushPrefEnv({ set: HIGH_CONTRAST_MODE_ON });
+ await menu.openReportBrokenSite();
+ await rbs.clickSend();
+ is(
+ defaultView.getComputedStyle(sentView).backgroundColor,
+ expectedPrefersReducedBGColor,
+ "Using default bgcolor when prefers-contrast"
+ );
+ await rbs.clickOkay();
+ }
+ );
+});
diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_reason_dropdown.js b/src/zen/tests/mochitests/reportbrokensite/browser_reason_dropdown.js
new file mode 100644
index 000000000..365b469a7
--- /dev/null
+++ b/src/zen/tests/mochitests/reportbrokensite/browser_reason_dropdown.js
@@ -0,0 +1,156 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests to ensure that the reason dropdown is shown or hidden
+ * based on its pref, and that its optional and required modes affect
+ * the Send button and report appropriately.
+ */
+
+"use strict";
+
+add_common_setup();
+
+requestLongerTimeout(2);
+
+async function clickSendAndCheckPing(rbs, expectedReason = null) {
+ await GleanPings.brokenSiteReport.testSubmission(
+ () =>
+ Assert.equal(
+ Glean.brokenSiteReport.breakageCategory.testGetValue(),
+ expectedReason
+ ),
+ () => rbs.clickSend()
+ );
+}
+
+add_task(async function testReasonDropdown() {
+ ensureReportBrokenSitePreffedOn();
+
+ await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
+ ensureReasonDisabled();
+
+ let rbs = await AppMenu().openReportBrokenSite();
+ await rbs.isReasonHidden();
+ await rbs.isSendButtonEnabled();
+ await clickSendAndCheckPing(rbs);
+ await rbs.clickOkay();
+
+ ensureReasonOptional();
+ rbs = await AppMenu().openReportBrokenSite();
+ await rbs.isReasonOptional();
+ await rbs.isSendButtonEnabled();
+ await clickSendAndCheckPing(rbs);
+ await rbs.clickOkay();
+
+ rbs = await AppMenu().openReportBrokenSite();
+ await rbs.isReasonOptional();
+ rbs.chooseReason("slow");
+ await rbs.isSendButtonEnabled();
+ await clickSendAndCheckPing(rbs, "slow");
+ await rbs.clickOkay();
+
+ ensureReasonRequired();
+ rbs = await AppMenu().openReportBrokenSite();
+ await rbs.isReasonRequired();
+ await rbs.isSendButtonEnabled();
+ const selectPromise = BrowserTestUtils.waitForSelectPopupShown(window);
+ EventUtils.synthesizeMouseAtCenter(rbs.sendButton, {}, window);
+ await selectPromise;
+ rbs.chooseReason("media");
+ await rbs.dismissDropdownPopup();
+ await rbs.isSendButtonEnabled();
+ await clickSendAndCheckPing(rbs, "media");
+ await rbs.clickOkay();
+ });
+});
+
+async function getListItems(rbs) {
+ const items = Array.from(rbs.reasonInput.querySelectorAll("option")).map(i =>
+ i.id.replace("report-broken-site-popup-reason-", "")
+ );
+ Assert.equal(items[0], "choose", "First option is always 'choose'");
+ return items.join(",");
+}
+
+add_task(async function testReasonDropdownRandomized() {
+ ensureReportBrokenSitePreffedOn();
+ ensureReasonOptional();
+
+ const USER_ID_PREF = "app.normandy.user_id";
+ const RANDOMIZE_PREF = "ui.new-webcompat-reporter.reason-dropdown.randomized";
+
+ const origNormandyUserID = Services.prefs.getCharPref(
+ USER_ID_PREF,
+ undefined
+ );
+
+ await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
+ // confirm that the default order is initially used
+ Services.prefs.setBoolPref(RANDOMIZE_PREF, false);
+ const rbs = await AppMenu().openReportBrokenSite();
+ const defaultOrder = [
+ "choose",
+ "checkout",
+ "load",
+ "slow",
+ "media",
+ "content",
+ "account",
+ "adblocker",
+ "notsupported",
+ "other",
+ ];
+ Assert.deepEqual(
+ await getListItems(rbs),
+ defaultOrder,
+ "non-random order is correct"
+ );
+
+ // confirm that a random order happens per user
+ let randomOrder;
+ let isRandomized = false;
+ Services.prefs.setBoolPref(RANDOMIZE_PREF, true);
+
+ // This becomes ClientEnvironment.randomizationId, which we can set to
+ // any value which results in a different order from the default ordering.
+ Services.prefs.setCharPref("app.normandy.user_id", "dummy");
+
+ // clicking cancel triggers a reset, which is when the randomization
+ // logic is called. so we must click cancel after pref-changes here.
+ rbs.clickCancel();
+ await AppMenu().openReportBrokenSite();
+ randomOrder = await getListItems(rbs);
+ Assert.notEqual(
+ randomOrder,
+ defaultOrder,
+ "options are randomized with pref on"
+ );
+
+ // confirm that the order doesn't change per user
+ isRandomized = false;
+ for (let attempt = 0; attempt < 5; ++attempt) {
+ rbs.clickCancel();
+ await AppMenu().openReportBrokenSite();
+ const order = await getListItems(rbs);
+
+ if (order != randomOrder) {
+ isRandomized = true;
+ break;
+ }
+ }
+ Assert.ok(!isRandomized, "options keep the same order per user");
+
+ // confirm that the order reverts to the default if pref flipped to false
+ Services.prefs.setBoolPref(RANDOMIZE_PREF, false);
+ rbs.clickCancel();
+ await AppMenu().openReportBrokenSite();
+ Assert.deepEqual(
+ defaultOrder,
+ await getListItems(rbs),
+ "reverts to non-random order correctly"
+ );
+ rbs.clickCancel();
+ });
+
+ Services.prefs.setCharPref(USER_ID_PREF, origNormandyUserID);
+});
diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_report_send.js b/src/zen/tests/mochitests/reportbrokensite/browser_report_send.js
new file mode 100644
index 000000000..bf849776d
--- /dev/null
+++ b/src/zen/tests/mochitests/reportbrokensite/browser_report_send.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests to ensure that sending or canceling reports with
+ * the Send and Cancel buttons work (as well as the Okay button)
+ */
+
+/* import-globals-from send.js */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ getRootDirectory(gTestPath) + "send.js",
+ this
+);
+
+add_common_setup();
+
+requestLongerTimeout(10);
+
+async function testCancel(menu, url, description) {
+ let rbs = await menu.openAndPrefillReportBrokenSite(url, description);
+ await rbs.clickCancel();
+ ok(!rbs.opened, "clicking Cancel closes Report Broken Site");
+
+ // re-opening the panel, the url and description should be reset
+ rbs = await menu.openReportBrokenSite();
+ rbs.isMainViewResetToCurrentTab();
+ rbs.close();
+}
+
+add_task(async function testSendButton() {
+ ensureReportBrokenSitePreffedOn();
+ ensureReasonOptional();
+
+ const tab1 = await openTab(REPORTABLE_PAGE_URL);
+
+ await testSend(tab1, AppMenu());
+
+ const tab2 = await openTab(REPORTABLE_PAGE_URL);
+
+ await testSend(tab2, ProtectionsPanel(), {
+ url: "https://test.org/test/#fake",
+ breakageCategory: "media",
+ description: "test description",
+ });
+
+ closeTab(tab1);
+ closeTab(tab2);
+});
+
+add_task(async function testCancelButton() {
+ ensureReportBrokenSitePreffedOn();
+
+ const tab1 = await openTab(REPORTABLE_PAGE_URL);
+
+ await testCancel(AppMenu());
+ await testCancel(ProtectionsPanel());
+ await testCancel(HelpMenu());
+
+ const tab2 = await openTab(REPORTABLE_PAGE_URL);
+
+ await testCancel(AppMenu());
+ await testCancel(ProtectionsPanel());
+ await testCancel(HelpMenu());
+
+ const win2 = await BrowserTestUtils.openNewBrowserWindow();
+ const tab3 = await openTab(REPORTABLE_PAGE_URL2, win2);
+
+ await testCancel(AppMenu(win2));
+ await testCancel(ProtectionsPanel(win2));
+ await testCancel(HelpMenu(win2));
+
+ closeTab(tab3);
+ await BrowserTestUtils.closeWindow(win2);
+
+ closeTab(tab1);
+ closeTab(tab2);
+});
diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_send_more_info.js b/src/zen/tests/mochitests/reportbrokensite/browser_send_more_info.js
new file mode 100644
index 000000000..9306f5161
--- /dev/null
+++ b/src/zen/tests/mochitests/reportbrokensite/browser_send_more_info.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests that the send more info link appears only when its pref
+ * is set to true, and that when clicked it will open a tab to
+ * the webcompat.com endpoint and send the right data.
+ */
+
+/* import-globals-from send_more_info.js */
+
+"use strict";
+
+const VIDEO_URL = `${BASE_URL}/videotest.mp4`;
+
+Services.scriptloader.loadSubScript(
+ getRootDirectory(gTestPath) + "send_more_info.js",
+ this
+);
+
+add_common_setup();
+
+requestLongerTimeout(2);
+
+add_task(async function testSendMoreInfoPref() {
+ ensureReportBrokenSitePreffedOn();
+
+ await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
+ await changeTab(gBrowser.selectedTab, REPORTABLE_PAGE_URL);
+
+ ensureSendMoreInfoDisabled();
+ let rbs = await AppMenu().openReportBrokenSite();
+ await rbs.isSendMoreInfoHidden();
+ await rbs.close();
+
+ ensureSendMoreInfoEnabled();
+ rbs = await AppMenu().openReportBrokenSite();
+ await rbs.isSendMoreInfoShown();
+ await rbs.close();
+ });
+});
+
+add_task(async function testSendingMoreInfo() {
+ ensureReportBrokenSitePreffedOn();
+ ensureSendMoreInfoEnabled();
+
+ const tab = await openTab(REPORTABLE_PAGE_URL);
+
+ await testSendMoreInfo(tab, AppMenu());
+
+ await changeTab(tab, REPORTABLE_PAGE_URL2);
+
+ await testSendMoreInfo(tab, ProtectionsPanel(), {
+ url: "https://override.com",
+ description: "another",
+ expectNoTabDetails: true,
+ });
+
+ // also load a video to ensure system codec
+ // information is loaded and properly sent
+ const tab2 = await openTab(VIDEO_URL);
+ await testSendMoreInfo(tab2, HelpMenu());
+ closeTab(tab2);
+
+ closeTab(tab);
+});
diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_tab_key_order.js b/src/zen/tests/mochitests/reportbrokensite/browser_tab_key_order.js
new file mode 100644
index 000000000..ab776fb26
--- /dev/null
+++ b/src/zen/tests/mochitests/reportbrokensite/browser_tab_key_order.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests of the expected tab key element focus order */
+
+"use strict";
+
+add_common_setup();
+
+requestLongerTimeout(2);
+
+async function ensureTabOrder(order, win = window) {
+ const config = { window: win };
+ for (let matches of order) {
+ // We need to tab through all elements in each match array in any order
+ if (!Array.isArray(matches)) {
+ matches = [matches];
+ }
+ let matchesLeft = matches.length;
+ while (matchesLeft--) {
+ const target = await pressKeyAndGetFocus("VK_TAB", config);
+ let foundMatch = false;
+ for (const [i, selector] of matches.entries()) {
+ foundMatch = selector && target.matches(selector);
+ if (foundMatch) {
+ matches[i] = "";
+ break;
+ }
+ }
+ ok(
+ foundMatch,
+ `Expected [${matches}] next, got id=${target.id}, class=${target.className}, ${target}`
+ );
+ if (!foundMatch) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+async function ensureExpectedTabOrder(
+ expectBackButton,
+ expectReason,
+ expectSendMoreInfo
+) {
+ const { activeElement } = window.document;
+ is(
+ activeElement?.id,
+ "report-broken-site-popup-url",
+ "URL is already focused"
+ );
+ const order = [];
+ if (expectReason) {
+ order.push("#report-broken-site-popup-reason");
+ }
+ order.push("#report-broken-site-popup-description");
+ order.push("#report-broken-site-popup-blocked-trackers-checkbox");
+ if (expectSendMoreInfo) {
+ order.push("#report-broken-site-popup-send-more-info-link");
+ }
+ // moz-button-groups swap the order of buttons to follow
+ // platform conventions, so the order of send/cancel will vary.
+ order.push([
+ "#report-broken-site-popup-cancel-button",
+ "#report-broken-site-popup-send-button",
+ ]);
+ if (expectBackButton) {
+ order.push(".subviewbutton-back");
+ }
+ order.push("#report-broken-site-popup-learn-more-link");
+ order.push("#report-broken-site-popup-url"); // check that we've cycled back
+ return ensureTabOrder(order);
+}
+
+async function testTabOrder(menu) {
+ ensureReasonDisabled();
+ ensureSendMoreInfoDisabled();
+
+ const { showsBackButton } = menu;
+
+ let rbs = await menu.openReportBrokenSite();
+ await ensureExpectedTabOrder(showsBackButton, false, false);
+ await rbs.close();
+
+ ensureSendMoreInfoEnabled();
+ rbs = await menu.openReportBrokenSite();
+ await ensureExpectedTabOrder(showsBackButton, false, true);
+ await rbs.close();
+
+ ensureReasonOptional();
+ rbs = await menu.openReportBrokenSite();
+ await ensureExpectedTabOrder(showsBackButton, true, true);
+ await rbs.close();
+
+ ensureReasonRequired();
+ rbs = await menu.openReportBrokenSite();
+ await ensureExpectedTabOrder(showsBackButton, true, true);
+ await rbs.close();
+ rbs = await menu.openReportBrokenSite();
+ rbs.chooseReason("slow");
+ await ensureExpectedTabOrder(showsBackButton, true, true);
+ await rbs.clickCancel();
+
+ ensureSendMoreInfoDisabled();
+ rbs = await menu.openReportBrokenSite();
+ await ensureExpectedTabOrder(showsBackButton, true, false);
+ await rbs.close();
+ rbs = await menu.openReportBrokenSite();
+ rbs.chooseReason("slow");
+ await ensureExpectedTabOrder(showsBackButton, true, false);
+ await rbs.clickCancel();
+
+ ensureReasonOptional();
+ rbs = await menu.openReportBrokenSite();
+ await ensureExpectedTabOrder(showsBackButton, true, false);
+ await rbs.close();
+
+ ensureReasonDisabled();
+ rbs = await menu.openReportBrokenSite();
+ await ensureExpectedTabOrder(showsBackButton, false, false);
+ await rbs.close();
+}
+
+add_task(async function testTabOrdering() {
+ ensureReportBrokenSitePreffedOn();
+ ensureSendMoreInfoEnabled();
+
+ await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
+ await testTabOrder(AppMenu());
+ await testTabOrder(ProtectionsPanel());
+ await testTabOrder(HelpMenu());
+ });
+});
diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_tab_switch_handling.js b/src/zen/tests/mochitests/reportbrokensite/browser_tab_switch_handling.js
new file mode 100644
index 000000000..db1190b89
--- /dev/null
+++ b/src/zen/tests/mochitests/reportbrokensite/browser_tab_switch_handling.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests to ensure that Report Broken Site popups will be
+ * reset to whichever tab the user is on as they change
+ * between windows and tabs. */
+
+"use strict";
+
+add_common_setup();
+
+add_task(async function testResetsProperlyOnTabSwitch() {
+ ensureReportBrokenSitePreffedOn();
+
+ const badTab = await openTab("about:blank");
+ const goodTab1 = await openTab(REPORTABLE_PAGE_URL);
+ const goodTab2 = await openTab(REPORTABLE_PAGE_URL2);
+
+ const appMenu = AppMenu();
+ const protPanel = ProtectionsPanel();
+
+ let rbs = await appMenu.openReportBrokenSite();
+ rbs.isMainViewResetToCurrentTab();
+ rbs.close();
+
+ gBrowser.selectedTab = goodTab1;
+
+ rbs = await protPanel.openReportBrokenSite();
+ rbs.isMainViewResetToCurrentTab();
+ rbs.close();
+
+ gBrowser.selectedTab = badTab;
+ await appMenu.open();
+ appMenu.isReportBrokenSiteDisabled();
+ await appMenu.close();
+
+ gBrowser.selectedTab = goodTab1;
+ rbs = await protPanel.openReportBrokenSite();
+ rbs.isMainViewResetToCurrentTab();
+ rbs.close();
+
+ closeTab(badTab);
+ closeTab(goodTab1);
+ closeTab(goodTab2);
+});
+
+add_task(async function testResetsProperlyOnWindowSwitch() {
+ ensureReportBrokenSitePreffedOn();
+
+ const tab1 = await openTab(REPORTABLE_PAGE_URL);
+
+ const win2 = await BrowserTestUtils.openNewBrowserWindow();
+ const tab2 = await openTab(REPORTABLE_PAGE_URL2, win2);
+
+ const appMenu1 = AppMenu();
+ const appMenu2 = ProtectionsPanel(win2);
+
+ let rbs2 = await appMenu2.openReportBrokenSite();
+ rbs2.isMainViewResetToCurrentTab();
+ rbs2.close();
+
+ // flip back to tab1's window and ensure its URL pops up instead of tab2's URL
+ await switchToWindow(window);
+ isSelectedTab(window, tab1); // sanity check
+
+ let rbs1 = await appMenu1.openReportBrokenSite();
+ rbs1.isMainViewResetToCurrentTab();
+ rbs1.close();
+
+ // likewise flip back to tab2's window and ensure its URL pops up instead of tab1's URL
+ await switchToWindow(win2);
+ isSelectedTab(win2, tab2); // sanity check
+
+ rbs2 = await appMenu2.openReportBrokenSite();
+ rbs2.isMainViewResetToCurrentTab();
+ rbs2.close();
+
+ closeTab(tab1);
+ closeTab(tab2);
+ await BrowserTestUtils.closeWindow(win2);
+});
diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_webcompat.com_fallback.js b/src/zen/tests/mochitests/reportbrokensite/browser_webcompat.com_fallback.js
new file mode 100644
index 000000000..4b155bbd9
--- /dev/null
+++ b/src/zen/tests/mochitests/reportbrokensite/browser_webcompat.com_fallback.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Tests that when Report Broken Site is disabled, it will
+ * send the user to webcompat.com when clicked and it the
+ * relevant tab's report data.
+ */
+
+/* import-globals-from send_more_info.js */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ getRootDirectory(gTestPath) + "send_more_info.js",
+ this
+);
+
+add_common_setup();
+
+const VIDEO_URL = `${BASE_URL}/videotest.mp4`;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["test.wait300msAfterTabSwitch", true]],
+ });
+});
+
+add_task(async function testWebcompatComFallbacks() {
+ ensureReportBrokenSitePreffedOff();
+
+ const tab = await openTab(REPORTABLE_PAGE_URL);
+
+ await testWebcompatComFallback(tab, AppMenu());
+
+ await changeTab(tab, REPORTABLE_PAGE_URL2);
+ await testWebcompatComFallback(tab, ProtectionsPanel());
+
+ // also load a video to ensure system codec
+ // information is loaded and properly sent
+ const tab2 = await openTab(VIDEO_URL);
+ await testWebcompatComFallback(tab2, HelpMenu());
+ closeTab(tab2);
+
+ closeTab(tab);
+});
diff --git a/src/zen/tests/mochitests/reportbrokensite/example_report_page.html b/src/zen/tests/mochitests/reportbrokensite/example_report_page.html
new file mode 100644
index 000000000..07602e3fb
--- /dev/null
+++ b/src/zen/tests/mochitests/reportbrokensite/example_report_page.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/zen/tests/mochitests/reportbrokensite/head.js b/src/zen/tests/mochitests/reportbrokensite/head.js
new file mode 100644
index 000000000..04357acad
--- /dev/null
+++ b/src/zen/tests/mochitests/reportbrokensite/head.js
@@ -0,0 +1,918 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { CustomizableUITestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CustomizableUITestUtils.sys.mjs"
+);
+
+const { EnterprisePolicyTesting, PoliciesPrefTracker } =
+ ChromeUtils.importESModule(
+ "resource://testing-common/EnterprisePolicyTesting.sys.mjs"
+ );
+
+const { UrlClassifierTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlClassifierTestUtils.sys.mjs"
+);
+
+const { ReportBrokenSite } = ChromeUtils.importESModule(
+ "moz-src:///browser/components/reportbrokensite/ReportBrokenSite.sys.mjs"
+);
+
+const BASE_URL =
+ "https://example.com/browser/browser/components/reportbrokensite/test/browser/";
+
+const REPORTABLE_PAGE_URL = "https://example.com";
+
+const REPORTABLE_PAGE_URL2 = REPORTABLE_PAGE_URL.replace(".com", ".org");
+
+const REPORTABLE_PAGE_URL3 = `${BASE_URL}example_report_page.html`;
+
+const SUMO_BASE_URL = Services.urlFormatter.formatURLPref(
+ "app.support.baseURL"
+);
+const LEARN_MORE_TEST_URL = `${SUMO_BASE_URL}report-broken-site`;
+
+const NEW_REPORT_ENDPOINT_TEST_URL = `${BASE_URL}sendMoreInfoTestEndpoint.html`;
+
+const PREFS = {
+ DATAREPORTING_ENABLED: "datareporting.healthreport.uploadEnabled",
+ REPORTER_ENABLED: "ui.new-webcompat-reporter.enabled",
+ REASON: "ui.new-webcompat-reporter.reason-dropdown",
+ SEND_MORE_INFO: "ui.new-webcompat-reporter.send-more-info-link",
+ NEW_REPORT_ENDPOINT: "ui.new-webcompat-reporter.new-report-endpoint",
+ TOUCH_EVENTS: "dom.w3c_touch_events.enabled",
+ USE_ACCESSIBILITY_THEME: "ui.useAccessibilityTheme",
+};
+
+function add_common_setup() {
+ add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [PREFS.NEW_REPORT_ENDPOINT, NEW_REPORT_ENDPOINT_TEST_URL],
+
+ // set touch events to auto-detect, as the pref gets set to 1 somewhere
+ // while tests are running, making hasTouchScreen checks unreliable.
+ [PREFS.TOUCH_EVENTS, 2],
+ ],
+ });
+ registerCleanupFunction(function () {
+ for (const prefName of Object.values(PREFS)) {
+ Services.prefs.clearUserPref(prefName);
+ }
+ Services.telemetry.clearEvents();
+ Services.fog.testResetFOG();
+ });
+ });
+}
+
+function areObjectsEqual(actual, expected, path = "") {
+ if (typeof expected == "function") {
+ try {
+ const passes = expected(actual);
+ if (!passes) {
+ info(`${path} not pass check function: ${actual}`);
+ }
+ return passes;
+ } catch (e) {
+ info(`${path} threw exception:
+ got: ${typeof actual}, ${actual}
+ expected: ${typeof expected}, ${expected}
+ exception: ${e.message}
+ ${e.stack}`);
+ return false;
+ }
+ }
+
+ if (typeof actual != typeof expected) {
+ info(`${path} types do not match:
+ got: ${typeof actual}, ${actual}
+ expected: ${typeof expected}, ${expected}`);
+ return false;
+ }
+ if (typeof actual != "object" || actual === null || expected === null) {
+ if (actual !== expected) {
+ info(`${path} does not match
+ got: ${typeof actual}, ${actual}
+ expected: ${typeof expected}, ${expected}`);
+ return false;
+ }
+ return true;
+ }
+ const prefix = path ? `${path}.` : path;
+ for (const [key, val] of Object.entries(actual)) {
+ if (!(key in expected)) {
+ info(`Extra ${prefix}${key}: ${val}`);
+ return false;
+ }
+ }
+ let result = true;
+ for (const [key, expectedVal] of Object.entries(expected)) {
+ if (key in actual) {
+ if (!areObjectsEqual(actual[key], expectedVal, `${prefix}${key}`)) {
+ result = false;
+ }
+ } else {
+ info(`Missing ${prefix}${key} (${expectedVal})`);
+ result = false;
+ }
+ }
+ return result;
+}
+
+function clickAndAwait(toClick, evt, target) {
+ const menuPromise = BrowserTestUtils.waitForEvent(target, evt);
+ EventUtils.synthesizeMouseAtCenter(toClick, {}, window);
+ return menuPromise;
+}
+
+async function openTab(url, win) {
+ const options = {
+ gBrowser:
+ win?.gBrowser ||
+ Services.wm.getMostRecentWindow("navigator:browser").gBrowser,
+ url,
+ };
+ return BrowserTestUtils.openNewForegroundTab(options);
+}
+
+async function changeTab(tab, url) {
+ BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, url);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+}
+
+function closeTab(tab) {
+ BrowserTestUtils.removeTab(tab);
+}
+
+function switchToWindow(win) {
+ const promises = [
+ BrowserTestUtils.waitForEvent(win, "focus"),
+ BrowserTestUtils.waitForEvent(win, "activate"),
+ ];
+ win.focus();
+ return Promise.all(promises);
+}
+
+function isSelectedTab(win, tab) {
+ const selectedTab = win.document.querySelector(".tabbrowser-tab[selected]");
+ is(selectedTab, tab);
+}
+
+async function setupPolicyEngineWithJson(json, customSchema) {
+ PoliciesPrefTracker.restoreDefaultValues();
+ if (typeof json != "object") {
+ let filePath = getTestFilePath(json ? json : "non-existing-file.json");
+ return EnterprisePolicyTesting.setupPolicyEngineWithJson(
+ filePath,
+ customSchema
+ );
+ }
+ return EnterprisePolicyTesting.setupPolicyEngineWithJson(json, customSchema);
+}
+
+async function ensureReportBrokenSiteDisabledByPolicy() {
+ await setupPolicyEngineWithJson({
+ policies: {
+ DisableFeedbackCommands: true,
+ },
+ });
+}
+
+registerCleanupFunction(async function resetPolicies() {
+ if (Services.policies.status != Ci.nsIEnterprisePolicies.INACTIVE) {
+ await setupPolicyEngineWithJson("");
+ }
+ EnterprisePolicyTesting.resetRunOnceState();
+ PoliciesPrefTracker.restoreDefaultValues();
+ PoliciesPrefTracker.stop();
+});
+
+function ensureReportBrokenSitePreffedOn() {
+ Services.prefs.setBoolPref(PREFS.DATAREPORTING_ENABLED, true);
+ Services.prefs.setBoolPref(PREFS.REPORTER_ENABLED, true);
+ ensureReasonDisabled();
+}
+
+function ensureReportBrokenSitePreffedOff() {
+ Services.prefs.setBoolPref(PREFS.REPORTER_ENABLED, false);
+}
+
+function ensureSendMoreInfoEnabled() {
+ Services.prefs.setBoolPref(PREFS.SEND_MORE_INFO, true);
+}
+
+function ensureSendMoreInfoDisabled() {
+ Services.prefs.setBoolPref(PREFS.SEND_MORE_INFO, false);
+}
+
+function ensureReasonDisabled() {
+ Services.prefs.setIntPref(PREFS.REASON, 0);
+}
+
+function ensureReasonOptional() {
+ Services.prefs.setIntPref(PREFS.REASON, 1);
+}
+
+function ensureReasonRequired() {
+ Services.prefs.setIntPref(PREFS.REASON, 2);
+}
+
+function isMenuItemEnabled(menuItem, itemDesc) {
+ ok(!menuItem.hidden, `${itemDesc} menu item is shown`);
+ ok(!menuItem.disabled, `${itemDesc} menu item is enabled`);
+}
+
+function isMenuItemHidden(menuItem, itemDesc) {
+ ok(
+ !menuItem || menuItem.hidden || !BrowserTestUtils.isVisible(menuItem),
+ `${itemDesc} menu item is hidden`
+ );
+}
+
+function isMenuItemDisabled(menuItem, itemDesc) {
+ ok(!menuItem.hidden, `${itemDesc} menu item is shown`);
+ ok(menuItem.disabled, `${itemDesc} menu item is disabled`);
+}
+
+function waitForWebcompatComTab(gBrowser) {
+ return BrowserTestUtils.waitForNewTab(gBrowser, NEW_REPORT_ENDPOINT_TEST_URL);
+}
+
+class ReportBrokenSiteHelper {
+ sourceMenu = undefined;
+ win = undefined;
+
+ constructor(sourceMenu) {
+ this.sourceMenu = sourceMenu;
+ this.win = sourceMenu.win;
+ }
+
+ getViewNode(id) {
+ return PanelMultiView.getViewNode(this.win.document, id);
+ }
+
+ get mainView() {
+ return this.getViewNode("report-broken-site-popup-mainView");
+ }
+
+ get sentView() {
+ return this.getViewNode("report-broken-site-popup-reportSentView");
+ }
+
+ get openPanel() {
+ return this.mainView?.closest("panel");
+ }
+
+ get opened() {
+ return this.openPanel?.hasAttribute("panelopen");
+ }
+
+ async click(triggerMenuItem) {
+ const window = triggerMenuItem.ownerGlobal;
+ await EventUtils.synthesizeMouseAtCenter(triggerMenuItem, {}, window);
+ }
+
+ async open(triggerMenuItem) {
+ const shownPromise = BrowserTestUtils.waitForEvent(
+ this.mainView,
+ "ViewShown"
+ );
+ const focusPromise = BrowserTestUtils.waitForEvent(this.URLInput, "focus");
+ await this.click(triggerMenuItem);
+ await shownPromise;
+ await focusPromise;
+ await BrowserTestUtils.waitForCondition(
+ () => this.URLInput.selectionStart === 0
+ );
+ }
+
+ async #assertClickAndViewChanges(button, view, newView, newFocus) {
+ ok(view.closest("panel").hasAttribute("panelopen"), "Panel is open");
+ ok(BrowserTestUtils.isVisible(button), "Button is visible");
+ ok(!button.disabled, "Button is enabled");
+ const promises = [];
+ if (newView) {
+ if (newView.nodeName == "panel") {
+ promises.push(BrowserTestUtils.waitForEvent(newView, "popupshown"));
+ } else {
+ promises.push(BrowserTestUtils.waitForEvent(newView, "ViewShown"));
+ }
+ } else {
+ promises.push(BrowserTestUtils.waitForEvent(view, "ViewHiding"));
+ }
+ if (newFocus) {
+ promises.push(BrowserTestUtils.waitForEvent(newFocus, "focus"));
+ }
+ EventUtils.synthesizeMouseAtCenter(button, {}, this.win);
+ await Promise.all(promises);
+ }
+
+ async awaitReportSentViewOpened() {
+ await Promise.all([
+ BrowserTestUtils.waitForEvent(this.sentView, "ViewShown"),
+ BrowserTestUtils.waitForEvent(this.okayButton, "focus"),
+ ]);
+ }
+
+ async clickSend() {
+ await this.#assertClickAndViewChanges(
+ this.sendButton,
+ this.mainView,
+ this.sentView,
+ this.okayButton
+ );
+ }
+
+ waitForSendMoreInfoTab() {
+ return BrowserTestUtils.waitForNewTab(
+ this.win.gBrowser,
+ NEW_REPORT_ENDPOINT_TEST_URL
+ );
+ }
+
+ async clickSendMoreInfo() {
+ const newTabPromise = waitForWebcompatComTab(this.win.gBrowser);
+ EventUtils.synthesizeMouseAtCenter(this.sendMoreInfoLink, {}, this.win);
+ const newTab = await newTabPromise;
+ const receivedData = await SpecialPowers.spawn(
+ newTab.linkedBrowser,
+ [],
+ async function () {
+ await content.wrappedJSObject.messageArrived;
+ return content.wrappedJSObject.message;
+ }
+ );
+ this.win.gBrowser.removeCurrentTab();
+ return receivedData;
+ }
+
+ async clickCancel() {
+ await this.#assertClickAndViewChanges(this.cancelButton, this.mainView);
+ }
+
+ async clickOkay() {
+ await this.#assertClickAndViewChanges(this.okayButton, this.sentView);
+ }
+
+ async clickBack() {
+ await this.#assertClickAndViewChanges(
+ this.backButton,
+ this.sourceMenu.popup
+ );
+ }
+
+ isBackButtonEnabled() {
+ ok(BrowserTestUtils.isVisible(this.backButton), "Back button is visible");
+ ok(!this.backButton.disabled, "Back button is enabled");
+ }
+
+ close() {
+ if (this.opened) {
+ this.openPanel?.hidePopup(false);
+ }
+ this.sourceMenu?.close();
+ }
+
+ // UI element getters
+ get URLInput() {
+ return this.getViewNode("report-broken-site-popup-url");
+ }
+
+ get URLInvalidMessage() {
+ return this.getViewNode("report-broken-site-popup-invalid-url-msg");
+ }
+
+ get reasonInput() {
+ return this.getViewNode("report-broken-site-popup-reason");
+ }
+
+ get reasonDropdownPopup() {
+ return this.win.document.getElementById("ContentSelectDropdown").menupopup;
+ }
+
+ get reasonRequiredMessage() {
+ return this.getViewNode("report-broken-site-popup-missing-reason-msg");
+ }
+
+ get reasonLabelRequired() {
+ return this.getViewNode("report-broken-site-popup-reason-label");
+ }
+
+ get reasonLabelOptional() {
+ return this.getViewNode("report-broken-site-popup-reason-optional-label");
+ }
+
+ get descriptionTextarea() {
+ return this.getViewNode("report-broken-site-popup-description");
+ }
+
+ get learnMoreLink() {
+ return this.getViewNode("report-broken-site-popup-learn-more-link");
+ }
+
+ get sendMoreInfoLink() {
+ return this.getViewNode("report-broken-site-popup-send-more-info-link");
+ }
+
+ get backButton() {
+ return this.mainView.querySelector(".subviewbutton-back");
+ }
+
+ get blockedTrackersCheckbox() {
+ return this.getViewNode(
+ "report-broken-site-popup-blocked-trackers-checkbox"
+ );
+ }
+
+ set blockedTrackersCheckbox(checked) {
+ this.blockedTrackersCheckbox.checked = checked;
+ }
+
+ get sendButton() {
+ return this.getViewNode("report-broken-site-popup-send-button");
+ }
+
+ get cancelButton() {
+ return this.getViewNode("report-broken-site-popup-cancel-button");
+ }
+
+ get okayButton() {
+ return this.getViewNode("report-broken-site-popup-okay-button");
+ }
+
+ // Test helpers
+
+ #setInput(input, value) {
+ input.value = value;
+ input.dispatchEvent(
+ new UIEvent("input", { bubbles: true, view: this.win })
+ );
+ }
+
+ setURL(value) {
+ this.#setInput(this.URLInput, value);
+ }
+
+ chooseReason(value) {
+ const item = this.getViewNode(`report-broken-site-popup-reason-${value}`);
+ this.reasonInput.selectedIndex = item.index;
+ }
+
+ dismissDropdownPopup() {
+ const popup = this.reasonDropdownPopup;
+ const menuPromise = BrowserTestUtils.waitForPopupEvent(popup, "hidden");
+ popup.hidePopup();
+ return menuPromise;
+ }
+
+ setDescription(value) {
+ this.#setInput(this.descriptionTextarea, value);
+ }
+
+ isURL(expected) {
+ is(this.URLInput.value, expected);
+ }
+
+ isURLInvalidMessageShown() {
+ ok(
+ BrowserTestUtils.isVisible(this.URLInvalidMessage),
+ "'Please enter a valid URL' message is shown"
+ );
+ }
+
+ isURLInvalidMessageHidden() {
+ ok(
+ !BrowserTestUtils.isVisible(this.URLInvalidMessage),
+ "'Please enter a valid URL' message is hidden"
+ );
+ }
+
+ isReasonNeededMessageShown() {
+ ok(
+ BrowserTestUtils.isVisible(this.reasonRequiredMessage),
+ "'Please choose a reason' message is shown"
+ );
+ }
+
+ isReasonNeededMessageHidden() {
+ ok(
+ !BrowserTestUtils.isVisible(this.reasonRequiredMessage),
+ "'Please choose a reason' message is hidden"
+ );
+ }
+
+ isSendButtonEnabled() {
+ ok(BrowserTestUtils.isVisible(this.sendButton), "Send button is visible");
+ ok(!this.sendButton.disabled, "Send button is enabled");
+ }
+
+ isSendButtonDisabled() {
+ ok(BrowserTestUtils.isVisible(this.sendButton), "Send button is visible");
+ ok(this.sendButton.disabled, "Send button is disabled");
+ }
+
+ isSendMoreInfoShown() {
+ ok(
+ BrowserTestUtils.isVisible(this.sendMoreInfoLink),
+ "send more info is shown"
+ );
+ }
+
+ isSendMoreInfoHidden() {
+ ok(
+ !BrowserTestUtils.isVisible(this.sendMoreInfoLink),
+ "send more info is hidden"
+ );
+ }
+
+ isSendMoreInfoShownOrHiddenAppropriately() {
+ if (Services.prefs.getBoolPref(PREFS.SEND_MORE_INFO)) {
+ this.isSendMoreInfoShown();
+ } else {
+ this.isSendMoreInfoHidden();
+ }
+ }
+
+ isReasonHidden() {
+ ok(
+ !BrowserTestUtils.isVisible(this.reasonInput),
+ "reason drop-down is hidden"
+ );
+ ok(
+ !BrowserTestUtils.isVisible(this.reasonLabelOptional),
+ "optional reason label is hidden"
+ );
+ ok(
+ !BrowserTestUtils.isVisible(this.reasonLabelRequired),
+ "required reason label is hidden"
+ );
+ }
+
+ isReasonRequired() {
+ ok(
+ BrowserTestUtils.isVisible(this.reasonInput),
+ "reason drop-down is shown"
+ );
+ ok(
+ !BrowserTestUtils.isVisible(this.reasonLabelOptional),
+ "optional reason label is hidden"
+ );
+ ok(
+ BrowserTestUtils.isVisible(this.reasonLabelRequired),
+ "required reason label is shown"
+ );
+ }
+
+ isReasonOptional() {
+ ok(
+ BrowserTestUtils.isVisible(this.reasonInput),
+ "reason drop-down is shown"
+ );
+ ok(
+ BrowserTestUtils.isVisible(this.reasonLabelOptional),
+ "optional reason label is shown"
+ );
+ ok(
+ !BrowserTestUtils.isVisible(this.reasonLabelRequired),
+ "required reason label is hidden"
+ );
+ }
+
+ isReasonShownOrHiddenAppropriately() {
+ const pref = Services.prefs.getIntPref(PREFS.REASON);
+ if (pref == 2) {
+ this.isReasonOptional();
+ } else if (pref == 1) {
+ this.isReasonOptional();
+ } else {
+ this.isReasonHidden();
+ }
+ }
+
+ isDescription(expected) {
+ return this.descriptionTextarea.value == expected;
+ }
+
+ isMainViewResetToCurrentTab() {
+ this.isURL(this.win.gBrowser.selectedBrowser.currentURI.spec);
+ this.isDescription("");
+ this.isReasonShownOrHiddenAppropriately();
+ this.isSendMoreInfoShownOrHiddenAppropriately();
+ }
+}
+
+class MenuHelper {
+ menuDescription = undefined;
+
+ win = undefined;
+
+ constructor(win = window) {
+ this.win = win;
+ }
+
+ getViewNode(id) {
+ return PanelMultiView.getViewNode(this.win.document, id);
+ }
+
+ get showsBackButton() {
+ return true;
+ }
+
+ get reportBrokenSite() {
+ throw new Error("Should be defined in derived class");
+ }
+
+ get popup() {
+ throw new Error("Should be defined in derived class");
+ }
+
+ get opened() {
+ return this.popup?.hasAttribute("panelopen");
+ }
+
+ async open() {}
+
+ async close() {}
+
+ isReportBrokenSiteDisabled() {
+ return isMenuItemDisabled(this.reportBrokenSite, this.menuDescription);
+ }
+
+ isReportBrokenSiteEnabled() {
+ return isMenuItemEnabled(this.reportBrokenSite, this.menuDescription);
+ }
+
+ isReportBrokenSiteHidden() {
+ return isMenuItemHidden(this.reportBrokenSite, this.menuDescription);
+ }
+
+ async clickReportBrokenSiteAndAwaitWebCompatTabData() {
+ const newTabPromise = waitForWebcompatComTab(this.win.gBrowser);
+ await this.clickReportBrokenSite();
+ const newTab = await newTabPromise;
+ const receivedData = await SpecialPowers.spawn(
+ newTab.linkedBrowser,
+ [],
+ async function () {
+ await content.wrappedJSObject.messageArrived;
+ return content.wrappedJSObject.message;
+ }
+ );
+
+ this.win.gBrowser.removeCurrentTab();
+ return receivedData;
+ }
+
+ async clickReportBrokenSite() {
+ if (!this.opened) {
+ await this.open();
+ }
+ isMenuItemEnabled(this.reportBrokenSite, this.menuDescription);
+ const rbs = new ReportBrokenSiteHelper(this);
+ await rbs.click(this.reportBrokenSite);
+ return rbs;
+ }
+
+ async openReportBrokenSite() {
+ if (!this.opened) {
+ await this.open();
+ }
+ isMenuItemEnabled(this.reportBrokenSite, this.menuDescription);
+ const rbs = new ReportBrokenSiteHelper(this);
+ await rbs.open(this.reportBrokenSite);
+ return rbs;
+ }
+
+ async openAndPrefillReportBrokenSite(url = null, description = "") {
+ let rbs = await this.openReportBrokenSite();
+ rbs.isMainViewResetToCurrentTab();
+ if (url) {
+ rbs.setURL(url);
+ }
+ if (description) {
+ rbs.setDescription(description);
+ }
+ return rbs;
+ }
+}
+
+class AppMenuHelper extends MenuHelper {
+ menuDescription = "AppMenu";
+
+ get reportBrokenSite() {
+ return this.getViewNode("appMenu-report-broken-site-button");
+ }
+
+ get popup() {
+ return this.win.document.getElementById("appMenu-popup");
+ }
+
+ async open() {
+ await new CustomizableUITestUtils(this.win).openMainMenu();
+ }
+
+ async close() {
+ if (this.opened) {
+ await new CustomizableUITestUtils(this.win).hideMainMenu();
+ }
+ }
+}
+
+class HelpMenuHelper extends MenuHelper {
+ menuDescription = "Help Menu";
+
+ get showsBackButton() {
+ return false;
+ }
+
+ get reportBrokenSite() {
+ return this.win.document.getElementById("help_reportBrokenSite");
+ }
+
+ get popup() {
+ return this.getViewNode("PanelUI-helpView");
+ }
+
+ get helpMenu() {
+ return this.win.document.getElementById("menu_HelpPopup");
+ }
+
+ async openReportBrokenSite() {
+ // We can't actually open the Help menu properly in testing, so the best
+ // we can do to open its Report Broken Site panel is to force its DOM to be
+ // prepared, and then soft-click the Report Broken Site menuitem to open it.
+ await this.open();
+ const shownPromise = BrowserTestUtils.waitForEvent(
+ this.win,
+ "ViewShown",
+ true,
+ e => e.target.classList.contains("report-broken-site-view")
+ );
+ this.reportBrokenSite.click();
+ await shownPromise;
+ return new ReportBrokenSiteHelper(this);
+ }
+
+ async clickReportBrokenSite() {
+ await this.open();
+ this.reportBrokenSite.click();
+ return new ReportBrokenSiteHelper(this);
+ }
+
+ async open() {
+ const { helpMenu } = this;
+ const promise = BrowserTestUtils.waitForEvent(helpMenu, "popupshown");
+
+ // This event-faking method was copied from browser_title_case_menus.js.
+ // We can't actually open the Help menu in testing, but this lets us
+ // force its DOM to be properly built.
+ helpMenu.dispatchEvent(new MouseEvent("popupshowing", { bubbles: true }));
+ helpMenu.dispatchEvent(new MouseEvent("popupshown", { bubbles: true }));
+
+ await promise;
+ }
+
+ async close() {
+ const { helpMenu } = this;
+ const promise = BrowserTestUtils.waitForPopupEvent(helpMenu, "hidden");
+
+ // (Also copied from browser_title_case_menus.js)
+ // Just for good measure, we'll fire the popuphiding/popuphidden events
+ // after we close the menupopups.
+ helpMenu.dispatchEvent(new MouseEvent("popuphiding", { bubbles: true }));
+ helpMenu.dispatchEvent(new MouseEvent("popuphidden", { bubbles: true }));
+
+ await promise;
+ }
+}
+
+class ProtectionsPanelHelper extends MenuHelper {
+ menuDescription = "Protections Panel";
+
+ get reportBrokenSite() {
+ this.win.gProtectionsHandler._initializePopup();
+ return this.getViewNode("protections-popup-report-broken-site-button");
+ }
+
+ get popup() {
+ this.win.gProtectionsHandler._initializePopup();
+ return this.win.document.getElementById("protections-popup");
+ }
+
+ async open() {
+ const promise = BrowserTestUtils.waitForEvent(
+ this.win,
+ "popupshown",
+ true,
+ e => e.target.id == "protections-popup"
+ );
+ this.win.gProtectionsHandler.showProtectionsPopup();
+ await promise;
+ }
+
+ async close() {
+ if (this.opened) {
+ const popup = this.popup;
+ const promise = BrowserTestUtils.waitForPopupEvent(popup, "hidden");
+ PanelMultiView.hidePopup(popup, false);
+ await promise;
+ }
+ }
+}
+
+function AppMenu(win = window) {
+ return new AppMenuHelper(win);
+}
+
+function HelpMenu(win = window) {
+ return new HelpMenuHelper(win);
+}
+
+function ProtectionsPanel(win = window) {
+ return new ProtectionsPanelHelper(win);
+}
+
+function pressKeyAndAwait(event, key, config = {}) {
+ const win = config.window || window;
+ if (!event.then) {
+ event = BrowserTestUtils.waitForEvent(win, event, config.timeout || 200);
+ }
+ EventUtils.synthesizeKey(key, config, win);
+ return event;
+}
+
+async function pressKeyAndGetFocus(key, config = {}) {
+ return (await pressKeyAndAwait("focus", key, config)).target;
+}
+
+async function tabTo(match, win = window) {
+ const config = { window: win };
+ const { activeElement } = win.document;
+ if (activeElement?.matches(match)) {
+ return activeElement;
+ }
+ let initial = await pressKeyAndGetFocus("VK_TAB", config);
+ let target = initial;
+ do {
+ if (target.matches(match)) {
+ return target;
+ }
+ target = await pressKeyAndGetFocus("VK_TAB", config);
+ } while (target && target !== initial);
+ return undefined;
+}
+
+function filterFrameworkDetectorFails(ping, expected) {
+ // the framework detector's frame-script may fail to run in low memory or other
+ // weird corner-cases, so we ignore the results in that case if they don't match.
+ if (!areObjectsEqual(ping.frameworks, expected.frameworks)) {
+ const { fastclick, mobify, marfeel } = ping.frameworks;
+ if (!fastclick && !mobify && !marfeel) {
+ console.info("Ignoring failure to get framework data");
+ expected.frameworks = ping.frameworks;
+ }
+ }
+}
+
+async function setupStrictETP() {
+ await UrlClassifierTestUtils.addTestTrackers();
+ registerCleanupFunction(() => {
+ UrlClassifierTestUtils.cleanupTestTrackers();
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.mixed_content.block_active_content", true],
+ ["security.mixed_content.block_display_content", true],
+ ["security.mixed_content.upgrade_display_content", false],
+ [
+ "urlclassifier.trackingTable",
+ "content-track-digest256,mochitest2-track-simple",
+ ],
+ ["browser.contentblocking.category", "strict"],
+ ],
+ });
+}
+
+// copied from browser/base/content/test/protectionsUI/head.js
+function waitForContentBlockingEvent(numChanges = 1, win = null) {
+ if (!win) {
+ win = window;
+ }
+ return new Promise(resolve => {
+ let n = 0;
+ let listener = {
+ onContentBlockingEvent(webProgress, request, event) {
+ n = n + 1;
+ info(
+ `Received onContentBlockingEvent event: ${event} (${n} of ${numChanges})`
+ );
+ if (n >= numChanges) {
+ win.gBrowser.removeProgressListener(listener);
+ resolve(n);
+ }
+ },
+ };
+ win.gBrowser.addProgressListener(listener);
+ });
+}
diff --git a/src/zen/tests/mochitests/reportbrokensite/send.js b/src/zen/tests/mochitests/reportbrokensite/send.js
new file mode 100644
index 000000000..5083868cf
--- /dev/null
+++ b/src/zen/tests/mochitests/reportbrokensite/send.js
@@ -0,0 +1,355 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Helper methods for testing sending reports with
+ * the Report Broken Site feature.
+ */
+
+/* import-globals-from head.js */
+
+"use strict";
+
+const { Troubleshoot } = ChromeUtils.importESModule(
+ "resource://gre/modules/Troubleshoot.sys.mjs"
+);
+
+function getSysinfoProperty(propertyName, defaultValue) {
+ try {
+ return Services.sysinfo.getProperty(propertyName);
+ } catch (e) {}
+ return defaultValue;
+}
+
+function securityStringToArray(str) {
+ return str ? str.split(";") : null;
+}
+
+function getExpectedGraphicsDevices(snapshot) {
+ const { graphics } = snapshot;
+ return [
+ graphics.adapterDeviceID,
+ graphics.adapterVendorID,
+ graphics.adapterDeviceID2,
+ graphics.adapterVendorID2,
+ ]
+ .filter(i => i)
+ .sort();
+}
+
+function compareGraphicsDevices(expected, rawActual) {
+ const actual = rawActual
+ .map(({ deviceID, vendorID }) => [deviceID, vendorID])
+ .flat()
+ .filter(i => i)
+ .sort();
+ return areObjectsEqual(actual, expected);
+}
+
+function getExpectedGraphicsDrivers(snapshot) {
+ const { graphics } = snapshot;
+ const expected = [];
+ for (let i = 1; i < 3; ++i) {
+ const version = graphics[`webgl${i}Version`];
+ if (version && version != "-") {
+ expected.push(graphics[`webgl${i}Renderer`]);
+ expected.push(version);
+ }
+ }
+ return expected.filter(i => i).sort();
+}
+
+function compareGraphicsDrivers(expected, rawActual) {
+ const actual = rawActual
+ .map(({ renderer, version }) => [renderer, version])
+ .flat()
+ .filter(i => i)
+ .sort();
+ return areObjectsEqual(actual, expected);
+}
+
+function getExpectedGraphicsFeatures(snapshot) {
+ const expected = {};
+ for (let { name, log, status } of snapshot.graphics.featureLog.features) {
+ for (const item of log?.reverse() ?? []) {
+ if (item.failureId && item.status == status) {
+ status = `${status} (${item.message || item.failureId})`;
+ }
+ }
+ expected[name] = status;
+ }
+ return expected;
+}
+
+async function getExpectedWebCompatInfo(tab, snapshot, fullAppData = false) {
+ const gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+
+ const { application, graphics, intl, securitySoftware } = snapshot;
+
+ const { fissionAutoStart, memorySizeBytes, updateChannel, userAgent } =
+ application;
+
+ const app = {
+ defaultLocales: intl.localeService.available,
+ defaultUseragentString: userAgent,
+ fissionEnabled: fissionAutoStart,
+ };
+ if (fullAppData) {
+ app.applicationName = application.name;
+ app.osArchitecture = getSysinfoProperty("arch", null);
+ app.osName = getSysinfoProperty("name", null);
+ app.osVersion = getSysinfoProperty("version", null);
+ app.updateChannel = updateChannel;
+ app.version = application.version;
+ }
+
+ const hasTouchScreen = graphics.info.ApzTouchInput == 1;
+
+ const { registeredAntiVirus, registeredAntiSpyware, registeredFirewall } =
+ securitySoftware;
+
+ const browserInfo = {
+ addons: [],
+ app,
+ experiments: [],
+ graphics: {
+ devicesJson(actualStr) {
+ const expected = getExpectedGraphicsDevices(snapshot);
+ // If undefined is saved to the Glean value here, we'll get the string "undefined" (invalid JSON).
+ // We should stop using JSON like this in bug 1875185.
+ if (!actualStr || actualStr == "undefined") {
+ return !expected.length;
+ }
+ return compareGraphicsDevices(expected, JSON.parse(actualStr));
+ },
+ driversJson(actualStr) {
+ const expected = getExpectedGraphicsDrivers(snapshot);
+ // If undefined is saved to the Glean value here, we'll get the string "undefined" (invalid JSON).
+ // We should stop using JSON like this in bug 1875185.
+ if (!actualStr || actualStr == "undefined") {
+ return !expected.length;
+ }
+ return compareGraphicsDrivers(expected, JSON.parse(actualStr));
+ },
+ featuresJson(actualStr) {
+ const expected = getExpectedGraphicsFeatures(snapshot);
+ // If undefined is saved to the Glean value here, we'll get the string "undefined" (invalid JSON).
+ // We should stop using JSON like this in bug 1875185.
+ if (!actualStr || actualStr == "undefined") {
+ return !expected.length;
+ }
+ return areObjectsEqual(JSON.parse(actualStr), expected);
+ },
+ hasTouchScreen,
+ monitorsJson(actualStr) {
+ const expected = gfxInfo.getMonitors();
+ // If undefined is saved to the Glean value here, we'll get the string "undefined" (invalid JSON).
+ // We should stop using JSON like this in bug 1875185.
+ if (!actualStr || actualStr == "undefined") {
+ return !expected.length;
+ }
+ return areObjectsEqual(JSON.parse(actualStr), expected);
+ },
+ },
+ prefs: {
+ cookieBehavior: Services.prefs.getIntPref(
+ "network.cookie.cookieBehavior",
+ -1
+ ),
+ forcedAcceleratedLayers: Services.prefs.getBoolPref(
+ "layers.acceleration.force-enabled",
+ false
+ ),
+ globalPrivacyControlEnabled: Services.prefs.getBoolPref(
+ "privacy.globalprivacycontrol.enabled",
+ false
+ ),
+ installtriggerEnabled: Services.prefs.getBoolPref(
+ "extensions.InstallTrigger.enabled",
+ false
+ ),
+ opaqueResponseBlocking: Services.prefs.getBoolPref(
+ "browser.opaqueResponseBlocking",
+ false
+ ),
+ resistFingerprintingEnabled: Services.prefs.getBoolPref(
+ "privacy.resistFingerprinting",
+ false
+ ),
+ softwareWebrender: Services.prefs.getBoolPref(
+ "gfx.webrender.software",
+ false
+ ),
+ thirdPartyCookieBlockingEnabled: Services.prefs.getBoolPref(
+ "network.cookie.cookieBehavior.optInPartitioning",
+ false
+ ),
+ thirdPartyCookieBlockingEnabledInPbm: Services.prefs.getBoolPref(
+ "network.cookie.cookieBehavior.optInPartitioning.pbmode",
+ false
+ ),
+ },
+ security: {
+ antispyware: securityStringToArray(registeredAntiSpyware),
+ antivirus: securityStringToArray(registeredAntiVirus),
+ firewall: securityStringToArray(registeredFirewall),
+ },
+ system: {
+ isTablet: getSysinfoProperty("tablet", false),
+ memory: Math.round(memorySizeBytes / 1024 / 1024),
+ },
+ };
+
+ const tabInfo = await tab.linkedBrowser.ownerGlobal.SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ async function () {
+ return {
+ devicePixelRatio: `${content.devicePixelRatio}`,
+ antitracking: {
+ blockList: "basic",
+ blockedOrigins: null,
+ isPrivateBrowsing: false,
+ hasTrackingContentBlocked: false,
+ hasMixedActiveContentBlocked: false,
+ hasMixedDisplayContentBlocked: false,
+ btpHasPurgedSite: false,
+ etpCategory: "standard",
+ },
+ frameworks: {
+ fastclick: false,
+ marfeel: false,
+ mobify: false,
+ },
+ languages: content.navigator.languages,
+ useragentString: content.navigator.userAgent,
+ };
+ }
+ );
+
+ browserInfo.graphics.devicePixelRatio = tabInfo.devicePixelRatio;
+ delete tabInfo.devicePixelRatio;
+
+ return { browserInfo, tabInfo };
+}
+
+function extractPingData(branch) {
+ const data = {};
+ for (const [name, value] of Object.entries(branch)) {
+ data[name] = value.testGetValue();
+ }
+ return data;
+}
+
+function extractBrokenSiteReportFromGleanPing(Glean) {
+ const ping = extractPingData(Glean.brokenSiteReport);
+ ping.tabInfo = extractPingData(Glean.brokenSiteReportTabInfo);
+ ping.tabInfo.antitracking = extractPingData(
+ Glean.brokenSiteReportTabInfoAntitracking
+ );
+ ping.tabInfo.frameworks = extractPingData(
+ Glean.brokenSiteReportTabInfoFrameworks
+ );
+ ping.browserInfo = {
+ addons: Array.from(Glean.brokenSiteReportBrowserInfo.addons.testGetValue()),
+ app: extractPingData(Glean.brokenSiteReportBrowserInfoApp),
+ graphics: extractPingData(Glean.brokenSiteReportBrowserInfoGraphics),
+ experiments: Array.from(
+ Glean.brokenSiteReportBrowserInfo.experiments.testGetValue()
+ ),
+ prefs: extractPingData(Glean.brokenSiteReportBrowserInfoPrefs),
+ security: extractPingData(Glean.brokenSiteReportBrowserInfoSecurity),
+ system: extractPingData(Glean.brokenSiteReportBrowserInfoSystem),
+ };
+ return ping;
+}
+
+async function testSend(tab, menu, expectedOverrides = {}) {
+ const url = expectedOverrides.url ?? menu.win.gBrowser.currentURI.spec;
+ const description = expectedOverrides.description ?? "";
+ const breakageCategory = expectedOverrides.breakageCategory ?? null;
+
+ let rbs = await menu.openAndPrefillReportBrokenSite(url, description);
+
+ const snapshot = await Troubleshoot.snapshot();
+ const expected = await getExpectedWebCompatInfo(tab, snapshot);
+
+ expected.url = url;
+ expected.description = description;
+ expected.breakageCategory = breakageCategory;
+
+ if (expectedOverrides.addons) {
+ expected.browserInfo.addons = expectedOverrides.addons;
+ }
+
+ if (expectedOverrides.experiments) {
+ expected.browserInfo.experiments = expectedOverrides.experiments;
+ }
+
+ if (expectedOverrides.antitracking) {
+ expected.tabInfo.antitracking = expectedOverrides.antitracking;
+
+ if (expectedOverrides.antitracking.blockedOrigins) {
+ rbs.blockedTrackersCheckbox = true;
+ }
+ }
+
+ if (expectedOverrides.frameworks) {
+ expected.tabInfo.frameworks = expectedOverrides.frameworks;
+ }
+
+ if (breakageCategory) {
+ rbs.chooseReason(breakageCategory);
+ }
+
+ Services.fog.testResetFOG();
+ await GleanPings.brokenSiteReport.testSubmission(
+ () => {
+ const ping = extractBrokenSiteReportFromGleanPing(Glean);
+
+ // sanity checks
+ const { browserInfo, tabInfo } = ping;
+ ok(ping.url?.length, "Got a URL");
+ ok(
+ ["basic", "strict"].includes(tabInfo.antitracking.blockList),
+ "Got a blockList"
+ );
+ if (rbs.blockedTrackersCheckbox.checked) {
+ ok(
+ Array.isArray(tabInfo.antitracking.blockedOrigins),
+ "Got an array for blockedOrigins"
+ );
+ } else {
+ ok(!tabInfo.antitracking.blockedOrigins, "No blockedOrigins included");
+ }
+ ok(tabInfo.useragentString?.length, "Got a final UA string");
+ ok(
+ browserInfo.app.defaultUseragentString?.length,
+ "Got a default UA string"
+ );
+
+ filterFrameworkDetectorFails(ping.tabInfo, expected.tabInfo);
+
+ ok(areObjectsEqual(ping, expected), "ping matches expectations");
+ },
+ () => rbs.clickSend()
+ );
+
+ await rbs.clickOkay();
+
+ const telemetry = Glean.webcompatreporting.send.testGetValue();
+ is(telemetry?.length, 1, "Got a 'send' telemetry event");
+ is(
+ telemetry[0].extra.sent_with_blocked_trackers,
+ String(!!expectedOverrides.antitracking?.blockedOrigins),
+ "Got correct 'sent_with_blocked_trackers' flag"
+ );
+
+ // re-opening the panel, the url and description should be reset
+ rbs = await menu.openReportBrokenSite();
+ rbs.isMainViewResetToCurrentTab();
+ ok(
+ !rbs.blockedTrackersCheckbox.checked,
+ "blocked trackers checkbox is reset"
+ );
+ rbs.close();
+}
diff --git a/src/zen/tests/mochitests/reportbrokensite/sendMoreInfoTestEndpoint.html b/src/zen/tests/mochitests/reportbrokensite/sendMoreInfoTestEndpoint.html
new file mode 100644
index 000000000..39b1b3d25
--- /dev/null
+++ b/src/zen/tests/mochitests/reportbrokensite/sendMoreInfoTestEndpoint.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/zen/tests/mochitests/reportbrokensite/send_more_info.js b/src/zen/tests/mochitests/reportbrokensite/send_more_info.js
new file mode 100644
index 000000000..f5d28248d
--- /dev/null
+++ b/src/zen/tests/mochitests/reportbrokensite/send_more_info.js
@@ -0,0 +1,307 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Helper methods for testing the "send more info" link
+ * of the Report Broken Site feature.
+ */
+
+/* import-globals-from head.js */
+/* import-globals-from send.js */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ getRootDirectory(gTestPath) + "send.js",
+ this
+);
+
+async function reformatExpectedWebCompatInfo(tab, overrides) {
+ const gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+ const snapshot = await Troubleshoot.snapshot();
+ const expected = await getExpectedWebCompatInfo(tab, snapshot, true);
+ const { browserInfo, tabInfo } = expected;
+ const { app, graphics, prefs, security } = browserInfo;
+ const {
+ applicationName,
+ defaultUseragentString,
+ fissionEnabled,
+ osArchitecture,
+ osName,
+ osVersion,
+ updateChannel,
+ version,
+ } = app;
+ const { devicePixelRatio, hasTouchScreen } = graphics;
+ const { antitracking, languages, useragentString } = tabInfo;
+
+ const addons = overrides.addons || [];
+ const experiments = overrides.experiments || [];
+ const atOverrides = overrides.antitracking;
+ const blockList = atOverrides?.blockList ?? antitracking.blockList;
+ const blockedOrigins =
+ atOverrides?.blockedOrigins ?? antitracking.blockedOrigins ?? [];
+ const hasMixedActiveContentBlocked =
+ atOverrides?.hasMixedActiveContentBlocked ??
+ antitracking.hasMixedActiveContentBlocked;
+ const hasMixedDisplayContentBlocked =
+ atOverrides?.hasMixedDisplayContentBlocked ??
+ antitracking.hasMixedDisplayContentBlocked;
+ const hasTrackingContentBlocked =
+ atOverrides?.hasTrackingContentBlocked ??
+ antitracking.hasTrackingContentBlocked;
+ const isPrivateBrowsing =
+ atOverrides?.isPrivateBrowsing ?? antitracking.isPrivateBrowsing;
+ const btpHasPurgedSite =
+ atOverrides?.btpHasPurgedSite ?? antitracking.btpHasPurgedSite;
+ const etpCategory = atOverrides?.etpCategory ?? antitracking.etpCategory;
+
+ const extra_labels = [];
+ const frameworks = overrides.frameworks ?? {
+ fastclick: false,
+ mobify: false,
+ marfeel: false,
+ };
+
+ // ignore the console log unless explicily testing for it.
+ const consoleLog = overrides.consoleLog ?? (() => true);
+
+ const finalPrefs = {};
+ for (const [key, pref] of Object.entries({
+ cookieBehavior: "network.cookie.cookieBehavior",
+ forcedAcceleratedLayers: "layers.acceleration.force-enabled",
+ globalPrivacyControlEnabled: "privacy.globalprivacycontrol.enabled",
+ installtriggerEnabled: "extensions.InstallTrigger.enabled",
+ opaqueResponseBlocking: "browser.opaqueResponseBlocking",
+ resistFingerprintingEnabled: "privacy.resistFingerprinting",
+ softwareWebrender: "gfx.webrender.software",
+ thirdPartyCookieBlockingEnabled:
+ "network.cookie.cookieBehavior.optInPartitioning",
+ thirdPartyCookieBlockingEnabledInPbm:
+ "network.cookie.cookieBehavior.optInPartitioning.pbmode",
+ })) {
+ if (key in prefs) {
+ finalPrefs[pref] = prefs[key];
+ }
+ }
+
+ const reformatted = {
+ blockList,
+ details: {
+ additionalData: {
+ addons,
+ applicationName,
+ blockList,
+ blockedOrigins,
+ buildId: snapshot.application.buildID,
+ devicePixelRatio: parseInt(devicePixelRatio),
+ experiments,
+ finalUserAgent: useragentString,
+ fissionEnabled,
+ gfxData: {
+ devices(actual) {
+ const devices = getExpectedGraphicsDevices(snapshot);
+ return compareGraphicsDevices(devices, actual);
+ },
+ drivers(actual) {
+ const drvs = getExpectedGraphicsDrivers(snapshot);
+ return compareGraphicsDrivers(drvs, actual);
+ },
+ features(actual) {
+ const features = getExpectedGraphicsFeatures(snapshot);
+ return areObjectsEqual(actual, features);
+ },
+ hasTouchScreen,
+ monitors(actual) {
+ return areObjectsEqual(actual, gfxInfo.getMonitors());
+ },
+ },
+ hasMixedActiveContentBlocked,
+ hasMixedDisplayContentBlocked,
+ hasTrackingContentBlocked,
+ btpHasPurgedSite,
+ isPB: isPrivateBrowsing,
+ etpCategory,
+ languages,
+ locales: snapshot.intl.localeService.available,
+ memoryMB: browserInfo.system.memory,
+ osArchitecture,
+ osName,
+ osVersion,
+ prefs: finalPrefs,
+ version,
+ },
+ blockList,
+ channel: updateChannel,
+ consoleLog,
+ defaultUserAgent: defaultUseragentString,
+ frameworks,
+ hasTouchScreen,
+ "gfx.webrender.software": prefs.softwareWebrender,
+ "mixed active content blocked": hasMixedActiveContentBlocked,
+ "mixed passive content blocked": hasMixedDisplayContentBlocked,
+ "tracking content blocked": hasTrackingContentBlocked
+ ? `true (${blockList})`
+ : "false",
+ "btp has purged site": btpHasPurgedSite,
+ },
+ extra_labels,
+ src: "desktop-reporter",
+ utm_campaign: "report-broken-site",
+ utm_source: "desktop-reporter",
+ };
+
+ const { gfxData } = reformatted.details.additionalData;
+ for (const optional of [
+ "directWriteEnabled",
+ "directWriteVersion",
+ "clearTypeParameters",
+ "targetFrameRate",
+ ]) {
+ if (optional in snapshot.graphics) {
+ gfxData[optional] = snapshot.graphics[optional];
+ }
+ }
+
+ // We only care about this pref on Linux right now on webcompat.com.
+ if (AppConstants.platform != "linux") {
+ delete finalPrefs["layers.acceleration.force-enabled"];
+ } else {
+ reformatted.details["layers.acceleration.force-enabled"] =
+ finalPrefs["layers.acceleration.force-enabled"];
+ }
+
+ // Only bother adding the security key if it has any data
+ if (Object.values(security).filter(e => e).length) {
+ reformatted.details.additionalData.sec = security;
+ }
+
+ const expectedCodecs = snapshot.media.codecSupportInfo
+ .replaceAll(" NONE", "")
+ .split("\n")
+ .sort()
+ .join("\n");
+ if (expectedCodecs) {
+ reformatted.details.additionalData.gfxData.codecSupport = rawActual => {
+ const actual = Object.entries(rawActual)
+ .map(
+ ([
+ name,
+ { hardwareDecode, softwareDecode, hardwareEncode, softwareEncode },
+ ]) =>
+ (
+ `${name} ` +
+ `${softwareDecode ? "SWDEC " : ""}` +
+ `${hardwareDecode ? "HWDEC " : ""}` +
+ `${softwareEncode ? "SWENC " : ""}` +
+ `${hardwareEncode ? "HWENC " : ""}`
+ ).trim()
+ )
+ .sort()
+ .join("\n");
+ return areObjectsEqual(actual, expectedCodecs);
+ };
+ }
+
+ if (blockList != "basic") {
+ extra_labels.push(`type-tracking-protection-${blockList}`);
+ }
+
+ if (overrides.expectNoTabDetails) {
+ delete reformatted.details.frameworks;
+ delete reformatted.details.consoleLog;
+ delete reformatted.details["mixed active content blocked"];
+ delete reformatted.details["mixed passive content blocked"];
+ delete reformatted.details["tracking content blocked"];
+ delete reformatted.details["btp has purged site"];
+ } else {
+ const { fastclick, mobify, marfeel } = frameworks;
+ if (fastclick) {
+ extra_labels.push("type-fastclick");
+ reformatted.details.fastclick = true;
+ }
+ if (mobify) {
+ extra_labels.push("type-mobify");
+ reformatted.details.mobify = true;
+ }
+ if (marfeel) {
+ extra_labels.push("type-marfeel");
+ reformatted.details.marfeel = true;
+ }
+ }
+
+ extra_labels.sort();
+
+ return reformatted;
+}
+
+async function testSendMoreInfo(tab, menu, expectedOverrides = {}) {
+ const url = expectedOverrides.url ?? menu.win.gBrowser.currentURI.spec;
+ const description = expectedOverrides.description ?? "";
+
+ let rbs = await menu.openAndPrefillReportBrokenSite(url, description);
+
+ const receivedData = await rbs.clickSendMoreInfo();
+ await checkWebcompatComPayload(
+ tab,
+ url,
+ description,
+ expectedOverrides,
+ receivedData
+ );
+
+ // re-opening the panel, the url and description should be reset
+ rbs = await menu.openReportBrokenSite();
+ rbs.isMainViewResetToCurrentTab();
+ rbs.close();
+}
+
+async function testWebcompatComFallback(tab, menu) {
+ const url = menu.win.gBrowser.currentURI.spec;
+ const receivedData =
+ await menu.clickReportBrokenSiteAndAwaitWebCompatTabData();
+ await checkWebcompatComPayload(tab, url, "", {}, receivedData);
+ menu.close();
+}
+
+async function checkWebcompatComPayload(
+ tab,
+ url,
+ description,
+ expectedOverrides,
+ receivedData
+) {
+ const expected = await reformatExpectedWebCompatInfo(tab, expectedOverrides);
+ expected.url = url;
+ expected.description = description;
+
+ // sanity checks
+ const { message } = receivedData;
+ const { details } = message;
+ const { additionalData } = details;
+ ok(message.url?.length, "Got a URL");
+ ok(["basic", "strict"].includes(details.blockList), "Got a blockList");
+ ok(additionalData.applicationName?.length, "Got an app name");
+ ok(additionalData.osArchitecture?.length, "Got an OS arch");
+ ok(additionalData.osName?.length, "Got an OS name");
+ ok(additionalData.osVersion?.length, "Got an OS version");
+ ok(additionalData.version?.length, "Got an app version");
+ ok(details.channel?.length, "Got an app channel");
+ ok(details.defaultUserAgent?.length, "Got a default UA string");
+ ok(additionalData.finalUserAgent?.length, "Got a final UA string");
+
+ // If we're sending any tab-specific data (which includes console logs),
+ // check that there is also a valid screenshot.
+ if ("consoleLog" in details) {
+ const isScreenshotValid = await new Promise(done => {
+ var image = new Image();
+ image.onload = () => done(image.width > 0);
+ image.onerror = () => done(false);
+ image.src = receivedData.screenshot;
+ });
+ ok(isScreenshotValid, "Got a valid screenshot");
+ }
+
+ filterFrameworkDetectorFails(message.details, expected.details);
+
+ ok(areObjectsEqual(message, expected), "sent info matches expectations");
+}
diff --git a/src/zen/tests/mochitests/safebrowsing/browser.toml b/src/zen/tests/mochitests/safebrowsing/browser.toml
new file mode 100644
index 000000000..8745e7efc
--- /dev/null
+++ b/src/zen/tests/mochitests/safebrowsing/browser.toml
@@ -0,0 +1,14 @@
+[DEFAULT]
+support-files = [
+ "head.js",
+ "empty_file.html",
+]
+
+["browser_bug400731.js"]
+
+["browser_bug415846.js"]
+skip-if = ["true"] # Bug 1248632
+
+["browser_mixedcontent_aboutblocked.js"]
+
+["browser_whitelisted.js"]
diff --git a/src/zen/tests/mochitests/safebrowsing/browser_bug400731.js b/src/zen/tests/mochitests/safebrowsing/browser_bug400731.js
new file mode 100644
index 000000000..e3861bc7a
--- /dev/null
+++ b/src/zen/tests/mochitests/safebrowsing/browser_bug400731.js
@@ -0,0 +1,65 @@
+/* Check presence of the "Ignore this warning" button */
+
+function checkWarningState() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ return !!content.document.getElementById("ignore_warning_link");
+ });
+}
+
+add_task(async function testMalware() {
+ await new Promise(resolve => waitForDBInit(resolve));
+
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+
+ const url = "http://www.itisatrap.org/firefox/its-an-attack.html";
+ BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, url);
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ url,
+ true
+ );
+
+ let buttonPresent = await checkWarningState();
+ ok(buttonPresent, "Ignore warning link should be present for malware");
+});
+
+add_task(async function testUnwanted() {
+ Services.prefs.setBoolPref("browser.safebrowsing.allowOverride", false);
+
+ // Now launch the unwanted software test
+ const url = "http://www.itisatrap.org/firefox/unwanted.html";
+ BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, url);
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ url,
+ true
+ );
+
+ // Confirm that "Ignore this warning" is visible - bug 422410
+ let buttonPresent = await checkWarningState();
+ ok(
+ !buttonPresent,
+ "Ignore warning link should be missing for unwanted software"
+ );
+});
+
+add_task(async function testPhishing() {
+ Services.prefs.setBoolPref("browser.safebrowsing.allowOverride", true);
+
+ // Now launch the phishing test
+ const url = "http://www.itisatrap.org/firefox/its-a-trap.html";
+ BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, url);
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ url,
+ true
+ );
+
+ let buttonPresent = await checkWarningState();
+ ok(buttonPresent, "Ignore warning link should be present for phishing");
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/src/zen/tests/mochitests/safebrowsing/browser_bug415846.js b/src/zen/tests/mochitests/safebrowsing/browser_bug415846.js
new file mode 100644
index 000000000..8cd2ee36f
--- /dev/null
+++ b/src/zen/tests/mochitests/safebrowsing/browser_bug415846.js
@@ -0,0 +1,98 @@
+/* Check for the correct behaviour of the report web forgery/not a web forgery
+menu items.
+
+Mac makes this astonishingly painful to test since their help menu is special magic,
+but we can at least test it on the other platforms.*/
+
+const NORMAL_PAGE = "http://example.com";
+const PHISH_PAGE = "http://www.itisatrap.org/firefox/its-a-trap.html";
+
+/**
+ * Opens a new tab and browses to some URL, tests for the existence
+ * of the phishing menu items, and then runs a test function to check
+ * the state of the menu once opened. This function will take care of
+ * opening and closing the menu.
+ *
+ * @param url (string)
+ * The URL to browse the tab to.
+ * @param testFn (function)
+ * The function to run once the menu has been opened. This
+ * function will be passed the "reportMenu" and "errorMenu"
+ * DOM nodes as arguments, in that order. This function
+ * should not yield anything.
+ * @returns Promise
+ */
+function check_menu_at_page(url, testFn) {
+ return BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:blank",
+ },
+ async function (browser) {
+ // We don't get load events when the DocShell redirects to error
+ // pages, but we do get DOMContentLoaded, so we'll wait for that.
+ let dclPromise = SpecialPowers.spawn(browser, [], async function () {
+ await ContentTaskUtils.waitForEvent(this, "DOMContentLoaded", false);
+ });
+ BrowserTestUtils.startLoadingURIString(browser, url);
+ await dclPromise;
+
+ let menu = document.getElementById("menu_HelpPopup");
+ ok(menu, "Help menu should exist");
+
+ let reportMenu = document.getElementById(
+ "menu_HelpPopup_reportPhishingtoolmenu"
+ );
+ ok(reportMenu, "Report phishing menu item should exist");
+
+ let errorMenu = document.getElementById(
+ "menu_HelpPopup_reportPhishingErrortoolmenu"
+ );
+ ok(errorMenu, "Report phishing error menu item should exist");
+
+ let menuOpen = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ menu.openPopup(null, "", 0, 0, false, null);
+ await menuOpen;
+
+ testFn(reportMenu, errorMenu);
+
+ let menuClose = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ menu.hidePopup();
+ await menuClose;
+ }
+ );
+}
+
+/**
+ * Tests that we show the "Report this page" menu item at a normal
+ * page.
+ */
+add_task(async function () {
+ await check_menu_at_page(NORMAL_PAGE, (reportMenu, errorMenu) => {
+ ok(
+ !reportMenu.hidden,
+ "Report phishing menu should be visible on normal sites"
+ );
+ ok(
+ errorMenu.hidden,
+ "Report error menu item should be hidden on normal sites"
+ );
+ });
+});
+
+/**
+ * Tests that we show the "Report this page is okay" menu item at
+ * a reported attack site.
+ */
+add_task(async function () {
+ await check_menu_at_page(PHISH_PAGE, (reportMenu, errorMenu) => {
+ ok(
+ reportMenu.hidden,
+ "Report phishing menu should be hidden on phishing sites"
+ );
+ ok(
+ !errorMenu.hidden,
+ "Report error menu item should be visible on phishing sites"
+ );
+ });
+});
diff --git a/src/zen/tests/mochitests/safebrowsing/browser_mixedcontent_aboutblocked.js b/src/zen/tests/mochitests/safebrowsing/browser_mixedcontent_aboutblocked.js
new file mode 100644
index 000000000..5057cef19
--- /dev/null
+++ b/src/zen/tests/mochitests/safebrowsing/browser_mixedcontent_aboutblocked.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const SECURE_CONTAINER_URL =
+ "https://example.com/browser/browser/components/safebrowsing/content/test/empty_file.html";
+
+add_task(async function testNormalBrowsing() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.safebrowsing.only_top_level", false]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ SECURE_CONTAINER_URL,
+ async function (browser) {
+ // Before we load the phish url, we have to make sure the hard-coded
+ // black list has been added to the database.
+ await new Promise(resolve => waitForDBInit(resolve));
+
+ let promise = new Promise(resolve => {
+ // Register listener before loading phish URL.
+ let removeFunc = BrowserTestUtils.addContentEventListener(
+ browser,
+ "AboutBlockedLoaded",
+ () => {
+ removeFunc();
+ resolve();
+ },
+ { wantUntrusted: true }
+ );
+ });
+
+ await SpecialPowers.spawn(
+ browser,
+ [PHISH_URL],
+ async function (aPhishUrl) {
+ // Create an iframe which is going to load a phish url.
+ let iframe = content.document.createElement("iframe");
+ iframe.src = aPhishUrl;
+ content.document.body.appendChild(iframe);
+ }
+ );
+
+ await promise;
+ ok(true, "about:blocked is successfully loaded!");
+ }
+ );
+});
diff --git a/src/zen/tests/mochitests/safebrowsing/browser_whitelisted.js b/src/zen/tests/mochitests/safebrowsing/browser_whitelisted.js
new file mode 100644
index 000000000..eb217d618
--- /dev/null
+++ b/src/zen/tests/mochitests/safebrowsing/browser_whitelisted.js
@@ -0,0 +1,46 @@
+/* Ensure that hostnames in the whitelisted pref are not blocked. */
+
+const PREF_WHITELISTED_HOSTNAMES = "urlclassifier.skipHostnames";
+const TEST_PAGE = "http://www.itisatrap.org/firefox/its-an-attack.html";
+var tabbrowser = null;
+
+registerCleanupFunction(function () {
+ tabbrowser = null;
+ Services.prefs.clearUserPref(PREF_WHITELISTED_HOSTNAMES);
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeCurrentTab();
+ }
+});
+
+function testBlockedPage() {
+ info("Non-whitelisted pages must be blocked");
+ ok(true, "about:blocked was shown");
+}
+
+function testWhitelistedPage(window) {
+ info("Whitelisted pages must be skipped");
+ var getmeout_button = window.document.getElementById("getMeOutButton");
+ var ignorewarning_button = window.document.getElementById(
+ "ignoreWarningButton"
+ );
+ ok(!getmeout_button, "GetMeOut button not present");
+ ok(!ignorewarning_button, "IgnoreWarning button not present");
+}
+
+add_task(async function testNormalBrowsing() {
+ tabbrowser = gBrowser;
+ let tab = (tabbrowser.selectedTab = BrowserTestUtils.addTab(tabbrowser));
+
+ info("Load a test page that's whitelisted");
+ Services.prefs.setCharPref(
+ PREF_WHITELISTED_HOSTNAMES,
+ "example.com,www.ItIsaTrap.org,example.net"
+ );
+ await promiseTabLoadEvent(tab, TEST_PAGE, "load");
+ testWhitelistedPage(tab.ownerGlobal);
+
+ info("Load a test page that's no longer whitelisted");
+ Services.prefs.setCharPref(PREF_WHITELISTED_HOSTNAMES, "");
+ await promiseTabLoadEvent(tab, TEST_PAGE, "AboutBlockedLoaded");
+ testBlockedPage(tab.ownerGlobal);
+});
diff --git a/src/zen/tests/mochitests/safebrowsing/empty_file.html b/src/zen/tests/mochitests/safebrowsing/empty_file.html
new file mode 100644
index 000000000..0dc101b53
--- /dev/null
+++ b/src/zen/tests/mochitests/safebrowsing/empty_file.html
@@ -0,0 +1 @@
+
diff --git a/src/zen/tests/mochitests/safebrowsing/head.js b/src/zen/tests/mochitests/safebrowsing/head.js
new file mode 100644
index 000000000..ffbdb18d1
--- /dev/null
+++ b/src/zen/tests/mochitests/safebrowsing/head.js
@@ -0,0 +1,103 @@
+// This url must sync with the table, url in SafeBrowsing.sys.mjs addMozEntries
+const PHISH_TABLE = "moztest-phish-simple";
+const PHISH_URL = "https://www.itisatrap.org/firefox/its-a-trap.html";
+
+/**
+ * Waits for a load (or custom) event to finish in a given tab. If provided
+ * load an uri into the tab.
+ *
+ * @param tab
+ * The tab to load into.
+ * @param [optional] url
+ * The url to load, or the current url.
+ * @param [optional] event
+ * The load event type to wait for. Defaults to "load".
+ * @return {Promise} resolved when the event is handled.
+ * @resolves to the received event
+ * @rejects if a valid load event is not received within a meaningful interval
+ */
+function promiseTabLoadEvent(tab, url, eventType = "load") {
+ info(`Wait tab event: ${eventType}`);
+
+ function handle(loadedUrl) {
+ if (loadedUrl === "about:blank" || (url && loadedUrl !== url)) {
+ info(`Skipping spurious load event for ${loadedUrl}`);
+ return false;
+ }
+
+ info("Tab event received: load");
+ return true;
+ }
+
+ let loaded;
+ if (eventType === "load") {
+ loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, handle);
+ } else {
+ // No need to use handle.
+ loaded = BrowserTestUtils.waitForContentEvent(
+ tab.linkedBrowser,
+ eventType,
+ true,
+ undefined,
+ true
+ );
+ }
+
+ if (url) {
+ BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, url);
+ }
+
+ return loaded;
+}
+
+// This function is mostly ported from classifierCommon.js
+// under toolkit/components/url-classifier/tests/mochitest.
+function waitForDBInit(callback) {
+ // Since there are two cases that may trigger the callback,
+ // we have to carefully avoid multiple callbacks and observer
+ // leaking.
+ let didCallback = false;
+ function callbackOnce() {
+ if (!didCallback) {
+ Services.obs.removeObserver(obsFunc, "mozentries-update-finished");
+ callback();
+ }
+ didCallback = true;
+ }
+
+ // The first part: listen to internal event.
+ function obsFunc() {
+ ok(true, "Received internal event!");
+ callbackOnce();
+ }
+ Services.obs.addObserver(obsFunc, "mozentries-update-finished");
+
+ // The second part: we might have missed the event. Just do
+ // an internal database lookup to confirm if the url has been
+ // added.
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(PHISH_URL),
+ {}
+ );
+
+ let dbService = Cc["@mozilla.org/url-classifier/dbservice;1"].getService(
+ Ci.nsIUrlClassifierDBService
+ );
+ dbService.lookup(principal, PHISH_TABLE, value => {
+ if (value === PHISH_TABLE) {
+ ok(true, "DB lookup success!");
+ callbackOnce();
+ }
+ });
+}
+
+Services.prefs.setCharPref(
+ "urlclassifier.malwareTable",
+ "moztest-malware-simple,moztest-unwanted-simple,moztest-harmful-simple"
+);
+Services.prefs.setCharPref("urlclassifier.phishTable", "moztest-phish-simple");
+Services.prefs.setCharPref(
+ "urlclassifier.blockedTable",
+ "moztest-block-simple"
+);
+SafeBrowsing.init();
diff --git a/src/zen/tests/mochitests/shell/browser.toml b/src/zen/tests/mochitests/shell/browser.toml
new file mode 100644
index 000000000..6439fa043
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/browser.toml
@@ -0,0 +1,103 @@
+[DEFAULT]
+
+["browser_1119088.js"]
+disabled="Disabled by import_external_tests.py"
+support-files = ["mac_desktop_image.py"]
+run-if = ["os == 'mac'"]
+tags = "os_integration"
+skip-if = ["os == 'mac' && os_version == '14.70' && processor == 'x86_64'"] # Bug 1869703
+
+["browser_420786.js"]
+run-if = ["os == 'linux'"]
+
+["browser_633221.js"]
+run-if = ["os == 'linux'"]
+
+["browser_createWindowsShortcut.js"]
+run-if = ["os == 'win'"]
+
+["browser_doesAppNeedPin.js"]
+
+["browser_headless_screenshot_1.js"]
+support-files = [
+ "head.js",
+ "headless.html",
+]
+skip-if = [
+ "os == 'win'",
+ "ccov",
+ "tsan", # Bug 1429950, Bug 1583315, Bug 1696109, Bug 1701449
+]
+tags = "os_integration"
+
+["browser_headless_screenshot_2.js"]
+support-files = [
+ "head.js",
+ "headless.html",
+]
+skip-if = [
+ "os == 'win'",
+ "ccov",
+ "tsan", # Bug 1429950, Bug 1583315, Bug 1696109, Bug 1701449
+]
+
+["browser_headless_screenshot_3.js"]
+support-files = [
+ "head.js",
+ "headless.html",
+]
+skip-if = [
+ "os == 'win'",
+ "ccov",
+ "tsan", # Bug 1429950, Bug 1583315, Bug 1696109, Bug 1701449
+]
+
+["browser_headless_screenshot_4.js"]
+support-files = [
+ "head.js",
+ "headless.html",
+]
+skip-if = [
+ "os == 'win'",
+ "ccov",
+ "tsan", # Bug 1429950, Bug 1583315, Bug 1696109, Bug 1701449
+]
+
+["browser_headless_screenshot_cross_origin.js"]
+support-files = [
+ "head.js",
+ "headless_cross_origin.html",
+ "headless_iframe.html",
+]
+skip-if = [
+ "os == 'win'",
+ "ccov",
+ "tsan", # Bug 1429950, Bug 1583315, Bug 1696109, Bug 1701449
+]
+
+["browser_headless_screenshot_redirect.js"]
+support-files = [
+ "head.js",
+ "headless.html",
+ "headless_redirect.html",
+ "headless_redirect.html^headers^",
+]
+skip-if = [
+ "os == 'win'",
+ "ccov",
+ "tsan", # Bug 1429950, Bug 1583315, Bug 1696109, Bug 1701449
+]
+
+["browser_processAUMID.js"]
+run-if = ["os == 'win'"]
+
+["browser_setDefaultBrowser.js"]
+tags = "os_integration"
+
+["browser_setDefaultPDFHandler.js"]
+run-if = ["os == 'win'"]
+tags = "os_integration"
+
+["browser_setDesktopBackgroundPreview.js"]
+disabled="Disabled by import_external_tests.py"
+tags = "os_integration"
diff --git a/src/zen/tests/mochitests/shell/browser_1119088.js b/src/zen/tests/mochitests/shell/browser_1119088.js
new file mode 100644
index 000000000..6702769a3
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/browser_1119088.js
@@ -0,0 +1,173 @@
+// Where we save the desktop background to (~/Pictures).
+const NS_OSX_PICTURE_DOCUMENTS_DIR = "Pct";
+
+// Paths used to run the CLI command (python script) that is used to
+// 1) check the desktop background image matches what we set it to via
+// nsIShellService::setDesktopBackground() and
+// 2) revert the desktop background image to the OS default
+
+let kPythonPath = "/usr/bin/python";
+if (AppConstants.isPlatformAndVersionAtLeast("macosx", 23.0)) {
+ kPythonPath = "/usr/local/bin/python3";
+}
+
+const kDesktopCheckerScriptPath =
+ "browser/browser/components/shell/test/mac_desktop_image.py";
+const kDefaultBackgroundImage =
+ "/System/Library/Desktop Pictures/Solid Colors/Teal.png";
+
+ChromeUtils.defineESModuleGetters(this, {
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+});
+
+function getPythonExecutableFile() {
+ let python = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ info(`Using python at location ${kPythonPath}`);
+ python.initWithPath(kPythonPath);
+ return python;
+}
+
+function createProcess() {
+ return Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+}
+
+// Use a CLI command to set the desktop background to |imagePath|. Returns the
+// exit code of the CLI command which reflects whether or not the background
+// image was successfully set. Returns 0 on success.
+function setDesktopBackgroundCLI(imagePath) {
+ let setBackgroundProcess = createProcess();
+ setBackgroundProcess.init(getPythonExecutableFile());
+ let args = [
+ kDesktopCheckerScriptPath,
+ "--verbose",
+ "--set-background-image",
+ imagePath,
+ ];
+ setBackgroundProcess.run(true, args, args.length);
+ return setBackgroundProcess.exitValue;
+}
+
+// Check the desktop background is |imagePath| using a CLI command.
+// Returns the exit code of the CLI command which reflects whether or not
+// the provided image path matches the path of the current desktop background
+// image. A return value of 0 indicates success/match.
+function checkDesktopBackgroundCLI(imagePath) {
+ let checkBackgroundProcess = createProcess();
+ checkBackgroundProcess.init(getPythonExecutableFile());
+ let args = [
+ kDesktopCheckerScriptPath,
+ "--verbose",
+ "--check-background-image",
+ imagePath,
+ ];
+ checkBackgroundProcess.run(true, args, args.length);
+ return checkBackgroundProcess.exitValue;
+}
+
+// Use the python script to set/check the desktop background is |imagePath|
+function setAndCheckDesktopBackgroundCLI(imagePath) {
+ Assert.ok(FileUtils.File(imagePath).exists(), `${imagePath} exists`);
+
+ let setExitCode = setDesktopBackgroundCLI(imagePath);
+ Assert.equal(setExitCode, 0, `Setting background via CLI to ${imagePath}`);
+
+ let checkExitCode = checkDesktopBackgroundCLI(imagePath);
+ Assert.equal(checkExitCode, 0, `Checking background via CLI is ${imagePath}`);
+}
+
+// Restore the automation default background image. i.e., the default used
+// in the automated test environment, not the OS default.
+function restoreDefaultBackground() {
+ let defaultBackgroundPath;
+ defaultBackgroundPath = kDefaultBackgroundImage;
+ setAndCheckDesktopBackgroundCLI(defaultBackgroundPath);
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["test.wait300msAfterTabSwitch", true]],
+ });
+});
+
+/**
+ * Tests "Set As Desktop Background" platform implementation on macOS.
+ *
+ * Sets the desktop background image to the browser logo from the about:logo
+ * page and verifies it was set successfully. Setting the desktop background
+ * (which uses the nsIShellService::setDesktopBackground() interface method)
+ * downloads the image to ~/Pictures using a unique file name and sets the
+ * desktop background to the downloaded file leaving the download in place.
+ * After setDesktopBackground() is called, the test uses a python script to
+ * validate that the current desktop background is in fact set to the
+ * downloaded logo.
+ */
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:logo",
+ },
+ async () => {
+ let dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService(
+ Ci.nsIDirectoryServiceProvider
+ );
+ let uuidGenerator = Services.uuid;
+ let shellSvc = Cc["@mozilla.org/browser/shell-service;1"].getService(
+ Ci.nsIShellService
+ );
+
+ // Ensure we are starting with the default background. Log a
+ // failure if we can not set the background to the default, but
+ // ignore the case where the background is not already set as that
+ // that may be due to a previous test failure.
+ restoreDefaultBackground();
+
+ // Generate a UUID (with non-alphanumberic characters removed) to build
+ // up a filename for the desktop background. Use a UUID to distinguish
+ // between runs so we won't be confused by images that were not properly
+ // cleaned up after previous runs.
+ let uuid = uuidGenerator.generateUUID().toString().replace(/\W/g, "");
+
+ // Set the background image path to be $HOME/Pictures/.png.
+ // nsIShellService.setDesktopBackground() downloads the image to this
+ // path and then sets it as the desktop background image, leaving the
+ // image in place.
+ let backgroundImage = dirSvc.getFile(NS_OSX_PICTURE_DOCUMENTS_DIR, {});
+ backgroundImage.append(uuid + ".png");
+ if (backgroundImage.exists()) {
+ backgroundImage.remove(false);
+ }
+
+ // For simplicity, we're going to reach in and access the image on the
+ // page directly, which means the page shouldn't be running in a remote
+ // browser. Thankfully, about:logo runs in the parent process for now.
+ Assert.ok(
+ !gBrowser.selectedBrowser.isRemoteBrowser,
+ "image can be accessed synchronously from the parent process"
+ );
+ let image = gBrowser.selectedBrowser.contentDocument.images[0];
+
+ info(`Setting/saving desktop background to ${backgroundImage.path}`);
+
+ // Saves the file in ~/Pictures
+ shellSvc.setDesktopBackground(image, 0, backgroundImage.leafName);
+
+ await BrowserTestUtils.waitForCondition(() => backgroundImage.exists());
+ info(`${backgroundImage.path} downloaded`);
+ Assert.ok(
+ FileUtils.File(backgroundImage.path).exists(),
+ `${backgroundImage.path} exists`
+ );
+
+ // Check that the desktop background image is the image we set above.
+ let exitCode = checkDesktopBackgroundCLI(backgroundImage.path);
+ Assert.equal(exitCode, 0, `background should be ${backgroundImage.path}`);
+
+ // Restore the background image to the Mac default.
+ restoreDefaultBackground();
+
+ // We no longer need the downloaded image.
+ backgroundImage.remove(false);
+ }
+ );
+});
diff --git a/src/zen/tests/mochitests/shell/browser_420786.js b/src/zen/tests/mochitests/shell/browser_420786.js
new file mode 100644
index 000000000..b9becb49c
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/browser_420786.js
@@ -0,0 +1,101 @@
+const DG_BACKGROUND = "/desktop/gnome/background";
+const DG_IMAGE_KEY = DG_BACKGROUND + "/picture_filename";
+const DG_OPTION_KEY = DG_BACKGROUND + "/picture_options";
+const DG_DRAW_BG_KEY = DG_BACKGROUND + "/draw_background";
+
+const GS_BG_SCHEMA = "org.gnome.desktop.background";
+const GS_IMAGE_KEY = "picture-uri";
+const GS_OPTION_KEY = "picture-options";
+const GS_DRAW_BG_KEY = "draw-background";
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:logo",
+ },
+ () => {
+ var brandName = Services.strings
+ .createBundle("chrome://branding/locale/brand.properties")
+ .GetStringFromName("brandShortName");
+
+ var dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService(
+ Ci.nsIDirectoryServiceProvider
+ );
+ var homeDir = dirSvc.getFile("Home", {});
+
+ var wpFile = homeDir.clone();
+ wpFile.append(brandName + "_wallpaper.png");
+
+ // Backup the existing wallpaper so that this test doesn't change the user's
+ // settings.
+ var wpFileBackup = homeDir.clone();
+ wpFileBackup.append(brandName + "_wallpaper.png.backup");
+
+ if (wpFileBackup.exists()) {
+ wpFileBackup.remove(false);
+ }
+
+ if (wpFile.exists()) {
+ wpFile.copyTo(null, wpFileBackup.leafName);
+ }
+
+ var shell = Cc["@mozilla.org/browser/shell-service;1"]
+ .getService(Ci.nsIShellService)
+ .QueryInterface(Ci.nsIGNOMEShellService);
+
+ // For simplicity, we're going to reach in and access the image on the
+ // page directly, which means the page shouldn't be running in a remote
+ // browser. Thankfully, about:logo runs in the parent process for now.
+ Assert.ok(
+ !gBrowser.selectedBrowser.isRemoteBrowser,
+ "image can be accessed synchronously from the parent process"
+ );
+
+ var image = content.document.images[0];
+
+ let checkWallpaper, restoreSettings;
+ try {
+ const prevImage = shell.getGSettingsString(GS_BG_SCHEMA, GS_IMAGE_KEY);
+ const prevOption = shell.getGSettingsString(
+ GS_BG_SCHEMA,
+ GS_OPTION_KEY
+ );
+
+ checkWallpaper = function (position, expectedGSettingsPosition) {
+ shell.setDesktopBackground(image, position, "");
+ ok(wpFile.exists(), "Wallpaper was written to disk");
+ is(
+ shell.getGSettingsString(GS_BG_SCHEMA, GS_IMAGE_KEY),
+ encodeURI("file://" + wpFile.path),
+ "Wallpaper file GSettings key is correct"
+ );
+ is(
+ shell.getGSettingsString(GS_BG_SCHEMA, GS_OPTION_KEY),
+ expectedGSettingsPosition,
+ "Wallpaper position GSettings key is correct"
+ );
+ };
+
+ restoreSettings = function () {
+ shell.setGSettingsString(GS_BG_SCHEMA, GS_IMAGE_KEY, prevImage);
+ shell.setGSettingsString(GS_BG_SCHEMA, GS_OPTION_KEY, prevOption);
+ };
+ } catch (e) {}
+
+ checkWallpaper(Ci.nsIShellService.BACKGROUND_TILE, "wallpaper");
+ checkWallpaper(Ci.nsIShellService.BACKGROUND_STRETCH, "stretched");
+ checkWallpaper(Ci.nsIShellService.BACKGROUND_CENTER, "centered");
+ checkWallpaper(Ci.nsIShellService.BACKGROUND_FILL, "zoom");
+ checkWallpaper(Ci.nsIShellService.BACKGROUND_FIT, "scaled");
+ checkWallpaper(Ci.nsIShellService.BACKGROUND_SPAN, "spanned");
+
+ restoreSettings();
+
+ // Restore files
+ if (wpFileBackup.exists()) {
+ wpFileBackup.moveTo(null, wpFile.leafName);
+ }
+ }
+ );
+});
diff --git a/src/zen/tests/mochitests/shell/browser_633221.js b/src/zen/tests/mochitests/shell/browser_633221.js
new file mode 100644
index 000000000..dbc66e286
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/browser_633221.js
@@ -0,0 +1,11 @@
+function test() {
+ ShellService.setDefaultBrowser(false);
+ ok(
+ ShellService.isDefaultBrowser(true, false),
+ "we got here and are the default browser"
+ );
+ ok(
+ ShellService.isDefaultBrowser(true, true),
+ "we got here and are the default browser"
+ );
+}
diff --git a/src/zen/tests/mochitests/shell/browser_createWindowsShortcut.js b/src/zen/tests/mochitests/shell/browser_createWindowsShortcut.js
new file mode 100644
index 000000000..0b951bde2
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/browser_createWindowsShortcut.js
@@ -0,0 +1,218 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ FileTestUtils: "resource://testing-common/FileTestUtils.sys.mjs",
+ MockRegistrar: "resource://testing-common/MockRegistrar.sys.mjs",
+});
+
+const gBase = Services.dirsvc.get("ProfD", Ci.nsIFile);
+gBase.append("CreateWindowsShortcut");
+createDirectory(gBase);
+
+const gTmpDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+
+const gDirectoryServiceProvider = {
+ getFile(prop, persistent) {
+ persistent.value = false;
+
+ // We only expect a narrow range of calls.
+ let folder = gBase.clone();
+ switch (prop) {
+ case "Progs":
+ folder.append("Programs");
+ break;
+ case "Desk":
+ folder.append("Desktop");
+ break;
+ case "UpdRootD":
+ // We really want DataRoot, but UpdateSubdir is what we usually get.
+ folder.append("DataRoot");
+ folder.append("UpdateDir");
+ folder.append("UpdateSubdir");
+ break;
+ case "ProfD":
+ // Used by test infrastructure.
+ folder = folder.parent;
+ break;
+ case "TmpD":
+ // Used by FileTestUtils.
+ folder = gTmpDir;
+ break;
+ default:
+ console.error(`Access to unexpected directory '${prop}'`);
+ return Cr.NS_ERROR_FAILURE;
+ }
+
+ createDirectory(folder);
+ return folder;
+ },
+ QueryInterface: ChromeUtils.generateQI([Ci.nsIDirectoryServiceProvider]),
+};
+
+add_setup(() => {
+ Services.dirsvc
+ .QueryInterface(Ci.nsIDirectoryService)
+ .registerProvider(gDirectoryServiceProvider);
+});
+
+registerCleanupFunction(() => {
+ gBase.remove(true);
+ Services.dirsvc
+ .QueryInterface(Ci.nsIDirectoryService)
+ .unregisterProvider(gDirectoryServiceProvider);
+});
+
+add_task(async function test_CreateWindowsShortcut() {
+ const DEST = "browser_createWindowsShortcut_TestFile.lnk";
+
+ const file = FileTestUtils.getTempFile("program.exe");
+ const iconPath = FileTestUtils.getTempFile("program.ico");
+
+ let shortcut;
+
+ const defaults = {
+ shellService: Cc["@mozilla.org/toolkit/shell-service;1"].getService(),
+ targetFile: file,
+ iconFile: iconPath,
+ description: "made by browser_createWindowsShortcut.js",
+ aumid: "TESTTEST",
+ };
+
+ shortcut = Services.dirsvc.get("Progs", Ci.nsIFile);
+ shortcut.append(DEST);
+ await testShortcut({
+ shortcutFile: shortcut,
+ relativePath: DEST,
+ specialFolder: "Programs",
+ logHeader: "STARTMENU",
+ ...defaults,
+ });
+
+ let subdir = Services.dirsvc.get("Progs", Ci.nsIFile);
+ subdir.append("Shortcut Test");
+ tryRemove(subdir);
+
+ shortcut = subdir.clone();
+ shortcut.append(DEST);
+ await testShortcut({
+ shortcutFile: shortcut,
+ relativePath: "Shortcut Test\\" + DEST,
+ specialFolder: "Programs",
+ logHeader: "STARTMENU",
+ ...defaults,
+ });
+ tryRemove(subdir);
+
+ shortcut = Services.dirsvc.get("Desk", Ci.nsIFile);
+ shortcut.append(DEST);
+ await testShortcut({
+ shortcutFile: shortcut,
+ relativePath: DEST,
+ specialFolder: "Desktop",
+ logHeader: "DESKTOP",
+ ...defaults,
+ });
+});
+
+async function testShortcut({
+ shortcutFile,
+ relativePath,
+ specialFolder,
+ logHeader,
+
+ // Generally provided by the defaults.
+ shellService,
+ targetFile,
+ iconFile,
+ description,
+ aumid,
+}) {
+ // If it already exists, remove it.
+ tryRemove(shortcutFile);
+
+ await shellService.createShortcut(
+ targetFile,
+ [],
+ description,
+ iconFile,
+ 0,
+ aumid,
+ specialFolder,
+ relativePath
+ );
+ ok(
+ shortcutFile.exists(),
+ `${specialFolder}\\${relativePath}: Shortcut should exist`
+ );
+ ok(
+ queryShortcutLog(relativePath, logHeader),
+ `${specialFolder}\\${relativePath}: Shortcut log entry was added`
+ );
+ await shellService.deleteShortcut(specialFolder, relativePath);
+ ok(
+ !shortcutFile.exists(),
+ `${specialFolder}\\${relativePath}: Shortcut does not exist after deleting`
+ );
+ ok(
+ !queryShortcutLog(relativePath, logHeader),
+ `${specialFolder}\\${relativePath}: Shortcut log entry was removed`
+ );
+}
+
+function queryShortcutLog(aShortcutName, aSection) {
+ const parserFactory = Cc[
+ "@mozilla.org/xpcom/ini-parser-factory;1"
+ ].createInstance(Ci.nsIINIParserFactory);
+
+ const dir = Services.dirsvc.get("UpdRootD", Ci.nsIFile).parent.parent;
+ const enumerator = dir.directoryEntries;
+
+ for (const file of enumerator) {
+ // We don't know the user's SID from JS-land, so just look at all of them.
+ if (!file.path.match(/[^_]+_S[^_]*_shortcuts.ini/)) {
+ continue;
+ }
+
+ const parser = parserFactory.createINIParser(file);
+ parser.QueryInterface(Ci.nsIINIParser);
+ parser.QueryInterface(Ci.nsIINIParserWriter);
+
+ for (let i = 0; ; i++) {
+ try {
+ let string = parser.getString(aSection, `Shortcut${i}`);
+ if (string == aShortcutName) {
+ enumerator.close();
+ return true;
+ }
+ } catch (e) {
+ // The key didn't exist, stop here.
+ break;
+ }
+ }
+ }
+
+ enumerator.close();
+ return false;
+}
+
+function createDirectory(aFolder) {
+ try {
+ aFolder.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ } catch (e) {
+ if (e.result != Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
+ throw e;
+ }
+ }
+}
+
+function tryRemove(file) {
+ try {
+ file.remove(false);
+ return true;
+ } catch (e) {
+ return false;
+ }
+}
diff --git a/src/zen/tests/mochitests/shell/browser_doesAppNeedPin.js b/src/zen/tests/mochitests/shell/browser_doesAppNeedPin.js
new file mode 100644
index 000000000..2a261988a
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/browser_doesAppNeedPin.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+ NimbusTestUtils: "resource://testing-common/NimbusTestUtils.sys.mjs",
+});
+
+let defaultValue;
+add_task(async function default_need() {
+ defaultValue = await ShellService.doesAppNeedPin();
+
+ Assert.notStrictEqual(
+ defaultValue,
+ undefined,
+ "Got a default app need pin value"
+ );
+});
+
+add_task(async function remote_disable() {
+ if (defaultValue === false) {
+ info("Default pin already false, so nothing to test");
+ return;
+ }
+
+ let doCleanup = await NimbusTestUtils.enrollWithFeatureConfig(
+ {
+ featureId: NimbusFeatures.shellService.featureId,
+ value: { disablePin: true, enabled: true },
+ },
+ { isRollout: true }
+ );
+
+ Assert.equal(
+ await ShellService.doesAppNeedPin(),
+ false,
+ "Pinning disabled via nimbus"
+ );
+
+ await doCleanup();
+});
+
+add_task(async function restore_default() {
+ if (defaultValue === undefined) {
+ info("No default pin value set, so nothing to test");
+ return;
+ }
+
+ Assert.equal(
+ await ShellService.doesAppNeedPin(),
+ defaultValue,
+ "Pinning restored to original"
+ );
+});
diff --git a/src/zen/tests/mochitests/shell/browser_headless_screenshot_1.js b/src/zen/tests/mochitests/shell/browser_headless_screenshot_1.js
new file mode 100644
index 000000000..b5f84f0f6
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/browser_headless_screenshot_1.js
@@ -0,0 +1,74 @@
+"use strict";
+
+add_task(async function () {
+ // Test all four basic variations of the "screenshot" argument
+ // when a file path is specified.
+ await testFileCreationPositive(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "-screenshot",
+ screenshotPath,
+ ],
+ screenshotPath
+ );
+ await testFileCreationPositive(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ `-screenshot=${screenshotPath}`,
+ ],
+ screenshotPath
+ );
+ await testFileCreationPositive(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "--screenshot",
+ screenshotPath,
+ ],
+ screenshotPath
+ );
+ await testFileCreationPositive(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ `--screenshot=${screenshotPath}`,
+ ],
+ screenshotPath
+ );
+
+ // Test when the requested URL redirects
+ await testFileCreationPositive(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless_redirect.html",
+ "-screenshot",
+ screenshotPath,
+ ],
+ screenshotPath
+ );
+
+ // Test with additional command options
+ await testFileCreationPositive(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "-screenshot",
+ screenshotPath,
+ "-attach-console",
+ ],
+ screenshotPath
+ );
+ await testFileCreationPositive(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "-attach-console",
+ "-screenshot",
+ screenshotPath,
+ "-headless",
+ ],
+ screenshotPath
+ );
+});
diff --git a/src/zen/tests/mochitests/shell/browser_headless_screenshot_2.js b/src/zen/tests/mochitests/shell/browser_headless_screenshot_2.js
new file mode 100644
index 000000000..871895b45
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/browser_headless_screenshot_2.js
@@ -0,0 +1,48 @@
+"use strict";
+add_task(async function () {
+ const cwdScreenshotPath = PathUtils.join(
+ Services.dirsvc.get("CurWorkD", Ci.nsIFile).path,
+ "screenshot.png"
+ );
+
+ // Test variations of the "screenshot" argument when a file path
+ // isn't specified.
+ await testFileCreationPositive(
+ [
+ "-screenshot",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ ],
+ cwdScreenshotPath
+ );
+ await testFileCreationPositive(
+ [
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "-screenshot",
+ ],
+ cwdScreenshotPath
+ );
+ await testFileCreationPositive(
+ [
+ "--screenshot",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ ],
+ cwdScreenshotPath
+ );
+ await testFileCreationPositive(
+ [
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "--screenshot",
+ ],
+ cwdScreenshotPath
+ );
+
+ // Test with additional command options
+ await testFileCreationPositive(
+ [
+ "--screenshot",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "-attach-console",
+ ],
+ cwdScreenshotPath
+ );
+});
diff --git a/src/zen/tests/mochitests/shell/browser_headless_screenshot_3.js b/src/zen/tests/mochitests/shell/browser_headless_screenshot_3.js
new file mode 100644
index 000000000..b55635652
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/browser_headless_screenshot_3.js
@@ -0,0 +1,59 @@
+"use strict";
+
+add_task(async function () {
+ const cwdScreenshotPath = PathUtils.join(
+ Services.dirsvc.get("CurWorkD", Ci.nsIFile).path,
+ "screenshot.png"
+ );
+
+ // Test invalid URL arguments (either no argument or too many arguments).
+ await testFileCreationNegative(["-screenshot"], cwdScreenshotPath);
+ await testFileCreationNegative(
+ [
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "http://mochi.test:8888/headless.html",
+ "-screenshot",
+ ],
+ cwdScreenshotPath
+ );
+
+ // Test all four basic variations of the "window-size" argument.
+ await testFileCreationPositive(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "-screenshot",
+ "-window-size",
+ "800",
+ ],
+ cwdScreenshotPath
+ );
+ await testFileCreationPositive(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "-screenshot",
+ "-window-size=800",
+ ],
+ cwdScreenshotPath
+ );
+ await testFileCreationPositive(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "-screenshot",
+ "--window-size",
+ "800",
+ ],
+ cwdScreenshotPath
+ );
+ await testFileCreationPositive(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "-screenshot",
+ "--window-size=800",
+ ],
+ cwdScreenshotPath
+ );
+});
diff --git a/src/zen/tests/mochitests/shell/browser_headless_screenshot_4.js b/src/zen/tests/mochitests/shell/browser_headless_screenshot_4.js
new file mode 100644
index 000000000..4b93ba516
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/browser_headless_screenshot_4.js
@@ -0,0 +1,31 @@
+"use strict";
+
+add_task(async function () {
+ const cwdScreenshotPath = PathUtils.join(
+ Services.dirsvc.get("CurWorkD", Ci.nsIFile).path,
+ "screenshot.png"
+ );
+ // Test other variations of the "window-size" argument.
+ await testWindowSizePositive(800, 600);
+ await testWindowSizePositive(1234);
+ await testFileCreationNegative(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "-screenshot",
+ "-window-size",
+ "hello",
+ ],
+ cwdScreenshotPath
+ );
+ await testFileCreationNegative(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "-screenshot",
+ "-window-size",
+ "800,",
+ ],
+ cwdScreenshotPath
+ );
+});
diff --git a/src/zen/tests/mochitests/shell/browser_headless_screenshot_cross_origin.js b/src/zen/tests/mochitests/shell/browser_headless_screenshot_cross_origin.js
new file mode 100644
index 000000000..954777358
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/browser_headless_screenshot_cross_origin.js
@@ -0,0 +1,9 @@
+"use strict";
+
+add_task(async function () {
+ // Test cross origin iframes work.
+ await testGreen(
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless_cross_origin.html",
+ screenshotPath
+ );
+});
diff --git a/src/zen/tests/mochitests/shell/browser_headless_screenshot_redirect.js b/src/zen/tests/mochitests/shell/browser_headless_screenshot_redirect.js
new file mode 100644
index 000000000..c50b847b6
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/browser_headless_screenshot_redirect.js
@@ -0,0 +1,14 @@
+"use strict";
+
+add_task(async function () {
+ // Test when the requested URL redirects
+ await testFileCreationPositive(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless_redirect.html",
+ "-screenshot",
+ screenshotPath,
+ ],
+ screenshotPath
+ );
+});
diff --git a/src/zen/tests/mochitests/shell/browser_processAUMID.js b/src/zen/tests/mochitests/shell/browser_processAUMID.js
new file mode 100644
index 000000000..cf98316f8
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/browser_processAUMID.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Bug 1950734 tracks how calling PinCurrentAppToTaskbarWin11
+ * on MSIX may cause the process AUMID to be unnecessarily changed.
+ * This test verifies that the behaviour will no longer happen
+ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ ShellService: "moz-src:///browser/components/shell/ShellService.sys.mjs",
+});
+
+add_task(async function test_processAUMID() {
+ let processAUMID = ShellService.checkCurrentProcessAUMIDForTesting();
+
+ // This function will trigger the relevant code paths that
+ // incorrectly changes the process AUMID on MSIX, prior to
+ // Bug 1950734 being fixed
+ await ShellService.checkPinCurrentAppToTaskbarAsync(false);
+
+ is(
+ processAUMID,
+ ShellService.checkCurrentProcessAUMIDForTesting(),
+ "The process AUMID should not be changed"
+ );
+});
diff --git a/src/zen/tests/mochitests/shell/browser_setDefaultBrowser.js b/src/zen/tests/mochitests/shell/browser_setDefaultBrowser.js
new file mode 100644
index 000000000..731bb7f1e
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/browser_setDefaultBrowser.js
@@ -0,0 +1,239 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs",
+ ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+ NimbusTestUtils: "resource://testing-common/NimbusTestUtils.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+const setDefaultBrowserUserChoiceStub = async () => {
+ throw Components.Exception("", Cr.NS_ERROR_WDBA_NO_PROGID);
+};
+
+const defaultAgentStub = sinon
+ .stub(ShellService, "defaultAgent")
+ .value({ setDefaultBrowserUserChoiceAsync: setDefaultBrowserUserChoiceStub });
+
+const _userChoiceImpossibleTelemetryResultStub = sinon
+ .stub(ShellService, "_userChoiceImpossibleTelemetryResult")
+ .callsFake(() => null);
+
+const userChoiceStub = sinon
+ .stub(ShellService, "setAsDefaultUserChoice")
+ .resolves();
+const setDefaultStub = sinon.stub();
+const shellStub = sinon
+ .stub(ShellService, "shellService")
+ .value({ setDefaultBrowser: setDefaultStub });
+
+const sendTriggerStub = sinon.stub(ASRouter, "sendTriggerMessage");
+
+registerCleanupFunction(() => {
+ sinon.restore();
+});
+
+let defaultUserChoice;
+add_task(async function need_user_choice() {
+ await ShellService.setDefaultBrowser();
+ defaultUserChoice = userChoiceStub.called;
+
+ Assert.notStrictEqual(
+ defaultUserChoice,
+ undefined,
+ "Decided which default browser method to use"
+ );
+ Assert.equal(
+ setDefaultStub.notCalled,
+ defaultUserChoice,
+ "Only one default behavior was used"
+ );
+});
+
+add_task(async function remote_disable() {
+ if (defaultUserChoice === false) {
+ info("Default behavior already not user choice, so nothing to test");
+ return;
+ }
+
+ userChoiceStub.resetHistory();
+ setDefaultStub.resetHistory();
+ let doCleanup = await NimbusTestUtils.enrollWithFeatureConfig(
+ {
+ featureId: NimbusFeatures.shellService.featureId,
+ value: {
+ setDefaultBrowserUserChoice: false,
+ enabled: true,
+ },
+ },
+ { isRollout: true }
+ );
+
+ await ShellService.setDefaultBrowser();
+
+ Assert.ok(
+ userChoiceStub.notCalled,
+ "Set default with user choice disabled via nimbus"
+ );
+ Assert.ok(setDefaultStub.called, "Used plain set default instead");
+
+ await doCleanup();
+});
+
+add_task(async function restore_default() {
+ if (defaultUserChoice === undefined) {
+ info("No default user choice behavior set, so nothing to test");
+ return;
+ }
+
+ userChoiceStub.resetHistory();
+ setDefaultStub.resetHistory();
+
+ await ShellService.setDefaultBrowser();
+
+ Assert.equal(
+ userChoiceStub.called,
+ defaultUserChoice,
+ "Set default with user choice restored to original"
+ );
+ Assert.equal(
+ setDefaultStub.notCalled,
+ defaultUserChoice,
+ "Plain set default behavior restored to original"
+ );
+});
+
+add_task(async function ensure_fallback() {
+ if (AppConstants.platform != "win") {
+ info("Nothing to test on non-Windows");
+ return;
+ }
+
+ let userChoicePromise = Promise.resolve();
+ userChoiceStub.callsFake(function (...args) {
+ return (userChoicePromise = userChoiceStub.wrappedMethod.apply(this, args));
+ });
+ userChoiceStub.resetHistory();
+ setDefaultStub.resetHistory();
+ let doCleanup = await NimbusTestUtils.enrollWithFeatureConfig(
+ {
+ featureId: NimbusFeatures.shellService.featureId,
+ value: {
+ setDefaultBrowserUserChoice: true,
+ setDefaultPDFHandler: false,
+ enabled: true,
+ },
+ },
+ { isRollout: true }
+ );
+
+ await ShellService.setDefaultBrowser();
+
+ Assert.ok(userChoiceStub.called, "Set default with user choice called");
+
+ let message = "";
+ await userChoicePromise.catch(err => (message = err.message || ""));
+
+ Assert.ok(
+ message.includes("ErrExeProgID"),
+ "Set default with user choice threw an expected error"
+ );
+ Assert.ok(setDefaultStub.called, "Fallbacked to plain set default");
+
+ await doCleanup();
+});
+
+async function setUpNotificationTests(guidanceEnabled, oneClick) {
+ sinon.reset();
+ const experimentCleanup = await NimbusTestUtils.enrollWithFeatureConfig(
+ {
+ featureId: NimbusFeatures.shellService.featureId,
+ value: {
+ setDefaultGuidanceNotifications: guidanceEnabled,
+ setDefaultBrowserUserChoice: oneClick,
+ setDefaultBrowserUserChoiceRegRename: oneClick,
+ enabled: true,
+ },
+ },
+ { isRollout: true }
+ );
+
+ const doCleanup = async () => {
+ await experimentCleanup();
+ sinon.reset();
+ };
+
+ await ShellService.setDefaultBrowser();
+ return doCleanup;
+}
+
+add_task(
+ async function show_notification_when_set_to_default_guidance_enabled_and_one_click_disabled() {
+ if (!AppConstants.isPlatformAndVersionAtLeast("win", 10)) {
+ info("Nothing to test on non-Windows or older Windows versions");
+ return;
+ }
+ const doCleanup = await setUpNotificationTests(
+ true, // guidance enabled
+ false // one-click disabled
+ );
+
+ Assert.ok(setDefaultStub.called, "Fallback method used to set default");
+
+ Assert.equal(
+ sendTriggerStub.firstCall.args[0].id,
+ "deeplinkedToWindowsSettingsUI",
+ `Set to default guidance message trigger was sent`
+ );
+
+ await doCleanup();
+ }
+);
+
+add_task(
+ async function do_not_show_notification_when_set_to_default_guidance_disabled_and_one_click_enabled() {
+ if (!AppConstants.isPlatformAndVersionAtLeast("win", 10)) {
+ info("Nothing to test on non-Windows or older Windows versions");
+ return;
+ }
+
+ const doCleanup = await setUpNotificationTests(
+ false, // guidance disabled
+ true // one-click enabled
+ );
+
+ Assert.ok(setDefaultStub.notCalled, "Fallback method not called");
+
+ Assert.equal(
+ sendTriggerStub.callCount,
+ 0,
+ `Set to default guidance message trigger was not sent`
+ );
+
+ await doCleanup();
+ }
+);
+
+add_task(
+ async function do_not_show_notification_when_set_to_default_guidance_enabled_and_one_click_enabled() {
+ if (!AppConstants.isPlatformAndVersionAtLeast("win", 10)) {
+ info("Nothing to test on non-Windows or older Windows versions");
+ return;
+ }
+
+ const doCleanup = await setUpNotificationTests(
+ true, // guidance enabled
+ true // one-click enabled
+ );
+
+ Assert.ok(setDefaultStub.notCalled, "Fallback method not called");
+ Assert.equal(
+ sendTriggerStub.callCount,
+ 0,
+ `Set to default guidance message trigger was not sent`
+ );
+ await doCleanup();
+ }
+);
diff --git a/src/zen/tests/mochitests/shell/browser_setDefaultPDFHandler.js b/src/zen/tests/mochitests/shell/browser_setDefaultPDFHandler.js
new file mode 100644
index 000000000..443c8f69f
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/browser_setDefaultPDFHandler.js
@@ -0,0 +1,279 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+ NimbusTestUtils: "resource://testing-common/NimbusTestUtils.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+const setDefaultBrowserUserChoiceStub = sinon.stub();
+const setDefaultExtensionHandlersUserChoiceStub = sinon
+ .stub()
+ .callsFake(() => Promise.resolve());
+
+const defaultAgentStub = sinon.stub(ShellService, "defaultAgent").value({
+ setDefaultBrowserUserChoiceAsync: setDefaultBrowserUserChoiceStub,
+ setDefaultExtensionHandlersUserChoice:
+ setDefaultExtensionHandlersUserChoiceStub,
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "XreDirProvider",
+ "@mozilla.org/xre/directory-provider;1",
+ Ci.nsIXREDirProvider
+);
+
+const _userChoiceImpossibleTelemetryResultStub = sinon
+ .stub(ShellService, "_userChoiceImpossibleTelemetryResult")
+ .callsFake(() => null);
+
+// Ensure we don't fall back to a real implementation.
+const setDefaultStub = sinon.stub();
+// We'll dynamically update this as needed during the tests.
+const queryCurrentDefaultHandlerForStub = sinon.stub();
+const shellStub = sinon.stub(ShellService, "shellService").value({
+ setDefaultBrowser: setDefaultStub,
+ queryCurrentDefaultHandlerFor: queryCurrentDefaultHandlerForStub,
+});
+
+registerCleanupFunction(() => {
+ defaultAgentStub.restore();
+ _userChoiceImpossibleTelemetryResultStub.restore();
+ shellStub.restore();
+});
+
+add_task(async function ready() {
+ await ExperimentAPI.ready();
+});
+
+// Everything here is Windows.
+Assert.equal(AppConstants.platform, "win", "Platform is Windows");
+
+add_task(async function remoteEnableWithPDF() {
+ let doCleanup = await NimbusTestUtils.enrollWithFeatureConfig(
+ {
+ featureId: NimbusFeatures.shellService.featureId,
+ value: {
+ setDefaultBrowserUserChoice: true,
+ setDefaultPDFHandlerOnlyReplaceBrowsers: false,
+ setDefaultPDFHandler: true,
+ enabled: true,
+ },
+ },
+ { isRollout: true }
+ );
+
+ Assert.equal(
+ NimbusFeatures.shellService.getVariable("setDefaultBrowserUserChoice"),
+ true
+ );
+ Assert.equal(
+ NimbusFeatures.shellService.getVariable("setDefaultPDFHandler"),
+ true
+ );
+
+ setDefaultBrowserUserChoiceStub.resetHistory();
+ await ShellService.setDefaultBrowser();
+
+ const aumi = XreDirProvider.getInstallHash();
+ Assert.ok(setDefaultBrowserUserChoiceStub.called);
+ Assert.deepEqual(setDefaultBrowserUserChoiceStub.firstCall.args, [
+ aumi,
+ [".pdf", "FirefoxPDF"],
+ ]);
+
+ await doCleanup();
+});
+
+add_task(async function remoteEnableWithPDF_testOnlyReplaceBrowsers() {
+ let doCleanup = await NimbusTestUtils.enrollWithFeatureConfig(
+ {
+ featureId: NimbusFeatures.shellService.featureId,
+ value: {
+ setDefaultBrowserUserChoice: true,
+ setDefaultPDFHandlerOnlyReplaceBrowsers: true,
+ setDefaultPDFHandler: true,
+ enabled: true,
+ },
+ },
+ { isRollout: true }
+ );
+
+ Assert.equal(
+ NimbusFeatures.shellService.getVariable("setDefaultBrowserUserChoice"),
+ true
+ );
+ Assert.equal(
+ NimbusFeatures.shellService.getVariable("setDefaultPDFHandler"),
+ true
+ );
+ Assert.equal(
+ NimbusFeatures.shellService.getVariable(
+ "setDefaultPDFHandlerOnlyReplaceBrowsers"
+ ),
+ true
+ );
+
+ const aumi = XreDirProvider.getInstallHash();
+
+ // We'll take the default from a missing association or a known browser.
+ for (let progId of ["", "MSEdgePDF"]) {
+ queryCurrentDefaultHandlerForStub.callsFake(() => progId);
+
+ setDefaultBrowserUserChoiceStub.resetHistory();
+ await ShellService.setDefaultBrowser();
+
+ Assert.ok(setDefaultBrowserUserChoiceStub.called);
+ Assert.deepEqual(
+ setDefaultBrowserUserChoiceStub.firstCall.args,
+ [aumi, [".pdf", "FirefoxPDF"]],
+ `Will take default from missing association or known browser with ProgID '${progId}'`
+ );
+ }
+
+ // But not from a non-browser.
+ queryCurrentDefaultHandlerForStub.callsFake(() => "Acrobat.Document.DC");
+
+ setDefaultBrowserUserChoiceStub.resetHistory();
+ await ShellService.setDefaultBrowser();
+
+ Assert.ok(setDefaultBrowserUserChoiceStub.called);
+ Assert.deepEqual(
+ setDefaultBrowserUserChoiceStub.firstCall.args,
+ [aumi, []],
+ `Will not take default from non-browser`
+ );
+
+ await doCleanup();
+});
+
+add_task(async function remoteEnableWithoutPDF() {
+ let doCleanup = await NimbusTestUtils.enrollWithFeatureConfig(
+ {
+ featureId: NimbusFeatures.shellService.featureId,
+ value: {
+ setDefaultBrowserUserChoice: true,
+ setDefaultPDFHandler: false,
+ enabled: true,
+ },
+ },
+ { isRollout: true }
+ );
+
+ Assert.equal(
+ NimbusFeatures.shellService.getVariable("setDefaultBrowserUserChoice"),
+ true
+ );
+ Assert.equal(
+ NimbusFeatures.shellService.getVariable("setDefaultPDFHandler"),
+ false
+ );
+
+ setDefaultBrowserUserChoiceStub.resetHistory();
+ await ShellService.setDefaultBrowser();
+
+ const aumi = XreDirProvider.getInstallHash();
+ Assert.ok(setDefaultBrowserUserChoiceStub.called);
+ Assert.deepEqual(setDefaultBrowserUserChoiceStub.firstCall.args, [aumi, []]);
+
+ await doCleanup();
+});
+
+add_task(async function remoteDisable() {
+ let doCleanup = await NimbusTestUtils.enrollWithFeatureConfig(
+ {
+ featureId: NimbusFeatures.shellService.featureId,
+ value: {
+ setDefaultBrowserUserChoice: false,
+ setDefaultPDFHandler: true,
+ enabled: false,
+ },
+ },
+ { isRollout: true }
+ );
+
+ Assert.equal(
+ NimbusFeatures.shellService.getVariable("setDefaultBrowserUserChoice"),
+ false
+ );
+ Assert.equal(
+ NimbusFeatures.shellService.getVariable("setDefaultPDFHandler"),
+ true
+ );
+
+ setDefaultBrowserUserChoiceStub.resetHistory();
+ await ShellService.setDefaultBrowser();
+
+ Assert.ok(setDefaultBrowserUserChoiceStub.notCalled);
+ Assert.ok(setDefaultStub.called);
+
+ await doCleanup();
+});
+
+add_task(async function test_setAsDefaultPDFHandler_knownBrowser() {
+ const sandbox = sinon.createSandbox();
+
+ const aumi = XreDirProvider.getInstallHash();
+ const expectedArguments = [aumi, [".pdf", "FirefoxPDF"]];
+
+ try {
+ const pdfHandlerResult = { registered: true, knownBrowser: true };
+ sandbox
+ .stub(ShellService, "getDefaultPDFHandler")
+ .returns(pdfHandlerResult);
+
+ info("Testing setAsDefaultPDFHandler(true) when knownBrowser = true");
+ ShellService.setAsDefaultPDFHandler(true);
+ Assert.ok(
+ setDefaultExtensionHandlersUserChoiceStub.called,
+ "Called default browser agent"
+ );
+ Assert.deepEqual(
+ setDefaultExtensionHandlersUserChoiceStub.firstCall.args,
+ expectedArguments,
+ "Called default browser agent with expected arguments"
+ );
+ setDefaultExtensionHandlersUserChoiceStub.resetHistory();
+
+ info("Testing setAsDefaultPDFHandler(false) when knownBrowser = true");
+ ShellService.setAsDefaultPDFHandler(false);
+ Assert.ok(
+ setDefaultExtensionHandlersUserChoiceStub.called,
+ "Called default browser agent"
+ );
+ Assert.deepEqual(
+ setDefaultExtensionHandlersUserChoiceStub.firstCall.args,
+ expectedArguments,
+ "Called default browser agent with expected arguments"
+ );
+ setDefaultExtensionHandlersUserChoiceStub.resetHistory();
+
+ pdfHandlerResult.knownBrowser = false;
+
+ info("Testing setAsDefaultPDFHandler(true) when knownBrowser = false");
+ ShellService.setAsDefaultPDFHandler(true);
+ Assert.ok(
+ setDefaultExtensionHandlersUserChoiceStub.notCalled,
+ "Did not call default browser agent"
+ );
+ setDefaultExtensionHandlersUserChoiceStub.resetHistory();
+
+ info("Testing setAsDefaultPDFHandler(false) when knownBrowser = false");
+ ShellService.setAsDefaultPDFHandler(false);
+ Assert.ok(
+ setDefaultExtensionHandlersUserChoiceStub.called,
+ "Called default browser agent"
+ );
+ Assert.deepEqual(
+ setDefaultExtensionHandlersUserChoiceStub.firstCall.args,
+ expectedArguments,
+ "Called default browser agent with expected arguments"
+ );
+ setDefaultExtensionHandlersUserChoiceStub.resetHistory();
+ } finally {
+ sandbox.restore();
+ }
+});
diff --git a/src/zen/tests/mochitests/shell/browser_setDesktopBackgroundPreview.js b/src/zen/tests/mochitests/shell/browser_setDesktopBackgroundPreview.js
new file mode 100644
index 000000000..b847d0998
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/browser_setDesktopBackgroundPreview.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check whether the preview image for setDesktopBackground is rendered
+ * correctly, without stretching
+ */
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["test.wait300msAfterTabSwitch", true]],
+ });
+});
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:logo",
+ },
+ async () => {
+ const dialogLoad = BrowserTestUtils.domWindowOpened(null, async win => {
+ await BrowserTestUtils.waitForEvent(win, "load");
+ Assert.equal(
+ win.document.documentElement.getAttribute("windowtype"),
+ "Shell:SetDesktopBackground",
+ "Opened correct window"
+ );
+ return true;
+ });
+
+ const image = content.document.images[0];
+ EventUtils.synthesizeMouseAtCenter(image, { type: "contextmenu" });
+
+ const menu = document.getElementById("contentAreaContextMenu");
+ await BrowserTestUtils.waitForPopupEvent(menu, "shown");
+ const menuClosed = BrowserTestUtils.waitForPopupEvent(menu, "hidden");
+
+ const menuItem = document.getElementById("context-setDesktopBackground");
+ try {
+ menu.activateItem(menuItem);
+ } catch (ex) {
+ ok(
+ menuItem.hidden,
+ "should only fail to activate when menu item is hidden"
+ );
+ ok(
+ !ShellService.canSetDesktopBackground,
+ "Should only hide when not able to set the desktop background"
+ );
+ is(
+ AppConstants.platform,
+ "linux",
+ "Should always be able to set desktop background on non-linux platforms"
+ );
+ todo(false, "Skipping test on this configuration");
+
+ menu.hidePopup();
+ await menuClosed;
+ return;
+ }
+
+ await menuClosed;
+
+ const win = await dialogLoad;
+
+ /* setDesktopBackground.js does a setTimeout to wait for correct
+ dimensions. If we don't wait here we could read the preview dimensions
+ before they're changed to match the screen */
+ await TestUtils.waitForTick();
+
+ const canvas = win.document.getElementById("screen");
+ const screenRatio = screen.width / screen.height;
+ const previewRatio = canvas.clientWidth / canvas.clientHeight;
+
+ info(`Screen dimensions are ${screen.width}x${screen.height}`);
+ info(`Screen's raw ratio is ${screenRatio}`);
+ info(
+ `Preview dimensions are ${canvas.clientWidth}x${canvas.clientHeight}`
+ );
+ info(`Preview's raw ratio is ${previewRatio}`);
+
+ Assert.ok(
+ previewRatio < screenRatio + 0.01 && previewRatio > screenRatio - 0.01,
+ "Preview's aspect ratio is within ±.01 of screen's"
+ );
+
+ win.close();
+
+ await menuClosed;
+ }
+ );
+});
diff --git a/src/zen/tests/mochitests/shell/gtest/LimitedAccessFeatureTests.cpp b/src/zen/tests/mochitests/shell/gtest/LimitedAccessFeatureTests.cpp
new file mode 100644
index 000000000..ddf3a7548
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/gtest/LimitedAccessFeatureTests.cpp
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "gtest/gtest.h"
+
+#include "Windows11LimitedAccessFeatures.h"
+#include "WinUtils.h"
+
+TEST(LimitedAccessFeature, VerifyGeneratedInfo)
+{
+ // If running on MSIX we have no guarantee that the
+ // generated LAF info will match the known values.
+ if (mozilla::widget::WinUtils::HasPackageIdentity()) {
+ return;
+ }
+
+ LimitedAccessFeatureInfo knownLafInfo = {
+ // Win11LimitedAccessFeatureType::Taskbar
+ "Win11LimitedAccessFeatureType::Taskbar"_ns, // debugName
+ u"com.microsoft.windows.taskbar.pin"_ns, // feature
+ u"kRFiWpEK5uS6PMJZKmR7MQ=="_ns, // token
+ u"pcsmm0jrprpb2 has registered their use of "_ns // attestation
+ u"com.microsoft.windows.taskbar.pin with Microsoft and agrees to the "_ns
+ u"terms "_ns
+ u"of use."_ns};
+
+ auto generatedLafInfoResult = GenerateLimitedAccessFeatureInfo(
+ "Win11LimitedAccessFeatureType::Taskbar"_ns,
+ u"com.microsoft.windows.taskbar.pin"_ns);
+ ASSERT_TRUE(generatedLafInfoResult.isOk());
+ LimitedAccessFeatureInfo generatedLafInfo = generatedLafInfoResult.unwrap();
+
+ // Check for equality between generated values and known good values
+ ASSERT_TRUE(knownLafInfo.debugName.Equals(generatedLafInfo.debugName));
+ ASSERT_TRUE(knownLafInfo.feature.Equals(generatedLafInfo.feature));
+ ASSERT_TRUE(knownLafInfo.token.Equals(generatedLafInfo.token));
+ ASSERT_TRUE(knownLafInfo.attestation.Equals(generatedLafInfo.attestation));
+}
diff --git a/src/zen/tests/mochitests/shell/gtest/ShellLinkTests.cpp b/src/zen/tests/mochitests/shell/gtest/ShellLinkTests.cpp
new file mode 100644
index 000000000..2cf1b8ed6
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/gtest/ShellLinkTests.cpp
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "gtest/gtest.h"
+
+#include "nsDirectoryServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsWindowsShellServiceInternal.h"
+#include "nsXULAppAPI.h"
+
+TEST(ShellLink, NarrowCharacterArguments)
+{
+ nsCOMPtr exe;
+ nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(exe));
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ RefPtr link;
+ rv = CreateShellLinkObject(exe, {u"test"_ns}, u"test"_ns, exe, 0, u"aumid"_ns,
+ getter_AddRefs(link));
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+ ASSERT_TRUE(link != nullptr);
+
+ std::wstring testArgs = L"\"test\" ";
+
+ wchar_t resultArgs[sizeof(testArgs)];
+ HRESULT hr = link->GetArguments(resultArgs, sizeof(resultArgs));
+ ASSERT_TRUE(SUCCEEDED(hr));
+
+ ASSERT_TRUE(testArgs == resultArgs);
+}
+
+TEST(ShellLink, WideCharacterArguments)
+{
+ nsCOMPtr exe;
+ nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(exe));
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ RefPtr link;
+ rv = CreateShellLinkObject(exe, {u"Test\\テスト用アカウント\\Test"_ns},
+ u"test"_ns, exe, 0, u"aumid"_ns,
+ getter_AddRefs(link));
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+ ASSERT_TRUE(link != nullptr);
+
+ std::wstring testArgs = L"\"Test\\テスト用アカウント\\Test\" ";
+
+ wchar_t resultArgs[sizeof(testArgs)];
+ HRESULT hr = link->GetArguments(resultArgs, sizeof(resultArgs));
+ ASSERT_TRUE(SUCCEEDED(hr));
+
+ ASSERT_TRUE(testArgs == resultArgs);
+}
diff --git a/src/zen/tests/mochitests/shell/gtest/moz.build b/src/zen/tests/mochitests/shell/gtest/moz.build
new file mode 100644
index 000000000..e33786fad
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/gtest/moz.build
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows":
+ LOCAL_INCLUDES += ["/browser/components/shell"]
+
+ UNIFIED_SOURCES += [
+ "LimitedAccessFeatureTests.cpp",
+ "ShellLinkTests.cpp",
+ ]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/src/zen/tests/mochitests/shell/head.js b/src/zen/tests/mochitests/shell/head.js
new file mode 100644
index 000000000..a6cebb3d1
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/head.js
@@ -0,0 +1,159 @@
+"use strict";
+
+const { Subprocess } = ChromeUtils.importESModule(
+ "resource://gre/modules/Subprocess.sys.mjs"
+);
+
+const TEMP_DIR = Services.dirsvc.get("TmpD", Ci.nsIFile).path;
+
+const screenshotPath = PathUtils.join(TEMP_DIR, "headless_test_screenshot.png");
+
+async function runFirefox(args) {
+ const XRE_EXECUTABLE_FILE = "XREExeF";
+ const firefoxExe = Services.dirsvc.get(XRE_EXECUTABLE_FILE, Ci.nsIFile).path;
+ const NS_APP_PREFS_50_FILE = "PrefF";
+ const mochiPrefsFile = Services.dirsvc.get(NS_APP_PREFS_50_FILE, Ci.nsIFile);
+ const mochiPrefsPath = mochiPrefsFile.path;
+ const mochiPrefsName = mochiPrefsFile.leafName;
+ const profilePath = PathUtils.join(
+ TEMP_DIR,
+ "headless_test_screenshot_profile"
+ );
+ const prefsPath = PathUtils.join(profilePath, mochiPrefsName);
+ const firefoxArgs = ["-profile", profilePath];
+
+ await IOUtils.makeDirectory(profilePath);
+ await IOUtils.copy(mochiPrefsPath, prefsPath);
+ let proc = await Subprocess.call({
+ command: firefoxExe,
+ arguments: firefoxArgs.concat(args),
+ // Disable leak detection to avoid intermittent failure bug 1331152.
+ environmentAppend: true,
+ environment: {
+ ASAN_OPTIONS:
+ "detect_leaks=0:quarantine_size=50331648:malloc_context_size=5",
+ // Don't enable Marionette.
+ MOZ_MARIONETTE: null,
+ },
+ });
+ let stdout;
+ while ((stdout = await proc.stdout.readString())) {
+ dump(`>>> ${stdout}\n`);
+ }
+ let { exitCode } = await proc.wait();
+ is(exitCode, 0, "Firefox process should exit with code 0");
+ await IOUtils.remove(profilePath, { recursive: true });
+}
+
+async function testFileCreationPositive(args, path) {
+ await runFirefox(args);
+
+ let saved = IOUtils.exists(path);
+ ok(saved, "A screenshot should be saved as " + path);
+ if (!saved) {
+ return;
+ }
+
+ let info = await IOUtils.stat(path);
+ Assert.greater(info.size, 0, "Screenshot should not be an empty file");
+ await IOUtils.remove(path);
+}
+
+async function testFileCreationNegative(args, path) {
+ await runFirefox(args);
+
+ let saved = await IOUtils.exists(path);
+ ok(!saved, "A screenshot should not be saved");
+ await IOUtils.remove(path);
+}
+
+async function testWindowSizePositive(width, height) {
+ let size = String(width);
+ if (height) {
+ size += "," + height;
+ }
+
+ await runFirefox([
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "-screenshot",
+ screenshotPath,
+ "-window-size",
+ size,
+ ]);
+
+ let saved = await IOUtils.exists(screenshotPath);
+ ok(saved, "A screenshot should be saved in the tmp directory");
+ if (!saved) {
+ return;
+ }
+
+ let data = await IOUtils.read(screenshotPath);
+ await new Promise(resolve => {
+ let blob = new Blob([data], { type: "image/png" });
+ let reader = new FileReader();
+ reader.onloadend = function () {
+ let screenshot = new Image();
+ screenshot.onload = function () {
+ is(
+ screenshot.width,
+ width,
+ "Screenshot should be " + width + " pixels wide"
+ );
+ if (height) {
+ is(
+ screenshot.height,
+ height,
+ "Screenshot should be " + height + " pixels tall"
+ );
+ }
+ resolve();
+ };
+ screenshot.src = reader.result;
+ };
+ reader.readAsDataURL(blob);
+ });
+ await IOUtils.remove(screenshotPath);
+}
+
+async function testGreen(url, path) {
+ await runFirefox(["-url", url, `--screenshot=${path}`]);
+
+ let saved = await IOUtils.exists(path);
+ ok(saved, "A screenshot should be saved in the tmp directory");
+ if (!saved) {
+ return;
+ }
+
+ let data = await IOUtils.read(path);
+ let image = await new Promise(resolve => {
+ let blob = new Blob([data], { type: "image/png" });
+ let reader = new FileReader();
+ reader.onloadend = function () {
+ let screenshot = new Image();
+ screenshot.onload = function () {
+ resolve(screenshot);
+ };
+ screenshot.src = reader.result;
+ };
+ reader.readAsDataURL(blob);
+ });
+ let canvas = document.createElement("canvas");
+ canvas.width = image.naturalWidth;
+ canvas.height = image.naturalHeight;
+ let ctx = canvas.getContext("2d");
+ ctx.drawImage(image, 0, 0);
+ let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ let rgba = imageData.data;
+
+ let found = false;
+ for (let i = 0; i < rgba.length; i += 4) {
+ if (rgba[i] === 0 && rgba[i + 1] === 255 && rgba[i + 2] === 0) {
+ found = true;
+ break;
+ }
+ }
+ ok(found, "There should be a green pixel in the screenshot.");
+
+ await IOUtils.remove(path);
+}
diff --git a/src/zen/tests/mochitests/shell/headless.html b/src/zen/tests/mochitests/shell/headless.html
new file mode 100644
index 000000000..bbde89507
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/headless.html
@@ -0,0 +1,6 @@
+
+
+
+Hi
+
+
diff --git a/src/zen/tests/mochitests/shell/headless_cross_origin.html b/src/zen/tests/mochitests/shell/headless_cross_origin.html
new file mode 100644
index 000000000..3bb09aa5d
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/headless_cross_origin.html
@@ -0,0 +1,7 @@
+
+
+
+
+Hi
+
+
diff --git a/src/zen/tests/mochitests/shell/headless_iframe.html b/src/zen/tests/mochitests/shell/headless_iframe.html
new file mode 100644
index 000000000..f318cbe0f
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/headless_iframe.html
@@ -0,0 +1,6 @@
+
+
+
+Hi
+
+
diff --git a/src/zen/tests/mochitests/shell/headless_redirect.html b/src/zen/tests/mochitests/shell/headless_redirect.html
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/zen/tests/mochitests/shell/headless_redirect.html^headers^ b/src/zen/tests/mochitests/shell/headless_redirect.html^headers^
new file mode 100644
index 000000000..1d7db20ea
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/headless_redirect.html^headers^
@@ -0,0 +1,2 @@
+HTTP 302 Moved Temporarily
+Location: headless.html
\ No newline at end of file
diff --git a/src/zen/tests/mochitests/shell/mac_desktop_image.py b/src/zen/tests/mochitests/shell/mac_desktop_image.py
new file mode 100755
index 000000000..e3ccc7719
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/mac_desktop_image.py
@@ -0,0 +1,168 @@
+#!/usr/bin/python
+# 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/. */
+
+"""
+mac_desktop_image.py
+
+Mac-specific utility to get/set the desktop background image or check that
+the current background image path matches a provided path.
+
+Depends on Objective-C python binding imports which are in the python import
+paths by default when using macOS's /usr/bin/python.
+
+Includes generous amount of logging to aid debugging for use in automated tests.
+"""
+
+import argparse
+import logging
+import os
+import sys
+
+#
+# These Objective-C bindings imports are included in the import path by default
+# for the Mac-bundled python installed in /usr/bin/python. They're needed to
+# call the Objective-C API's to set and retrieve the current desktop background
+# image.
+#
+from AppKit import NSScreen, NSWorkspace
+from Cocoa import NSURL
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Utility to print, set, or "
+ + "check the path to image being used as "
+ + "the desktop background image. By "
+ + "default, prints the path to the "
+ + "current desktop background image."
+ )
+ parser.add_argument(
+ "-v",
+ "--verbose",
+ action="store_true",
+ help="print verbose debugging information",
+ default=False,
+ )
+ group = parser.add_mutually_exclusive_group()
+ group.add_argument(
+ "-s",
+ "--set-background-image",
+ dest="newBackgroundImagePath",
+ required=False,
+ help="path to the new background image to set. A zero "
+ + "exit code indicates no errors occurred.",
+ default=None,
+ )
+ group.add_argument(
+ "-c",
+ "--check-background-image",
+ dest="checkBackgroundImagePath",
+ required=False,
+ help="check if the provided background image path "
+ + "matches the provided path. A zero exit code "
+ + "indicates the paths match.",
+ default=None,
+ )
+ args = parser.parse_args()
+
+ # Using logging for verbose output
+ if args.verbose:
+ logging.basicConfig(level=logging.DEBUG)
+ else:
+ logging.basicConfig(level=logging.CRITICAL)
+ logger = logging.getLogger("desktopImage")
+
+ # Print what we're going to do
+ if args.checkBackgroundImagePath is not None:
+ logger.debug(
+ "checking provided desktop image %s matches current "
+ "image" % args.checkBackgroundImagePath
+ )
+ elif args.newBackgroundImagePath is not None:
+ logger.debug("setting image to %s " % args.newBackgroundImagePath)
+ else:
+ logger.debug("retrieving desktop image path")
+
+ focussedScreen = NSScreen.mainScreen()
+ if not focussedScreen:
+ raise RuntimeError("mainScreen error")
+
+ ws = NSWorkspace.sharedWorkspace()
+ if not ws:
+ raise RuntimeError("sharedWorkspace error")
+
+ # If we're just checking the image path, check it and then return.
+ # A successful exit code (0) indicates the paths match.
+ if args.checkBackgroundImagePath is not None:
+ # Get existing desktop image path and resolve it
+ existingImageURL = getCurrentDesktopImageURL(focussedScreen, ws, logger)
+ existingImagePath = existingImageURL.path()
+ existingImagePathReal = os.path.realpath(existingImagePath)
+ logger.debug("existing desktop image: %s" % existingImagePath)
+ logger.debug("existing desktop image realpath: %s" % existingImagePath)
+
+ # Resolve the path we're going to check
+ checkImagePathReal = os.path.realpath(args.checkBackgroundImagePath)
+ logger.debug("check desktop image: %s" % args.checkBackgroundImagePath)
+ logger.debug("check desktop image realpath: %s" % checkImagePathReal)
+
+ if existingImagePathReal == checkImagePathReal:
+ print("desktop image path matches provided path")
+ return True
+
+ print("desktop image path does NOT match provided path")
+ return False
+
+ # Log the current desktop image
+ if args.verbose:
+ existingImageURL = getCurrentDesktopImageURL(focussedScreen, ws, logger)
+ logger.debug("existing desktop image: %s" % existingImageURL.path())
+
+ # Set the desktop image
+ if args.newBackgroundImagePath is not None:
+ newImagePath = args.newBackgroundImagePath
+ if not os.path.exists(newImagePath):
+ logger.critical("%s does not exist" % newImagePath)
+ return False
+ if not os.access(newImagePath, os.R_OK):
+ logger.critical("%s is not readable" % newImagePath)
+ return False
+
+ logger.debug("new desktop image to set: %s" % newImagePath)
+ newImageURL = NSURL.fileURLWithPath_(newImagePath)
+ logger.debug("new desktop image URL to set: %s" % newImageURL)
+
+ status = False
+ (status, error) = ws.setDesktopImageURL_forScreen_options_error_(
+ newImageURL, focussedScreen, None, None
+ )
+ if not status:
+ raise RuntimeError("setDesktopImageURL error")
+
+ # Print the current desktop image
+ imageURL = getCurrentDesktopImageURL(focussedScreen, ws, logger)
+ imagePath = imageURL.path()
+ imagePathReal = os.path.realpath(imagePath)
+ logger.debug("updated desktop image URL: %s" % imageURL)
+ logger.debug("updated desktop image path: %s" % imagePath)
+ logger.debug("updated desktop image path (resolved): %s" % imagePathReal)
+ print(imagePathReal)
+ return True
+
+
+def getCurrentDesktopImageURL(focussedScreen, workspace, logger):
+ imageURL = workspace.desktopImageURLForScreen_(focussedScreen)
+ if not imageURL:
+ raise RuntimeError("desktopImageURLForScreen returned invalid URL")
+ if not imageURL.isFileURL():
+ logger.warning("desktop image URL is not a file URL")
+ return imageURL
+
+
+if __name__ == "__main__":
+ if not main():
+ sys.exit(1)
+ else:
+ sys.exit(0)
diff --git a/src/zen/tests/mochitests/shell/unit/test_macOS_showSecurityPreferences.js b/src/zen/tests/mochitests/shell/unit/test_macOS_showSecurityPreferences.js
new file mode 100644
index 000000000..3b9837aac
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/unit/test_macOS_showSecurityPreferences.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test the macOS ShowSecurityPreferences shell service method.
+ */
+
+"use strict";
+
+// eslint-disable-next-line mozilla/no-redeclare-with-import-autofix
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+function killSystemPreferences() {
+ let killallFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ killallFile.initWithPath("/usr/bin/killall");
+ let sysPrefsArg = ["System Preferences"];
+ if (AppConstants.isPlatformAndVersionAtLeast("macosx", 22)) {
+ sysPrefsArg = ["System Settings"];
+ }
+ let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+ process.init(killallFile);
+ process.run(true, sysPrefsArg, 1);
+ return process.exitValue;
+}
+
+add_setup(async function () {
+ info("Ensure System Preferences isn't already running");
+ killSystemPreferences();
+});
+
+add_task(async function test_prefsOpen() {
+ let shellSvc = Cc["@mozilla.org/browser/shell-service;1"].getService(
+ Ci.nsIMacShellService
+ );
+ shellSvc.showSecurityPreferences("Privacy_AllFiles");
+
+ equal(killSystemPreferences(), 0, "Ensure System Preferences was started");
+});
diff --git a/src/zen/tests/mochitests/shell/unit/xpcshell.toml b/src/zen/tests/mochitests/shell/unit/xpcshell.toml
new file mode 100644
index 000000000..445dc9edd
--- /dev/null
+++ b/src/zen/tests/mochitests/shell/unit/xpcshell.toml
@@ -0,0 +1,7 @@
+[DEFAULT]
+run-if = ["os != 'android'"]
+firefox-appdir = "browser"
+tags = "os_integration"
+
+["test_macOS_showSecurityPreferences.js"]
+run-if = ["os == 'mac'"]
diff --git a/src/zen/tests/mochitests/tooltiptext/browser.toml b/src/zen/tests/mochitests/tooltiptext/browser.toml
new file mode 100644
index 000000000..42aca4a7a
--- /dev/null
+++ b/src/zen/tests/mochitests/tooltiptext/browser.toml
@@ -0,0 +1,17 @@
+[DEFAULT]
+
+["browser_bug329212.js"]
+support-files = ["title_test.svg"]
+
+["browser_bug331772_xul_tooltiptext_in_html.js"]
+support-files = ["xul_tooltiptext.xhtml"]
+
+["browser_bug561623.js"]
+
+["browser_bug581947.js"]
+
+["browser_input_file_tooltips.js"]
+
+["browser_nac_tooltip.js"]
+
+["browser_shadow_dom_tooltip.js"]
diff --git a/src/zen/tests/mochitests/tooltiptext/browser_bug329212.js b/src/zen/tests/mochitests/tooltiptext/browser_bug329212.js
new file mode 100644
index 000000000..d669e60c2
--- /dev/null
+++ b/src/zen/tests/mochitests/tooltiptext/browser_bug329212.js
@@ -0,0 +1,48 @@
+"use strict";
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "http://mochi.test:8888/browser/toolkit/components/tooltiptext/tests/title_test.svg",
+ },
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [""], function () {
+ let tttp = Cc[
+ "@mozilla.org/embedcomp/default-tooltiptextprovider;1"
+ ].getService(Ci.nsITooltipTextProvider);
+ function checkElement(id, expectedTooltipText) {
+ let el = content.document.getElementById(id);
+ let textObj = {};
+ let shouldHaveTooltip = expectedTooltipText !== null;
+ is(
+ tttp.getNodeText(el, textObj, {}),
+ shouldHaveTooltip,
+ "element " +
+ id +
+ " should " +
+ (shouldHaveTooltip ? "" : "not ") +
+ "have a tooltip"
+ );
+ if (shouldHaveTooltip) {
+ is(
+ textObj.value,
+ expectedTooltipText,
+ "element " + id + " should have the right tooltip text"
+ );
+ }
+ }
+ checkElement("svg1", "This is a non-root SVG element title");
+ checkElement("text1", "\n\n\n This is a title\n\n ");
+ checkElement("text2", null);
+ checkElement("text3", null);
+ checkElement("link1", "\n This is a title\n ");
+ checkElement("text4", "\n This is a title\n ");
+ checkElement("link2", null);
+ checkElement("link3", "This is an xlink:title attribute");
+ checkElement("link4", "This is an xlink:title attribute");
+ checkElement("text5", null);
+ });
+ }
+ );
+});
diff --git a/src/zen/tests/mochitests/tooltiptext/browser_bug331772_xul_tooltiptext_in_html.js b/src/zen/tests/mochitests/tooltiptext/browser_bug331772_xul_tooltiptext_in_html.js
new file mode 100644
index 000000000..61c50e542
--- /dev/null
+++ b/src/zen/tests/mochitests/tooltiptext/browser_bug331772_xul_tooltiptext_in_html.js
@@ -0,0 +1,30 @@
+/**
+ * Tests that the tooltiptext attribute is used for XUL elements in an HTML doc.
+ */
+add_task(async function () {
+ await SpecialPowers.pushPermissions([
+ { type: "allowXULXBL", allow: true, context: "http://mochi.test:8888" },
+ ]);
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "http://mochi.test:8888/browser/toolkit/components/tooltiptext/tests/xul_tooltiptext.xhtml",
+ },
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [""], function () {
+ let textObj = {};
+ let tttp = Cc[
+ "@mozilla.org/embedcomp/default-tooltiptextprovider;1"
+ ].getService(Ci.nsITooltipTextProvider);
+ let xulToolbarButton =
+ content.document.getElementById("xulToolbarButton");
+ ok(
+ tttp.getNodeText(xulToolbarButton, textObj, {}),
+ "should get tooltiptext"
+ );
+ is(textObj.value, "XUL tooltiptext");
+ });
+ }
+ );
+});
diff --git a/src/zen/tests/mochitests/tooltiptext/browser_bug561623.js b/src/zen/tests/mochitests/tooltiptext/browser_bug561623.js
new file mode 100644
index 000000000..93f68d307
--- /dev/null
+++ b/src/zen/tests/mochitests/tooltiptext/browser_bug561623.js
@@ -0,0 +1,33 @@
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "data:text/html,",
+ },
+ async function (browser) {
+ await SpecialPowers.spawn(browser, [""], function () {
+ let tttp = Cc[
+ "@mozilla.org/embedcomp/default-tooltiptextprovider;1"
+ ].getService(Ci.nsITooltipTextProvider);
+ let i = content.document.getElementById("i");
+
+ ok(
+ !tttp.getNodeText(i, {}, {}),
+ "No tooltip should be shown when @title is null"
+ );
+
+ i.title = "foo";
+ ok(
+ tttp.getNodeText(i, {}, {}),
+ "A tooltip should be shown when @title is not the empty string"
+ );
+
+ i.pattern = "bar";
+ ok(
+ tttp.getNodeText(i, {}, {}),
+ "A tooltip should be shown when @title is not the empty string"
+ );
+ });
+ }
+ );
+});
diff --git a/src/zen/tests/mochitests/tooltiptext/browser_bug581947.js b/src/zen/tests/mochitests/tooltiptext/browser_bug581947.js
new file mode 100644
index 000000000..097173547
--- /dev/null
+++ b/src/zen/tests/mochitests/tooltiptext/browser_bug581947.js
@@ -0,0 +1,107 @@
+function check(aBrowser, aElementName, aBarred, aType) {
+ return SpecialPowers.spawn(
+ aBrowser,
+ [[aElementName, aBarred, aType]],
+ async function ([aElementName, aBarred, aType]) {
+ let e = content.document.createElement(aElementName);
+ let contentElement = content.document.getElementById("content");
+ contentElement.appendChild(e);
+
+ if (aType) {
+ e.type = aType;
+ }
+
+ let tttp = Cc[
+ "@mozilla.org/embedcomp/default-tooltiptextprovider;1"
+ ].getService(Ci.nsITooltipTextProvider);
+ ok(
+ !tttp.getNodeText(e, {}, {}),
+ "No tooltip should be shown when the element is valid"
+ );
+
+ e.setCustomValidity("foo");
+ if (aBarred) {
+ ok(
+ !tttp.getNodeText(e, {}, {}),
+ "No tooltip should be shown when the element is barred from constraint validation"
+ );
+ } else {
+ ok(
+ tttp.getNodeText(e, {}, {}),
+ e.tagName + " A tooltip should be shown when the element isn't valid"
+ );
+ }
+
+ e.setAttribute("title", "");
+ ok(
+ !tttp.getNodeText(e, {}, {}),
+ "No tooltip should be shown if the title attribute is set"
+ );
+
+ e.removeAttribute("title");
+ contentElement.setAttribute("novalidate", "");
+ ok(
+ !tttp.getNodeText(e, {}, {}),
+ "No tooltip should be shown if the novalidate attribute is set on the form owner"
+ );
+ contentElement.removeAttribute("novalidate");
+
+ e.remove();
+ }
+ );
+}
+
+function todo_check(aBrowser, aElementName, aBarred) {
+ return SpecialPowers.spawn(
+ aBrowser,
+ [[aElementName, aBarred]],
+ async function ([aElementName]) {
+ let e = content.document.createElement(aElementName);
+ let contentElement = content.document.getElementById("content");
+ contentElement.appendChild(e);
+
+ let caught = false;
+ try {
+ e.setCustomValidity("foo");
+ } catch (e) {
+ caught = true;
+ }
+
+ todo(!caught, "setCustomValidity should exist for " + aElementName);
+
+ e.remove();
+ }
+ );
+}
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "data:text/html,",
+ },
+ async function (browser) {
+ let testData = [
+ /* element name, barred */
+ ["input", false, null],
+ ["textarea", false, null],
+ ["button", true, "button"],
+ ["button", false, "submit"],
+ ["select", false, null],
+ ["output", true, null],
+ ["fieldset", true, null],
+ ["object", true, null],
+ ];
+
+ for (let data of testData) {
+ await check(browser, data[0], data[1], data[2]);
+ }
+
+ let todo_testData = [["keygen", "false"]];
+
+ for (let data of todo_testData) {
+ await todo_check(browser, data[0], data[1]);
+ }
+ }
+ );
+});
diff --git a/src/zen/tests/mochitests/tooltiptext/browser_input_file_tooltips.js b/src/zen/tests/mochitests/tooltiptext/browser_input_file_tooltips.js
new file mode 100644
index 000000000..2f1385f37
--- /dev/null
+++ b/src/zen/tests/mochitests/tooltiptext/browser_input_file_tooltips.js
@@ -0,0 +1,131 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+let tempFile;
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({ set: [["ui.tooltip.delay_ms", 0]] });
+ tempFile = createTempFile();
+ registerCleanupFunction(function () {
+ tempFile.remove(true);
+ });
+});
+
+add_task(async function test_singlefile_selected() {
+ await do_test({ value: true, result: "testfile_bug1251809" });
+});
+
+add_task(async function test_title_set() {
+ await do_test({ title: "foo", result: "foo" });
+});
+
+add_task(async function test_nofile_selected() {
+ await do_test({ result: "No file selected." });
+});
+
+add_task(async function test_multipleset_nofile_selected() {
+ await do_test({ multiple: true, result: "No files selected." });
+});
+
+add_task(async function test_requiredset() {
+ await do_test({ required: true, result: "Please select a file." });
+});
+
+async function do_test(test) {
+ info(`starting test ${JSON.stringify(test)}`);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ info("Moving mouse out of the way.");
+ await EventUtils.synthesizeAndWaitNativeMouseMove(
+ tab.linkedBrowser,
+ 300,
+ 300
+ );
+
+ info("creating input field");
+ await SpecialPowers.spawn(tab.linkedBrowser, [test], async function (test) {
+ let doc = content.document;
+ let input = doc.createElement("input");
+ doc.body.appendChild(input);
+ input.id = "test_input";
+ input.setAttribute("style", "position: absolute; top: 0; left: 0;");
+ input.type = "file";
+ if (test.title) {
+ input.setAttribute("title", test.title);
+ }
+ if (test.multiple) {
+ input.multiple = true;
+ }
+ if (test.required) {
+ input.required = true;
+ }
+ });
+
+ if (test.value) {
+ info("Creating mock filepicker to select files");
+ let MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(window.browsingContext);
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+ MockFilePicker.displayDirectory = FileUtils.getDir("TmpD", []);
+ MockFilePicker.setFiles([tempFile]);
+ MockFilePicker.afterOpenCallback = MockFilePicker.cleanup;
+
+ try {
+ // Open the File Picker dialog (MockFilePicker) to select
+ // the files for the test.
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#test_input",
+ {},
+ tab.linkedBrowser
+ );
+ info("Waiting for the input to have the requisite files");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ let input = content.document.querySelector("#test_input");
+ await ContentTaskUtils.waitForCondition(
+ () => input.files.length,
+ "The input should have at least one file selected"
+ );
+ info(`The input has ${input.files.length} file(s) selected.`);
+ });
+ } catch (e) {}
+ } else {
+ info("No real file selection required.");
+ }
+
+ let awaitTooltipOpen = new Promise(resolve => {
+ let tooltipId = Services.appinfo.browserTabsRemoteAutostart
+ ? "remoteBrowserTooltip"
+ : "aHTMLTooltip";
+ let tooltip = document.getElementById(tooltipId);
+ tooltip.addEventListener(
+ "popupshown",
+ function (event) {
+ resolve(event.target);
+ },
+ { once: true }
+ );
+ });
+ info("Initial mouse move");
+ await EventUtils.synthesizeAndWaitNativeMouseMove(tab.linkedBrowser, 50, 5);
+ info("Waiting");
+ await new Promise(resolve => setTimeout(resolve, 400));
+ info("Second mouse move");
+ await EventUtils.synthesizeAndWaitNativeMouseMove(tab.linkedBrowser, 70, 5);
+ info("Waiting for tooltip to open");
+ let tooltip = await awaitTooltipOpen;
+
+ is(
+ tooltip.getAttribute("label"),
+ test.result,
+ "tooltip label should match expectation"
+ );
+
+ info("Closing tab");
+ BrowserTestUtils.removeTab(tab);
+}
+
+function createTempFile() {
+ let file = FileUtils.getDir("TmpD", []);
+ file.append("testfile_bug1251809");
+ file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+ return file;
+}
diff --git a/src/zen/tests/mochitests/tooltiptext/browser_nac_tooltip.js b/src/zen/tests/mochitests/tooltiptext/browser_nac_tooltip.js
new file mode 100644
index 000000000..449c8b9da
--- /dev/null
+++ b/src/zen/tests/mochitests/tooltiptext/browser_nac_tooltip.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+"use strict";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({ set: [["ui.tooltip.delay_ms", 0]] });
+});
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "data:text/html,",
+ },
+ async function (browser) {
+ info("Moving mouse out of the way.");
+ await EventUtils.synthesizeAndWaitNativeMouseMove(browser, 300, 300);
+
+ await SpecialPowers.spawn(browser, [], function () {
+ let widget = content.document.insertAnonymousContent();
+ widget.root.innerHTML = ``;
+ let tttp = Cc[
+ "@mozilla.org/embedcomp/default-tooltiptextprovider;1"
+ ].getService(Ci.nsITooltipTextProvider);
+
+ let text = {};
+ let dir = {};
+ ok(
+ tttp.getNodeText(widget.root.querySelector("button"), text, dir),
+ "A tooltip should be shown for NAC"
+ );
+ is(text.value, "foo", "Tooltip text should be correct");
+ });
+
+ let awaitTooltipOpen = new Promise(resolve => {
+ let tooltipId = Services.appinfo.browserTabsRemoteAutostart
+ ? "remoteBrowserTooltip"
+ : "aHTMLTooltip";
+ let tooltip = document.getElementById(tooltipId);
+ tooltip.addEventListener(
+ "popupshown",
+ function (event) {
+ resolve(event.target);
+ },
+ { once: true }
+ );
+ });
+
+ info("Initial mouse move");
+ await EventUtils.synthesizeAndWaitNativeMouseMove(browser, 50, 5);
+ info("Waiting");
+ await new Promise(resolve => setTimeout(resolve, 400));
+ info("Second mouse move");
+ await EventUtils.synthesizeAndWaitNativeMouseMove(browser, 70, 5);
+ info("Waiting for tooltip to open");
+ let tooltip = await awaitTooltipOpen;
+ is(
+ tooltip.getAttribute("label"),
+ "foo",
+ "tooltip label should match expectation"
+ );
+ }
+ );
+});
diff --git a/src/zen/tests/mochitests/tooltiptext/browser_shadow_dom_tooltip.js b/src/zen/tests/mochitests/tooltiptext/browser_shadow_dom_tooltip.js
new file mode 100644
index 000000000..4ceb918da
--- /dev/null
+++ b/src/zen/tests/mochitests/tooltiptext/browser_shadow_dom_tooltip.js
@@ -0,0 +1,166 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({ set: [["ui.tooltip.delay_ms", 0]] });
+});
+
+add_task(async function test_title_in_shadow_dom() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ info("Moving mouse out of the way.");
+ await EventUtils.synthesizeAndWaitNativeMouseMove(
+ tab.linkedBrowser,
+ 300,
+ 300
+ );
+
+ info("creating host");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ let doc = content.document;
+ let host = doc.createElement("div");
+ doc.body.appendChild(host);
+ host.setAttribute("style", "position: absolute; top: 0; left: 0;");
+ var sr = host.attachShadow({ mode: "closed" });
+ sr.innerHTML =
+ "shadow
";
+ });
+
+ let awaitTooltipOpen = new Promise(resolve => {
+ let tooltipId = Services.appinfo.browserTabsRemoteAutostart
+ ? "remoteBrowserTooltip"
+ : "aHTMLTooltip";
+ let tooltip = document.getElementById(tooltipId);
+ tooltip.addEventListener(
+ "popupshown",
+ function (event) {
+ resolve(event.target);
+ },
+ { once: true }
+ );
+ });
+ info("Initial mouse move");
+ await EventUtils.synthesizeAndWaitNativeMouseMove(tab.linkedBrowser, 50, 5);
+ info("Waiting");
+ await new Promise(resolve => setTimeout(resolve, 400));
+ info("Second mouse move");
+ await EventUtils.synthesizeAndWaitNativeMouseMove(tab.linkedBrowser, 70, 5);
+ info("Waiting for tooltip to open");
+ let tooltip = await awaitTooltipOpen;
+
+ is(
+ tooltip.getAttribute("label"),
+ "shadow",
+ "tooltip label should match expectation"
+ );
+
+ info("Closing tab");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_title_in_light_dom() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ info("Moving mouse out of the way.");
+ await EventUtils.synthesizeAndWaitNativeMouseMove(
+ tab.linkedBrowser,
+ 300,
+ 300
+ );
+
+ info("creating host");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ let doc = content.document;
+ let host = doc.createElement("div");
+ host.title = "light";
+ doc.body.appendChild(host);
+ host.setAttribute("style", "position: absolute; top: 0; left: 0;");
+ var sr = host.attachShadow({ mode: "closed" });
+ sr.innerHTML = "shadow
";
+ });
+
+ let awaitTooltipOpen = new Promise(resolve => {
+ let tooltipId = Services.appinfo.browserTabsRemoteAutostart
+ ? "remoteBrowserTooltip"
+ : "aHTMLTooltip";
+ let tooltip = document.getElementById(tooltipId);
+ tooltip.addEventListener(
+ "popupshown",
+ function (event) {
+ resolve(event.target);
+ },
+ { once: true }
+ );
+ });
+ info("Initial mouse move");
+ await EventUtils.synthesizeAndWaitNativeMouseMove(tab.linkedBrowser, 50, 5);
+ info("Waiting");
+ await new Promise(resolve => setTimeout(resolve, 400));
+ info("Second mouse move");
+ await EventUtils.synthesizeAndWaitNativeMouseMove(tab.linkedBrowser, 70, 5);
+ info("Waiting for tooltip to open");
+ let tooltip = await awaitTooltipOpen;
+
+ is(
+ tooltip.getAttribute("label"),
+ "light",
+ "tooltip label should match expectation"
+ );
+
+ info("Closing tab");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_title_through_slot() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ info("Moving mouse out of the way.");
+ await EventUtils.synthesizeAndWaitNativeMouseMove(
+ tab.linkedBrowser,
+ 300,
+ 300
+ );
+
+ info("creating host");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ let doc = content.document;
+ let host = doc.createElement("div");
+ host.title = "light";
+ host.innerHTML = "light
";
+ doc.body.appendChild(host);
+ host.setAttribute("style", "position: absolute; top: 0; left: 0;");
+ var sr = host.attachShadow({ mode: "closed" });
+ sr.innerHTML =
+ "
";
+ });
+
+ let awaitTooltipOpen = new Promise(resolve => {
+ let tooltipId = Services.appinfo.browserTabsRemoteAutostart
+ ? "remoteBrowserTooltip"
+ : "aHTMLTooltip";
+ let tooltip = document.getElementById(tooltipId);
+ tooltip.addEventListener(
+ "popupshown",
+ function (event) {
+ resolve(event.target);
+ },
+ { once: true }
+ );
+ });
+ info("Initial mouse move");
+ await EventUtils.synthesizeAndWaitNativeMouseMove(tab.linkedBrowser, 50, 5);
+ info("Waiting");
+ await new Promise(resolve => setTimeout(resolve, 400));
+ info("Second mouse move");
+ await EventUtils.synthesizeAndWaitNativeMouseMove(tab.linkedBrowser, 70, 5);
+ info("Waiting for tooltip to open");
+ let tooltip = await awaitTooltipOpen;
+
+ is(
+ tooltip.getAttribute("label"),
+ "shadow",
+ "tooltip label should match expectation"
+ );
+
+ info("Closing tab");
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/src/zen/tests/mochitests/tooltiptext/title_test.svg b/src/zen/tests/mochitests/tooltiptext/title_test.svg
new file mode 100644
index 000000000..80390a3cc
--- /dev/null
+++ b/src/zen/tests/mochitests/tooltiptext/title_test.svg
@@ -0,0 +1,59 @@
+
diff --git a/src/zen/tests/mochitests/tooltiptext/xul_tooltiptext.xhtml b/src/zen/tests/mochitests/tooltiptext/xul_tooltiptext.xhtml
new file mode 100644
index 000000000..8288ffc5f
--- /dev/null
+++ b/src/zen/tests/mochitests/tooltiptext/xul_tooltiptext.xhtml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+