Merge branch 'drag-and-drop-refactor' of https://github.com/zen-browser/desktop into drag-and-drop-refactor

This commit is contained in:
mr. m
2025-12-24 22:48:21 +01:00
250 changed files with 11787 additions and 2147 deletions

View File

@@ -1,5 +1,5 @@
diff --git a/browser/base/content/browser-addons.js b/browser/base/content/browser-addons.js
index fadcbfca95ee28140579430c0371baad0e2f216a..7454b801b4ad892d6ad122277eb7c7736e976f9f 100644
index fadcbfca95ee28140579430c0371baad0e2f216a..1947b4ab1c0b4ba0099147c9f13ae78d6e425bdf 100644
--- a/browser/base/content/browser-addons.js
+++ b/browser/base/content/browser-addons.js
@@ -1069,7 +1069,7 @@ var gXPInstallObserver = {
@@ -38,20 +38,92 @@ index fadcbfca95ee28140579430c0371baad0e2f216a..7454b801b4ad892d6ad122277eb7c773
}
return anchorID;
@@ -2657,11 +2657,7 @@ var gUnifiedExtensions = {
// Lazy load the unified-extensions-panel panel the first time we need to
// display it.
if (!this._panel) {
- let template = document.getElementById(
- "unified-extensions-panel-template"
- );
- template.replaceWith(template.content);
- this._panel = document.getElementById("unified-extensions-panel");
+ this._panel = document.getElementById("zen-unified-site-data-panel");
let customizationArea = this._panel.querySelector(
"#unified-extensions-area"
@@ -2547,7 +2547,7 @@ var gUnifiedExtensions = {
requestAnimationFrame(() => this.updateAttention());
},
- onToolbarVisibilityChange(toolbarId, isVisible) {
+ onToolbarVisibilityChange(toolbarId, isVisible, panel = this.panel) {
// A list of extension widget IDs (possibly empty).
let widgetIDs;
@@ -2561,7 +2561,7 @@ var gUnifiedExtensions = {
}
// The list of overflowed extensions in the extensions panel.
- const overflowedExtensionsList = this.panel.querySelector(
+ const overflowedExtensionsList = panel.querySelector(
"#overflowed-extensions-list"
);
@@ -2662,37 +2662,41 @@ var gUnifiedExtensions = {
);
@@ -2714,6 +2710,7 @@ var gUnifiedExtensions = {
template.replaceWith(template.content);
this._panel = document.getElementById("unified-extensions-panel");
- let customizationArea = this._panel.querySelector(
- "#unified-extensions-area"
- );
- CustomizableUI.registerPanelNode(
- customizationArea,
- CustomizableUI.AREA_ADDONS
- );
- CustomizableUI.addPanelCloseListeners(this._panel);
-
- this._panel
- .querySelector("#unified-extensions-manage-extensions")
- .addEventListener("command", () => {
- BrowserAddonUI.openAddonsMgr("addons://list/extension");
- });
-
- // Lazy-load the l10n strings. Those strings are used for the CUI and
- // non-CUI extensions in the unified extensions panel.
- document
- .getElementById("unified-extensions-context-menu")
- .querySelectorAll("[data-lazy-l10n-id]")
- .forEach(el => {
- el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id"));
- el.removeAttribute("data-lazy-l10n-id");
- });
+ this.initializePanel(this._panel);
}
return this._panel;
},
+ initializePanel(panel) {
+ let customizationArea = panel.querySelector(
+ "#unified-extensions-area"
+ );
+ CustomizableUI.registerPanelNode(
+ customizationArea,
+ CustomizableUI.AREA_ADDONS
+ );
+ CustomizableUI.addPanelCloseListeners(panel);
+
+ panel
+ .querySelector("#unified-extensions-manage-extensions")
+ .addEventListener("command", () => {
+ BrowserAddonUI.openAddonsMgr("addons://list/extension");
+ });
+
+ // Lazy-load the l10n strings. Those strings are used for the CUI and
+ // non-CUI extensions in the unified extensions panel.
+ document
+ .getElementById("unified-extensions-context-menu")
+ .querySelectorAll("[data-lazy-l10n-id]")
+ .forEach(el => {
+ el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id"));
+ el.removeAttribute("data-lazy-l10n-id");
+ });
+ },
+
// `aEvent` and `reason` are optional. If `reason` is specified, it should be
// a valid argument to gUnifiedExtensions.recordButtonTelemetry().
- async togglePanel(aEvent, reason) {
+ async togglePanel(aEvent, reason, panel = this._panel, view = "unified-extensions-view", button = this._button) {
if (!CustomizationHandler.isCustomizing()) {
if (aEvent) {
if (
@@ -2714,6 +2718,7 @@ var gUnifiedExtensions = {
// and no alternative content is available for display in the panel.
const policies = this.getActivePolicies();
if (
@@ -59,16 +131,46 @@ index fadcbfca95ee28140579430c0371baad0e2f216a..7454b801b4ad892d6ad122277eb7c773
policies.length &&
!this.hasExtensionsInPanel(policies) &&
!this.isPrivateWindowMissingExtensionsWithoutPBMAccess() &&
@@ -2754,7 +2751,7 @@ var gUnifiedExtensions = {
@@ -2729,32 +2734,30 @@ var gUnifiedExtensions = {
this.blocklistAttentionInfo =
await AddonManager.getBlocklistAttentionInfo();
- let panel = this.panel;
-
if (!this._listView) {
this._listView = PanelMultiView.getViewNode(
document,
- "unified-extensions-view"
+ view,
);
this._listView.addEventListener("ViewShowing", this);
this._listView.addEventListener("ViewHiding", this);
}
- if (this._button.open) {
+ if (button.open) {
PanelMultiView.hidePopup(panel);
- this._button.open = false;
+ button.open = false;
} else {
// Overflow extensions placed in collapsed toolbars, if any.
for (const toolbarId of CustomizableUI.getCollapsedToolbarIds(window)) {
// We pass `false` because all these toolbars are collapsed.
- this.onToolbarVisibilityChange(toolbarId, /* isVisible */ false);
+ this.onToolbarVisibilityChange(toolbarId, /* isVisible */ false, panel);
}
panel.hidden = false;
this.recordButtonTelemetry(reason || "extensions_panel_showing");
this.ensureButtonShownBeforeAttachingPanel(panel);
PanelMultiView.openPopup(panel, this._button, {
- PanelMultiView.openPopup(panel, this._button, {
- position: "bottomright topright",
+ position: gZenUIManager.panelUIPosition(panel, this._button),
+ PanelMultiView.openPopup(panel, button, {
+ position: gZenUIManager.panelUIPosition(panel, button),
triggerEvent: aEvent,
});
}
@@ -2941,18 +2938,20 @@ var gUnifiedExtensions = {
@@ -2941,18 +2944,20 @@ var gUnifiedExtensions = {
this._maybeMoveWidgetNodeBack(widgetId);
}

View File

@@ -1,5 +1,5 @@
diff --git a/browser/base/content/navigator-toolbox.js b/browser/base/content/navigator-toolbox.js
index 7b776b15d52367a008ce6bf53dcfcbbe007b7453..d9a3404905b73db7c8f202ab166d5f3c625351f6 100644
index 7b776b15d52367a008ce6bf53dcfcbbe007b7453..da23f716c753f5a43f17bb5ed7a3d335891168c2 100644
--- a/browser/base/content/navigator-toolbox.js
+++ b/browser/base/content/navigator-toolbox.js
@@ -6,7 +6,7 @@
@@ -27,21 +27,28 @@ index 7b776b15d52367a008ce6bf53dcfcbbe007b7453..d9a3404905b73db7c8f202ab166d5f3c
gBrowser.handleNewTabMiddleClick(element, event);
break;
@@ -315,7 +317,7 @@ document.addEventListener(
#pageActionButton,
@@ -316,6 +318,7 @@ document.addEventListener(
#downloads-button,
#fxa-toolbar-menu-button,
- #unified-extensions-button,
#unified-extensions-button,
+ #zen-site-data-icon-button,
#library-button
`);
if (!element) {
@@ -394,7 +396,7 @@ document.addEventListener(
gSync.toggleAccountPanel(element, event);
break;
- case "unified-extensions-button":
+ case "zen-site-data-icon-button":
@@ -398,6 +401,16 @@ document.addEventListener(
gUnifiedExtensions.togglePanel(event);
break;
+ case "zen-site-data-icon-button":
+ gUnifiedExtensions.togglePanel(
+ event,
+ null,
+ window.gZenSiteDataPanel.unifiedPanel,
+ window.gZenSiteDataPanel.unifiedPanelView,
+ window.gZenSiteDataPanel.anchor,
+ );
+ break;
+
case "library-button":
PanelUI.showSubView("appMenu-libraryView", element, event);
break;

View File

@@ -4,7 +4,6 @@
#include ../../../zen/common/jar.inc.mn
#include ../../../zen/compact-mode/jar.inc.mn
#include ../../../zen/drag-and-drop/jar.inc.mn
#include ../../../zen/split-view/jar.inc.mn
#include ../../../zen/mods/jar.inc.mn
#include ../../../zen/workspaces/jar.inc.mn

View File

@@ -36,7 +36,7 @@
<menupopup id="zenFolderActions">
<menuitem id="context_zenFolderRename" data-l10n-id="zen-folders-panel-rename-folder"/>
<menuitem id="context_zenFolderChangeIcon" data-l10n-id="zen-folders-panel-change-icon-folder"/>
<menuitem id="context_zenFolderChangeIcon" data-l10n-id="tab-context-zen-edit-icon"/>
<menuseparator />
<menuitem id="context_zenFolderUnloadAll" data-l10n-id="zen-folders-unload-folder"/>
<menuitem id="context_zenFolderNewSubfolder" data-l10n-id="zen-folders-new-subfolder"/>

View File

@@ -32,7 +32,7 @@
command="Browser:AddBookmarkAs"
flex="1" />
</hbox>
<vbox class="zen-site-data-section">
<vbox id="zen-site-data-section-addons" class="zen-site-data-section">
<hbox class="zen-site-data-section-header">
<label data-l10n-id="unified-extensions-header-title" flex="1" />
<label data-l10n-id="zen-generic-manage" id="zen-site-data-manage-addons" />
@@ -72,7 +72,7 @@
# for this specific button / id
<toolbarbutton id="unified-extensions-manage-extensions"
class="subviewbutton panel-subview-footer-button unified-extensions-manage-extensions"
data-l10n-id="unified-extensions-manage-extensions"
data-l10n-id="unified-extensions-manage-extensions"
hidden="true" />
</vbox>
<vbox class="zen-site-data-section">
@@ -85,7 +85,7 @@
</vbox>
</vbox>
<hbox id="zen-site-data-footer">
<toolbarbutton id="zen-site-data-security-info"
<toolbarbutton id="zen-site-data-security-info"
class="subviewbutton zen-interactive-button" />
<toolbarbutton id="zen-site-data-actions"
class="subviewbutton zen-interactive-button"

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/sessionstore/SessionStore.sys.mjs b/browser/components/sessionstore/SessionStore.sys.mjs
index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..5ce4eb0b21cf4ed2b8e7c6ad0c57e77416a2ab48 100644
index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..33f630dea72da70352e131144c87549060ccd78d 100644
--- a/browser/components/sessionstore/SessionStore.sys.mjs
+++ b/browser/components/sessionstore/SessionStore.sys.mjs
@@ -127,6 +127,8 @@ const TAB_EVENTS = [
@@ -119,7 +119,18 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..5ce4eb0b21cf4ed2b8e7c6ad0c57e774
userContextId: state.userContextId,
skipLoad: true,
preferredRemoteType,
@@ -5374,7 +5399,7 @@ var SessionStoreInternal = {
@@ -5032,7 +5057,10 @@ var SessionStoreInternal = {
!activePageData ||
(activePageData && activePageData.url != "about:blank")
) {
+ let hadZenStaticFlag = tab.hasAttribute("zen-has-static-icon");
+ tab.removeAttribute("zen-has-static-icon");
win.gBrowser.setIcon(tab, tabData.image);
+ if (hadZenStaticFlag) tab.setAttribute("zen-has-static-icon", true);
}
lazy.TabStateCache.update(browser.permanentKey, {
image: null,
@@ -5374,7 +5402,7 @@ var SessionStoreInternal = {
for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) {
let tab = tabbrowser.tabs[i];
@@ -128,16 +139,16 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..5ce4eb0b21cf4ed2b8e7c6ad0c57e774
removableTabs.push(tab);
}
}
@@ -5483,7 +5508,7 @@ var SessionStoreInternal = {
@@ -5483,7 +5511,7 @@ var SessionStoreInternal = {
// collect the data for all windows
for (ix in this._windows) {
- if (this._windows[ix]._restoring || this._windows[ix].isTaskbarTab) {
+ if (this._windows[ix]._restoring || this._windows[ix].isTaskbarTab || this._windows[ix].isZenUnsynced) {
+ if (this._windows[ix]._restoring || this._windows[ix].isTaskbarTab && !this._windows[ix].isZenUnsynced) {
// window data is still in _statesToRestore
continue;
}
@@ -5625,11 +5650,16 @@ var SessionStoreInternal = {
@@ -5625,11 +5653,12 @@ var SessionStoreInternal = {
}
let tabbrowser = aWindow.gBrowser;
@@ -147,15 +158,21 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..5ce4eb0b21cf4ed2b8e7c6ad0c57e774
let winData = this._windows[aWindow.__SSi];
let tabsData = (winData.tabs = []);
+ winData.activeZenSpace = aWindow.gZenWorkspaces?.activeWorkspace || null;
+ winData.splitViewData = aWindow.gZenViewSplitter?.storeDataForSessionStore();
+ winData.folders = aWindow.gZenFolders?.storeDataForSessionStore() || [];
+ winData.spaces = aWindow.gZenWorkspaces?.getWorkspaces();
+
// update the internal state data for this window
for (let tab of tabs) {
if (tab == aWindow.FirefoxViewHandler.tab) {
@@ -5652,7 +5682,7 @@ var SessionStoreInternal = {
@@ -5640,6 +5669,9 @@ var SessionStoreInternal = {
tabsData.push(tabData);
}
+ winData.folders = aWindow.gZenFolders?.storeDataForSessionStore() || [];
+ winData.activeZenSpace = aWindow.gZenWorkspaces?.activeWorkspace || null;
+ winData.spaces = aWindow.gZenWorkspaces?.getWorkspaces();
// update tab group state for this window
winData.groups = [];
for (let tabGroup of aWindow.gBrowser.tabGroups) {
@@ -5652,7 +5684,7 @@ var SessionStoreInternal = {
// a window is closed, point to the first item in the tab strip instead (it will never be the Firefox View tab,
// since it's only inserted into the tab strip after it's selected).
if (aWindow.FirefoxViewHandler.tab?.selected) {
@@ -164,7 +181,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..5ce4eb0b21cf4ed2b8e7c6ad0c57e774
winData.title = tabbrowser.tabs[0].label;
}
winData.selected = selectedIndex;
@@ -5765,8 +5795,8 @@ var SessionStoreInternal = {
@@ -5765,8 +5797,8 @@ var SessionStoreInternal = {
// selectTab represents.
let selectTab = 0;
if (overwriteTabs) {
@@ -175,17 +192,25 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..5ce4eb0b21cf4ed2b8e7c6ad0c57e774
selectTab = Math.min(selectTab, winData.tabs.length);
}
@@ -5809,6 +5839,9 @@ var SessionStoreInternal = {
winData.tabs,
winData.groups ?? []
@@ -5788,6 +5820,7 @@ var SessionStoreInternal = {
if (overwriteTabs) {
for (let i = tabbrowser.browsers.length - 1; i >= 0; i--) {
if (!tabbrowser.tabs[i].selected) {
+ aWindow.gZenWorkspaces._shouldOverrideTabs = true;
tabbrowser.removeTab(tabbrowser.tabs[i]);
}
}
@@ -5821,6 +5854,9 @@ var SessionStoreInternal = {
savedTabGroup => !openTabGroupIdsInWindow.has(savedTabGroup.id)
);
+ aWindow.gZenFolders?.restoreDataFromSessionStore(winData.folders);
+ aWindow.gZenViewSplitter?.restoreDataFromSessionStore(winData.splitViewData);
+ aWindow.gZenWorkspaces?.restoreWorkspacesFromSessionStore(winData);
this._log.debug(
`restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs`
);
@@ -6372,6 +6405,25 @@ var SessionStoreInternal = {
}
+ aWindow.gZenFolders?.restoreDataFromSessionStore(winData.folders);
+ aWindow.gZenViewSplitter?.restoreDataFromSessionStore(winData.splitViewData);
+ aWindow.gZenWorkspaces?.restoreWorkspacesFromSessionStore(winData);
// Move the originally open tabs to the end.
if (initialTabs) {
@@ -6372,6 +6408,28 @@ var SessionStoreInternal = {
// Most of tabData has been restored, now continue with restoring
// attributes that may trigger external events.
@@ -196,8 +221,11 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..5ce4eb0b21cf4ed2b8e7c6ad0c57e774
+ if (tabData.zenIsEmpty) {
+ tab.setAttribute("zen-empty-tab", "true");
+ }
+ if (tabData.zenHasStaticLabel) {
+ tab.setAttribute("zen-has-static-label", "true");
+ if (tabData.zenStaticLabel) {
+ tab.zenStaticLabel = tabData.zenStaticLabel;
+ }
+ if (tabData.zenHasStaticIcon) {
+ tab.setAttribute("zen-has-static-icon", "true");
+ }
+ if (tabData.zenSyncId) {
+ tab.setAttribute("id", tabData.zenSyncId);
@@ -211,7 +239,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..5ce4eb0b21cf4ed2b8e7c6ad0c57e774
if (tabData.pinned) {
tabbrowser.pinTab(tab);
@@ -7290,7 +7342,7 @@ var SessionStoreInternal = {
@@ -7290,7 +7348,7 @@ var SessionStoreInternal = {
let groupsToSave = new Map();
for (let tIndex = 0; tIndex < window.tabs.length; ) {
@@ -220,7 +248,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..5ce4eb0b21cf4ed2b8e7c6ad0c57e774
// Adjust window.selected
if (tIndex + 1 < window.selected) {
window.selected -= 1;
@@ -7305,7 +7357,7 @@ var SessionStoreInternal = {
@@ -7305,7 +7363,7 @@ var SessionStoreInternal = {
);
// We don't want to increment tIndex here.
continue;

View File

@@ -1,8 +1,8 @@
diff --git a/browser/components/sessionstore/TabState.sys.mjs b/browser/components/sessionstore/TabState.sys.mjs
index 82721356d191055bec0d4b0ca49e481221988801..80547ec951f881bef134b637730954eb1525c623 100644
index 82721356d191055bec0d4b0ca49e481221988801..e0fde4fc475471d170ce46c1207e6e7b67fcaf1d 100644
--- a/browser/components/sessionstore/TabState.sys.mjs
+++ b/browser/components/sessionstore/TabState.sys.mjs
@@ -85,7 +85,24 @@ class _TabState {
@@ -85,7 +85,25 @@ class _TabState {
tabData.groupId = tab.group.id;
}
@@ -14,7 +14,8 @@ index 82721356d191055bec0d4b0ca49e481221988801..80547ec951f881bef134b637730954eb
+ tabData.zenPinnedEntry = tab.getAttribute("zen-pinned-entry");
+ tabData.zenPinnedIcon = tab.getAttribute("zen-pinned-icon");
+ tabData.zenIsEmpty = tab.hasAttribute("zen-empty-tab");
+ tabData.zenHasStaticLabel = tab.hasAttribute("zen-has-static-label");
+ tabData.zenStaticLabel = tab.zenStaticLabel;
+ tabData.zenHasStaticIcon = tab.hasAttribute("zen-has-static-icon");
+ tabData.zenGlanceId = tab.getAttribute("glance-id");
+ tabData.zenIsGlance = tab.hasAttribute("zen-glance-tab");
+ tabData._zenPinnedInitialState = tab._zenPinnedInitialState;
@@ -27,7 +28,7 @@ index 82721356d191055bec0d4b0ca49e481221988801..80547ec951f881bef134b637730954eb
tabData.userContextId = tab.userContextId || 0;
@@ -98,7 +115,7 @@ class _TabState {
@@ -98,7 +116,7 @@ class _TabState {
// Copy data from the tab state cache only if the tab has fully finished
// restoring. We don't want to overwrite data contained in __SS_data.

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js
index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18e829ca07 100644
index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..a87bcaf4e9b92a9f63e025409efabca90aa52900 100644
--- a/browser/components/tabbrowser/content/tabbrowser.js
+++ b/browser/components/tabbrowser/content/tabbrowser.js
@@ -386,6 +386,7 @@
@@ -123,16 +123,19 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
});
aTab.style.marginInlineStart = "";
@@ -1098,6 +1159,8 @@
@@ -1098,6 +1159,11 @@
let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"];
+ try {
+ gZenPinnedTabManager.onTabIconChanged(aTab, aIconURL);
+ if (aTab.hasAttribute("zen-has-static-icon")) {
+ return;
+ }
+ gZenPinnedTabManager.onTabIconChanged(aTab, aIconURL);
if (
aIconURL &&
!LOCAL_PROTOCOLS.some(protocol => aIconURL.startsWith(protocol))
@@ -1107,6 +1170,9 @@
@@ -1107,6 +1173,9 @@
);
return;
}
@@ -142,7 +145,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
let browser = this.getBrowserForTab(aTab);
browser.mIconURL = aIconURL;
@@ -1379,7 +1445,6 @@
@@ -1379,7 +1448,6 @@
// Preview mode should not reset the owner
if (!this._previewMode && !oldTab.selected) {
@@ -150,7 +153,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
}
let lastRelatedTab = this._lastRelatedTabMap.get(oldTab);
@@ -1470,6 +1535,7 @@
@@ -1470,6 +1538,7 @@
if (!this._previewMode) {
newTab.recordTimeFromUnloadToReload();
newTab.updateLastAccessed();
@@ -158,7 +161,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
oldTab.updateLastAccessed();
// if this is the foreground window, update the last-seen timestamps.
if (this.ownerGlobal == BrowserWindowTracker.getTopWindow()) {
@@ -1622,6 +1688,9 @@
@@ -1622,6 +1691,9 @@
}
let activeEl = document.activeElement;
@@ -168,20 +171,19 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
// If focus is on the old tab, move it to the new tab.
if (activeEl == oldTab) {
newTab.focus();
@@ -1945,7 +2014,11 @@
@@ -1945,6 +2017,11 @@
}
_setTabLabel(aTab, aLabel, { beforeTabOpen, isContentTitle, isURL } = {}) {
- if (!aLabel || aLabel.includes("about:reader?")) {
+ if (!aTab._zenContentsVisible && !aTab._zenChangeLabelFlag && !aTab._labelIsInitialTitle && !gZenWorkspaces.privateWindowOrDisabled) {
+ return false;
+ }
+ aLabel = aTab.zenStaticLabel || aLabel;
+ gZenPinnedTabManager.onTabLabelChanged(aTab);
+ if (!aLabel || aLabel.includes("about:reader?") || (aTab.hasAttribute("zen-has-static-label") && !aTab._zenChangeLabelFlag)) {
if (!aLabel || aLabel.includes("about:reader?")) {
return false;
}
@@ -2053,7 +2126,7 @@
@@ -2053,7 +2130,7 @@
newIndex = this.selectedTab._tPos + 1;
}
@@ -190,7 +192,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
if (this.isTabGroupLabel(targetTab)) {
throw new Error(
"Replacing a tab group label with a tab is not supported"
@@ -2328,6 +2401,7 @@
@@ -2328,6 +2405,7 @@
uriIsAboutBlank,
userContextId,
skipLoad,
@@ -198,7 +200,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
} = {}) {
let b = document.createXULElement("browser");
// Use the JSM global to create the permanentKey, so that if the
@@ -2401,8 +2475,7 @@
@@ -2401,8 +2479,7 @@
// we use a different attribute name for this?
b.setAttribute("name", name);
}
@@ -208,7 +210,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
b.setAttribute("transparent", "true");
}
@@ -2567,7 +2640,7 @@
@@ -2567,7 +2644,7 @@
let panel = this.getPanel(browser);
let uniqueId = this._generateUniquePanelID();
@@ -217,7 +219,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
aTab.linkedPanel = uniqueId;
// Inject the <browser> into the DOM if necessary.
@@ -2626,8 +2699,8 @@
@@ -2626,8 +2703,8 @@
// If we transitioned from one browser to two browsers, we need to set
// hasSiblings=false on both the existing browser and the new browser.
if (this.tabs.length == 2) {
@@ -228,7 +230,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
} else {
aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1;
}
@@ -2814,7 +2887,6 @@
@@ -2814,7 +2891,6 @@
this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, {
tabIndex: tab._tPos + 1,
userContextId: tab.userContextId,
@@ -236,7 +238,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
focusUrlBar: true,
});
resolve(this.selectedBrowser);
@@ -2923,6 +2995,9 @@
@@ -2923,6 +2999,9 @@
schemelessInput,
hasValidUserGestureActivation = false,
textDirectiveUserActivation = false,
@@ -246,7 +248,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
} = {}
) {
// all callers of addTab that pass a params object need to pass
@@ -2933,10 +3008,17 @@
@@ -2933,10 +3012,17 @@
);
}
@@ -264,7 +266,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
// If we're opening a foreground tab, set the owner by default.
ownerTab ??= inBackground ? null : this.selectedTab;
@@ -2944,6 +3026,7 @@
@@ -2944,6 +3030,7 @@
if (this.selectedTab.owner) {
this.selectedTab.owner = null;
}
@@ -272,7 +274,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
// Find the tab that opened this one, if any. This is used for
// determining positioning, and inherited attributes such as the
@@ -2996,6 +3079,21 @@
@@ -2996,6 +3083,21 @@
noInitialLabel,
skipBackgroundNotify,
});
@@ -294,7 +296,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
if (insertTab) {
// Insert the tab into the tab container in the correct position.
this.#insertTabAtIndex(t, {
@@ -3004,6 +3102,7 @@
@@ -3004,6 +3106,7 @@
ownerTab,
openerTab,
pinned,
@@ -302,7 +304,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
bulkOrderedOpen,
tabGroup: tabGroup ?? openerTab?.group,
});
@@ -3022,6 +3121,7 @@
@@ -3022,6 +3125,7 @@
openWindowInfo,
skipLoad,
triggeringRemoteType,
@@ -310,7 +312,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
}));
if (focusUrlBar) {
@@ -3146,6 +3246,12 @@
@@ -3146,6 +3250,12 @@
}
}
@@ -323,7 +325,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
// Additionally send pinned tab events
if (pinned) {
this.#notifyPinnedStatus(t);
@@ -3349,10 +3455,10 @@
@@ -3349,10 +3459,10 @@
isAdoptingGroup = false,
isUserTriggered = false,
telemetryUserCreateSource = "unknown",
@@ -335,7 +337,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
}
if (!color) {
@@ -3373,9 +3479,14 @@
@@ -3373,9 +3483,14 @@
label,
isAdoptingGroup
);
@@ -352,7 +354,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
);
group.addTabs(tabs);
@@ -3496,7 +3607,7 @@
@@ -3496,7 +3611,7 @@
}
this.#handleTabMove(tab, () =>
@@ -361,7 +363,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
);
}
@@ -3698,6 +3809,7 @@
@@ -3698,6 +3813,7 @@
openWindowInfo,
skipLoad,
triggeringRemoteType,
@@ -369,7 +371,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
}
) {
// If we don't have a preferred remote type (or it is `NOT_REMOTE`), and
@@ -3767,6 +3879,7 @@
@@ -3767,6 +3883,7 @@
openWindowInfo,
name,
skipLoad,
@@ -377,7 +379,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
});
}
@@ -3955,7 +4068,7 @@
@@ -3955,7 +4072,7 @@
// Add a new tab if needed.
if (!tab) {
let createLazyBrowser =
@@ -386,7 +388,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
let url = "about:blank";
if (tabData.entries?.length) {
@@ -3992,8 +4105,10 @@
@@ -3992,8 +4109,10 @@
insertTab: false,
skipLoad: true,
preferredRemoteType,
@@ -398,7 +400,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
if (select) {
tabToSelect = tab;
}
@@ -4005,7 +4120,8 @@
@@ -4005,7 +4124,8 @@
this.pinTab(tab);
// Then ensure all the tab open/pinning information is sent.
this._fireTabOpen(tab, {});
@@ -408,7 +410,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
let { groupId } = tabData;
const tabGroup = tabGroupWorkingData.get(groupId);
// if a tab refers to a tab group we don't know, skip any group
@@ -4019,7 +4135,10 @@
@@ -4019,7 +4139,10 @@
tabGroup.stateData.id,
tabGroup.stateData.color,
tabGroup.stateData.collapsed,
@@ -420,7 +422,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
);
tabsFragment.appendChild(tabGroup.node);
}
@@ -4064,9 +4183,23 @@
@@ -4064,9 +4187,23 @@
// to remove the old selected tab.
if (tabToSelect) {
let leftoverTab = this.selectedTab;
@@ -436,15 +438,15 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
+ gZenWorkspaces._initialTab._shouldRemove = true;
+ }
+ }
}
+ }
+ else {
+ gZenWorkspaces._tabToRemoveForEmpty = this.selectedTab;
+ }
}
+ this._hasAlreadyInitializedZenSessionStore = true;
if (tabs.length > 1 || !tabs[0].selected) {
this._updateTabsAfterInsert();
@@ -4257,11 +4390,14 @@
@@ -4257,11 +4394,14 @@
if (ownerTab) {
tab.owner = ownerTab;
}
@@ -460,7 +462,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
if (
!bulkOrderedOpen &&
((openerTab &&
@@ -4273,7 +4409,7 @@
@@ -4273,7 +4413,7 @@
let lastRelatedTab =
openerTab && this._lastRelatedTabMap.get(openerTab);
let previousTab = lastRelatedTab || openerTab || this.selectedTab;
@@ -469,7 +471,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
tabGroup = previousTab.group;
}
if (
@@ -4284,7 +4420,7 @@
@@ -4284,7 +4424,7 @@
) {
elementIndex = Infinity;
} else if (previousTab.visible) {
@@ -478,7 +480,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
} else if (previousTab == FirefoxViewHandler.tab) {
elementIndex = 0;
}
@@ -4312,14 +4448,14 @@
@@ -4312,14 +4452,14 @@
}
// Ensure index is within bounds.
if (tab.pinned) {
@@ -497,7 +499,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
if (pinned && !itemAfter?.pinned) {
itemAfter = null;
@@ -4330,7 +4466,7 @@
@@ -4330,7 +4470,7 @@
this.tabContainer._invalidateCachedTabs();
@@ -506,7 +508,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
if (this.isTab(itemAfter) && itemAfter.group == tabGroup) {
// Place at the front of, or between tabs in, the same tab group
this.tabContainer.insertBefore(tab, itemAfter);
@@ -4358,7 +4494,11 @@
@@ -4358,7 +4498,11 @@
const tabContainer = pinned
? this.tabContainer.pinnedTabsContainer
: this.tabContainer;
@@ -518,7 +520,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
}
this._updateTabsAfterInsert();
@@ -4366,6 +4506,7 @@
@@ -4366,6 +4510,7 @@
if (pinned) {
this._updateTabBarForPinnedTabs();
}
@@ -526,7 +528,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
TabBarVisibility.update();
}
@@ -4916,6 +5057,7 @@
@@ -4916,6 +5061,7 @@
telemetrySource,
} = {}
) {
@@ -534,7 +536,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
// When 'closeWindowWithLastTab' pref is enabled, closing all tabs
// can be considered equivalent to closing the window.
if (
@@ -5005,6 +5147,7 @@
@@ -5005,6 +5151,7 @@
if (lastToClose) {
this.removeTab(lastToClose, aParams);
}
@@ -542,7 +544,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
} catch (e) {
console.error(e);
}
@@ -5043,6 +5186,12 @@
@@ -5043,6 +5190,12 @@
aTab._closeTimeNoAnimTimerId = Glean.browserTabclose.timeNoAnim.start();
}
@@ -555,7 +557,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
// Handle requests for synchronously removing an already
// asynchronously closing tab.
if (!animate && aTab.closing) {
@@ -5057,6 +5206,9 @@
@@ -5057,6 +5210,9 @@
// state).
let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width;
let isLastTab = this.#isLastTabInWindow(aTab);
@@ -565,7 +567,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
if (
!this._beginRemoveTab(aTab, {
closeWindowFastpath: true,
@@ -5105,7 +5257,13 @@
@@ -5105,7 +5261,13 @@
// We're not animating, so we can cancel the animation stopwatch.
Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId);
aTab._closeTimeAnimTimerId = null;
@@ -580,7 +582,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
return;
}
@@ -5239,7 +5397,7 @@
@@ -5239,7 +5401,7 @@
closeWindowWithLastTab != null
? closeWindowWithLastTab
: !window.toolbar.visible ||
@@ -589,7 +591,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
if (closeWindow) {
// We've already called beforeunload on all the relevant tabs if we get here,
@@ -5263,6 +5421,7 @@
@@ -5263,6 +5425,7 @@
newTab = true;
}
@@ -597,7 +599,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
aTab._endRemoveArgs = [closeWindow, newTab];
// swapBrowsersAndCloseOther will take care of closing the window without animation.
@@ -5303,13 +5462,7 @@
@@ -5303,13 +5466,7 @@
aTab._mouseleave();
if (newTab) {
@@ -612,7 +614,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
} else {
TabBarVisibility.update();
}
@@ -5442,6 +5595,7 @@
@@ -5442,6 +5599,7 @@
this.tabs[i]._tPos = i;
}
@@ -620,7 +622,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
if (!this._windowIsClosing) {
// update tab close buttons state
this.tabContainer._updateCloseButtons();
@@ -5663,6 +5817,7 @@
@@ -5663,6 +5821,7 @@
}
let excludeTabs = new Set(aExcludeTabs);
@@ -628,7 +630,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
// If this tab has a successor, it should be selectable, since
// hiding or closing a tab removes that tab as a successor.
@@ -5675,13 +5830,13 @@
@@ -5675,13 +5834,13 @@
!excludeTabs.has(aTab.owner) &&
Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")
) {
@@ -644,7 +646,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
);
let tab = this.tabContainer.findNextTab(aTab, {
@@ -5697,7 +5852,7 @@
@@ -5697,7 +5856,7 @@
}
if (tab) {
@@ -653,7 +655,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
}
// If no qualifying visible tab was found, see if there is a tab in
@@ -5718,7 +5873,7 @@
@@ -5718,7 +5877,7 @@
});
}
@@ -662,7 +664,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
}
_blurTab(aTab) {
@@ -5729,7 +5884,7 @@
@@ -5729,7 +5888,7 @@
* @returns {boolean}
* False if swapping isn't permitted, true otherwise.
*/
@@ -671,7 +673,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
// Do not allow transfering a private tab to a non-private window
// and vice versa.
if (
@@ -5783,6 +5938,7 @@
@@ -5783,6 +5942,7 @@
// fire the beforeunload event in the process. Close the other
// window if this was its last tab.
if (
@@ -679,7 +681,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
!remoteBrowser._beginRemoveTab(aOtherTab, {
adoptedByTab: aOurTab,
closeWindowWithLastTab: true,
@@ -5794,7 +5950,7 @@
@@ -5794,7 +5954,7 @@
// If this is the last tab of the window, hide the window
// immediately without animation before the docshell swap, to avoid
// about:blank being painted.
@@ -688,7 +690,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
if (closeWindow) {
let win = aOtherTab.ownerGlobal;
win.windowUtils.suppressAnimation(true);
@@ -5918,11 +6074,13 @@
@@ -5918,11 +6078,13 @@
}
// Finish tearing down the tab that's going away.
@@ -702,7 +704,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
this.setTabTitle(aOurTab);
@@ -6124,10 +6282,10 @@
@@ -6124,10 +6286,10 @@
SessionStore.deleteCustomTabValue(aTab, "hiddenBy");
}
@@ -715,7 +717,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
aTab.selected ||
aTab.closing ||
// Tabs that are sharing the screen, microphone or camera cannot be hidden.
@@ -6185,7 +6343,8 @@
@@ -6185,7 +6347,8 @@
*
* @param {MozTabbrowserTab|MozTabbrowserTabGroup|MozTabbrowserTabGroup.labelElement} aTab
*/
@@ -725,7 +727,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
if (this.tabs.length == 1) {
return null;
}
@@ -6209,12 +6368,14 @@
@@ -6209,12 +6372,14 @@
}
// tell a new window to take the "dropped" tab
@@ -741,7 +743,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
}
/**
@@ -6319,7 +6480,7 @@
@@ -6319,7 +6484,7 @@
* `true` if element is a `<tab-group>`
*/
isTabGroup(element) {
@@ -750,7 +752,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
}
/**
@@ -6404,8 +6565,8 @@
@@ -6404,8 +6569,8 @@
}
// Don't allow mixing pinned and unpinned tabs.
@@ -761,7 +763,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
} else {
tabIndex = Math.max(tabIndex, this.pinnedTabCount);
}
@@ -6431,10 +6592,16 @@
@@ -6431,10 +6596,16 @@
this.#handleTabMove(
element,
() => {
@@ -780,7 +782,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
if (neighbor && this.isTab(element) && tabIndex > element._tPos) {
neighbor.after(element);
} else {
@@ -6492,23 +6659,28 @@
@@ -6492,23 +6663,28 @@
#moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) {
if (this.isTabGroupLabel(targetElement)) {
targetElement = targetElement.group;
@@ -815,7 +817,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
} else if (!element.pinned && targetElement && targetElement.pinned) {
// If the caller asks to move an unpinned element next to a pinned
// tab, move the unpinned element to be the first unpinned element
@@ -6521,14 +6693,34 @@
@@ -6521,14 +6697,34 @@
// move the tab group right before the first unpinned tab.
// 4. Moving a tab group and the first unpinned tab is grouped:
// move the tab group right before the first unpinned tab's tab group.
@@ -851,7 +853,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
element.pinned
? this.tabContainer.pinnedTabsContainer
: this.tabContainer;
@@ -6537,7 +6729,7 @@
@@ -6537,7 +6733,7 @@
element,
() => {
if (moveBefore) {
@@ -860,7 +862,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
} else if (targetElement) {
targetElement.after(element);
} else {
@@ -6607,10 +6799,10 @@
@@ -6607,10 +6803,10 @@
* @param {TabMetricsContext} [metricsContext]
*/
moveTabToGroup(aTab, aGroup, metricsContext) {
@@ -873,7 +875,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
return;
}
if (aTab.group && aTab.group.id === aGroup.id) {
@@ -6656,6 +6848,7 @@
@@ -6656,6 +6852,7 @@
let state = {
tabIndex: tab._tPos,
@@ -881,7 +883,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
};
if (tab.visible) {
state.elementIndex = tab.elementIndex;
@@ -6682,7 +6875,7 @@
@@ -6682,7 +6879,7 @@
let changedTabGroup =
previousTabState.tabGroupId != currentTabState.tabGroupId;
@@ -890,7 +892,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
tab.dispatchEvent(
new CustomEvent("TabMove", {
bubbles: true,
@@ -6723,6 +6916,10 @@
@@ -6723,6 +6920,10 @@
moveActionCallback();
@@ -901,7 +903,16 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
// Clear tabs cache after moving nodes because the order of tabs may have
// changed.
this.tabContainer._invalidateCachedTabs();
@@ -7623,7 +7820,7 @@
@@ -6815,6 +7016,8 @@
params.userContextId = aTab.getAttribute("usercontextid");
}
let newTab = this.addWebTab("about:blank", params);
+ newTab._zenContentsVisible = true;
+ newTab.zenStaticLabel = aTab.zenStaticLabel;
let newBrowser = this.getBrowserForTab(newTab);
aTab.container.tabDragAndDrop.finishAnimateTabMove();
@@ -7623,7 +7826,7 @@
// preventDefault(). It will still raise the window if appropriate.
break;
}
@@ -910,7 +921,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
window.focus();
aEvent.preventDefault();
break;
@@ -7640,7 +7837,6 @@
@@ -7640,7 +7843,6 @@
}
case "TabGroupCollapse":
aEvent.target.tabs.forEach(tab => {
@@ -918,7 +929,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
});
break;
case "TabGroupCreateByUser":
@@ -8589,6 +8785,7 @@
@@ -8589,6 +8791,7 @@
aWebProgress.isTopLevel
) {
this.mTab.setAttribute("busy", "true");
@@ -926,7 +937,15 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18
gBrowser._tabAttrModified(this.mTab, ["busy"]);
this.mTab._notselectedsinceload = !this.mTab.selected;
}
@@ -9623,7 +9820,7 @@ var TabContextMenu = {
@@ -8670,6 +8873,7 @@
// known defaults. Note we use the original URL since about:newtab
// redirects to a prerendered page.
const shouldRemoveFavicon =
+ !this.mTab.hasAttribute("zen-has-static-icon") &&
!this.mBrowser.mIconURL &&
!ignoreBlank &&
!(originalLocation.spec in FAVICON_DEFAULTS);
@@ -9623,7 +9827,7 @@ var TabContextMenu = {
);
contextUnpinSelectedTabs.hidden =
!this.contextTab.pinned || !this.multiselected;

View File

@@ -1,5 +1,5 @@
diff --git a/browser/themes/shared/tabbrowser/tabs.css b/browser/themes/shared/tabbrowser/tabs.css
index 8a4d3860b344e2dc2128cfc2b1673de582e19c7c..f8e108d2c1c71070375fdae12df432311b0081e1 100644
index 8a4d3860b344e2dc2128cfc2b1673de582e19c7c..086171783cd9a8629ccf4b2e90813e58c4793878 100644
--- a/browser/themes/shared/tabbrowser/tabs.css
+++ b/browser/themes/shared/tabbrowser/tabs.css
@@ -21,7 +21,7 @@
@@ -39,12 +39,7 @@ index 8a4d3860b344e2dc2128cfc2b1673de582e19c7c..f8e108d2c1c71070375fdae12df43231
&::before {
position: absolute;
@@ -498,14 +498,11 @@
.tab-icon-image {
-moz-context-properties: fill, stroke;
fill: currentColor;
+ border-radius: 4px;
@@ -502,10 +502,6 @@
/* stylelint-disable-next-line media-query-no-invalid */
@media -moz-pref("browser.tabs.fadeOutUnloadedTabs") {
&[pending] {
@@ -55,7 +50,7 @@ index 8a4d3860b344e2dc2128cfc2b1673de582e19c7c..f8e108d2c1c71070375fdae12df43231
opacity: 0.5;
/* Fade the favicon out */
transition-property: filter, opacity;
@@ -522,10 +519,6 @@
@@ -522,10 +518,6 @@
/* stylelint-disable-next-line media-query-no-invalid */
@media -moz-pref("browser.tabs.fadeOutExplicitlyUnloadedTabs") {
&[pending][discarded] {
@@ -66,7 +61,7 @@ index 8a4d3860b344e2dc2128cfc2b1673de582e19c7c..f8e108d2c1c71070375fdae12df43231
opacity: 0.5;
/* Fade the favicon out */
transition-property: filter, opacity;
@@ -593,7 +586,7 @@
@@ -593,7 +585,7 @@
z-index: 1; /* Overlay tab title */
#tabbrowser-tabs[orient="vertical"] & {
@@ -75,7 +70,7 @@ index 8a4d3860b344e2dc2128cfc2b1673de582e19c7c..f8e108d2c1c71070375fdae12df43231
}
&[crashed] {
@@ -601,7 +594,7 @@
@@ -601,7 +593,7 @@
}
#tabbrowser-tabs[orient="vertical"]:not([expanded]) &:not([crashed]),
@@ -84,7 +79,7 @@ index 8a4d3860b344e2dc2128cfc2b1673de582e19c7c..f8e108d2c1c71070375fdae12df43231
&[soundplaying] {
list-style-image: url("chrome://browser/skin/tabbrowser/tab-audio-playing-small.svg");
}
@@ -658,7 +651,7 @@
@@ -658,7 +650,7 @@
}
}
@@ -93,7 +88,7 @@ index 8a4d3860b344e2dc2128cfc2b1673de582e19c7c..f8e108d2c1c71070375fdae12df43231
&[crashed] {
display: revert;
}
@@ -759,7 +752,7 @@
@@ -759,7 +751,7 @@
has not been added to root. There are certain scenarios when that attribute is temporarily
removed from root such as when toggling the sidebar to expand with the toolbar button. */
#tabbrowser-tabs[orient="horizontal"] &:not([pinned]):not([crashed]),
@@ -102,7 +97,7 @@ index 8a4d3860b344e2dc2128cfc2b1673de582e19c7c..f8e108d2c1c71070375fdae12df43231
&:is([soundplaying], [muted], [activemedia-blocked]) {
display: flex;
}
@@ -1446,7 +1439,7 @@ tab-group {
@@ -1446,7 +1438,7 @@ tab-group {
}
#tabbrowser-tabs[orient="vertical"][expanded] {
@@ -111,16 +106,16 @@ index 8a4d3860b344e2dc2128cfc2b1673de582e19c7c..f8e108d2c1c71070375fdae12df43231
&[movingtab][movingtab-addToGroup]:not([movingtab-group], [movingtab-ungroup]) .tabbrowser-tab:is(:active, [multiselected]) {
margin-inline-start: var(--space-medium);
}
@@ -1893,7 +1886,7 @@ tab-group {
@@ -1893,7 +1885,7 @@ tab-group {
}
}
-#tabbrowser-arrowscrollbox[orient="vertical"] > #tabbrowser-arrowscrollbox-periphery > #tabs-newtab-button,
+#tabbrowser-arrowscrollbox[orient="vertical"] #tabbrowser-arrowscrollbox-periphery > #tabs-newtab-button,
+#tabs-newtab-button,
#vertical-tabs-newtab-button {
appearance: none;
min-height: var(--tab-min-height);
@@ -1904,7 +1897,7 @@ tab-group {
@@ -1904,7 +1896,7 @@ tab-group {
margin-inline: var(--tab-inner-inline-margin);
#tabbrowser-tabs[orient="vertical"]:not([expanded]) & > .toolbarbutton-text {
@@ -129,7 +124,7 @@ index 8a4d3860b344e2dc2128cfc2b1673de582e19c7c..f8e108d2c1c71070375fdae12df43231
}
&:hover {
@@ -1928,7 +1921,7 @@ tab-group {
@@ -1928,7 +1920,7 @@ tab-group {
* flex container. #tabs-newtab-button is a child of the arrowscrollbox where
* we don't want a gap (between tabs), so we have to add some margin.
*/
@@ -138,7 +133,7 @@ index 8a4d3860b344e2dc2128cfc2b1673de582e19c7c..f8e108d2c1c71070375fdae12df43231
margin-block: var(--tab-block-margin);
}
@@ -2116,7 +2109,6 @@ tab-group {
@@ -2116,7 +2108,6 @@ tab-group {
&:not([expanded]) {
.tabbrowser-tab[pinned] {
@@ -146,7 +141,7 @@ index 8a4d3860b344e2dc2128cfc2b1673de582e19c7c..f8e108d2c1c71070375fdae12df43231
}
.tab-background {
@@ -2156,8 +2148,8 @@ tab-group {
@@ -2156,8 +2147,8 @@ tab-group {
display: block;
position: absolute;
inset: auto;
@@ -157,7 +152,7 @@ index 8a4d3860b344e2dc2128cfc2b1673de582e19c7c..f8e108d2c1c71070375fdae12df43231
&:-moz-window-inactive {
background-image:
@@ -2276,7 +2268,6 @@ toolbar:not(#TabsToolbar) #firefox-view-button {
@@ -2276,7 +2267,6 @@ toolbar:not(#TabsToolbar) #firefox-view-button {
list-style-image: url(chrome://global/skin/icons/plus.svg);
}

View File

@@ -1,8 +1,39 @@
diff --git a/browser/themes/shared/toolbarbuttons.css b/browser/themes/shared/toolbarbuttons.css
index e2b8a7cae70ed2bd3c80ee4214a09dbdb68a0d01..71320c7268d92aaa06bfa15c74bdbf02f1442745 100644
index e2b8a7cae70ed2bd3c80ee4214a09dbdb68a0d01..e320faa158125d73876126f9be8847d395e465e3 100644
--- a/browser/themes/shared/toolbarbuttons.css
+++ b/browser/themes/shared/toolbarbuttons.css
@@ -279,7 +279,7 @@ toolbar .toolbaritem-combined-buttons {
@@ -44,20 +44,6 @@
--toolbarbutton-inner-padding: 9px;
--bookmark-block-padding: 7px;
}
- @media (width <= 900px) {
- --toolbarbutton-outer-padding: 1px;
- }
- @media (width <= 800px) {
- --toolbarbutton-inner-padding: 7px;
- &:where([uidensity="touch"]) {
- --toolbarbutton-inner-padding: 8px;
- }
- }
- /* 700px is just above half of the popular 1366px screen width, so two browser
- windows put next to each other will be below this threshold. */
- @media (width <= 700px) {
- --toolbarbutton-inner-padding: 6px;
- }
&:where([uidensity="compact"]) {
--toolbarbutton-inner-padding: 6px;
--bookmark-block-padding: 1px;
@@ -67,9 +53,7 @@
#TabsToolbar {
/* Override the inner padding to ensure the dimensions match the tabs, but also making sure
different types of buttons (combined-buttons-dropmarker or text) still look correct. */
- @media (width > 900px) {
--toolbarbutton-inner-padding: calc((var(--tab-min-height) - 16px) / 2);
- }
}
/* Primary toolbar buttons */
@@ -279,7 +263,7 @@ toolbar .toolbaritem-combined-buttons {
#nav-bar-overflow-button {
list-style-image: url("chrome://global/skin/icons/chevron.svg");
@@ -11,7 +42,7 @@ index e2b8a7cae70ed2bd3c80ee4214a09dbdb68a0d01..71320c7268d92aaa06bfa15c74bdbf02
display: none;
}
@@ -489,7 +489,7 @@ toolbarbutton.bookmark-item:not(.subviewbutton) {
@@ -489,7 +473,7 @@ toolbarbutton.bookmark-item:not(.subviewbutton) {
*/
align-items: stretch;
> .toolbarbutton-icon {

View File

@@ -34,6 +34,7 @@ class nsZenEmojiPicker extends nsZenDOMOperatedFeature {
#panel;
#anchor;
#emojiAsSVG = false;
#currentPromise = null;
#currentPromiseResolve = null;
@@ -199,14 +200,22 @@ class nsZenEmojiPicker extends nsZenDOMOperatedFeature {
}
#selectEmoji(emoji) {
if (this.#emojiAsSVG && emoji && !emoji.startsWith('chrome://')) {
emoji = `data:image/svg+xml;base64,${btoa(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><text y="28" font-size="28" x="0">${unescape(
encodeURIComponent(emoji)
)}</text></svg>`
)}`;
}
this.#currentPromiseResolve?.(emoji);
this.#panel.hidePopup();
}
open(anchor, { onlySvgIcons = false } = {}) {
open(anchor, { onlySvgIcons = false, emojiAsSVG = false } = {}) {
if (this.#currentPromise) {
return null;
}
this.#emojiAsSVG = emojiAsSVG;
this.#currentPromise = new Promise((resolve, reject) => {
this.#currentPromiseResolve = resolve;
this.#currentPromiseReject = reject;

View File

@@ -21,8 +21,11 @@ class ZenSessionStore extends nsZenPreloadedFeature {
if (tabData.zenSyncId || tabData.zenPinnedId) {
tab.setAttribute('id', tabData.zenSyncId || tabData.zenPinnedId);
}
if (tabData.zenHasStaticLabel) {
tab.setAttribute('zen-has-static-label', 'true');
if (tabData.zenStaticLabel) {
tab.zenStaticLabel = tabData.zenStaticLabel;
}
if (tabData.zenHasStaticIcon) {
tab.setAttribute('zen-has-static-icon', 'true');
}
if (tabData.zenEssential) {
tab.setAttribute('zen-essential', 'true');

View File

@@ -125,6 +125,8 @@ window.gZenUIManager = {
}
menu.setAttribute('hidden', 'true');
}
// The first separator in the tab context menu is now useless.
document.getElementById('tabContextMenu').querySelector('menuseparator').remove();
},
_initCreateNewPopup() {
@@ -669,7 +671,9 @@ window.gZenUIManager = {
}
if (
(gZenVerticalTabsManager._hasSetSingleToolbar && gZenVerticalTabsManager._prefsRightSide) ||
(panel?.id === 'zen-unified-site-data-panel' && !gZenVerticalTabsManager._hasSetSingleToolbar)
(panel?.id === 'zen-unified-site-data-panel' &&
!gZenVerticalTabsManager._hasSetSingleToolbar) ||
(panel?.id === 'unified-extensions-panel' && gZenVerticalTabsManager._hasSetSingleToolbar)
) {
block = 'bottomright';
inline = 'topright';
@@ -1292,6 +1296,7 @@ window.gZenVerticalTabsManager = {
: this._tabEdited;
let input = document.getElementById('tab-label-input');
let newName = input.value.replace(/\s+/g, ' ').trim();
const hasChanged = input.value !== input._originalValue && newName;
document.documentElement.removeAttribute('zen-renaming-tab');
input.remove();
@@ -1301,12 +1306,12 @@ window.gZenVerticalTabsManager = {
// Check if name is blank, reset if so
// Always remove, so we can always rename and if it's empty,
// it will reset to the original name anyway
this._tabEdited.removeAttribute('zen-has-static-label');
if (newName) {
if (hasChanged) {
this._tabEdited.zenStaticLabel = newName;
gBrowser._setTabLabel(this._tabEdited, newName);
this._tabEdited.setAttribute('zen-has-static-label', 'true');
gZenUIManager.showToast('zen-tabs-renamed');
} else {
delete this._tabEdited.zenStaticLabel;
gBrowser.setTabTitle(this._tabEdited);
}
@@ -1335,7 +1340,8 @@ window.gZenVerticalTabsManager = {
},
renameTabStart(event) {
const isTab = !!event.target.closest('.tabbrowser-tab');
let target = TabContextMenu.contextTab || event.target;
const isTab = !!target.closest('.tabbrowser-tab');
if (
this._tabEdited ||
((!Services.prefs.getBoolPref('zen.tabs.rename-tabs') ||
@@ -1345,13 +1351,10 @@ window.gZenVerticalTabsManager = {
)
return;
this._tabEdited =
event.target.closest('.tabbrowser-tab') ||
event.target.closest('.zen-current-workspace-indicator-name') ||
(event.explicit && event.target.closest('.tab-group-label'));
if (
!this._tabEdited ||
((!this._tabEdited.pinned || this._tabEdited.hasAttribute('zen-essential')) && isTab)
) {
target.closest('.tabbrowser-tab') ||
target.closest('.zen-current-workspace-indicator-name') ||
(event.explicit && target.closest('.tab-group-label'));
if (!this._tabEdited || (this._tabEdited.hasAttribute('zen-essential') && isTab)) {
this._tabEdited = null;
return;
}
@@ -1368,8 +1371,10 @@ window.gZenVerticalTabsManager = {
label.after(container);
}
const input = document.createElement('input');
const content = isTab ? this._tabEdited.label : this._tabEdited.textContent;
input.id = 'tab-label-input';
input.value = isTab ? this._tabEdited.label : this._tabEdited.textContent;
input._originalValue = content;
input.value = content;
input.addEventListener('keydown', this.renameTabKeydown.bind(this));
if (isTab) {

View File

@@ -495,20 +495,22 @@ body > #confetti {
}
@media -moz-pref('zen.theme.hide-unified-extensions-button') {
#unified-extensions-button {
#unified-extensions-button:not([showing]) {
display: none !important;
}
}
#unified-extensions-button:not([showing]) {
display: none !important;
@media not -moz-pref('zen.theme.hide-unified-extensions-button') {
#zen-site-data-section-addons {
display: none;
}
}
#zen-site-data-header {
gap: 8px;
align-items: center;
padding: 10px 9px;
padding-bottom: 0;
padding-bottom: 8px;
:root[zen-single-toolbar='true']:not([zen-right-side='true']) & {
flex-direction: row-reverse;
@@ -520,7 +522,7 @@ body > #confetti {
-moz-context-properties: fill;
fill: currentColor;
color: light-dark(rgba(0, 0, 0, 0.8), rgba(255, 255, 255, 0.8));
max-width: 48px;
width: 48px;
height: 32px;
position: relative;

View File

@@ -131,6 +131,9 @@ class nsZenDownloadAnimationElement extends HTMLElement {
}
#areTabsOnRightSide() {
const position = Services.prefs.getIntPref('zen.downloads.icon-popup-position', 0);
if (position === 1) return false;
if (position === 2) return true;
return Services.prefs.getBoolPref('zen.tabs.vertical.right-side');
}

View File

@@ -187,7 +187,6 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
window.addEventListener('TabSelect', this);
window.addEventListener('TabOpen', this);
const onNewFolder = this.#onNewFolder.bind(this);
document.getElementById('zen-context-menu-new-folder').addEventListener('command', onNewFolder);
document
.getElementById('zen-context-menu-new-folder-toolbar')
.addEventListener('command', onNewFolder);
@@ -543,9 +542,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
// that we want it to initially be collapsed.
setTimeout(
(folder) => {
gZenPinnedTabManager.promiseInitializedPinned.then(() => {
folder.collapsed = !!options.collapsed;
});
folder.collapsed = !!options.collapsed;
},
0,
folder

View File

@@ -142,7 +142,11 @@ export class nsZenSessionManager {
// gotten the opportunity to save the session yet.
if (!Services.prefs.getBoolPref(MIGRATION_PREF, false)) {
Services.prefs.setBoolPref(MIGRATION_PREF, true);
for (const winData of initialState.windows || []) {
this.#sidebar = {
...this.#sidebar,
spaces: this._migrationSpaceData || [],
};
for (const winData of initialState?.windows || []) {
winData.spaces = this._migrationSpaceData || [];
}
delete this._migrationSpaceData;

View File

@@ -109,6 +109,9 @@ class nsZenWindowSync {
for (let topic of OBSERVING) {
Services.obs.addObserver(this, topic);
}
lazy.SessionStore.promiseAllWindowsRestored.then(() => {
this.#onSessionStoreInitialized();
});
}
uninit() {
@@ -135,7 +138,9 @@ class nsZenWindowSync {
// 1. We are passing `zen-unsynced` in the window arguments.
// 2. We are trying to open a link in a new window where other synced
// windows already exist
let forcedSync = false;
// Note, we force syncing if the window is private or workspaces is disabled
// to avoid confusing the old private window behavior.
let forcedSync = !aWindow.gZenWorkspaces?.privateWindowOrDisabled;
let hasUnsyncedArg = false;
if (aWindow._zenStartupSyncFlag === 'synced') {
forcedSync = true;
@@ -160,6 +165,27 @@ class nsZenWindowSync {
}
}
/**
* Called when the session store has finished initializing for a window.
*
* @param {Window} aWindow - The browser window that has initialized session store.
*/
#onSessionStoreInitialized() {
// For every tab we have in where there's no sync ID, we need to
// assign one and sync it to other windows.
// This should only happen really when updating from an older version
// that didn't have this feature.
this.#runOnAllWindows(null, (aWindow) => {
const { gBrowser } = aWindow;
for (let tab of gBrowser.tabs) {
if (!tab.id) {
tab.id = this.#newTabSyncId;
lazy.TabStateFlusher.flush(tab.linkedBrowser);
}
}
});
}
/**
* @returns {string} A unique tab ID.
*/
@@ -277,6 +303,9 @@ class nsZenWindowSync {
* @returns {MozTabbrowserTab|MozTabbrowserTabGroup|null} The item element if found, otherwise null.
*/
#getItemFromWindow(aWindow, aItemId) {
if (!aItemId) {
return null;
}
return aWindow.document.getElementById(aItemId);
}
@@ -308,19 +337,24 @@ class nsZenWindowSync {
}
const { gBrowser, gZenFolders } = aWindow;
if (flags & SYNC_FLAG_ICON) {
aTargetItem.removeAttribute('zen-has-static-icon');
if (gBrowser.isTab(aOriginalItem)) {
gBrowser.setIcon(aTargetItem, gBrowser.getIcon(aOriginalItem));
gBrowser.setIcon(
aTargetItem,
aOriginalItem.getAttribute('image') || gBrowser.getIcon(aOriginalItem)
);
} else if (aOriginalItem.isZenFolder) {
// Icons are a zen-only feature for tab groups.
gZenFolders.setFolderUserIcon(aTargetItem, aOriginalItem.iconURL);
}
this.#maybeSyncAttributeChange(aOriginalItem, aTargetItem, 'zen-has-static-icon');
}
if (flags & SYNC_FLAG_LABEL) {
if (gBrowser.isTab(aOriginalItem)) {
aTargetItem._zenChangeLabelFlag = true;
aTargetItem.zenStaticLabel = aOriginalItem.zenStaticLabel;
gBrowser._setTabLabel(aTargetItem, aOriginalItem.label);
delete aTargetItem._zenChangeLabelFlag;
this.#maybeSyncAttributeChange(aOriginalItem, aTargetItem, 'zen-has-static-label');
} else if (gBrowser.isTabGroup(aOriginalItem)) {
aTargetItem.label = aOriginalItem.label;
}
@@ -486,7 +520,7 @@ class nsZenWindowSync {
}
// Restore the listeners for the swapped in tab.
if (!onClose) {
if (!onClose && filter) {
tabListener = new otherTabBrowser.zenTabProgressListener(aTab, otherBrowser, true, false);
otherTabBrowser._tabListeners.set(aTab, tabListener);
@@ -746,18 +780,21 @@ class nsZenWindowSync {
* Sets the initial pinned state for a tab across all windows.
*
* @param {Object} aTab - The tab to set the pinned state for.
* @returns {Promise} A promise that resolves when the operation is complete.
*/
setPinnedTabState(aTab) {
const state = this.#getTabState(aTab);
const initialState = {
entry: state.entries[state.index - 1],
image: state.image,
};
this.#runOnAllWindows(null, (win) => {
const targetTab = this.#getItemFromWindow(win, aTab.id);
if (targetTab) {
targetTab._zenPinnedInitialState = initialState;
}
return lazy.TabStateFlusher.flush(aTab.linkedBrowser).finally(() => {
const state = this.#getTabState(aTab);
const initialState = {
entry: state.entries[state.index - 1],
image: state.image,
};
this.#runOnAllWindows(null, (win) => {
const targetTab = this.#getItemFromWindow(win, aTab.id);
if (targetTab) {
targetTab._zenPinnedInitialState = initialState;
}
});
});
}
@@ -784,7 +821,7 @@ class nsZenWindowSync {
);
const selectedTab = aWindow.gBrowser.selectedTab;
let win = [...this.#browserWindows][0];
const moveAllTabsToWindow = (allowSelected = false) => {
const moveAllTabsToWindow = async (allowSelected = false) => {
const { gBrowser, gZenWorkspaces } = win;
win.focus();
let success = true;
@@ -797,14 +834,13 @@ class nsZenWindowSync {
success = false;
continue;
}
newTab._zenContentsVisible = true;
gBrowser.setTabTitle(newTab);
gZenWorkspaces.moveTabToWorkspace(newTab, aWorkspaceId);
}
}
if (success) {
aWindow.close();
win.gBrowser.selectedTab.linkedBrowser.focus();
await gZenWorkspaces.changeWorkspaceWithID(aWorkspaceId);
gBrowser.selectedBrowser.focus();
}
};
if (!win) {

View File

@@ -1055,14 +1055,20 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
let tab = window.gBrowser.getTabForBrowser(browser);
const ignoreSplit = tab.hasAttribute('zen-dont-split-glance');
tab.removeAttribute('zen-dont-split-glance');
let isGlanceTab = false;
if (tab.hasAttribute('zen-glance-tab') && !ignoreSplit) {
// Extract from parent node so we are not selecting the wrong (current) tab
tab = tab.parentNode.closest('.tabbrowser-tab');
isGlanceTab = true;
console.assert(tab, 'Tab not found for zen-glance-tab');
}
if (tab) {
this.updateSplitView(tab);
tab.linkedBrowser.docShellIsActive = true;
if (isGlanceTab) {
// See issues https://github.com/zen-browser/desktop/issues/11641
this.removeSplitters();
}
}
this._maybeRemoveFakeBrowser();
{
@@ -1209,7 +1215,9 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
const oldView = this.currentView;
const newView = this._data.findIndex((group) => group.tabs.includes(tab));
if (oldView === newView) return;
if (newView === oldView && oldView < 0) {
return;
}
if (newView < 0 && oldView >= 0) {
this.deactivateCurrentSplitView();
return;
@@ -2000,40 +2008,43 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
this._data.push(data);
this.activateSplitView(data);
gBrowser.selectedTab = emptyTab;
window.addEventListener(
'ZenURLBarClosed',
(event) => {
const { onElementPicked, onSwitch } = event.detail;
const groupIndex = this._data.findIndex((group) => group.tabs.includes(emptyTab));
const newSelectedTab = gBrowser.selectedTab;
const cleanup = () => {
this.removeTabFromGroup(emptyTab, groupIndex, { changeTab: !onSwitch, forUnsplit: true });
const command = document.getElementById('cmd_zenNewEmptySplit');
command.removeAttribute('disabled');
};
if (onElementPicked) {
if (
newSelectedTab === emptyTab ||
newSelectedTab === selectedTab ||
selectedTab.getAttribute('zen-workspace-id') !==
newSelectedTab.getAttribute('zen-workspace-id')
) {
cleanup();
return;
}
this.removeTabFromGroup(emptyTab, groupIndex, { forUnsplit: true });
gBrowser.selectedTab = selectedTab;
this.resetTabState(emptyTab, false);
this.splitTabs([selectedTab, newSelectedTab], 'grid', 1);
} else {
cleanup();
}
},
{ once: true }
);
setTimeout(() => {
window.addEventListener(
'ZenURLBarClosed',
(event) => {
const { onElementPicked, onSwitch } = event.detail;
const groupIndex = this._data.findIndex((group) => group.tabs.includes(emptyTab));
const newSelectedTab = gBrowser.selectedTab;
const cleanup = () => {
this.removeTabFromGroup(emptyTab, groupIndex, {
changeTab: !onSwitch,
forUnsplit: true,
});
const command = document.getElementById('cmd_zenNewEmptySplit');
command.removeAttribute('disabled');
};
if (onElementPicked) {
if (
newSelectedTab === emptyTab ||
newSelectedTab === selectedTab ||
selectedTab.getAttribute('zen-workspace-id') !==
newSelectedTab.getAttribute('zen-workspace-id')
) {
cleanup();
return;
}
this.removeTabFromGroup(emptyTab, groupIndex, { forUnsplit: true });
gBrowser.selectedTab = selectedTab;
this.resetTabState(emptyTab, false);
this.splitTabs([selectedTab, newSelectedTab], 'grid', 1);
} else {
cleanup();
}
},
{ once: true }
);
gZenUIManager.handleNewTab(false, false, 'tab', true);
}, 0);
});
}
get splitViewBrowsers() {

View File

@@ -67,7 +67,7 @@
#tabbrowser-tabpanels[zen-split-view='true'] .browserSidebarContainer.deck-selected {
&:not(.zen-glance-overlay) {
outline: 2px solid var(--zen-primary-color) !important;
outline: 2px solid light-dark(var(--zen-primary-color), var(--button-background-color-primary)) !important;
}
&.zen-glance-overlay {

View File

@@ -32,6 +32,7 @@ class ZenPinnedTabsObserver {
);
ChromeUtils.defineESModuleGetters(lazy, {
E10SUtils: 'resource://gre/modules/E10SUtils.sys.mjs',
TabStateCache: 'resource:///modules/sessionstore/TabStateCache.sys.mjs',
});
this.#listenPinnedTabEvents();
}
@@ -60,10 +61,6 @@ class ZenPinnedTabsObserver {
}
class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
promiseInitializedPinned = new Promise((resolve) => {
this._resolvePinnedInitializedInternal = resolve;
});
init() {
if (!this.enabled) {
return;
@@ -463,43 +460,73 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
data-lazy-l10n-id="tab-context-zen-remove-essential"
hidden="true"
command="cmd_contextZenRemoveFromEssentials"/>
<menuseparator/>
<menuitem id="context_zen-edit-tab-title"
data-lazy-l10n-id="tab-context-zen-edit-title"
hidden="true"/>
<menuitem id="context_zen-edit-tab-icon"
data-lazy-l10n-id="tab-context-zen-edit-icon"/>
<menuseparator/>
`);
document.getElementById('context_pinTab')?.before(element);
document.getElementById('context_zen-edit-tab-title').addEventListener('command', (event) => {
gZenVerticalTabsManager.renameTabStart(event);
});
document.getElementById('context_zen-edit-tab-icon').addEventListener('command', () => {
const tab = TabContextMenu.contextTab;
tab.removeAttribute('zen-has-static-icon');
gZenEmojiPicker
.open(tab.iconImage, { emojiAsSVG: true })
.then((icon) => {
gBrowser.setIcon(tab, icon);
if (icon) {
tab.setAttribute('zen-has-static-icon', 'true');
}
lazy.TabStateCache.update(tab.permanentKey, {
image: null,
});
})
.catch((err) => {
console.error(err);
return;
});
});
}
async updatePinnedTabContextMenu(contextTab) {
updatePinnedTabContextMenu(contextTab) {
if (!this.enabled) {
document.getElementById('context_pinTab').hidden = true;
return;
}
const isVisible = contextTab.pinned && !contextTab.multiselected;
const isEssential = contextTab.getAttribute('zen-essential');
const zenAddEssential = document.getElementById('context_zen-add-essential');
document.getElementById('context_zen-reset-pinned-tab').hidden = !isVisible;
document.getElementById('context_zen-replace-pinned-url-with-current').hidden = !isVisible;
zenAddEssential.hidden = contextTab.getAttribute('zen-essential') || !!contextTab.group;
zenAddEssential.setAttribute(
'badge',
await document.l10n.formatValue('tab-context-zen-add-essential-badge', {
zenAddEssential.hidden = isEssential || !!contextTab.group;
document.l10n
.formatValue('tab-context-zen-add-essential-badge', {
num: gBrowser._numZenEssentials,
max: this.maxEssentialTabs,
})
);
.then((badgeText) => {
zenAddEssential.setAttribute('badge', badgeText);
});
document
.getElementById('cmd_contextZenAddToEssentials')
.setAttribute('disabled', !this.canEssentialBeAdded(contextTab));
document.getElementById('context_closeTab').hidden = contextTab.hasAttribute('zen-essential');
document.getElementById('context_zen-remove-essential').hidden =
!contextTab.getAttribute('zen-essential');
document.getElementById('zen-context-menu-new-folder').hidden =
contextTab.getAttribute('zen-essential');
document.getElementById('context_zen-remove-essential').hidden = !isEssential;
document.getElementById('context_unpinTab').hidden =
document.getElementById('context_unpinTab').hidden ||
contextTab.getAttribute('zen-essential');
document.getElementById('context_unpinTab').hidden || isEssential;
document.getElementById('context_unpinSelectedTabs').hidden =
document.getElementById('context_unpinSelectedTabs').hidden ||
contextTab.getAttribute('zen-essential');
document.getElementById('context_unpinSelectedTabs').hidden || isEssential;
document.getElementById('context_zen-pinned-tab-separator').hidden = !isVisible;
document.getElementById('context_zen-edit-tab-title').hidden =
isEssential ||
!Services.prefs.getBoolPref('zen.tabs.rename-tabs') ||
gZenVerticalTabsManager._prefsSidebarExpanded;
}
moveToAnotherTabContainerIfNecessary(event, movingTabs) {
@@ -675,16 +702,6 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
return document.documentElement.getAttribute('zen-sidebar-expanded') === 'true';
}
async updatePinTitle(tab, newTitle, isEdited = true) {
tab.removeAttribute('zen-has-static-label');
if (isEdited) {
gBrowser._setTabLabel(tab, newTitle);
tab.setAttribute('zen-has-static-label', 'true');
} else {
gBrowser.setTabTitle(tab);
}
}
canEssentialBeAdded(tab) {
return (
!(

View File

@@ -141,7 +141,10 @@
position: relative;
opacity: 1;
align-items: center;
padding: 0 5px;
@media (-moz-platform: macos) {
padding: 0 5px;
}
& toolbarseparator {
height: 1px;
@@ -330,6 +333,10 @@
}
& .tab-icon-image {
-moz-context-properties: fill, stroke, fill-opacity;
fill-opacity: 0.5;
border-radius: 4px;
&:not([src]),
&:-moz-broken {
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3C/svg%3E") !important;

View File

@@ -10,5 +10,9 @@
"zen.mods.last-update",
"zen.view.compact.enable-at-startup",
"zen.urlbar.suggestions-learner",
"browser.newtabpage.activity-stream.trendingSearch.defaultSearchEngine"
"browser.newtabpage.activity-stream.trendingSearch.defaultSearchEngine",
// From the imported safebrowsing tests
"urlclassifier.phishTable",
"urlclassifier.malwareTable"
]

View File

@@ -0,0 +1,30 @@
# 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/.
[reportbrokensite]
source = "browser/components/reportbrokensite/test/browser"
is_direct_path = true
disable = [
"browser_addon_data_sent.js"
]
[reportbrokensite.replace-manifest]
"../../../../../" = "../../../../"
[safebrowsing]
source = "browser/components/safebrowsing/content/test"
is_direct_path = true
[shell]
source = "browser/components/shell/test"
is_direct_path = true
disable = [
"browser_1119088.js",
"browser_setDesktopBackgroundPreview.js",
]
[tooltiptext]
source = "toolkit/components/tooltiptext"

View File

@@ -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",
]

View File

@@ -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",
]

View File

@@ -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();
});

View File

@@ -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);
});

View File

@@ -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();
});
});

View File

@@ -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();
}
});
});

View File

@@ -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();
}
});

View File

@@ -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();
}
});
});

View File

@@ -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");
});

View File

@@ -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 <command>'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`
);
}
});
});

View File

@@ -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();
}
);
});

View File

@@ -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);
});

View File

@@ -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);
});

View File

@@ -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);
});

View File

@@ -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());
});
});

View File

@@ -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);
});

View File

@@ -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);
});

View File

@@ -0,0 +1,22 @@
<!DOCTYPE HTML>
<!-- 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/. -->
<html dir="ltr" xml:lang="en-US" lang="en-US">
<head>
<meta charset="utf8">
<script>
window.marfeel = 1;
window.Mobify = { Tag: 1 };
window.FastClick = 1;
</script>
</head>
<body>
<!-- blocked tracking content -->
<iframe src="https://trackertest.org/"></iframe>
<!-- mixed active content -->
<iframe src="http://tracking.example.org/browser/browser/base/content/test/protectionsUI/benignPage.html"></iframe>
<!-- mixed display content -->
<img src="http://example.com/tests/image/test/mochitest/blue.png"></img>
</body>
</html>

View File

@@ -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);
});
}

View File

@@ -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();
}

View File

@@ -0,0 +1,27 @@
<!DOCTYPE HTML>
<!-- 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/. -->
<html dir="ltr" xml:lang="en-US" lang="en-US">
<head>
<meta charset="utf8">
</head>
<body>
<script>
let ready;
window.wrtReady = new Promise(r => ready = r);
let arrived;
window.messageArrived = new Promise(r => arrived = r);
window.addEventListener("message", e => {
window.message = e.data;
arrived();
});
window.addEventListener("load", () => {
setTimeout(ready, 100);
});
</script>
</body>
</html>

View File

@@ -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");
}

View File

@@ -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"]

View File

@@ -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();
});

View File

@@ -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"
);
});
});

View File

@@ -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!");
}
);
});

View File

@@ -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);
});

View File

@@ -0,0 +1 @@
<html><body></body></html>

View File

@@ -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();

View File

@@ -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"

View File

@@ -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/<UUID>.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);
}
);
});

View File

@@ -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);
}
}
);
});

View File

@@ -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"
);
}

View File

@@ -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;
}
}

View File

@@ -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"
);
});

View File

@@ -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
);
});

View File

@@ -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
);
});

View File

@@ -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
);
});

View File

@@ -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
);
});

View File

@@ -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
);
});

View File

@@ -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
);
});

View File

@@ -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"
);
});

View File

@@ -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();
}
);

View File

@@ -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();
}
});

View File

@@ -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;
}
);
});

View File

@@ -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));
}

View File

@@ -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<nsIFile> exe;
nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(exe));
ASSERT_TRUE(NS_SUCCEEDED(rv));
RefPtr<IShellLinkW> 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<nsIFile> exe;
nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(exe));
ASSERT_TRUE(NS_SUCCEEDED(rv));
RefPtr<IShellLinkW> 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);
}

View File

@@ -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"

View File

@@ -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);
}

View File

@@ -0,0 +1,6 @@
<html>
<head><meta content="text/html; charset=utf-8" http-equiv="Content-Type"></head>
<body style="background-color: rgb(0, 255, 0); color: rgb(0, 0, 255)">
Hi
</body>
</html>

View File

@@ -0,0 +1,7 @@
<html>
<head><meta content="text/html; charset=utf-8" http-equiv="Content-Type"></head>
<body>
<iframe width="300" height="200" src="http://example.com/browser/browser/components/shell/test/headless_iframe.html"></iframe>
Hi
</body>
</html>

View File

@@ -0,0 +1,6 @@
<html>
<head><meta content="text/html; charset=utf-8" http-equiv="Content-Type"></head>
<body style="background-color: rgb(0, 255, 0);">
Hi
</body>
</html>

View File

@@ -0,0 +1,2 @@
HTTP 302 Moved Temporarily
Location: headless.html

View File

@@ -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)

View File

@@ -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");
});

View File

@@ -0,0 +1,7 @@
[DEFAULT]
run-if = ["os != 'android'"]
firefox-appdir = "browser"
tags = "os_integration"
["test_macOS_showSecurityPreferences.js"]
run-if = ["os == 'mac'"]

View File

@@ -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"]

View File

@@ -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);
});
}
);
});

View File

@@ -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");
});
}
);
});

View File

@@ -0,0 +1,33 @@
add_task(async function () {
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: "data:text/html,<!DOCTYPE html><html><body><input id='i'></body></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"
);
});
}
);
});

View File

@@ -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,<!DOCTYPE html><html><body><form id='content'></form></body></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]);
}
}
);
});

View File

@@ -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;
}

View File

@@ -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,<!DOCTYPE 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 = `<button style="pointer-events: auto; position: absolute; width: 200px; height: 200px;" title="foo">bar</button>`;
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"
);
}
);
});

View File

@@ -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 =
"<div title='shadow' style='width: 200px; height: 200px;'>shadow</div>";
});
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 = "<div style='width: 200px; height: 200px;'>shadow</div>";
});
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 = "<div style='width: 200px; height: 200px;'>light</div>";
doc.body.appendChild(host);
host.setAttribute("style", "position: absolute; top: 0; left: 0;");
var sr = host.attachShadow({ mode: "closed" });
sr.innerHTML =
"<div title='shadow' style='width: 200px; height: 200px;'><slot></slot></div>";
});
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);
});

View File

@@ -0,0 +1,59 @@
<svg width="640px" height="480px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>This is a root SVG element's title</title>
<foreignObject>
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<svg xmlns="http://www.w3.org/2000/svg" id="svg1">
<title>This is a non-root SVG element title</title>
</svg>
</body>
</html>
</foreignObject>
<text id="text1" x="10px" y="32px" font-size="24px">
This contains only &lt;title&gt;
<title>
This is a title
</title>
</text>
<text id="text2" x="10px" y="96px" font-size="24px">
This contains only &lt;desc&gt;
<desc>This is a desc</desc>
</text>
<text id="text3" x="10px" y="128px" font-size="24px" title="ignored for SVG">
This contains nothing.
</text>
<a id="link1" href="#">
This link contains &lt;title&gt;
<title>
This is a title
</title>
<text id="text4" x="10px" y="192px" font-size="24px">
</text>
</a>
<a id="link2" href="#">
<text x="10px" y="192px" font-size="24px">
This text contains &lt;title&gt;
<title>
This is a title
</title>
</text>
</a>
<a id="link3" href="#" xlink:title="This is an xlink:title attribute">
<text x="10px" y="224px" font-size="24px">
This link contains &lt;title&gt; &amp; xlink:title attr.
<title>This is a title</title>
</text>
</a>
<a id="link4" href="#" xlink:title="This is an xlink:title attribute">
<text x="10px" y="256px" font-size="24px">
This link contains xlink:title attr.
</text>
</a>
<text id="text5" x="10px" y="160px" font-size="24px"
xlink:title="This is an xlink:title attribute but it isn't on a link" >
This contains nothing.
</text>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0"?>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<xul:toolbox xmlns:html="http://www.w3.org/1999/xhtml"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<toolbar>
<toolbarbutton id="xulToolbarButton"
tooltiptext="XUL tooltiptext"
title="XUL title"/>
</toolbar>
</xul:toolbox>
</html>

View File

@@ -15,3 +15,7 @@ BROWSER_CHROME_MANIFESTS += [
"welcome/browser.toml",
"workspaces/browser.toml",
]
DIRS += [
"mochitests",
]

View File

@@ -4,6 +4,11 @@
import { AppConstants } from 'resource://gre/modules/AppConstants.sys.mjs';
const ADDONS_BUTTONS_HIDDEN = Services.prefs.getBoolPref(
'zen.theme.hide-unified-extensions-button',
true
);
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
@@ -21,7 +26,19 @@ export class nsZenSiteDataPanel {
this.window = window;
this.document = window.document;
this.panel = this.document.getElementById('zen-unified-site-data-panel');
this.unifiedPanel = this.#initUnifiedPanel();
this.unifiedPanelView = 'unified-extensions-view';
this.extensionsPanelView = 'original-unified-extensions-view';
if (ADDONS_BUTTONS_HIDDEN) {
this.window.gUnifiedExtensions._panel = this.unifiedPanel;
// Remove the old permissions dialog
this.document.getElementById('unified-extensions-panel-template').remove();
} else {
this.extensionsPanel = this.#initExtensionsPanel();
}
this.#init();
}
@@ -34,22 +51,23 @@ export class nsZenSiteDataPanel {
`);
this.anchor = button.querySelector('#zen-site-data-icon-button');
this.document.getElementById('identity-icon-box').before(button);
this.window.gUnifiedExtensions._button = this.anchor;
this.extensionsPanelButton = this.document.getElementById('unified-extensions-button');
this.window.gUnifiedExtensions._button = ADDONS_BUTTONS_HIDDEN
? this.anchor
: this.extensionsPanelButton;
this.document
.getElementById('nav-bar')
.setAttribute('addon-webext-overflowbutton', 'zen-site-data-icon-button');
// Remove the old permissions dialog
this.document.getElementById('unified-extensions-panel-template').remove();
this.#initCopyUrlButton();
this.#initEventListeners();
this.#maybeShowFeatureCallout();
}
#initEventListeners() {
this.panel.addEventListener('popupshowing', this);
this.unifiedPanel.addEventListener('popupshowing', this);
this.document.getElementById('zen-site-data-manage-addons').addEventListener('click', this);
this.document.getElementById('zen-site-data-settings-more').addEventListener('click', this);
this.anchor.addEventListener('click', this);
@@ -124,6 +142,24 @@ export class nsZenSiteDataPanel {
}
}
#initExtensionsPanel() {
const panel = this.window.gUnifiedExtensions.panel;
const extensionsView = panel?.querySelector('#unified-extensions-view');
extensionsView.setAttribute('id', this.extensionsPanelView);
const panelMultiView = panel?.querySelector('panelmultiview');
panelMultiView.setAttribute('mainViewId', this.extensionsPanelView);
return panel;
}
#initUnifiedPanel() {
const panel = this.document.getElementById('zen-unified-site-data-panel');
this.window.gUnifiedExtensions.initializePanel(panel);
return panel;
}
#preparePanel() {
this.#setSitePermissions();
this.#setSiteSecurityInfo();
@@ -498,7 +534,7 @@ export class nsZenSiteDataPanel {
this.window.gZenCommonActions.copyCurrentURLToClipboard();
}
if (AppConstants.platform !== 'macosx') {
this.panel.hidePopup();
this.unifiedPanel.hidePopup();
}
}
}
@@ -556,7 +592,13 @@ export class nsZenSiteDataPanel {
break;
}
case 'zen-site-data-icon-button': {
this.window.gUnifiedExtensions.togglePanel(event);
this.window.gUnifiedExtensions.togglePanel(
event,
null,
this.unifiedPanel,
this.unifiedPanelView,
this.anchor
);
break;
}
default: {

View File

@@ -183,6 +183,14 @@ const globalActionsTemplate = [
return lazy.currentTheme !== 0;
},
},
{
label: 'Print',
command: 'cmd_print',
icon: 'chrome://browser/skin/zen-icons/print.svg',
isAvailable: (window) => {
return isNotEmptyTab(window);
},
},
];
export const globalActions = globalActionsTemplate.map((action) => ({

View File

@@ -23,6 +23,8 @@ class nsZenWorkspaces {
direction: null,
};
_workspaceCache = [];
#lastScrollTime = 0;
bookmarkMenus = [
@@ -746,13 +748,13 @@ class nsZenWorkspaces {
}
set activeWorkspace(value) {
if (value === this.#activeWorkspace) {
return;
}
const spaces = this.getWorkspaces();
if (!spaces.some((ws) => ws.uuid === value)) {
value = spaces[0]?.uuid || '';
}
if (value === this.#activeWorkspace) {
return;
}
this.#activeWorkspace = value;
if (this.privateWindowOrDisabled) {
return;
@@ -804,7 +806,7 @@ class nsZenWorkspaces {
getWorkspaceFromId(id) {
try {
return this._workspaceCache.find((workspace) => workspace.uuid === id);
return this.getWorkspaces().find((workspace) => workspace.uuid === id);
} catch {
return null;
}
@@ -822,7 +824,7 @@ class nsZenWorkspaces {
this.#activeWorkspace = this._tempWorkspace?.uuid;
return this._workspaceCache;
}
return this._workspaceCache;
return [...this._workspaceCache];
}
async workspaceBookmarks() {
@@ -848,10 +850,13 @@ class nsZenWorkspaces {
return this._workspaceCache;
}
async restoreWorkspacesFromSessionStore(aWinData) {
this._workspaceCache = aWinData.spaces || [
await this.createAndSaveWorkspace('Space', undefined, true),
];
async restoreWorkspacesFromSessionStore(aWinData = {}) {
if (this.#hasInitialized) {
return;
}
this._workspaceCache = aWinData.spaces?.length
? aWinData.spaces
: [await this.createAndSaveWorkspace('Space', undefined, true)];
this.activeWorkspace = aWinData.activeZenSpace || this._workspaceCache[0].uuid;
await this.initializeWorkspaces();
this.#hasInitialized = true;
@@ -907,6 +912,7 @@ class nsZenWorkspaces {
const cleanup = () => {
delete this._tabToSelect;
delete this._tabToRemoveForEmpty;
delete this._shouldOverrideTabs;
resolveSelectPromise();
};
@@ -922,7 +928,7 @@ class nsZenWorkspaces {
delete this._initialTab;
}
if (this._tabToRemoveForEmpty && !removedEmptyTab) {
if (this._tabToRemoveForEmpty && !removedEmptyTab && !this._shouldOverrideTabs) {
const tabs = gBrowser.tabs.filter((tab) => !tab.collapsed);
if (
typeof this._tabToSelect === 'number' &&
@@ -1146,7 +1152,9 @@ class nsZenWorkspaces {
const item = document.createXULElement('menuitem');
item.className = 'zen-workspace-context-menu-item';
item.setAttribute('zen-workspace-id', workspace.uuid);
item.setAttribute('disabled', workspace.uuid === this.activeWorkspace);
if (workspace.uuid === this.activeWorkspace) {
item.setAttribute('disabled', true);
}
let name = workspace.name;
const iconIsSvg = workspace.icon && workspace.icon.endsWith('.svg');
if (workspace.icon && workspace.icon !== '' && !iconIsSvg) {
@@ -1202,7 +1210,7 @@ class nsZenWorkspaces {
}
if (!this.#contextMenuData.workspaceId) {
separator.hidden = false;
for (const workspace of [...this._workspaceCache].reverse()) {
for (const workspace of this.getWorkspaces().reverse()) {
const item = this.generateMenuItemForWorkspace(workspace);
item.addEventListener('command', (e) => {
this.changeWorkspaceWithID(e.target.closest('menuitem').getAttribute('zen-workspace-id'));
@@ -1240,7 +1248,7 @@ class nsZenWorkspaces {
if (this.privateWindowOrDisabled) {
return;
}
const workspacesData = this.getWorkspaces();
const workspacesData = this._workspaceCache;
const index = workspacesData.findIndex((ws) => ws.uuid === workspaceData.uuid);
if (index !== -1) {
workspacesData[index] = workspaceData;
@@ -1355,7 +1363,7 @@ class nsZenWorkspaces {
if (this.privateWindowOrDisabled) {
return;
}
const workspaces = this.getWorkspaces();
const workspaces = this._workspaceCache;
const workspace = workspaces.find((w) => w.uuid === id);
if (!workspace) {
console.warn(`Workspace with ID ${id} not found for reordering.`);
@@ -1374,8 +1382,10 @@ class nsZenWorkspaces {
return;
}
workspaces.splice(newPosition, 0, workspace);
// Propagate the changes
this.#propagateWorkspaceData();
// Propagate the changes if the order has changed
if (currentIndex !== newPosition) {
this.#propagateWorkspaceData();
}
}
async openWorkspaceCreation() {
@@ -2305,25 +2315,20 @@ class nsZenWorkspaces {
if (gZenWorkspaces.privateWindowOrDisabled) return;
const workspaces = this.getWorkspaces();
const menuPopup = document.getElementById('context-zen-change-workspace-tab-menu-popup');
const menuPopup = document.getElementById('moveTabOptionsMenu');
if (!menuPopup) {
return;
}
menuPopup.innerHTML = '';
const activeWorkspace = this.getActiveWorkspace();
for (let workspace of workspaces) {
const menuItem = document.createXULElement('menuitem');
menuItem.setAttribute('label', workspace.name);
menuItem.setAttribute('zen-workspace-id', workspace.uuid);
for (const item of menuPopup.querySelectorAll('.zen-workspace-context-menu-item')) {
item.remove();
}
const separator = document.createXULElement('menuseparator');
separator.classList.add('zen-workspace-context-menu-item');
menuPopup.prepend(separator);
for (let workspace of workspaces.reverse()) {
const menuItem = this.generateMenuItemForWorkspace(workspace);
menuItem.setAttribute('command', 'cmd_zenChangeWorkspaceTab');
if (workspace.uuid === activeWorkspace.uuid) {
menuItem.setAttribute('disabled', 'true');
}
menuPopup.appendChild(menuItem);
menuPopup.prepend(menuItem);
}
}
@@ -2681,7 +2686,6 @@ class nsZenWorkspaces {
const commandsToDisable = [
'cmd_zenOpenFolderCreation',
'cmd_zenOpenWorkspaceCreation',
'zen-context-menu-new-folder',
'zen-context-menu-new-folder-toolbar',
];
commandsToDisable.forEach((cmd) => {
@@ -2692,16 +2696,6 @@ class nsZenWorkspaces {
});
return;
}
const menu = document.createXULElement('menu');
menu.setAttribute('id', 'context-zen-change-workspace-tab');
menu.setAttribute('data-l10n-id', 'context-zen-change-workspace-tab');
const menuPopup = document.createXULElement('menupopup');
menuPopup.setAttribute('id', 'context-zen-change-workspace-tab-menu-popup');
menu.appendChild(menuPopup);
document.getElementById('context_moveTabOptions').after(menu);
}
async changeTabWorkspace(workspaceID) {

View File

@@ -26,8 +26,7 @@ window.ZenWorkspaceBookmarksStorage = {
workspace_uuid TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
UNIQUE(bookmark_guid, workspace_uuid),
FOREIGN KEY(workspace_uuid) REFERENCES zen_workspaces(uuid) ON DELETE CASCADE,
UNIQUE(bookmark_guid),
FOREIGN KEY(bookmark_guid) REFERENCES moz_bookmarks(guid) ON DELETE CASCADE
)
`);
@@ -46,8 +45,7 @@ window.ZenWorkspaceBookmarksStorage = {
workspace_uuid TEXT NOT NULL,
change_type TEXT NOT NULL,
timestamp INTEGER NOT NULL,
UNIQUE(bookmark_guid, workspace_uuid),
FOREIGN KEY(workspace_uuid) REFERENCES zen_workspaces(uuid) ON DELETE CASCADE,
UNIQUE(bookmark_guid),
FOREIGN KEY(bookmark_guid) REFERENCES moz_bookmarks(guid) ON DELETE CASCADE
)
`);

View File

@@ -99,6 +99,8 @@
& toolbarbutton {
margin: 0;
background: transparent;
/* Ignore press state when in overflow, to allow clicks to happen */
scale: 1 !important;
}
/* Inlcude separately since ther'es a bug in the