Update On Thu Mar 27 19:54:58 CET 2025

This commit is contained in:
github-action[bot] 2025-03-27 19:54:59 +01:00
parent 0c665687bb
commit 6ed3f92ece
980 changed files with 33080 additions and 10620 deletions

2
Cargo.lock generated
View file

@ -7217,7 +7217,7 @@ version = "0.3.100"
[[package]]
name = "webdriver"
version = "0.52.0"
version = "0.52.1"
dependencies = [
"base64 0.22.1",
"bytes",

View file

@ -2,6 +2,7 @@
subsuite = "a11y"
support-files = [
"head.js",
"doc_language.html",
"doc_treeupdate_ariadialog.html",
"doc_treeupdate_ariaowns.html",
"doc_treeupdate_imagemap.html",

View file

@ -122,14 +122,6 @@ addAccessibleTask(
-->
<area id="area" href="#" shape="rect" coords="0,0,2,2" alt="area">
</map>
<script>
// Attach dummy event handlers here, because inline event handler attributes
// will be blocked in the chrome context.
for (const el of document.querySelectorAll('[data-event]')) {
el["on" + el.dataset.event] = () => {};
}
</script>
`,
async function (browser, docAcc) {
is(docAcc.actionCount, 0, "Doc should not have any actions");
@ -255,6 +247,13 @@ addAccessibleTask(
topLevel: true,
iframe: true,
remoteIframe: true,
contentSetup: async function contentSetup() {
// Attach dummy event handlers here, because inline event handler attributes
// will be blocked in the chrome context.
for (const el of content.document.querySelectorAll("[data-event]")) {
el["on" + el.dataset.event] = () => {};
}
},
}
);

View file

@ -600,19 +600,6 @@ addAccessibleTask(
<div id="popover2" popover>popover2</div>
<button id="toggle5">toggle5</button>
</template></div>
<script>
const toggle1 = document.getElementById("toggle1");
const popover1 = document.getElementById("popover1");
toggle1.popoverTargetElement = popover1;
const toggle3 = document.getElementById("toggle3");
const shadow = document.getElementById("shadowHost").shadowRoot;
const toggle4 = shadow.getElementById("toggle4");
const popover2 = shadow.getElementById("popover2");
toggle3.popoverTargetElement = popover2;
toggle4.popoverTargetElement = popover2;
const toggle5 = shadow.getElementById("toggle5");
toggle5.popoverTargetElement = popover1;
</script>
`,
async function (browser, docAcc) {
const toggle1 = findAccessibleChildByID(docAcc, "toggle1");
@ -710,7 +697,24 @@ addAccessibleTask(
// toggle4 is in the same shadow DOM as popover2.
testStates(toggle4, STATE_COLLAPSED);
},
{ chrome: true, topLevel: true }
{
chrome: true,
topLevel: true,
contentSetup: async function contentSetup() {
const doc = content.document;
const toggle1 = doc.getElementById("toggle1");
const popover1 = doc.getElementById("popover1");
toggle1.popoverTargetElement = popover1;
const toggle3 = doc.getElementById("toggle3");
const shadow = doc.getElementById("shadowHost").shadowRoot;
const toggle4 = shadow.getElementById("toggle4");
const popover2 = shadow.getElementById("popover2");
toggle3.popoverTargetElement = popover2;
toggle4.popoverTargetElement = popover2;
const toggle5 = shadow.getElementById("toggle5");
toggle5.popoverTargetElement = popover1;
},
}
);
/**

View file

@ -5,21 +5,7 @@
"use strict";
addAccessibleTask(
`
<script>
// We can't include the html element in snippets, so set lang on it here.
document.documentElement.lang = "en";
</script>
<div id="inheritEn">inheritEn</div>
<img id="imgInheritEn" src="https://example.com/a11y/accessible/tests/mochitest/moz.png" alt="imgInheritEn">
<div id="de" lang="de">
<div id="inheritDe">inheritDe <span id="es" role="none" lang="es">esLeaf</span></div>
<img id="imgInheritDe" src="https://example.com/a11y/accessible/tests/mochitest/moz.png" alt="imgInheritDe">
<div id="fr" lang="fr"></div>
<img id="imgFr" src="https://example.com/a11y/accessible/tests/mochitest/moz.png" lang="fr" alt="imgFr">
<input id="radioFr" type="radio" lang="fr" aria-label="radioFr">
</div>
`,
"e10s/doc_language.html",
async function (browser, docAcc) {
is(docAcc.language, "en", "Document language correct");
const inheritEn = findAccessibleChildByID(docAcc, "inheritEn");

View file

@ -42,11 +42,6 @@ const snippet = `
<div id="hidden-subtree-2">D</div>
<div id="shadow-host"></div>
</div>
<script>
const host = document.querySelector("#shadow-host");
const shadowRoot = host.attachShadow({ mode: "open" });
shadowRoot.innerHTML = "<div id='shadowDiv'>E</div>";
</script>
`;
async function setContentVisibility(browser, value) {
@ -79,5 +74,15 @@ addAccessibleTask(
await setContentVisibility(browser, "hidden");
testAccessibleTree(target, { SECTION: [] });
},
{ iframe: true, remoteIframe: true, chrome: true }
{
iframe: true,
remoteIframe: true,
chrome: true,
contentSetup: async function contentSetup() {
const host = content.document.querySelector("#shadow-host");
const shadowRoot = host.attachShadow({ mode: "open" });
shadowRoot.innerHTML = "<div id='shadowDiv'>E</div>";
},
}
);

View file

@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Language Test</title>
</head>
<body id="body">
<div id="inheritEn">inheritEn</div>
<img id="imgInheritEn" src="https://example.com/a11y/accessible/tests/mochitest/moz.png" alt="imgInheritEn">
<div id="de" lang="de">
<div id="inheritDe">inheritDe <span id="es" role="none" lang="es">esLeaf</span></div>
<img id="imgInheritDe" src="https://example.com/a11y/accessible/tests/mochitest/moz.png" alt="imgInheritDe">
<div id="fr" lang="fr"></div>
<img id="imgFr" src="https://example.com/a11y/accessible/tests/mochitest/moz.png" lang="fr" alt="imgFr">
<input id="radioFr" type="radio" lang="fr" aria-label="radioFr">
</div>
</body>
</html>

View file

@ -160,12 +160,7 @@ addAccessibleTask(
// test dynamic translation
addAccessibleTask(
`<div id="container" style="position: absolute; left: -300px; top: 100px;">Hello</div>
<button id="b">Move</button>
<script>
document.getElementById("b").onclick = () => {
container.style.transform = 'translateX(400px)'
};
</script>`,
<button id="b">Move</button>`,
async function (browser, accDoc) {
const container = findAccessibleChildByID(accDoc, "container");
await untilCacheOk(
@ -183,5 +178,15 @@ addAccessibleTask(
"container should be on screen and visible"
);
},
{ chrome: true, iframe: true, remoteIframe: true }
{
chrome: true,
iframe: true,
remoteIframe: true,
contentSetup: async function contentSetup() {
content.document.getElementById("b").onclick = () => {
content.document.getElementById("container").style.transform =
"translateX(400px)";
};
},
}
);

View file

@ -73,16 +73,7 @@ addAccessibleTask(snippet2, async function (browser, accDoc) {
* messes with the body element and we don't want that to impact other tests.
*/
addAccessibleTask(
`
<div id="host"></div>
<script>
const host = document.getElementById("host");
host.attachShadow({ mode: "open" });
const emptyScript = document.createElement("script");
emptyScript.id = "emptyScript";
document.head.append(emptyScript);
</script>
`,
`<div id="host"></div>`,
async function (browser, docAcc) {
info("Moving body and setting slot on body");
let reordered = waitForEvent(EVENT_REORDER, docAcc);
@ -97,7 +88,20 @@ addAccessibleTask(
await reordered;
is(docAcc.childCount, 0, "document has no children after body move");
},
{ chrome: true, topLevel: true, iframe: true, remoteIframe: true }
{
chrome: true,
topLevel: true,
iframe: true,
remoteIframe: true,
contentSetup: async function contentSetup() {
const doc = content.document;
const host = doc.getElementById("host");
host.attachShadow({ mode: "open" });
const emptyScript = doc.createElement("script");
emptyScript.id = "emptyScript";
doc.head.append(emptyScript);
},
}
);
addAccessibleTask(

View file

@ -174,7 +174,8 @@ export class DOMFullscreenParent extends JSWindowActorParent {
this.waitingForChildExitFullscreen = false;
Services.obs.notifyObservers(window, "fullscreen-painted");
this.sendAsyncMessage("DOMFullscreen:Painted", {});
TelemetryStopwatch.finish("FULLSCREEN_CHANGE_MS");
Glean.fullscreen.change.stopAndAccumulate(this.timerId);
this.timerId = null;
break;
}
}
@ -215,7 +216,7 @@ export class DOMFullscreenParent extends JSWindowActorParent {
window.gXPInstallObserver.removeAllNotifications(browser);
}
TelemetryStopwatch.start("FULLSCREEN_CHANGE_MS");
this.timerId = Glean.fullscreen.change.start();
window.FullScreen.enterDomFullscreen(browser, this);
this.updateFullscreenWindowReference(window);
@ -227,7 +228,7 @@ export class DOMFullscreenParent extends JSWindowActorParent {
break;
}
case "MozDOMFullscreen:Exited": {
TelemetryStopwatch.start("FULLSCREEN_CHANGE_MS");
this.timerId = Glean.fullscreen.change.start();
// Make sure that the actor has not been destroyed before
// accessing its browsing context. Otherwise, a error may

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/.
# Adding a new metric? We have docs for that!
# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html
---
$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
$tags:
- 'Firefox :: General'
fullscreen:
change:
type: timing_distribution
description: >
The time content uses to enter/exit fullscreen regardless of fullscreen
transition timeout
This metric was generated to correspond to the Legacy Telemetry
exponential histogram FULLSCREEN_CHANGE_MS.
time_unit: millisecond
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1271160
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1271160
notification_emails:
- mozilla-telemetry@upsuper.org
expires: never
telemetry_mirror: FULLSCREEN_CHANGE_MS

View file

@ -835,9 +835,8 @@ pref("browser.search.widget.removeAfterDaysUnused", 120);
// capped at 100.
pref("browser.search.totalSearches", 0);
// Enable new experimental shopping features. This is solely intended as a
// rollout/"emergency stop" button - it will go away once the feature has
// rolled out. There will be separate controls for user opt-in/opt-out.
// Enables the Review Checker feature in the Shopping sidebar.
// There are separate controls for user opt-in/opt-out.
pref("browser.shopping.experience2023.enabled", false);
// Ternary int-valued pref indicating if the user has opted into the new
@ -886,16 +885,11 @@ pref("browser.shopping.experience2023.sidebarClosedCount", 0);
// When conditions are met, shows a prompt on the shopping sidebar asking users if they want to disable auto-open behavior
pref("browser.shopping.experience2023.showKeepSidebarClosedMessage", true);
// Integrates the Review Checker shopping feature into the global sidebar
// shoppingSidebar pref should be opposite of this to disable
// Integrates the Review Checker feature into the global sidebar.
// `enabled` pref should be opposite of this to disable
// the custom shopping sidebar.
pref("browser.shopping.experience2023.integratedSidebar", false);
// Enables showing the Review Checker in the Shopping sidebar.
// integratedSidebar pref should be opposite of this to disable
// the Review Checker sidebar panel.
pref("browser.shopping.experience2023.shoppingSidebar", true);
// If true, users have already seen a card in the Review Checker sidebar panel
// notifying users of the feature's new location and asking if they want to
// move the sidebar to the left or right side. Else if false, users are yet to

View file

@ -207,7 +207,8 @@
<panelview id="PanelUI-profiles"
flex="1"
has-custom-header="true">
<box id="PanelUI-profiles-header" class="panel-header">
<box id="PanelUI-profiles-header"
class="panel-header panel-header-with-additional-element">
<toolbarbutton class="subviewbutton subviewbutton-iconic subviewbutton-back"
id="profiles-appmenu-back-button"
tabindex="0"
@ -217,7 +218,7 @@
<html:span id="profiles-header-content"></html:span>
</html:h1>
<toolbarbutton id="profiles-edit-this-profile-button"
class="subviewbutton subviewbutton-iconic toolbarbutton-1"
class="subviewbutton subviewbutton-iconic"
data-l10n-id="appmenu-edit-profile"
/>
</box>

View file

@ -222,7 +222,7 @@ document.addEventListener(
if (!gContextMenu) {
throw new Error("Context menu doesn't seem to be open.");
}
gContextMenu.addSearchFieldAsEngine();
gContextMenu.addSearchFieldAsEngine().catch(console.error);
break;
case "context-searchselect": {
let { searchTerms, usePrivate, principal, csp } = event.target;

View file

@ -525,7 +525,7 @@ var FullScreen = {
// If there is no appropriate actor to send the message we have
// no way to complete the transition and should abort by exiting
// fullscreen.
this._abortEnterFullscreen();
this._abortEnterFullscreen(aActor);
return;
}
// Record that the actor is waiting for its child to enter
@ -552,7 +552,7 @@ var FullScreen = {
// full-screen was made. Cancel full-screen.
Services.focus.activeWindow != window
) {
this._abortEnterFullscreen();
this._abortEnterFullscreen(aActor);
return;
}
@ -670,17 +670,18 @@ var FullScreen = {
return needToWaitForChildExit;
},
_abortEnterFullscreen() {
_abortEnterFullscreen(aActor) {
// This function is called synchronously in fullscreen change, so
// we have to avoid calling exitFullscreen synchronously here.
//
// This could reject if we're not currently in fullscreen
// so just ignore rejection.
setTimeout(() => document.exitFullscreen().catch(() => {}), 0);
if (TelemetryStopwatch.running("FULLSCREEN_CHANGE_MS")) {
if (aActor.timerId) {
// Cancel the stopwatch for any fullscreen change to avoid
// errors if it is started again.
TelemetryStopwatch.cancel("FULLSCREEN_CHANGE_MS");
Glean.fullscreen.change.cancel(aActor.timerId);
aActor.timerId = null;
}
},

View file

@ -1397,6 +1397,11 @@ var gProtectionsHandler = {
shimId: "TiktokEmbed",
displayName: "Tiktok",
},
{
sites: ["https://platform.twitter.com"],
shimId: "TwitterEmbed",
displayName: "X",
},
],
/**

View file

@ -8,12 +8,6 @@
document.addEventListener(
"DOMContentLoaded",
() => {
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
TabGroupMetrics:
"moz-src:///browser/components/tabbrowser/TabGroupMetrics.sys.mjs",
});
let mainPopupSet = document.getElementById("mainPopupSet");
// eslint-disable-next-line complexity
mainPopupSet.addEventListener("command", event => {
@ -136,11 +130,7 @@ document.addEventListener(
let tabGroup = gBrowser.getTabGroupById(tabGroupId);
// Tabs need to be removed by their owning `Tabbrowser` or else
// there are errors.
tabGroup.ownerGlobal.gBrowser.removeTabGroup(tabGroup, {
isUserTriggered: true,
telemetrySource:
lazy.TabGroupMetrics.METRIC_SOURCE.TAB_OVERFLOW_MENU,
});
tabGroup.ownerGlobal.gBrowser.removeTabGroup(tabGroup);
}
break;

View file

@ -2346,18 +2346,34 @@ export class nsContextMenu {
});
}
addSearchFieldAsEngine() {
this.actor
.getSearchFieldEngineData(this.targetIdentifier)
.then(async ({ url, formData, charset, method }) => {
let icon = this.browser.mIconURL;
let uri = Services.io.newURI(url);
await this.window.gDialogBox.open(
"chrome://browser/content/search/addEngine.xhtml",
{ uri, formData, charset, method, icon }
);
})
.catch(console.error);
async addSearchFieldAsEngine() {
let { url, formData, charset, method } =
await this.actor.getSearchFieldEngineData(this.targetIdentifier);
let { engineInfo } = await this.window.gDialogBox.open(
"chrome://browser/content/search/addEngine.xhtml",
{
mode: "FORM",
title: true,
nameTemplate: Services.io.newURI(url).host,
}
);
// If the user saved, engineInfo contains `name` and `alias`.
// Otherwise, it's undefined.
if (engineInfo) {
let searchEngine = await Services.search.addUserEngine({
name: engineInfo.name,
alias: engineInfo.alias,
url,
formData,
charset,
method,
icon: this.browser.mIconURL,
});
this.window.gURLBar.search("", { searchEngine });
}
}
/**
@ -2615,10 +2631,7 @@ export class nsContextMenu {
getImageText() {
let dialogBox = this.window.gBrowser.getTabDialogBox(this.browser);
const imageTextResult = this.actor.getImageText(this.targetIdentifier);
TelemetryStopwatch.start(
"TEXT_RECOGNITION_API_PERFORMANCE",
imageTextResult
);
let timerId = Glean.textRecognition.apiPerformance.start();
const { dialog } = dialogBox.open(
"chrome://browser/content/textrecognition/textrecognition.html",
{
@ -2627,7 +2640,8 @@ export class nsContextMenu {
},
imageTextResult,
() => dialog.resizeVertically(),
this.window.openLinkIn
this.window.openLinkIn,
timerId
);
}

View file

@ -1332,7 +1332,9 @@ add_task(async function test_dom_full_screen() {
await exited;
await BrowserTestUtils.waitForCondition(() => {
return !TelemetryStopwatch.running("FULLSCREEN_CHANGE_MS");
return !gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor(
"DOMFullscreen"
).timerId;
});
if (AppConstants.platform == "macosx") {

View file

@ -3,12 +3,6 @@
"use strict";
// This test tends to trigger a race in the fullscreen time telemetry,
// where the fullscreen enter and fullscreen exit events (which use the
// same histogram ID) overlap. That causes TelemetryStopwatch to log an
// error.
SimpleTest.ignoreAllUncaughtExceptions(true);
add_task(async function test_identityPopupCausesFSExit() {
let url = "https://example.com/";

View file

@ -7,12 +7,6 @@
* (bug 1850993), and that we don't show network statuses in DOM fullscreen
* (bug 1853896). */
// DOM FS tests tends to trigger a race in the fullscreen time telemetry,
// where the fullscreen enter and fullscreen exit events (which use the
// same histogram ID) overlap. That causes TelemetryStopwatch to log an
// error.
SimpleTest.ignoreAllUncaughtExceptions(true);
let statuspanel = document.getElementById("statuspanel");
let statuspanelLabel = document.getElementById("statuspanel-label");

View file

@ -3,11 +3,6 @@
"use strict";
// This test tends to trigger a race in the fullscreen time telemetry,
// where the fullscreen enter and fullscreen exit events (which use the
// same histogram ID) overlap. That causes TelemetryStopwatch to log an
// error.
SimpleTest.ignoreAllUncaughtExceptions(true);
const { PromiseTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/PromiseTestUtils.sys.mjs"
);

View file

@ -8,12 +8,6 @@ async function pause() {
return new Promise(resolve => setTimeout(resolve, 500));
}
// This test tends to trigger a race in the fullscreen time telemetry,
// where the fullscreen enter and fullscreen exit events (which use the
// same histogram ID) overlap. That causes TelemetryStopwatch to log an
// error.
SimpleTest.ignoreAllUncaughtExceptions(true);
const IFRAME_ID = "testIframe";
async function testWindowFocus(isPopup, iframeID) {

View file

@ -3,11 +3,6 @@
"use strict";
// This test tends to trigger a race in the fullscreen time telemetry,
// where the fullscreen enter and fullscreen exit events (which use the
// same histogram ID) overlap. That causes TelemetryStopwatch to log an
// error.
SimpleTest.ignoreAllUncaughtExceptions(true);
SimpleTest.requestLongerTimeout(2);
const IFRAME_ID = "testIframe";

View file

@ -2,12 +2,6 @@
"use strict";
// This test tends to trigger a race in the fullscreen time telemetry,
// where the fullscreen enter and fullscreen exit events (which use the
// same histogram ID) overlap. That causes TelemetryStopwatch to log an
// error.
SimpleTest.ignoreAllUncaughtExceptions(true);
function listenOneEvent(aEvent, aListener) {
function listener(evt) {
removeEventListener(aEvent, listener);

View file

@ -60,9 +60,6 @@ var gTests = [
Assert.equal(accountData.uid, "uid");
Assert.equal(accountData.unwrapBKey, "unwrap_b_key");
Assert.equal(accountData.verified, true);
client.tearDown();
resolve();
};
let client = new FxAccountsWebChannel({
@ -72,6 +69,12 @@ var gTests = [
login,
},
});
client._channel.send = (message, _context) => {
Assert.equal(message.data.ok, true);
client.tearDown();
resolve();
};
});
await BrowserTestUtils.withNewTab(
@ -144,9 +147,6 @@ var gTests = [
let promiseLogout = new Promise(resolve => {
let logout = uid => {
Assert.equal(uid, "uid");
client.tearDown();
resolve();
};
let client = new FxAccountsWebChannel({
@ -156,6 +156,12 @@ var gTests = [
logout,
},
});
client._channel.send = (message, _context) => {
Assert.equal(message.data.ok, true);
client.tearDown();
resolve();
};
});
await BrowserTestUtils.withNewTab(
@ -175,9 +181,6 @@ var gTests = [
let promiseDelete = new Promise(resolve => {
let logout = uid => {
Assert.equal(uid, "uid");
client.tearDown();
resolve();
};
let client = new FxAccountsWebChannel({
@ -187,6 +190,12 @@ var gTests = [
logout,
},
});
client._channel.send = (message, _context) => {
Assert.equal(message.data.ok, true);
client.tearDown();
resolve();
};
});
await BrowserTestUtils.withNewTab(
@ -216,9 +225,6 @@ var gTests = [
"function",
"We can reach the openTab method"
);
client.tearDown();
resolve();
};
let client = new FxAccountsWebChannel({
@ -228,6 +234,12 @@ var gTests = [
openFirefoxView,
},
});
client._channel.send = (message, _context) => {
Assert.equal(message.data.ok, true);
client.tearDown();
resolve();
};
});
await BrowserTestUtils.withNewTab(

View file

@ -881,7 +881,7 @@ let JSWINDOWACTORS = {
matches: ["about:shoppingsidebar"],
remoteTypes: ["privilegedabout"],
messageManagerGroups: ["shopping-sidebar", "browsers"],
enablePreference: "browser.shopping.experience2023.shoppingSidebar",
enablePreference: "browser.shopping.experience2023.enabled",
},
SpeechDispatcher: {

View file

@ -241,6 +241,7 @@ export class AboutLoginsChild extends JSWindowActorChild {
});
}
// eslint-disable-next-line consistent-return
receiveMessage(message) {
switch (message.name) {
case "AboutLogins:ImportReportData":
@ -255,6 +256,21 @@ export class AboutLoginsChild extends JSWindowActorChild {
case "AboutLogins:Setup":
this.#setup(message.data);
break;
case "AboutLogins:WaitForFocus": {
return new Promise(resolve => {
if (!this.document.hasFocus()) {
this.document.ownerGlobal.addEventListener(
"focus",
() => {
resolve();
},
{ once: true }
);
} else {
resolve();
}
});
}
default:
this.#passMessageDataToContent(message);
}

View file

@ -418,6 +418,13 @@ export class AboutLoginsParent extends JSWindowActorParent {
return;
}
if (!this.browsingContext.canOpenModalPicker) {
// Prompting for os auth removed the focus from about:logins.
// Waiting for about:logins window to re-gain the focus, because only
// active browsing contexts are allowed to open the file picker.
await this.sendQuery("AboutLogins:WaitForFocus");
}
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
function fpCallback(aResult) {
if (aResult != Ci.nsIFilePicker.returnCancel) {

View file

@ -1101,12 +1101,16 @@ export class AboutWelcomeShoppingChild extends AboutWelcomeChild {
productHostname = new URL(productUrl).hostname;
}
let content = lazy.isIntegratedSidebar
? this._AWGetOptInSidebarVariantContent(isProductPage, isSupportedSite)
? this._AWGetOptInSidebarVariantContent(
productUrl,
isProductPage,
isSupportedSite
)
: this._AWGetOptInDefaultContent(productHostname);
optInDynamicContent = content;
}
_AWGetOptInSidebarVariantContent(isProductPage, isSupportedSite) {
_AWGetOptInSidebarVariantContent(productUrl, isProductPage, isSupportedSite) {
let content;
if (!isProductPage && !isSupportedSite) {
@ -1117,6 +1121,33 @@ export class AboutWelcomeShoppingChild extends AboutWelcomeChild {
content = JSON.parse(JSON.stringify(OPTIN_SIDEBAR_VARIANT));
}
const [optInScreen] = content.screens;
switch (productUrl) {
case "www.walmart.com":
optInScreen.content.above_button_content[0].text.args = {
firstSite: "Walmart",
secondSite: "Amazon",
thirdSite: "Best Buy",
};
break;
case "www.bestbuy.com":
optInScreen.content.above_button_content[0].text.args = {
firstSite: "Best Buy",
secondSite: "Amazon",
thirdSite: "Walmart",
};
break;
case "www.amazon.com":
// Intentional fall-through
default:
optInScreen.content.above_button_content[0].text.args = {
firstSite: "Amazon",
secondSite: "Walmart",
thirdSite: "Best Buy",
};
}
return content;
}

View file

@ -49,8 +49,6 @@ skip-if = ["os == 'linux' && bits == 64"] # Bug 1757875
["browser_aboutwelcome_multistage_transitions.js"]
["browser_aboutwelcome_multistage_video.js"]
["browser_aboutwelcome_observer.js"]
https_first_disabled = true

View file

@ -1,97 +0,0 @@
"use strict";
const { PermissionTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/PermissionTestUtils.sys.mjs"
);
const videoUrl =
"https://www.mozilla.org/tests/dom/media/webaudio/test/noaudio.webm";
function testAutoplayPermission(browser) {
let principal = browser.contentPrincipal;
is(
PermissionTestUtils.testPermission(principal, "autoplay-media"),
Services.perms.ALLOW_ACTION,
`Autoplay is allowed on ${principal.origin}`
);
}
async function openAWWithVideo({
autoPlay = false,
video_url = videoUrl,
...rest
} = {}) {
const content = [
{
id: "VIDEO_ONBOARDING",
content: {
position: "center",
logo: {},
title: "Video onboarding",
secondary_button: { label: "Skip video", action: { navigate: true } },
video_container: {
video_url,
action: { navigate: true },
autoPlay,
...rest,
},
},
},
];
await setAboutWelcomeMultiStage(JSON.stringify(content));
let { cleanup, browser } = await openMRAboutWelcome();
return {
browser,
content,
async cleanup() {
await SpecialPowers.popPrefEnv();
await cleanup();
},
};
}
add_task(async function test_aboutwelcome_video_autoplay() {
let { cleanup, browser } = await openAWWithVideo({ autoPlay: true });
testAutoplayPermission(browser);
await SpecialPowers.spawn(browser, [videoUrl], async url => {
await ContentTaskUtils.waitForCondition(
() => content.document.querySelector("main.with-video"),
"Waiting for video onboarding screen"
);
let video = content.document.querySelector(`video[src='${url}'][autoplay]`);
await ContentTaskUtils.waitForCondition(
() =>
video.currentTime > 0 &&
!video.paused &&
!video.ended &&
video.readyState > 2,
"Waiting for video to play"
);
ok(!video.error, "Video should not have an error");
});
await cleanup();
});
add_task(async function test_aboutwelcome_video_no_autoplay() {
let { cleanup, browser } = await openAWWithVideo();
testAutoplayPermission(browser);
await SpecialPowers.spawn(browser, [videoUrl], async url => {
let video = await ContentTaskUtils.waitForCondition(
() =>
content.document.querySelector(`video[src='${url}']:not([autoplay])`),
"Waiting for video element to render"
);
await ContentTaskUtils.waitForCondition(
() => video.paused && !video.ended && video.readyState > 2,
"Waiting for video to be playable but not playing"
);
ok(!video.error, "Video should not have an error");
});
await cleanup();
});

View file

@ -537,10 +537,6 @@ const TEST_GLOBAL = {
settings: {},
},
},
TelemetryStopwatch: {
start: () => {},
finish: () => {},
},
Sampling: {
ratioSample(_seed, _ratios) {
return Promise.resolve(0);

View file

@ -466,6 +466,24 @@ messaging_system:
send_in_pings:
- messaging-system
message_request_time:
type: timing_distribution
description: >
Firefox: Time in ms spent selecting and matching messages to user profile.
This metric was generated to correspond to the Legacy Telemetry
exponential histogram MS_MESSAGE_REQUEST_TIME_MS.
time_unit: millisecond
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1600335
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1600335
notification_emails:
- omc@mozilla.com
- dmosedale@mozilla.com
expires: never
telemetry_mirror: MS_MESSAGE_REQUEST_TIME_MS
messaging_system.attribution:
source:

View file

@ -1946,7 +1946,7 @@ export class _ASRouter {
return this.loadMessagesFromAllProviders();
}
async sendPBNewTabMessage({ tabId, hideDefault }) {
async sendPBNewTabMessage({ hideDefault }) {
let message = null;
const PromoInfo = {
FOCUS: { enabledPref: "browser.promo.focus.enabled" },
@ -1980,12 +1980,11 @@ export class _ASRouter {
),
}));
const telemetryObject = { tabId };
TelemetryStopwatch.start("MS_MESSAGE_REQUEST_TIME_MS", telemetryObject);
const timerId = Glean.messagingSystem.messageRequestTime.start();
message = await this.handleMessageRequest({
template: "pb_newtab",
});
TelemetryStopwatch.finish("MS_MESSAGE_REQUEST_TIME_MS", telemetryObject);
Glean.messagingSystem.messageRequestTime.stopAndAccumulate(timerId);
// Format urls if any are defined
["infoLinkUrl"].forEach(key => {
@ -2026,7 +2025,6 @@ export class _ASRouter {
* @param {object} [trigger.context] an object with data about the source of
* the trigger, matched against the message's targeting expression
* @param {MozBrowser} trigger.browser the browser to route messages to
* @param {number} [trigger.tabId] identifier used only for exposure testing
* @param {boolean} [skipLoadingMessages=false] pass true to skip looking for
* new messages. use when calling from loadMessagesFromAllProviders to avoid
* recursion. we call this from loadMessagesFromAllProviders in order to
@ -2035,7 +2033,7 @@ export class _ASRouter {
* @resolves {message} an object with the routed message
*/
async sendTriggerMessage(
{ tabId, browser, ...trigger },
{ browser, ...trigger },
skipLoadingMessages = false
) {
if (!skipLoadingMessages) {
@ -2052,8 +2050,7 @@ export class _ASRouter {
browser === browser.ownerGlobal.gBrowser?.selectedBrowser;
}
}
const telemetryObject = { tabId };
TelemetryStopwatch.start("MS_MESSAGE_REQUEST_TIME_MS", telemetryObject);
const timerId = Glean.messagingSystem.messageRequestTime.start();
// Return all the messages so that it can record the Reach event
const messages =
(await this.handleMessageRequest({
@ -2062,7 +2059,7 @@ export class _ASRouter {
triggerContext: trigger.context,
returnAll: true,
})) || [];
TelemetryStopwatch.finish("MS_MESSAGE_REQUEST_TIME_MS", telemetryObject);
Glean.messagingSystem.messageRequestTime.stopAndAccumulate(timerId);
// Record the Reach event for all the messages with `forReachEvent`,
// only send the first message without forReachEvent to the target

View file

@ -41,7 +41,7 @@ export class ASRouterParentProcessMessageHandler {
}
}
handleMessage(name, data, { id: tabId, browser } = { browser: null }) {
handleMessage(name, data, { browser } = { browser: null }) {
switch (name) {
case msg.AS_ROUTER_TELEMETRY_USER_EVENT:
return this.handleTelemetry({
@ -70,14 +70,12 @@ export class ASRouterParentProcessMessageHandler {
case msg.TRIGGER: {
return this._router.sendTriggerMessage({
...(data && data.trigger),
tabId,
browser,
});
}
case msg.PBNEWTAB_MESSAGE_REQUEST: {
return this._router.sendPBNewTabMessage({
...data,
tabId,
browser,
});
}

View file

@ -1771,7 +1771,7 @@ const MESSAGES = () => {
anchors: [
{
selector:
"#sidebar-main:not([positionend]) > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
"#sidebar-main:not([sidebar-positionend]) > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
panel_position: {
anchor_attachment: "rightcenter",
callout_attachment: "topleft",
@ -1780,7 +1780,7 @@ const MESSAGES = () => {
},
{
selector:
"#sidebar-main[positionend] > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
"#sidebar-main[sidebar-positionend] > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
panel_position: {
anchor_attachment: "leftcenter",
callout_attachment: "topright",
@ -1792,7 +1792,7 @@ const MESSAGES = () => {
position: "callout",
width: "401px",
title: {
string_id: "shopping-opt-in-integrated-headline",
string_id: "shopping-callout-opt-in-integrated-headline",
fontSize: "20px",
letterSpacing: "0",
},
@ -2021,7 +2021,25 @@ const MESSAGES = () => {
anchors: [
{
selector:
"#sidebar-main:not([positionend]) > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
"#sidebar-main:not([sidebar-positionend]) > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar'].tools-overflow",
panel_position: {
anchor_attachment: "topcenter",
callout_attachment: "bottomleft",
},
no_open_on_anchor: true,
},
{
selector:
"#sidebar-main[sidebar-positionend] > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar'].tools-overflow",
panel_position: {
anchor_attachment: "topcenter",
callout_attachment: "bottomright",
},
no_open_on_anchor: true,
},
{
selector:
"#sidebar-main:not([sidebar-positionend]) > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
panel_position: {
anchor_attachment: "rightcenter",
callout_attachment: "bottomleft",
@ -2030,7 +2048,7 @@ const MESSAGES = () => {
},
{
selector:
"#sidebar-main[positionend] > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
"#sidebar-main[sidebar-positionend] > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
panel_position: {
anchor_attachment: "leftcenter",
callout_attachment: "bottomright",
@ -2042,7 +2060,7 @@ const MESSAGES = () => {
position: "callout",
width: "401px",
title: {
string_id: "shopping-opt-in-integrated-headline",
string_id: "shopping-callout-opt-in-integrated-headline",
fontSize: "20px",
letterSpacing: "0",
},
@ -2500,7 +2518,7 @@ const MESSAGES = () => {
anchors: [
{
selector:
"#sidebar-main:not([positionend]) > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
"#sidebar-main:not([sidebar-positionend]) > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
panel_position: {
anchor_attachment: "rightcenter",
callout_attachment: "topleft",
@ -2509,7 +2527,7 @@ const MESSAGES = () => {
},
{
selector:
"#sidebar-main[positionend] > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
"#sidebar-main[sidebar-positionend] > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
panel_position: {
anchor_attachment: "leftcenter",
callout_attachment: "topright",
@ -2521,12 +2539,11 @@ const MESSAGES = () => {
position: "callout",
width: "401px",
title: {
string_id:
"shopping-integrated-callout-disabled-auto-open-title",
string_id: "shopping-integrated-callout-sidebar-closed-title",
},
subtitle: {
string_id:
"shopping-integrated-callout-disabled-auto-open-subtitle",
"shopping-integrated-callout-sidebar-closed-subtitle",
letterSpacing: "0",
},
logo: {
@ -2572,7 +2589,25 @@ const MESSAGES = () => {
anchors: [
{
selector:
"#sidebar-main:not([positionend]) > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
"#sidebar-main:not([sidebar-positionend]) > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar'].tools-overflow",
panel_position: {
anchor_attachment: "topcenter",
callout_attachment: "bottomleft",
},
no_open_on_anchor: true,
},
{
selector:
"#sidebar-main[sidebar-positionend] > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar'].tools-overflow",
panel_position: {
anchor_attachment: "topcenter",
callout_attachment: "bottomright",
},
no_open_on_anchor: true,
},
{
selector:
"#sidebar-main:not([sidebar-positionend]) > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
panel_position: {
anchor_attachment: "rightcenter",
callout_attachment: "bottomleft",
@ -2581,7 +2616,7 @@ const MESSAGES = () => {
},
{
selector:
"#sidebar-main[positionend] > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
"#sidebar-main[sidebar-positionend] > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
panel_position: {
anchor_attachment: "leftcenter",
callout_attachment: "bottomright",
@ -2593,12 +2628,11 @@ const MESSAGES = () => {
position: "callout",
width: "401px",
title: {
string_id:
"shopping-integrated-callout-disabled-auto-open-title",
string_id: "shopping-integrated-callout-sidebar-closed-title",
},
subtitle: {
string_id:
"shopping-integrated-callout-disabled-auto-open-subtitle",
"shopping-integrated-callout-sidebar-closed-subtitle",
letterSpacing: "0",
},
logo: {
@ -2654,12 +2688,11 @@ const MESSAGES = () => {
position: "callout",
width: "401px",
title: {
string_id:
"shopping-integrated-callout-disabled-auto-open-title",
string_id: "shopping-integrated-callout-sidebar-closed-title",
},
subtitle: {
string_id:
"shopping-integrated-callout-no-logo-disabled-auto-open-subtitle",
"shopping-integrated-callout-no-logo-sidebar-closed-subtitle",
letterSpacing: "0",
},
dismiss_button: {
@ -2698,7 +2731,7 @@ const MESSAGES = () => {
anchors: [
{
selector:
"#sidebar-main:not([positionend]) > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
"#sidebar-main:not([sidebar-positionend]) > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
panel_position: {
anchor_attachment: "rightcenter",
callout_attachment: "topleft",
@ -2707,7 +2740,7 @@ const MESSAGES = () => {
},
{
selector:
"#sidebar-main[positionend] > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
"#sidebar-main[sidebar-positionend] > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
panel_position: {
anchor_attachment: "leftcenter",
callout_attachment: "topright",
@ -2770,7 +2803,25 @@ const MESSAGES = () => {
anchors: [
{
selector:
"#sidebar-main:not([positionend]) > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
"#sidebar-main:not([sidebar-positionend]) > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar'].tools-overflow",
panel_position: {
anchor_attachment: "topcenter",
callout_attachment: "bottomleft",
},
no_open_on_anchor: true,
},
{
selector:
"#sidebar-main[sidebar-positionend] > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar'].tools-overflow",
panel_position: {
anchor_attachment: "topcenter",
callout_attachment: "bottomright",
},
no_open_on_anchor: true,
},
{
selector:
"#sidebar-main:not([sidebar-positionend]) > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
panel_position: {
anchor_attachment: "rightcenter",
callout_attachment: "bottomleft",
@ -2779,7 +2830,7 @@ const MESSAGES = () => {
},
{
selector:
"#sidebar-main[positionend] > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
"#sidebar-main[sidebar-positionend] > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
panel_position: {
anchor_attachment: "leftcenter",
callout_attachment: "bottomright",
@ -2842,7 +2893,7 @@ const MESSAGES = () => {
anchors: [
{
selector:
"#sidebar-main:not([positionend]) > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
"#sidebar-main:not([sidebar-positionend]) > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
panel_position: {
anchor_attachment: "rightcenter",
callout_attachment: "topleft",
@ -2851,7 +2902,7 @@ const MESSAGES = () => {
},
{
selector:
"#sidebar-main[positionend] > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
"#sidebar-main[sidebar-positionend] > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
panel_position: {
anchor_attachment: "leftcenter",
callout_attachment: "topright",
@ -2914,7 +2965,25 @@ const MESSAGES = () => {
anchors: [
{
selector:
"#sidebar-main:not([positionend]) > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
"#sidebar-main:not([sidebar-positionend]) > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar'].tools-overflow",
panel_position: {
anchor_attachment: "topcenter",
callout_attachment: "bottomleft",
},
no_open_on_anchor: true,
},
{
selector:
"#sidebar-main[sidebar-positionend] > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar'].tools-overflow",
panel_position: {
anchor_attachment: "topcenter",
callout_attachment: "bottomright",
},
no_open_on_anchor: true,
},
{
selector:
"#sidebar-main:not([sidebar-positionend]) > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
panel_position: {
anchor_attachment: "rightcenter",
callout_attachment: "bottomleft",
@ -2923,7 +2992,7 @@ const MESSAGES = () => {
},
{
selector:
"#sidebar-main[positionend] > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
"#sidebar-main[sidebar-positionend] > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
panel_position: {
anchor_attachment: "leftcenter",
callout_attachment: "bottomright",

View file

@ -107,14 +107,13 @@ export class _MomentsPageHub {
}
async messageRequest({ triggerId, template }) {
const telemetryObject = { triggerId };
TelemetryStopwatch.start("MS_MESSAGE_REQUEST_TIME_MS", telemetryObject);
const timerId = Glean.messagingSystem.messageRequestTime.start();
const messages = await this._handleMessageRequest({
triggerId,
template,
returnAll: true,
});
TelemetryStopwatch.finish("MS_MESSAGE_REQUEST_TIME_MS", telemetryObject);
Glean.messagingSystem.messageRequestTime.stopAndAccumulate(timerId);
// Record the "reach" event for all the messages with `forReachEvent`,
// only execute action for the first message without forReachEvent.

View file

@ -224,13 +224,12 @@ export class _ToolbarBadgeHub {
}
async messageRequest({ triggerId, template }) {
const telemetryObject = { triggerId };
TelemetryStopwatch.start("MS_MESSAGE_REQUEST_TIME_MS", telemetryObject);
const timerId = Glean.messagingSystem.messageRequestTime.start();
const message = await this._handleMessageRequest({
triggerId,
template,
});
TelemetryStopwatch.finish("MS_MESSAGE_REQUEST_TIME_MS", telemetryObject);
Glean.messagingSystem.messageRequestTime.stopAndAccumulate(timerId);
if (message) {
this.registerBadgeNotificationListener(message);
}

View file

@ -262,7 +262,6 @@ add_task(async function test_exposure_ping() {
const exposureSpy = sinon.spy(NimbusTelemetry, "recordExposure");
await ASRouter.sendTriggerMessage({
tabId: 1,
browser: gBrowser.selectedBrowser,
id: "openURL",
param: { host: "messenger.com" },

View file

@ -1652,7 +1652,6 @@ describe("ASRouter", () => {
Router.loadMessagesFromAllProviders.onFirstCall().resolves();
await Router.sendTriggerMessage({
tabId: 0,
browser: gBrowser.selectedBrowser,
id: "firstRun",
});
@ -1668,35 +1667,24 @@ describe("ASRouter", () => {
);
});
it("should record telemetry information", async () => {
const startTelemetryStopwatch = sandbox.stub(
global.TelemetryStopwatch,
"start"
const fakeTimerId = 42;
const start = sandbox
.stub(global.Glean.messagingSystem.messageRequestTime, "start")
.returns(fakeTimerId);
const stopAndAccumulate = sandbox.stub(
global.Glean.messagingSystem.messageRequestTime,
"stopAndAccumulate"
);
const finishTelemetryStopwatch = sandbox.stub(
global.TelemetryStopwatch,
"finish"
);
const tabId = 123;
await Router.sendTriggerMessage({
tabId,
browser: {},
id: "firstRun",
});
assert.calledTwice(startTelemetryStopwatch);
assert.calledWithExactly(
startTelemetryStopwatch,
"MS_MESSAGE_REQUEST_TIME_MS",
{ tabId }
);
assert.calledTwice(finishTelemetryStopwatch);
assert.calledWithExactly(
finishTelemetryStopwatch,
"MS_MESSAGE_REQUEST_TIME_MS",
{ tabId }
);
assert.calledTwice(start);
assert.calledWithExactly(start);
assert.calledTwice(stopAndAccumulate);
assert.calledWithExactly(stopAndAccumulate, fakeTimerId);
});
it("should have previousSessionEnd in the message context", () => {
assert.propertyVal(
@ -1737,7 +1725,6 @@ describe("ASRouter", () => {
sandbox.spy(Glean.messagingExperiments.reachCfr, "record");
await Router.sendTriggerMessage({
tabId: 0,
browser: {},
id: "foo",
});
@ -1760,7 +1747,6 @@ describe("ASRouter", () => {
sandbox.spy(Glean.messagingExperiments.reachCfr, "record");
await Router.sendTriggerMessage({
tabId: 0,
browser: {},
id: "foo",
});
@ -1790,7 +1776,6 @@ describe("ASRouter", () => {
sandbox.stub(Router, "handleMessageRequest").resolves(messages);
await Router.sendTriggerMessage({
tabId: 0,
browser: {},
id: "foo",
});

View file

@ -206,7 +206,6 @@ describe("ASRouterParentProcessMessageHandler", () => {
assert.calledOnce(config.router.sendTriggerMessage);
assert.calledWith(config.router.sendTriggerMessage, {
stuff: {},
tabId: 100,
browser: { ownerGlobal: {} },
});
assert.deepEqual(result, { value: 1 });

View file

@ -46,6 +46,12 @@ describe("MomentsPageHub", () => {
record: () => {},
},
},
messagingSystem: {
messageRequestTime: {
start() {},
stopAndAccumulate() {},
},
},
},
});
});
@ -146,30 +152,22 @@ describe("MomentsPageHub", () => {
assert.notCalled(setStringPrefStub);
});
it("should record telemetry events", async () => {
const startTelemetryStopwatch = sandbox.stub(
global.TelemetryStopwatch,
"start"
);
const finishTelemetryStopwatch = sandbox.stub(
global.TelemetryStopwatch,
"finish"
it("should record a message request time", async () => {
const fakeTimerId = 42;
const start = sandbox
.stub(global.Glean.messagingSystem.messageRequestTime, "start")
.returns(fakeTimerId);
const stopAndAccumulate = sandbox.stub(
global.Glean.messagingSystem.messageRequestTime,
"stopAndAccumulate"
);
await instance.messageRequest({ triggerId: "trigger" });
assert.calledOnce(startTelemetryStopwatch);
assert.calledWithExactly(
startTelemetryStopwatch,
"MS_MESSAGE_REQUEST_TIME_MS",
{ triggerId: "trigger" }
);
assert.calledOnce(finishTelemetryStopwatch);
assert.calledWithExactly(
finishTelemetryStopwatch,
"MS_MESSAGE_REQUEST_TIME_MS",
{ triggerId: "trigger" }
);
assert.calledOnce(start);
assert.calledWithExactly(start);
assert.calledOnce(stopAndAccumulate);
assert.calledWithExactly(stopAndAccumulate, fakeTimerId);
});
it("should record Reach event for the Moments page experiment", async () => {
const momentsMessages = (await PanelTestProvider.getMessages()).filter(

View file

@ -163,31 +163,23 @@ describe("ToolbarBadgeHub", () => {
assert.notCalled(instance.registerBadgeNotificationListener);
});
it("should record telemetry events", async () => {
const startTelemetryStopwatch = sandbox.stub(
global.TelemetryStopwatch,
"start"
);
const finishTelemetryStopwatch = sandbox.stub(
global.TelemetryStopwatch,
"finish"
it("should record a message request time", async () => {
const fakeTimerId = 42;
const start = sandbox
.stub(global.Glean.messagingSystem.messageRequestTime, "start")
.returns(fakeTimerId);
const stopAndAccumulate = sandbox.stub(
global.Glean.messagingSystem.messageRequestTime,
"stopAndAccumulate"
);
handleMessageRequestStub.returns(null);
await instance.messageRequest({ triggerId: "trigger" });
assert.calledOnce(startTelemetryStopwatch);
assert.calledWithExactly(
startTelemetryStopwatch,
"MS_MESSAGE_REQUEST_TIME_MS",
{ triggerId: "trigger" }
);
assert.calledOnce(finishTelemetryStopwatch);
assert.calledWithExactly(
finishTelemetryStopwatch,
"MS_MESSAGE_REQUEST_TIME_MS",
{ triggerId: "trigger" }
);
assert.calledOnce(start);
assert.calledWithExactly(start);
assert.calledOnce(stopAndAccumulate);
assert.calledWithExactly(stopAndAccumulate, fakeTimerId);
});
});
describe("addToolbarNotification", () => {

View file

@ -549,10 +549,6 @@ const TEST_GLOBAL = {
settings: {},
},
},
TelemetryStopwatch: {
start: () => {},
finish: () => {},
},
Sampling: {
ratioSample(_seed, _ratios) {
return Promise.resolve(0);
@ -584,6 +580,12 @@ const TEST_GLOBAL = {
record() {},
},
},
messagingSystem: {
messageRequestTime: {
start() {},
stopAndAccumulate() {},
},
},
newtab: {
opened: {
record() {},

View file

@ -4,6 +4,10 @@
let lazy = {};
ChromeUtils.defineLazyGetter(lazy, "FluentStrings", function () {
return new Localization(["browser/toolbarDropHandler.ftl"], true);
});
ChromeUtils.defineESModuleGetters(lazy, {
HomePage: "resource:///modules/HomePage.sys.mjs",
OpenInTabsUtils:
@ -21,13 +25,19 @@ export var ToolbarDropHandler = {
}
},
_openHomeDialog(aURL, win) {
var promptTitle = win.gNavigatorBundle.getString("droponhometitle");
async _openHomeDialog(aURL, win) {
const [promptTitle, promptMsgSingle, promptMsgMultiple] =
await lazy.FluentStrings.formatValues([
"toolbar-drop-on-home-title",
"toolbar-drop-on-home-msg",
"toolbar-drop-on-home-msg-multiple",
]);
var promptMsg;
if (aURL.includes("|")) {
promptMsg = win.gNavigatorBundle.getString("droponhomemsgMultiple");
promptMsg = promptMsgMultiple;
} else {
promptMsg = win.gNavigatorBundle.getString("droponhomemsg");
promptMsg = promptMsgSingle;
}
var pressedVal = Services.prompt.confirmEx(

View file

@ -1,11 +1,5 @@
"use strict";
// This test tends to trigger a race in the fullscreen time telemetry,
// where the fullscreen enter and fullscreen exit events (which use the
// same histogram ID) overlap. That causes TelemetryStopwatch to log an
// error.
SimpleTest.ignoreAllUncaughtExceptions(true);
const { AppMenuNotifications } = ChromeUtils.importESModule(
"resource://gre/modules/AppMenuNotifications.sys.mjs"
);

View file

@ -142,9 +142,9 @@ class HistoryInView extends ViewPage {
Glean.firefoxviewNext.historyVisits.record();
if (this.controller.searchQuery) {
Services.telemetry
.getKeyedHistogramById("FIREFOX_VIEW_CUMULATIVE_SEARCHES")
.add("history", this.cumulativeSearches);
Glean.firefoxview.cumulativeSearches.history.accumulateSingleSample(
this.cumulativeSearches
);
this.cumulativeSearches = 0;
}
}

View file

@ -407,3 +407,30 @@ firefoxview_next:
Section within which show all is clicked.
type: string
telemetry_mirror: Firefoxview_next_SearchShowAll_Showallbutton
firefoxview:
cumulative_searches:
type: labeled_custom_distribution
description: >
Cumulative no. of searches performed before selecting a resulting tab.
This metric was generated to correspond to the Legacy Telemetry enumerated
histogram FIREFOX_VIEW_CUMULATIVE_SEARCHES.
labels:
- recentbrowsing
- opentabs
- recentlyclosed
- history
- syncedtabs
range_min: 0
range_max: 100
bucket_count: 101
histogram_type: linear
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1869765
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1869765
notification_emails:
- firefoxview@mozilla.com
expires: never
telemetry_mirror: FIREFOX_VIEW_CUMULATIVE_SEARCHES

View file

@ -494,12 +494,9 @@ class OpenTabsInViewCard extends ViewPageContent {
window: this.title || "Window 1 (Current)",
});
if (this.searchQuery) {
Services.telemetry
.getKeyedHistogramById("FIREFOX_VIEW_CUMULATIVE_SEARCHES")
.add(
this.recentBrowsing ? "recentbrowsing" : "opentabs",
this.cumulativeSearches
);
Glean.firefoxview.cumulativeSearches[
this.recentBrowsing ? "recentbrowsing" : "opentabs"
].accumulateSingleSample(this.cumulativeSearches);
this.cumulativeSearches = 0;
}
}

View file

@ -225,12 +225,9 @@ class RecentlyClosedTabsInView extends ViewPage {
page: this.recentBrowsing ? "recentbrowsing" : "recentlyclosed",
});
if (this.searchQuery) {
Services.telemetry
.getKeyedHistogramById("FIREFOX_VIEW_CUMULATIVE_SEARCHES")
.add(
this.recentBrowsing ? "recentbrowsing" : "recentlyclosed",
this.cumulativeSearches
);
Glean.firefoxview.cumulativeSearches[
this.recentBrowsing ? "recentbrowsing" : "recentlyclosed"
].accumulateSingleSample(this.cumulativeSearches);
this.cumulativeSearches = 0;
}
}

View file

@ -168,12 +168,9 @@ class SyncedTabsInView extends ViewPage {
});
if (this.controller.searchQuery) {
Services.telemetry
.getKeyedHistogramById("FIREFOX_VIEW_CUMULATIVE_SEARCHES")
.add(
this.recentBrowsing ? "recentbrowsing" : "syncedtabs",
this.cumulativeSearches
);
Glean.firefoxview.cumulativeSearches[
this.recentBrowsing ? "recentbrowsing" : "syncedtabs"
].accumulateSingleSample(this.cumulativeSearches);
this.cumulativeSearches = 0;
}
}

View file

@ -31,6 +31,11 @@
font-size: var(--font-size-large);
}
.optin-footer-message {
margin-top: var(--space-large);
text-align: center;
}
.optin-heading-icon {
width: 12px;
height: 12px;
@ -39,7 +44,7 @@
}
.optin-progress-bar-wrapper {
margin-top: var(--space-medium);
margin-bottom: var(--space-medium);
}
.optin-progress-bar {

View file

@ -18,6 +18,7 @@ class ModelOptin extends MozLitElement {
messageL10nId: { type: String, fluent: true },
optinButtonL10nId: { type: String, fluent: true },
optoutButtonL10nId: { type: String, fluent: true },
footerMessageL10nId: { type: String, fluent: true },
cancelDownloadButtonL10nId: { type: String, fluent: true },
isLoading: { type: Boolean, reflect: true },
progressStatus: { type: Number }, // Expected to be a number between 0 and 100
@ -28,6 +29,7 @@ class ModelOptin extends MozLitElement {
confirm: "MlModelOptinConfirm",
deny: "MlModelOptinDeny",
cancelDownload: "MlModelOptinCancelDownload",
footerLinkClick: "MlModelOptinFooterLinkClick",
};
static eventBehaviors = {
@ -65,6 +67,14 @@ class ModelOptin extends MozLitElement {
this.progressStatus = undefined;
}
handleFooterLinkClick(e) {
// ftl overrides the html, need to manually watch for event in parent.
if (e.target.id !== "optin-footer-link") {
return;
}
this.dispatch(ModelOptin.events.footerLinkClick);
}
render() {
return html`
<link
@ -72,6 +82,18 @@ class ModelOptin extends MozLitElement {
href="chrome://browser/content/genai/content/model-optin.css"
/>
<section ?hidden=${this.isHidden} class="optin-wrapper">
${this.isLoading
? html`
<div class="optin-progress-bar-wrapper">
<progress
class="optin-progress-bar"
value=${this.progressStatus}
max="100"
></progress>
</div>
`
: ""}
<div class="optin-header-wrapper">
<div class="optin-header">
${this.headingIcon
@ -90,28 +112,20 @@ class ModelOptin extends MozLitElement {
${this.isLoading
? html`
<div>
<div class="optin-actions">
<moz-button
data-l10n-id=${this.cancelDownloadButtonL10nId}
@click=${this.handleCancelDownloadClick}
>
</moz-button>
</div>
<!-- Inlined progress bar -->
<div class="optin-progress-bar-wrapper">
<progress
class="optin-progress-bar"
value=${this.progressStatus}
max="100"
></progress>
</div>
<div class="optin-actions">
<moz-button
size="small"
data-l10n-id=${this.cancelDownloadButtonL10nId}
@click=${this.handleCancelDownloadClick}
>
</moz-button>
</div>
`
: html`
<div class="optin-actions">
<moz-button-group>
<moz-button
size="small"
id="optin-confirm-button"
type="primary"
data-l10n-id=${this.optinButtonL10nId}
@ -119,6 +133,7 @@ class ModelOptin extends MozLitElement {
>
</moz-button>
<moz-button
size="small"
id="optin-deny-button"
data-l10n-id=${this.optoutButtonL10nId}
@click=${this.handleDenyClick}
@ -127,6 +142,21 @@ class ModelOptin extends MozLitElement {
</moz-button-group>
</div>
`}
${!this.isLoading && this.footerMessageL10nId !== ""
? html`
<p
class="optin-footer-message"
data-l10n-id=${this.footerMessageL10nId}
@click=${this.handleFooterLinkClick}
>
<a
id="optin-footer-link"
data-l10n-name="settings"
href="#"
></a>
</p>
`
: ""}
</section>
`;
}

View file

@ -1190,11 +1190,15 @@ var gPrivacyPane = {
this.initDataCollection();
if (AppConstants.MOZ_DATA_REPORTING) {
this.initSubmitHealthReport();
this.updateSubmitHealthReportFromPref();
Preferences.get(PREF_UPLOAD_ENABLED).on(
"change",
gPrivacyPane.updateSubmitHealthReportFromPref
);
setEventListener(
"submitHealthReportBox",
"command",
gPrivacyPane.updateSubmitHealthReport
gPrivacyPane.updateSubmitHealthReportToPref
);
if (AppConstants.MOZ_NORMANDY) {
this.initOptOutStudyCheckbox();
@ -3414,10 +3418,11 @@ var gPrivacyPane = {
},
/**
* Initialize the health report service reference and checkbox.
* Update the health report service checkbox from preference.
*/
initSubmitHealthReport() {
updateSubmitHealthReportFromPref() {
let checkbox = document.getElementById("submitHealthReportBox");
let telemetryContainer = document.getElementById("telemetry-container");
// Telemetry is only sending data if MOZ_TELEMETRY_REPORTING is defined.
// We still want to display the preferences panel if that's not the case, but
@ -3433,12 +3438,13 @@ var gPrivacyPane = {
checkbox.checked =
Services.prefs.getBoolPref(PREF_UPLOAD_ENABLED) &&
AppConstants.MOZ_TELEMETRY_REPORTING;
telemetryContainer.hidden = checkbox.checked;
},
/**
* Update the health report preference with state from checkbox.
*/
updateSubmitHealthReport() {
updateSubmitHealthReportToPref() {
let checkbox = document.getElementById("submitHealthReportBox");
let telemetryContainer = document.getElementById("telemetry-container");

View file

@ -166,12 +166,6 @@
data-l10n-id="search-restore-default"
/>
<spacer flex="1"/>
<button id="removeEngineButton"
is="highlightable-button"
class="searchEngineAction"
data-l10n-id="search-remove-engine"
disabled="true"
/>
<button id="addEngineButton"
is="highlightable-button"
class="searchEngineAction"
@ -185,6 +179,20 @@
add-engine-dialog.buttonlabelaccept,
"
/>
<button id="editEngineButton"
is="highlightable-button"
class="searchEngineAction"
hidden="true"
disabled="true"
label="Edit"
/>
<button id="removeEngineButton"
is="highlightable-button"
class="searchEngineAction"
data-l10n-id="search-remove-engine"
disabled="true"
/>
</hbox>
<hbox id="addEnginesBox" pack="start">
<label id="addEngines" data-l10n-id="search-find-more-link" is="text-link"></label>

View file

@ -31,6 +31,7 @@ Preferences.addAll([
{ id: "browser.urlbar.recentsearches.featureGate", type: "bool" },
{ id: "browser.urlbar.suggest.recentsearches", type: "bool" },
{ id: "browser.urlbar.scotchBonnet.enableOverride", type: "bool" },
{ id: "browser.urlbar.update2.engineAliasRefresh", type: "bool" },
]);
const ENGINE_FLAVOR = "text/x-moz-search-engine";
@ -84,6 +85,11 @@ var gSearchPane = {
"browser.search.suggest.enabled.private"
);
Preferences.get("browser.urlbar.update2.engineAliasRefresh").on(
"change",
() => gEngineView.updateUserEngineButtonVisibility()
);
let updateSuggestionCheckboxes =
this._updateSuggestionCheckboxes.bind(this);
suggestsPref.on("change", updateSuggestionCheckboxes);
@ -809,7 +815,7 @@ class EngineView {
this.loadL10nNames();
this.#addListeners();
this.#showAddEngineButton();
this.updateUserEngineButtonVisibility();
}
async loadL10nNames() {
@ -864,17 +870,15 @@ class EngineView {
}
/**
* Shows the "Add Search Engine" button if the pref is enabled.
* Shows the Add and Edit Search Engines buttons if the pref is enabled.
*/
#showAddEngineButton() {
updateUserEngineButtonVisibility() {
let aliasRefresh = Services.prefs.getBoolPref(
"browser.urlbar.update2.engineAliasRefresh",
false
);
if (aliasRefresh) {
let addButton = document.getElementById("addEngineButton");
addButton.hidden = false;
}
document.getElementById("addEngineButton").hidden = !aliasRefresh;
document.getElementById("editEngineButton").hidden = !aliasRefresh;
}
get lastEngineIndex() {
@ -1024,10 +1028,19 @@ class EngineView {
case "removeEngineButton":
Services.search.removeEngine(this.selectedEngine.originalEngine);
break;
case "editEngineButton":
gSubDialog.open(
"chrome://browser/content/search/addEngine.xhtml",
{ features: "resizable=no, modal=yes" },
{ engine: this.selectedEngine.originalEngine, mode: "EDIT" }
);
break;
case "addEngineButton":
gSubDialog.open("chrome://browser/content/search/addEngine.xhtml", {
features: "resizable=no, modal=yes",
});
gSubDialog.open(
"chrome://browser/content/search/addEngine.xhtml",
{ features: "resizable=no, modal=yes" },
{ mode: "NEW" }
);
break;
}
break;
@ -1078,6 +1091,8 @@ class EngineView {
#onTreeSelect() {
document.getElementById("removeEngineButton").disabled =
!this.isEngineSelectedAndRemovable();
document.getElementById("editEngineButton").disabled =
!this.selectedEngine?.isUserEngine;
}
#onTreeKeyPress(aEvent) {
@ -1328,19 +1343,14 @@ class EngineView {
}
}
async setCellText(index, column, value) {
let engine = this._engineStore.engines[index];
if (column.id == "engineKeyword") {
let valid = await this.#changeKeyword(
this._engineStore.engines[index],
value
);
let valid = await this.#changeKeyword(engine, value);
if (!valid) {
this.#startEditingAlias(index);
}
} else if (column.id == "engineName") {
let valid = await this.#changeName(
this._engineStore.engines[index],
value
);
} else if (column.id == "engineName" && engine.isUserEngine) {
let valid = await this.#changeName(engine, value);
if (!valid) {
this.#startEditingName(index);
}

View file

@ -90,9 +90,6 @@ run-if = ["os == 'win'"]
["browser_dns_over_https_exceptions_subdialog.js"]
["browser_engines.js"]
fail-if = ["a11y_checks"] # Bug 1854636 clicked treechildren#engineChildren may not be focusable
["browser_ensure_prefs_bindings_initted.js"]
["browser_etp_exceptions_dialog.js"]
@ -199,6 +196,8 @@ skip-if = ["true"]
["browser_privacy_syncDataClearing_v2.js"]
["browser_privacy_uploadEnabled.js"]
["browser_privacypane_2.js"]
["browser_privacypane_3.js"]
@ -220,6 +219,9 @@ support-files = ["!/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js
["browser_searchShowSuggestionsFirst.js"]
["browser_search_engineList.js"]
fail-if = ["a11y_checks"] # Bug 1854636 clicked treechildren#engineChildren may not be focusable
["browser_search_firefoxSuggest.js"]
["browser_search_no_results_change_category.js"]
@ -247,8 +249,12 @@ skip-if = ["tsan"] # Bug 1678829
["browser_search_subdialogs_within_preferences_8.js"]
["browser_search_subdialogs_within_preferences_9.js"]
["browser_search_subdialogs_within_preferences_site_data.js"]
["browser_search_userEngineDialog.js"]
["browser_search_within_preferences_1.js"]
["browser_search_within_preferences_2.js"]

View file

@ -1,141 +0,0 @@
const { SearchTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/SearchTestUtils.sys.mjs"
);
SearchTestUtils.init(this);
function getCellText(tree, i, cellName) {
return tree.view.getCellText(i, tree.columns.getNamedColumn(cellName));
}
add_setup(async function () {
await SearchTestUtils.installSearchExtension({
keyword: ["testing", "customkeyword"],
search_url: "https://example.com/engine1",
search_url_get_params: "search={searchTerms}",
});
});
add_task(async function test_engine_list() {
let prefs = await openPreferencesViaOpenPreferencesAPI("search", {
leaveOpen: true,
});
is(prefs.selectedPane, "paneSearch", "Search pane is selected by default");
let doc = gBrowser.contentDocument;
let tree = doc.querySelector("#engineList");
ok(
!tree.hidden,
"The search engine list should be visible when Search is requested"
);
// Check for default search engines to be displayed in the engineList
let defaultEngines = await Services.search.getAppProvidedEngines();
for (let i = 0; i < defaultEngines.length; i++) {
let engine = defaultEngines[i];
is(
getCellText(tree, i, "engineName"),
engine.name,
"Default search engine " + engine.name + " displayed correctly"
);
}
let customEngineIndex = defaultEngines.length;
is(
getCellText(tree, customEngineIndex, "engineKeyword"),
"testing, customkeyword",
"Show internal aliases"
);
// Scroll the treeview into view since mouse operations
// off screen can act confusingly.
tree.scrollIntoView();
let rect = tree.getCoordsForCellItem(
customEngineIndex,
tree.columns.getNamedColumn("engineKeyword"),
"text"
);
let x = rect.x + rect.width / 2;
let y = rect.y + rect.height / 2;
let win = tree.ownerGlobal;
let promise = BrowserTestUtils.waitForEvent(tree, "dblclick");
EventUtils.synthesizeMouse(tree.body, x, y, { clickCount: 1 }, win);
EventUtils.synthesizeMouse(tree.body, x, y, { clickCount: 2 }, win);
await promise;
EventUtils.sendString("newkeyword");
EventUtils.sendKey("RETURN");
await TestUtils.waitForCondition(() => {
return (
getCellText(tree, customEngineIndex, "engineKeyword") ===
"newkeyword, testing, customkeyword"
);
});
// Avoid duplicated keywords
tree.view.setCellText(
0,
tree.columns.getNamedColumn("engineKeyword"),
"keyword"
);
tree.view.setCellText(
1,
tree.columns.getNamedColumn("engineKeyword"),
"keyword"
);
isnot(
getCellText(tree, 1, "engineKeyword"),
"keyword",
"Do not allow duplicated keywords"
);
BrowserTestUtils.removeTab(gBrowser.selectedTab);
});
add_task(async function test_remove_button_disabled_state() {
let prefs = await openPreferencesViaOpenPreferencesAPI("search", {
leaveOpen: true,
});
is(prefs.selectedPane, "paneSearch", "Search pane is selected by default");
let doc = gBrowser.contentDocument;
let tree = doc.querySelector("#engineList");
ok(
!tree.hidden,
"The search engine list should be visible when Search is requested"
);
let defaultEngines = await Services.search.getAppProvidedEngines();
for (let i = 0; i < defaultEngines.length; i++) {
let engine = defaultEngines[i];
let isDefaultSearchEngine =
engine.name == Services.search.defaultEngine.name ||
engine.name == Services.search.defaultPrivateEngine.name;
tree.scrollIntoView();
let rect = tree.getCoordsForCellItem(
i,
tree.columns.getNamedColumn("engineName"),
"text"
);
let x = rect.x + rect.width / 2;
let y = rect.y + rect.height / 2;
let win = tree.ownerGlobal;
let promise = BrowserTestUtils.waitForEvent(tree, "click");
EventUtils.synthesizeMouse(tree.body, x, y, { clickCount: 1 }, win);
await promise;
let removeButton = doc.querySelector("#removeEngineButton");
is(
removeButton.disabled,
isDefaultSearchEngine,
"Remove button is in correct disable state"
);
}
BrowserTestUtils.removeTab(gBrowser.selectedTab);
});

View file

@ -0,0 +1,41 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests the submitHealthReportBox checkbox is automatically updated when the
// corresponding datareporting.healthreport.uploadEnabled pref is changed.
const PREF_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
add_task(async function test_updatePageFromPref() {
if (!AppConstants.MOZ_TELEMETRY_REPORTING) {
ok(true, "Skipping test because telemetry reporting is disabled");
return;
}
await SpecialPowers.pushPrefEnv({
set: [[PREF_UPLOAD_ENABLED, false]],
});
await openPreferencesViaOpenPreferencesAPI("panePrivacy", {
leaveOpen: true,
});
const doc = gBrowser.selectedBrowser.contentDocument;
const checkbox = doc.getElementById("submitHealthReportBox");
Assert.ok(!checkbox.checked, "checkbox should match pref state on page load");
await SpecialPowers.pushPrefEnv({
set: [[PREF_UPLOAD_ENABLED, true]],
});
let checkboxUpdated = BrowserTestUtils.waitForMutationCondition(
checkbox,
{ attributeFilter: ["checked"] },
() => checkbox.checked
);
await checkboxUpdated;
Assert.ok(checkbox.checked, "pref change should trigger checkbox update");
BrowserTestUtils.removeTab(gBrowser.selectedTab);
});

View file

@ -0,0 +1,228 @@
const { SearchTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/SearchTestUtils.sys.mjs"
);
const { SearchUtils } = ChromeUtils.importESModule(
"resource://gre/modules/SearchUtils.sys.mjs"
);
const { sinon } = ChromeUtils.importESModule(
"resource://testing-common/Sinon.sys.mjs"
);
SearchTestUtils.init(this);
let userEngine;
let extensionEngine;
let installedEngines;
function getCellText(tree, i, cellName) {
return tree.view.getCellText(i, tree.columns.getNamedColumn(cellName));
}
async function engine_list_test(fn) {
let task = async () => {
let prefs = await openPreferencesViaOpenPreferencesAPI("search", {
leaveOpen: true,
});
Assert.equal(
prefs.selectedPane,
"paneSearch",
"Search pane is selected by default"
);
let doc = gBrowser.contentDocument;
let tree = doc.querySelector("#engineList");
Assert.ok(
!tree.hidden,
"The search engine list should be visible when Search is requested"
);
// Scroll the treeview into view since mouse operations
// off screen can act confusingly.
tree.scrollIntoView();
await fn(tree, doc);
BrowserTestUtils.removeTab(gBrowser.selectedTab);
};
// Make sure the name of the passed function is used in logs.
Object.defineProperty(task, "name", { value: fn.name });
add_task(task);
}
add_setup(async function () {
installedEngines = await Services.search.getAppProvidedEngines();
await SearchTestUtils.installSearchExtension({
keyword: ["testing", "customkeyword"],
search_url: "https://example.com/engine1",
search_url_get_params: "search={searchTerms}",
name: "Extension Engine",
});
extensionEngine = Services.search.getEngineByName("Extension Engine");
installedEngines.push(extensionEngine);
userEngine = await Services.search.addUserEngine({
name: "User Engine",
url: "https://example.com/user?q={searchTerms}&b=ff",
alias: "u",
});
installedEngines.push(userEngine);
registerCleanupFunction(async () => {
await Services.search.removeEngine(userEngine);
// Extension engine is cleaned up by SearchTestUtils.
});
});
engine_list_test(async function test_engine_list(tree) {
let userEngineIndex = installedEngines.length - 1;
for (let i = 0; i < installedEngines.length; i++) {
let engine = installedEngines[i];
Assert.equal(
getCellText(tree, i, "engineName"),
engine.name,
"Search engine " + engine.name + " displayed correctly."
);
Assert.equal(
tree.view.isEditable(i, tree.columns.getNamedColumn("engineName")),
i == userEngineIndex,
"Only user engine name is editable."
);
}
});
engine_list_test(async function test_change_keyword(tree) {
let extensionEngineIndex = installedEngines.length - 2;
Assert.equal(
getCellText(tree, extensionEngineIndex, "engineKeyword"),
"testing, customkeyword",
"Internal keywords are displayed."
);
let rect = tree.getCoordsForCellItem(
extensionEngineIndex,
tree.columns.getNamedColumn("engineKeyword"),
"text"
);
// Test editing keyword of extension engine because it
// has user-defined and extension-provided keywords.
let x = rect.x + rect.width / 2;
let y = rect.y + rect.height / 2;
let win = tree.ownerGlobal;
let promise = BrowserTestUtils.waitForEvent(tree, "dblclick");
EventUtils.synthesizeMouse(tree.body, x, y, { clickCount: 1 }, win);
EventUtils.synthesizeMouse(tree.body, x, y, { clickCount: 2 }, win);
await promise;
promise = SearchTestUtils.promiseSearchNotification(
SearchUtils.MODIFIED_TYPE.CHANGED,
SearchUtils.TOPIC_ENGINE_MODIFIED
);
EventUtils.sendString("newkeyword");
EventUtils.sendKey("RETURN");
await promise;
Assert.equal(
getCellText(tree, extensionEngineIndex, "engineKeyword"),
"newkeyword, testing, customkeyword",
"User-defined keyword was added."
);
// Test duplicated keywords (different capitalization).
tree.view.setCellText(
0,
tree.columns.getNamedColumn("engineKeyword"),
"keyword"
);
await TestUtils.waitForTick();
let keywordBefore = getCellText(tree, 1, "engineKeyword");
let alertSpy = sinon.spy(win, "alert");
tree.view.setCellText(
1,
tree.columns.getNamedColumn("engineKeyword"),
"Keyword"
);
await TestUtils.waitForTick();
Assert.ok(alertSpy.calledOnce, "Warning was shown.");
Assert.equal(
getCellText(tree, 1, "engineKeyword"),
keywordBefore,
"Did not modify keywords."
);
});
engine_list_test(async function test_rename_engines(tree) {
// Test editing name of user search engine because
// only the names of user engines can be edited.
let userEngineIndex = installedEngines.length - 1;
let rect = tree.getCoordsForCellItem(
userEngineIndex,
tree.columns.getNamedColumn("engineName"),
"text"
);
let x = rect.x + rect.width / 2;
let y = rect.y + rect.height / 2;
let win = tree.ownerGlobal;
let promise = BrowserTestUtils.waitForEvent(tree, "dblclick");
EventUtils.synthesizeMouse(tree.body, x, y, { clickCount: 1 }, win);
EventUtils.synthesizeMouse(tree.body, x, y, { clickCount: 2 }, win);
await promise;
EventUtils.sendString("User Engine 2");
promise = SearchTestUtils.promiseSearchNotification(
SearchUtils.MODIFIED_TYPE.CHANGED,
SearchUtils.TOPIC_ENGINE_MODIFIED
);
EventUtils.sendKey("RETURN");
await promise;
Assert.equal(userEngine.name, "User Engine 2", "Engine was renamed.");
// Avoid duplicated engine names.
let alertSpy = sinon.spy(win, "alert");
tree.view.setCellText(
userEngineIndex,
tree.columns.getNamedColumn("engineName"),
"Extension Engine"
);
await TestUtils.waitForTick();
Assert.ok(alertSpy.calledOnce, "Warning was shown.");
Assert.equal(
getCellText(tree, userEngineIndex, "engineName"),
"User Engine 2",
"Name was not modified."
);
alertSpy.restore();
});
engine_list_test(async function test_remove_button_disabled_state(tree, doc) {
let appProvidedEngines = await Services.search.getAppProvidedEngines();
let win = tree.ownerGlobal;
for (let i = 0; i < appProvidedEngines.length; i++) {
let engine = appProvidedEngines[i];
let isDefaultSearchEngine =
engine.id == Services.search.defaultEngine.id ||
engine.id == Services.search.defaultPrivateEngine.id;
let rect = tree.getCoordsForCellItem(
i,
tree.columns.getNamedColumn("engineName"),
"text"
);
let x = rect.x + rect.width / 2;
let y = rect.y + rect.height / 2;
let promise = BrowserTestUtils.waitForEvent(tree, "click");
EventUtils.synthesizeMouse(tree.body, x, y, { clickCount: 1 }, win);
await promise;
Assert.equal(
doc.querySelector("#removeEngineButton").disabled,
isDefaultSearchEngine,
"Remove button is in correct disabled state."
);
}
});

View file

@ -0,0 +1,25 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/*
* This file contains tests for the "Add Engine" subdialog.
*/
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [["browser.urlbar.update2.engineAliasRefresh", true]],
});
});
/**
* Test for searching for the "Add Engine" subdialog.
*/
add_task(async function searchAddEngine() {
await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
leaveOpen: true,
});
await evaluateSearchResults("Add Engine", "oneClickSearchProvidersGroup");
BrowserTestUtils.removeTab(gBrowser.selectedTab);
});

View file

@ -0,0 +1,424 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { SearchTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/SearchTestUtils.sys.mjs"
);
const { SearchUtils } = ChromeUtils.importESModule(
"resource://gre/modules/SearchUtils.sys.mjs"
);
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [["browser.urlbar.update2.engineAliasRefresh", true]],
});
});
add_task(async function test_addEngine() {
await openPreferencesViaOpenPreferencesAPI("search", {
leaveOpen: true,
});
// Add new engine via add engine dialog.
let doc = gBrowser.contentDocument;
let addButton = doc.querySelector("#addEngineButton");
let dialogWin = await openDialogWith(doc, () => addButton.click());
Assert.equal(
dialogWin.document.getElementById("titleContainer").style.display,
"none",
"Adjustable title is hidden in add engine dialog."
);
setName("Bugzilla", dialogWin);
setUrl(
"https://bugzilla.mozilla.org/buglist.cgi?quicksearch=%s&list_id=17442621",
dialogWin
);
await setAlias("bz", dialogWin);
let promiseAdded = SearchTestUtils.promiseSearchNotification(
SearchUtils.MODIFIED_TYPE.ADDED,
SearchUtils.TOPIC_ENGINE_MODIFIED
);
EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin);
await promiseAdded;
Assert.ok(true, "Got added notification.");
// Check new engine.
let engine = Services.search.getEngineByName("Bugzilla");
Assert.equal(engine.name, "Bugzilla", "Name is correct.");
Assert.equal(
engine.getSubmission("föö").uri.spec,
"https://bugzilla.mozilla.org/buglist.cgi?quicksearch=f%C3%B6%C3%B6&list_id=17442621",
"URL is correct and encodes search terms using utf-8."
);
Assert.equal(engine.alias, "bz", "Alias is correct.");
// Clean up.
BrowserTestUtils.removeTab(gBrowser.selectedTab);
await Services.search.removeEngine(engine);
});
add_task(async function test_validation() {
await openPreferencesViaOpenPreferencesAPI("search", {
leaveOpen: true,
});
let existingEngine = await Services.search.addUserEngine({
name: "user",
url: "https://example.com/user?q={searchTerms}&b=ff",
alias: "u",
});
let doc = gBrowser.contentDocument;
let addButton = doc.querySelector("#addEngineButton");
let dialogWin = await openDialogWith(doc, () => addButton.click());
let button = dialogWin.document
.querySelector("dialog")
.shadowRoot.querySelector('button[dlgtype="accept"]');
let name = dialogWin.document.getElementById("engineName");
let url = dialogWin.document.getElementById("engineUrl");
let alias = dialogWin.document.getElementById("engineAlias");
Assert.ok(
name.value == "" && url.value == "" && alias.value == "",
"Everything is empty initially."
);
Assert.ok(button.disabled, "Button is disabled initially.");
setName("Example", dialogWin);
setUrl("https://example.com/search?q=%s", dialogWin);
await setAlias("abc", dialogWin);
Assert.ok(!button.disabled, "Button is enabled when everything is there.");
// Check URL
setUrl("", dialogWin);
Assert.ok(button.disabled, "URL is required.");
setUrl("javascript://%s", dialogWin);
Assert.ok(button.disabled, "Javascript URLs are not allowed.");
setUrl("https://example.com/search?q=%s", dialogWin);
Assert.ok(!button.disabled, "Good URL enables the button.");
// Check name
setName("", dialogWin);
Assert.ok(button.disabled, "Name is required.");
setName(existingEngine.name, dialogWin);
Assert.ok(button.disabled, "Existing name is not allowed.");
setName("Example", dialogWin);
Assert.ok(!button.disabled, "Good name enables the button.");
// Check alias
await setAlias("", dialogWin);
Assert.ok(!button.disabled, "Alias is not required.");
await setAlias(existingEngine.alias, dialogWin);
Assert.ok(button.disabled, "Existing alias is not allowed.");
await setAlias("abc", dialogWin);
Assert.ok(!button.disabled, "Good alias enables the button.");
// Clean up.
BrowserTestUtils.removeTab(gBrowser.selectedTab);
await Services.search.removeEngine(existingEngine);
});
add_task(async function test_editEngine() {
await openPreferencesViaOpenPreferencesAPI("search", {
leaveOpen: true,
});
let doc = gBrowser.contentDocument;
let tree = doc.querySelector("#engineList");
let view = tree.view.wrappedJSObject;
let engine = await Services.search.addUserEngine({
name: "user",
url: "https://example.com/user?q={searchTerms}&b=ff",
alias: "u",
});
// Check buttons of all search engines + local shortcuts.
let removeButton = doc.querySelector("#removeEngineButton");
let editButton = doc.querySelector("#editEngineButton");
let userEngineIndex = null;
for (let i = 0; i < tree.view.rowCount; i++) {
view.selection.select(i);
let selectedEngine = view.selectedEngine;
if (selectedEngine?.isUserEngine) {
Assert.equal(selectedEngine.name, "user", "Is the new engine.");
Assert.ok(!removeButton.disabled, "Remove button is enabled.");
Assert.ok(!editButton.disabled, "Edit button is enabled.");
userEngineIndex = i;
} else {
Assert.ok(editButton.disabled, "Edit button is disabled.");
}
}
// Check if table contains new engine without reloading.
Assert.ok(!!userEngineIndex, "User engine is in the table.");
view.selection.select(userEngineIndex);
// Open the dialog.
let dialogWin = await openDialogWith(doc, () => editButton.click());
Assert.equal(
dialogWin.document.getElementById("titleContainer").style.display,
"none",
"Adjustable title is hidden in edit engine dialog."
);
Assert.equal(
dialogWin.document.getElementById("engineName").value,
"user",
"Name in dialog is correct."
);
Assert.equal(
dialogWin.document.getElementById("engineUrl").value,
"https://example.com/user?q=%s&b=ff",
"URL in dialog is correct."
);
Assert.ok(
dialogWin.document.getElementById("enginePostDataRow").hidden,
"Post data input is hidden."
);
Assert.equal(
dialogWin.document.getElementById("suggestUrl").value,
"",
"Suggest URL in dialog is empty."
);
Assert.equal(
dialogWin.document.getElementById("engineAlias").value,
"u",
"Alias in dialog is correct."
);
// Set new values.
setName("Searchfox", dialogWin);
setUrl("https://searchfox.org/mozilla-central/search?q=%s", dialogWin);
await setAlias("sf", dialogWin);
// Save changes to engine.
let promiseChanged = SearchTestUtils.promiseSearchNotification(
SearchUtils.MODIFIED_TYPE.CHANGED,
SearchUtils.TOPIC_ENGINE_MODIFIED,
3
);
EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin);
await promiseChanged;
Assert.ok(true, "Got 3 change notifications.");
// Open dialog again and check values.
dialogWin = await openDialogWith(doc, () => editButton.click());
Assert.equal(
dialogWin.document.getElementById("engineName").value,
"Searchfox",
"Name in dialog reflects change."
);
Assert.equal(
dialogWin.document.getElementById("engineUrl").value,
"https://searchfox.org/mozilla-central/search?q=%s",
"URL in dialog reflects change."
);
Assert.equal(
dialogWin.document.getElementById("engineAlias").value,
"sf",
"Alias in dialog reflects change."
);
// Clean up.
BrowserTestUtils.removeTab(gBrowser.selectedTab);
await Services.search.removeEngine(engine);
});
add_task(async function test_editPostEngine() {
await openPreferencesViaOpenPreferencesAPI("search", {
leaveOpen: true,
});
let doc = gBrowser.contentDocument;
let tree = doc.querySelector("#engineList");
let view = tree.view.wrappedJSObject;
let formData = new FormData();
formData.append("q", "{searchTerms}");
let engine = await Services.search.addUserEngine({
name: "user post",
url: "https://example.com/user",
formData,
method: "POST",
alias: "u",
});
let editButton = doc.querySelector("#editEngineButton");
for (let i = 0; i < tree.view.rowCount; i++) {
view.selection.select(i);
let selectedEngine = view.selectedEngine;
if (selectedEngine?.isUserEngine) {
view.selection.select(i);
break;
}
}
// Open the dialog.
let dialogWin = await openDialogWith(doc, () => editButton.click());
Assert.equal(
dialogWin.document.getElementById("engineName").value,
"user post",
"Name in dialog is correct."
);
Assert.equal(
dialogWin.document.getElementById("engineUrl").value,
"https://example.com/user",
"URL in dialog is correct."
);
Assert.ok(
!dialogWin.document.getElementById("enginePostDataRow").hidden,
"Post data input is visible."
);
Assert.equal(
dialogWin.document.getElementById("enginePostData").value,
"q=%s",
"Post data in dialog is correct."
);
Assert.equal(
dialogWin.document.getElementById("suggestUrl").value,
"",
"Suggest URL in dialog is empty."
);
Assert.equal(
dialogWin.document.getElementById("engineAlias").value,
"u",
"Alias in dialog is correct."
);
// Set new values.
setName("Searchfox", dialogWin);
setUrl("https://searchfox.org/mozilla-central/search", dialogWin);
setPostData("q=%s&path=&case=false&regexp=false", dialogWin);
setSuggestUrl("https://searchfox.org/suggest/%s", dialogWin);
await setAlias("sf", dialogWin);
// Save changes to engine.
let promiseChanged = SearchTestUtils.promiseSearchNotification(
SearchUtils.MODIFIED_TYPE.CHANGED,
SearchUtils.TOPIC_ENGINE_MODIFIED,
3
);
EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin);
await promiseChanged;
Assert.ok(true, "Got 3 change notifications.");
// Open dialog again and check values.
dialogWin = await openDialogWith(doc, () => editButton.click());
Assert.equal(
dialogWin.document.getElementById("engineName").value,
"Searchfox",
"Name in dialog reflects change."
);
Assert.equal(
dialogWin.document.getElementById("engineUrl").value,
"https://searchfox.org/mozilla-central/search",
"URL in dialog reflects changes."
);
Assert.equal(
dialogWin.document.getElementById("enginePostData").value,
"q=%s&path=&case=false&regexp=false",
"Post data in dialog reflects changes."
);
Assert.equal(
dialogWin.document.getElementById("suggestUrl").value,
"https://searchfox.org/suggest/%s",
"Suggest URL in dialog reflects changes."
);
Assert.equal(
dialogWin.document.getElementById("engineAlias").value,
"sf",
"Alias in dialog reflects changes."
);
// Check values of search engine object.
Assert.equal(
engine.name,
"Searchfox",
"Name of search engine object was updated."
);
let submission = engine.getSubmission("foo");
Assert.equal(
submission.uri.spec,
"https://searchfox.org/mozilla-central/search",
"Submission URL reflects changes."
);
Assert.equal(
decodePostData(submission.postData),
"q=foo&path=&case=false&regexp=false",
"Submission post data reflects changes"
);
submission = engine.getSubmission("foo", SearchUtils.URL_TYPE.SUGGEST_JSON);
Assert.equal(
submission.uri.spec,
"https://searchfox.org/suggest/foo",
"Submission URL reflects changes."
);
Assert.equal(submission.postData, null, "Submission URL is still GET.");
Assert.equal(
engine.alias,
"sf",
"Alias of search engine object was updated."
);
// Clean up.
BrowserTestUtils.removeTab(gBrowser.selectedTab);
await Services.search.removeEngine(engine);
});
async function openDialogWith(doc, fn) {
let dialogLoaded = TestUtils.topicObserved("subdialog-loaded");
await fn();
let [dialogWin] = await dialogLoaded;
await doc.ownerGlobal.gSubDialog.dialogs[0]._dialogReady;
Assert.ok(true, "Engine dialog opened.");
return dialogWin;
}
function setName(value, win) {
fillTextField("engineName", value, win);
}
function setUrl(value, win) {
fillTextField("engineUrl", value, win);
}
function setPostData(value, win) {
fillTextField("enginePostData", value, win);
}
function setSuggestUrl(value, win) {
fillTextField("suggestUrl", value, win);
}
async function setAlias(value, win) {
fillTextField("engineAlias", value, win);
await TestUtils.waitForTick();
}
function fillTextField(id, text, win) {
let elt = win.document.getElementById(id);
elt.focus();
elt.select();
EventUtils.synthesizeKey("a", { metaKey: true }, win);
EventUtils.synthesizeKey("KEY_Backspace", {}, win);
for (let c of text.split("")) {
EventUtils.synthesizeKey(c, {}, win);
}
}
function decodePostData(postData) {
let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
Ci.nsIBinaryInputStream
);
binaryStream.setInputStream(postData.data);
return binaryStream
.readBytes(binaryStream.available())
.replace("searchTerms", "%s");
}

View file

@ -24,7 +24,7 @@ const PROFILE_THEMES_MAP = new Map([
[
"{b90acfd0-f0fc-4add-9195-f6306d25cdfa}",
{
dataL10nId: "profiles-marigold-theme",
dataL10nId: "profiles-marigold-theme-2",
downloadURL:
"https://addons.mozilla.org/firefox/downloads/file/4381985/marigold-1.9.xpi",
colors: {
@ -38,7 +38,7 @@ const PROFILE_THEMES_MAP = new Map([
[
"{388d9fae-8a28-4f9f-9aad-fb9e84e4f3c3}",
{
dataL10nId: "profiles-lichen-theme",
dataL10nId: "profiles-lichen-theme-2",
downloadURL:
"https://addons.mozilla.org/firefox/downloads/file/4381979/lichen_soft-1.3.xpi",
colors: {
@ -52,7 +52,7 @@ const PROFILE_THEMES_MAP = new Map([
[
"{3ac3b0d7-f017-40e1-b142-a26f794e7015}",
{
dataL10nId: "profiles-magnolia-theme",
dataL10nId: "profiles-magnolia-theme-2",
downloadURL:
"https://addons.mozilla.org/firefox/downloads/file/4381978/magnolia-1.1.xpi",
colors: {
@ -66,7 +66,7 @@ const PROFILE_THEMES_MAP = new Map([
[
"{ba48d251-0732-45c2-9f2f-39c68e82d047}",
{
dataL10nId: "profiles-lavender-theme",
dataL10nId: "profiles-lavender-theme-2",
downloadURL:
"https://addons.mozilla.org/firefox/downloads/file/4381983/lavender_soft-1.2.xpi",
colors: {
@ -93,7 +93,7 @@ const PROFILE_THEMES_MAP = new Map([
[
"{750fa518-b61f-4068-9974-330dcf45442f}",
{
dataL10nId: "profiles-ocean-theme",
dataL10nId: "profiles-ocean-theme-2",
downloadURL:
"https://addons.mozilla.org/firefox/downloads/file/4381977/ocean_dark-1.1.xpi",
colors: {
@ -107,7 +107,7 @@ const PROFILE_THEMES_MAP = new Map([
[
"{25b5a343-4238-4bae-b1f9-93a33f258167}",
{
dataL10nId: "profiles-terracotta-theme",
dataL10nId: "profiles-terracotta-theme-2",
downloadURL:
"https://addons.mozilla.org/firefox/downloads/file/4381976/terracotta_dark-1.1.xpi",
colors: {
@ -121,7 +121,7 @@ const PROFILE_THEMES_MAP = new Map([
[
"{f9261f02-c03c-4352-92ee-78dd8b41ca98}",
{
dataL10nId: "profiles-moss-theme",
dataL10nId: "profiles-moss-theme-2",
downloadURL:
"https://addons.mozilla.org/firefox/downloads/file/4381975/moss_dark-1.1.xpi",
colors: {

View file

@ -113,6 +113,7 @@ class SelectableProfileServiceClass extends EventEmitter {
"browser.crashReports.unsubmittedCheck.autoSubmit2",
"browser.discovery.enabled",
"browser.urlbar.quicksuggest.dataCollection.enabled",
"datareporting.healthreport.uploadEnabled",
"datareporting.policy.currentPolicyVersion",
"datareporting.policy.dataSubmissionEnabled",
"datareporting.policy.dataSubmissionPolicyAcceptedVersion",

View file

@ -140,7 +140,6 @@ export class EditProfileCard extends MozLitElement {
this.themes = themes;
this.setFavicon();
this.focusInput();
}
async getUpdateComplete() {
@ -155,13 +154,6 @@ export class EditProfileCard extends MozLitElement {
return result;
}
async focusInput() {
await this.getUpdateComplete();
this.nameInput.focus();
this.nameInput.value = "";
this.nameInput.value = this.profile.name;
}
setFavicon() {
let favicon = document.getElementById("favicon");
favicon.href = `chrome://browser/content/profiles/assets/16_${this.profile.avatar}.svg`;
@ -295,7 +287,10 @@ export class EditProfileCard extends MozLitElement {
}
headerTemplate() {
return html`<h1 data-l10n-id="edit-profile-page-header"></h1>`;
return html`<h1
id="profile-header"
data-l10n-id="edit-profile-page-header"
></h1>`;
}
nameInputTemplate() {
@ -449,7 +444,7 @@ export class EditProfileCard extends MozLitElement {
href="chrome://global/skin/in-content/common.css"
/>
<moz-card
><div id="edit-profile-card">
><div id="edit-profile-card" aria-labelledby="profile-header">
<img
id="header-avatar"
data-l10n-id=${this.profile.avatarL10nId}

View file

@ -55,7 +55,6 @@ export class NewProfileCard extends EditProfileCard {
}
async setInitialInput() {
await super.focusInput();
if (RPMGetBoolPref("browser.profiles.profile-name.updated", false)) {
return;
}

View file

@ -67,7 +67,7 @@ export class ProfilesThemeCard extends MozLitElement {
<moz-card class="theme-card">
<div class="theme-content">
<div class="img-holder">
<img />
<img alt="" />
</div>
<div
class="theme-name"

View file

@ -123,12 +123,6 @@ add_task(async function test_create_profile_name() {
await newProfileCard.getUpdateComplete();
Assert.equal(
Services.focus.focusedElement.id,
newProfileCard.nameInput.id,
"Name input is focused"
);
let nameInput = newProfileCard.nameInput;
Assert.equal(nameInput.value, "", "Profile name is empty to start");

View file

@ -106,12 +106,6 @@ add_task(async function test_edit_profile_name() {
await editProfileCard.getUpdateComplete();
Assert.equal(
Services.focus.focusedElement.id,
editProfileCard.nameInput.id,
"Name input is focused"
);
let nameInput = editProfileCard.nameInput;
nameInput.value = newProfileName;
nameInput.dispatchEvent(new content.Event("input"));

View file

@ -10,10 +10,7 @@ hbox {
width: 100%;
}
#engineNameLabel,
#engineUrlLabel,
#suggestUrlLabel,
#engineAliasLabel {
.dialogRow > label {
/* Align the labels with the inputs */
margin-inline-start: 4px;
}
@ -25,7 +22,7 @@ hbox {
text-align: match-parent;
}
.dialogRow {
.dialogRow:not([hidden]) {
--grid-padding: 16px;
margin-block: 0 var(--grid-padding);
display: flex;

View file

@ -4,112 +4,301 @@
/* globals AdjustableTitle */
let gAddEngineDialog = {
_form: null,
_name: null,
_alias: null,
loadedResolvers: Promise.withResolvers(),
// This is the dialog that is displayed when adding or editing a search engine
// in about:preferences, or when adding a search engine via the context menu of
// an HTML form. Depending on the scenario where it is used, different arguments
// must be supplied in an object in `window.arguments[0]`:
// - `mode` [required] - The type of dialog: NEW, EDIT or FORM.
// - `title` [optional] - To display a title in the window element.
// - all arguments required by the constructor of the dialog class
onLoad() {
try {
this.init();
} finally {
this.loadedResolvers.resolve();
}
},
const lazy = {};
init() {
ChromeUtils.defineESModuleGetters(lazy, {
SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs",
});
// Set the appropriate l10n id before the dialog's connectedCallback.
let mode = window.arguments[0].mode == "EDIT" ? "edit" : "add";
document.l10n.setAttributes(
document.querySelector("dialog"),
mode + "-engine-dialog"
);
document.l10n.setAttributes(
document.querySelector("window"),
mode + "-engine-window"
);
/**
* The abstract base class for all types of user search engine dialogs.
* All subclasses must implement the abstract method `onAddEngine`.
*/
class EngineDialog {
constructor() {
this._dialog = document.querySelector("dialog");
this._form = document.getElementById("addEngineForm");
this._name = document.getElementById("engineName");
this._alias = document.getElementById("engineAlias");
// These arguments only exist if this dialog was opened via
// "Add Search Engine" in the context menu.
if (window.arguments?.[0]) {
let { uri, formData, charset, method, icon } = window.arguments[0];
this._formData = formData;
this._charset = charset;
this._method = method;
this._icon = icon;
this._uri = uri.spec;
this._name.value = uri.host;
this.onFormInput();
document.getElementById("engineUrlRow").remove();
document.getElementById("suggestUrlRow").remove();
let title = { raw: document.title };
document.documentElement.setAttribute(
"headertitle",
JSON.stringify(title)
);
document.documentElement.style.setProperty(
"--icon-url",
'url("chrome://browser/skin/preferences/category-search.svg")'
);
} else {
AdjustableTitle.hide();
}
this._url = document.getElementById("engineUrl");
this._suggestUrl = document.getElementById("suggestUrl");
this._name.addEventListener("input", this.onNameInput.bind(this));
this._alias.addEventListener("input", this.onAliasInput.bind(this));
this._form.addEventListener("input", this.onFormInput.bind(this));
this._url.addEventListener("input", this.onFormInput.bind(this));
document.addEventListener("dialogaccept", this.onAddEngine.bind(this));
},
}
onAddEngine() {
let url =
this._uri ||
document.getElementById("engineUrl").value.replace(/%s/, "{searchTerms}");
throw new Error("abstract");
}
let suggestUrl = document
.getElementById("suggestUrl")
?.value.replace(/%s/, "{searchTerms}");
Services.search.addUserEngine({
url,
name: this._name.value,
alias: this._alias.value,
// The values below may be undefined.
formData: this._formData,
charset: this._charset,
method: this._method,
icon: this._icon,
suggestUrl,
});
},
isNameValid(name) {
if (!name) {
return false;
}
return !Services.search.getEngineByName(name);
}
onNameInput() {
if (this._name.value) {
let engine = Services.search.getEngineByName(this._name.value);
let validity = engine
? document.getElementById("engineNameExists").textContent
: "";
this._name.setCustomValidity(validity);
let name = this._name.value.trim();
let validity = this.isNameValid(name)
? ""
: document.getElementById("engineNameExists").textContent;
this._name.setCustomValidity(validity);
this.onFormInput();
}
async isAliasValid(alias) {
if (!alias) {
return true;
}
},
return !(await Services.search.getEngineByAlias(alias));
}
async onAliasInput() {
let validity = "";
if (this._alias.value) {
let engine = await Services.search.getEngineByAlias(this._alias.value);
if (engine) {
validity = document.getElementById("engineAliasExists").textContent;
}
}
let alias = this._alias.value.trim();
let validity = (await this.isAliasValid(alias))
? ""
: document.getElementById("engineAliasExists").textContent;
this._alias.setCustomValidity(validity);
},
this.onFormInput();
}
// This function is not set as a listener but called directly because it
// depends on the output of `isAliasValid`, but `isAliasValid` contains an
// await, so it would finish after this function.
onFormInput() {
this._dialog.setAttribute(
"buttondisabledaccept",
!this._form.checkValidity()
);
},
};
}
}
document.mozSubdialogReady = gAddEngineDialog.loadedResolvers.promise;
/**
* This dialog is opened when adding a new search engine in preferences.
*/
class NewEngineDialog extends EngineDialog {
onAddEngine() {
Services.search.addUserEngine({
name: this._name.value.trim(),
url: this._url.value.trim().replace(/%s/, "{searchTerms}"),
suggestUrl: this._suggestUrl.value.trim().replace(/%s/, "{searchTerms}"),
alias: this._alias.value.trim(),
});
}
}
window.addEventListener("load", () => gAddEngineDialog.onLoad());
/**
* This dialog is opened when editing a user search engine in preferences.
*/
class EditEngineDialog extends EngineDialog {
#engine;
/**
* Initializes the dialog with information from a user search engine.
*
* @param {object} args
* The arguments.
* @param {nsISearchEngine} args.engine
* The search engine to edit. Must be a UserSearchEngine.
*/
constructor({ engine }) {
super();
this._postData = document.getElementById("enginePostData");
this.#engine = engine;
this._name.value = engine.name;
this._alias.value = engine.alias ?? "";
let [url, postData] = this.getSubmissionTemplate(
lazy.SearchUtils.URL_TYPE.SEARCH
);
this._url.value = url;
if (postData) {
document.getElementById("enginePostDataRow").hidden = false;
this._postData.value = postData;
}
let [suggestUrl] = this.getSubmissionTemplate(
lazy.SearchUtils.URL_TYPE.SUGGEST_JSON
);
this._suggestUrl.value = suggestUrl ?? "";
this.onNameInput();
this.onAliasInput();
}
onAddEngine() {
this.#engine.wrappedJSObject.rename(this._name.value.trim());
this.#engine.alias = this._alias.value.trim();
let newURL = this._url.value.trim();
let newPostData = this._postData.value.trim();
// UserSearchEngine.changeUrl() does not check whether the URL has actually changed.
let [prevURL, prevPostData] = this.getSubmissionTemplate(
lazy.SearchUtils.URL_TYPE.SEARCH
);
if (newURL != prevURL || (prevPostData && prevPostData != newPostData)) {
this.#engine.wrappedJSObject.changeUrl(
lazy.SearchUtils.URL_TYPE.SEARCH,
newURL.replace(/%s/, "{searchTerms}"),
prevPostData ? newPostData.replace(/%s/, "{searchTerms}") : null
);
}
let newSuggestURL = this._suggestUrl.value.trim() || null;
let [prevSuggestUrl] = this.getSubmissionTemplate(
lazy.SearchUtils.URL_TYPE.SUGGEST_JSON
);
if (newSuggestURL != prevSuggestUrl) {
this.#engine.wrappedJSObject.changeUrl(
lazy.SearchUtils.URL_TYPE.SUGGEST_JSON,
newSuggestURL.replace(/%s/, "{searchTerms}"),
null
);
}
}
isNameValid(name) {
if (!name) {
return false;
}
let engine = Services.search.getEngineByName(this._name.value);
return !engine || engine.id == this.#engine.id;
}
async isAliasValid(alias) {
if (!alias) {
return true;
}
let engine = await Services.search.getEngineByAlias(this._alias.value);
return !engine || engine.id == this.#engine.id;
}
/**
* Returns url and post data templates of the requested type.
* Both contain %s in place of the search terms.
*
* If no url of the requested type exists, both are null.
* If the url is a GET url, the post data is null.
*
* @param {string} urlType
* The `SearchUtils.URL_TYPE`.
* @returns {[?string, ?string]}
* Array of the url and post data.
*/
getSubmissionTemplate(urlType) {
let submission = this.#engine.getSubmission("searchTerms", urlType);
if (!submission) {
return [null, null];
}
let postData = null;
if (submission.postData) {
let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
Ci.nsIBinaryInputStream
);
binaryStream.setInputStream(submission.postData.data);
postData = binaryStream
.readBytes(binaryStream.available())
.replace("searchTerms", "%s");
}
let url = submission.uri.spec.replace("searchTerms", "%s");
return [url, postData];
}
}
/**
* This dialog is opened via the context menu of an input and lets the
* user choose a name and an alias for an engine. Unlike the other two
* dialogs, it does not add or change an engine in the search service,
* and instead returns the user input to the caller.
*
* The chosen name and alias are returned via `window.arguments[0].engineInfo`.
* If the user chooses to not save the engine, it's undefined.
*/
class NewEngineFromFormDialog extends EngineDialog {
/**
* Initializes the dialog.
*
* @param {object} args
* The arguments.
* @param {string} args.nameTemplate
* The initial value of the name input.
*/
constructor({ nameTemplate }) {
super();
this._name.value = nameTemplate;
this.onNameInput();
this.onAliasInput();
document.getElementById("engineUrlRow").remove();
this._url = null;
document.getElementById("suggestUrlRow").remove();
this._suggestUrl = null;
let title = { raw: document.title };
document.documentElement.setAttribute("headertitle", JSON.stringify(title));
document.documentElement.style.setProperty(
"--icon-url",
'url("chrome://browser/skin/preferences/category-search.svg")'
);
}
onAddEngine() {
window.arguments[0].engineInfo = {
name: this._name.value.trim(),
// Empty string means no alias.
alias: this._alias.value.trim(),
};
}
}
let loadedResolvers = Promise.withResolvers();
document.mozSubdialogReady = loadedResolvers.promise;
let gAddEngineDialog = null;
window.addEventListener("DOMContentLoaded", () => {
if (!window.arguments[0].title) {
AdjustableTitle.hide();
}
try {
switch (window.arguments[0].mode) {
case "NEW":
gAddEngineDialog = new NewEngineDialog();
break;
case "EDIT":
gAddEngineDialog = new EditEngineDialog(window.arguments[0]);
break;
case "FORM":
gAddEngineDialog = new NewEngineFromFormDialog(window.arguments[0]);
break;
default:
throw new Error("Mode not supported for addEngine dialog.");
}
} finally {
loadedResolvers.resolve();
}
});

View file

@ -8,7 +8,6 @@
<window
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
data-l10n-id="add-engine-window"
data-l10n-attrs="title, style"
headerparent="add-engine-dialog"
neediconheader="true"
@ -18,7 +17,6 @@
id="add-engine-dialog"
buttons="accept,cancel"
buttondisabledaccept="true"
data-l10n-id="add-engine-dialog"
data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept"
>
<linkset>
@ -32,8 +30,8 @@
<html:link rel="localization" href="browser/search.ftl" />
</linkset>
<script src="chrome://browser/content/search/addEngine.js" />
<script src="chrome://global/content/adjustableTitle.js" />
<script src="chrome://browser/content/search/addEngine.js" />
<separator class="thin" />
@ -65,7 +63,7 @@
id="engineUrl"
type="url"
required="required"
pattern="https?:.*%s.*"
pattern="https?:.*"
/>
</hbox>
</html:div>
@ -77,7 +75,18 @@
data-l10n-id="add-engine-suggest-url"
/>
<hbox>
<html:input id="suggestUrl" type="url" pattern="https?:.*%s.*" />
<html:input id="suggestUrl" type="url" pattern="https?:.*" />
</hbox>
</html:div>
<html:div class="dialogRow" id="enginePostDataRow" hidden="hidden">
<html:label
id="enginePostDataLabel"
for="enginePostData"
data-l10n-id="add-engine-post-data"
/>
<hbox>
<html:input id="enginePostData" type="text" />
</hbox>
</html:div>

View file

@ -3,6 +3,14 @@
"use strict";
ChromeUtils.defineLazyGetter(this, "UrlbarTestUtils", () => {
const { UrlbarTestUtils: module } = ChromeUtils.importESModule(
"resource://testing-common/UrlbarTestUtils.sys.mjs"
);
module.init(this);
return module;
});
const TESTS = [
{
action: "/search",
@ -95,13 +103,13 @@ async function addEngine(browser, selector, name, alias) {
fillTextField("engineAlias", alias, dialogWin);
info("Saving engine.");
let promiseAdded = SearchTestUtils.promiseSearchNotification(
SearchUtils.MODIFIED_TYPE.ADDED,
SearchUtils.TOPIC_ENGINE_MODIFIED
);
EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin);
await window.gDialogBox.dialog._closingPromise;
await promiseAdded;
await TestUtils.waitForCondition(
() => gURLBar.searchMode?.engineName == name
);
Assert.ok(true, "Went into search mode.");
await UrlbarTestUtils.exitSearchMode(window);
return Services.search.getEngineByName(name);
}

View file

@ -325,12 +325,6 @@ export class ReviewCheckerChild extends RemotePageChild {
async setLocation() {
// check if can fetch and show data
let url = await this.sendQuery("GetCurrentURL");
// Bail out if we opted out in the meantime, or don't have a URI.
if (!this.canContinue(null, false)) {
return;
}
await this.locationChanged({ url });
}
@ -634,7 +628,7 @@ export class ReviewCheckerChild extends RemotePageChild {
adsEnabledByUser: lazy.adsEnabledByUser,
autoOpenEnabled: lazy.autoOpenEnabled,
autoOpenEnabledByUser: lazy.autoOpenEnabledByUser,
showOnboarding: !this.canFetchAndShowData,
showOnboarding: false,
data: null,
recommendationData: null,
focusCloseButton,

View file

@ -277,13 +277,16 @@ export class ReviewCheckerManager {
// Check if we should auto-open to allow opting in.
shouldAutoOpen = lazy.ShoppingUtils.handleAutoActivateOnProduct();
lazy.ShoppingUtils.sendTrigger({
browser: selectedBrowser,
id: "shoppingProductPageWithIntegratedRCSidebarClosed",
context: {
isReviewCheckerInSidebarClosed: !this.SidebarController?.isOpen,
},
});
// Only trigger the callout if the panel is not auto-opening
if (!shouldAutoOpen) {
lazy.ShoppingUtils.sendTrigger({
browser: selectedBrowser,
id: "shoppingProductPageWithIntegratedRCSidebarClosed",
context: {
isReviewCheckerInSidebarClosed: !this.SidebarController?.isOpen,
},
});
}
}
// Only show sidebar if no sidebar panel is currently showing,

View file

@ -41,23 +41,29 @@ export const ShoppingUtils = {
handledAutoActivate: false,
nimbusEnabled: false,
nimbusControl: false,
nimbusIntegratedSidebar: false,
everyWindowCallbackId: `shoppingutils-${Services.uuid.generateUUID()}`,
managers: new WeakMap(),
_updateNimbusVariables() {
this.nimbusIntegratedSidebar =
lazy.NimbusFeatures.shopping2023.getVariable("integratedSidebar");
this.nimbusEnabled =
this.nimbusIntegratedSidebar ||
lazy.NimbusFeatures.shopping2023.getVariable("enabled");
this.nimbusControl =
lazy.NimbusFeatures.shopping2023.getVariable("control");
},
onNimbusUpdate() {
if (this.initialized) {
ShoppingUtils.uninit();
}
this._updateNimbusVariables();
if (this.nimbusEnabled) {
ShoppingUtils.init();
Glean.shoppingSettings.nimbusDisabledShopping.set(false);
} else {
ShoppingUtils.uninit();
Glean.shoppingSettings.nimbusDisabledShopping.set(true);
}
},
@ -71,7 +77,6 @@ export const ShoppingUtils = {
}
this.onNimbusUpdate = this.onNimbusUpdate.bind(this);
this.onActiveUpdate = this.onActiveUpdate.bind(this);
this.onIntegratedSidebarUpdate = this.onIntegratedSidebarUpdate.bind(this);
this._addManagerForWindow = this._addManagerForWindow.bind(this);
this._removeManagerForWindow = this._removeManagerForWindow.bind(this);
@ -95,22 +100,17 @@ export const ShoppingUtils = {
this.recordUserAdsPreference();
this.recordUserAutoOpenPreference();
if (this.isAutoOpenEligible()) {
Services.prefs.setBoolPref(ACTIVE_PREF, true);
if (this.nimbusIntegratedSidebar) {
this._addReviewCheckerManagers();
} else {
if (this.isAutoOpenEligible()) {
Services.prefs.setBoolPref(ACTIVE_PREF, true);
}
Services.prefs.addObserver(ACTIVE_PREF, this.onActiveUpdate);
}
Services.prefs.addObserver(ACTIVE_PREF, this.onActiveUpdate);
Services.prefs.addObserver(
INTEGRATED_SIDEBAR_PREF,
this.onIntegratedSidebarUpdate
);
Services.prefs.setIntPref(SIDEBAR_CLOSED_COUNT_PREF, 0);
if (ShoppingUtils.integratedSidebar) {
this._addReviewCheckerManagers();
}
this.initialized = true;
},
@ -127,11 +127,6 @@ export const ShoppingUtils = {
Services.prefs.removeObserver(ACTIVE_PREF, this.onActiveUpdate);
Services.prefs.removeObserver(
INTEGRATED_SIDEBAR_PREF,
this.onIntegratedSidebarUpdate
);
if (this.managers.size) {
this._removeReviewCheckerManagers();
}

View file

@ -120,7 +120,7 @@ class NewPositionNotificationCard extends MozLitElement {
></h2>
<p
id="notification-card-body"
data-l10n-id="shopping-integrated-new-position-notification-subtitle"
data-l10n-id="shopping-integrated-new-position-notification-move-right-subtitle"
@click=${this.handleClickSettingsLink}
>
<a

View file

@ -6,77 +6,4 @@
### being translated as the feature is still in heavy development
### and strings are likely to change often.
## Opt-in message strings for Review Checker when it is integrated into the global sidebar.
shopping-opt-in-integrated-headline = Can you trust these reviews?
shopping-opt-in-integrated-subtitle = Turn on Review Checker in { -brand-product-name } to see how reliable product reviews are, before you buy. It uses AI technology to analyze reviews. <a data-l10n-name="learn_more">Learn more</a>
## Messages for callout for users not opted into the sidebar integrated version of Review Checker.
# Appears underneath shopping-opt-in-integrated-headline to answer the question 'Can you trust these reviews?'
shopping-callout-not-opted-in-integrated-paragraph1 = Turn on Review Checker from { -brand-product-name } to find out. Its powered by { -fakespot-brand-full-name } and uses AI technology to analyze reviews.
shopping-callout-not-opted-in-integrated-paragraph2 = By selecting “{ shopping-opt-in-integrated-button }” you agree to { -brand-product-name }s <a data-l10n-name="privacy_policy">privacy notice</a> and { -fakespot-brand-full-name }s <a data-l10n-name="terms_of_use">terms of use</a>.
shopping-callout-not-opted-in-integrated-reminder-dismiss-button = Dismiss
shopping-callout-not-opted-in-integrated-reminder-accept-button = Turn on Review Checker
shopping-callout-not-opted-in-integrated-reminder-do-not-show = Dont show this recommendation again
shopping-callout-not-opted-in-integrated-reminder-show-fewer = Show fewer recommendations
shopping-callout-not-opted-in-integrated-reminder-manage-settings = Manage settings
shopping-opt-in-integrated-subtitle-unsupported-site = Review Checker from { -brand-product-name } helps you know how reliable a products reviews are, before you buy. It uses AI technology to analyze reviews. <a data-l10n-name="learn_more">Learn more</a>
shopping-opt-in-integrated-privacy-policy-and-terms-of-use = Review Checker is powered by { -fakespot-brand-full-name }. By selecting “{ shopping-opt-in-integrated-button }“ you agree to { -brand-product-name }s <a data-l10n-name="privacy_policy">privacy notice</a> and { -fakespot-brand-name }s <a data-l10n-name="terms_of_use">terms of use.</a>
shopping-opt-in-integrated-button = Try Review Checker
## Message strings for Review Checker's empty states.
shopping-empty-state-header = Ready to check reviews
shopping-empty-state-supported-site = View a product and { -brand-product-name } will check if the reviews are reliable.
# We show a list of sites supported by Review Checker whenever a user opens the feature in an unsupported site.
# This string will be displayed above the list of sites. The list will be hardcoded and does not require localization.
shopping-empty-state-non-supported-site = Review Checker works when you shop on:
## Confirm disabling Review Checker for newly opted out users
shopping-integrated-callout-opted-out-title = Review Checker is off
shopping-integrated-callout-opted-out-subtitle = To turn it back on, select the price tag in the sidebar and turn on Review Checker.
## Callout for where to find Review Checker when the sidebar closes
shopping-integrated-callout-sidebar-closed-title = Get back to Review Checker
shopping-integrated-callout-sidebar-closed-subtitle = Select the price tag in the sidebar to see if you can trust a products reviews.
## Pref confirmation callout for auto-open
shopping-integrated-callout-disabled-auto-open-title = Get back to Review Checker
shopping-integrated-callout-disabled-auto-open-subtitle = Select the price tag in the sidebar to see if you can trust a products reviews.
shopping-integrated-callout-no-logo-disabled-auto-open-subtitle = Select the sidebar button to see if you can trust a products reviews.
## Strings for a notification card about Review Checker's new position in the sidebar.
## The card will only appear for users that have the default sidebar position, which is on the left side for non RTL locales.
## Review Checker in the sidebar is only available to en-US users at this time, so we can assume that the default position is on the left side.
shopping-integrated-new-position-notification-title = Same Review Checker, new spot
shopping-integrated-new-position-notification-subtitle = Keep Review Checker and the rest of the { -brand-product-name } sidebar here — or move them to the right. Switch now or anytime in <a data-l10n-name="sidebar_settings">sidebar settings</a>.
shopping-integrated-new-position-notification-move-right-button = Move right
shopping-integrated-new-position-notification-move-left-button = Move left
shopping-integrated-new-position-notification-dismiss-button = Got it
## Combined setting for auto-open and auto-close.
shopping-settings-auto-open-and-close-toggle =
.label = Automatically open and close Review Checker
# Description text for regions where we support three sites. Sites are limited to Amazon, Walmart and Best Buy.
# Variables:
# $firstSite (String) - The first shopping page name
# $secondSite (String) - The second shopping page name
# $thirdSite (String) - The third shopping page name
shopping-settings-auto-open-and-close-description-three-sites = Opens when you view products on { $firstSite }, { $secondSite }, and { $thirdSite } and closes when you leave
# Description text for regions where we support only one site (e.g. currently used in FR/DE with Amazon).
# Variables:
# $currentSite (String) - The current shopping page name
shopping-settings-auto-open-and-close-description-single-site = Opens when you view products on { $currentSite } and closes when you leave
##

View file

@ -21,7 +21,6 @@ prefs = [
"toolkit.shopping.ohttpRelayURL=https://example.com/relay", # These URLs don't actually host a relay or gateway config, but are needed to stop us making outside network connections.
"toolkit.shopping.ohttpConfigURL=https://example.com/ohttp-config",
"browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features=false", # Disable the fakespot feature callouts to avoid interference. Individual tests that need them can re-enable them as needed.
"browser.shopping.experience2023.shoppingSidebar=true",
"browser.shopping.experience2023.integratedSidebar=false",
]

View file

@ -12,7 +12,9 @@ support-files = [
]
prefs = [
"browser.shopping.experience2023.enabled=true",
"sidebar.revamp=true",
"browser.shopping.experience2023.integratedSidebar=true",
"browser.shopping.experience2023.enabled=false",
"browser.shopping.experience2023.optedIn=1",
"browser.shopping.experience2023.ads.enabled=true",
"browser.shopping.experience2023.ads.userEnabled=true",
@ -22,9 +24,6 @@ prefs = [
"toolkit.shopping.ohttpRelayURL=https://example.com/relay", # These URLs don't actually host a relay or gateway config, but are needed to stop us making outside network connections.
"toolkit.shopping.ohttpConfigURL=https://example.com/ohttp-config",
"browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features=false", # Disable the fakespot feature callouts to avoid interference. Individual tests that need them can re-enable them as needed.
"sidebar.revamp=true",
"browser.shopping.experience2023.shoppingSidebar=false",
"browser.shopping.experience2023.integratedSidebar=true",
"browser.shopping.experience2023.newPositionCard.hasSeen=true",
]

View file

@ -16,7 +16,7 @@ add_setup(async function setup() {
set: [
["sidebar.revamp", true],
["browser.shopping.experience2023.integratedSidebar", true],
["browser.shopping.experience2023.shoppingSidebar", false],
["browser.shopping.experience2023.enabled", false],
["sidebar.main.tools", "aichat,reviewchecker,syncedtabs,history"],
["toolkit.shopping.ohttpRelayURL", ""],
["toolkit.shopping.ohttpConfigURL", ""],

View file

@ -49,7 +49,7 @@ add_setup(async function setup() {
set: [
["sidebar.revamp", true],
["browser.shopping.experience2023.integratedSidebar", true],
["browser.shopping.experience2023.shoppingSidebar", false],
["browser.shopping.experience2023.enabled", false],
["sidebar.main.tools", "aichat,reviewchecker,syncedtabs,history"],
["toolkit.shopping.ohttpRelayURL", ""],
["toolkit.shopping.ohttpConfigURL", ""],

View file

@ -96,7 +96,7 @@ add_setup(async function setup() {
set: [
["sidebar.revamp", true],
["browser.shopping.experience2023.integratedSidebar", true],
["browser.shopping.experience2023.shoppingSidebar", false],
["browser.shopping.experience2023.enabled", false],
["browser.shopping.experience2023.autoOpen.enabled", true],
["browser.shopping.experience2023.autoOpen.userEnabled", true],
["sidebar.main.tools", "aichat,reviewchecker,syncedtabs,history"],

View file

@ -15,9 +15,15 @@ const PRODUCT_URI = Services.io.newURI(
"https://example.com/product/B09TJGHL5F"
);
const CONTENT_PAGE_URI = Services.io.newURI("https://example.com");
const UNSUPPORTED_NON_PDP_URI = Services.io.newURI("about:about");
const UNSUPPORTED_NON_PDP_URL = "about:about";
const UNSUPPORTED_NON_PDP_URI = Services.io.newURI(UNSUPPORTED_NON_PDP_URL);
const REVIEW_CHECKER_ACTOR = "ReviewChecker";
const EMPTY_STATE_CLASS_NAME = ".FS_OPT_IN_SIDEBAR_VARIANT";
const EMPTY_STATE_UNSUPPORTED_PDP_CLASS_NAME =
".FS_OPT_IN_SIDEBAR_VARIANT_UNSUPPORTED_NON_PDP";
/**
* Toggle prefs involved in automatically activating the sidebar on PDPs if the
* user has not opted in. Onboarding should only try to auto-activate the
@ -74,6 +80,82 @@ function setOnboardingPrefs(states = {}) {
}
}
/**
* Checks that we're rendering the expected onboarding UI in about:shoppingsidebar.
* Unlike assertEmptyStateTypeWithRC, this function is meant for tests that load
* about:shoppingsidebar in a tab and don't utilize the sidebar.
*
* @param {Browser} browser the current browser
* @param {string} messageName class name of the expected onboarding UI
*/
async function assertEmptyStateType(browser, messageName) {
await SpecialPowers.spawn(browser, [messageName], async className => {
let shoppingContainer = await ContentTaskUtils.waitForCondition(
() => content.document.querySelector("shopping-container"),
"shopping-container"
);
let containerElem =
shoppingContainer.shadowRoot.getElementById("shopping-container");
let messageSlot = containerElem.getElementsByTagName("slot");
// Check multi-stage-message-slot used to show opt-In message is
// rendered inside shopping container when user optedIn pref value is 0
ok(messageSlot.length, `message slot element exists`);
is(
messageSlot[0].name,
"multi-stage-message-slot",
"multi-stage-message-slot showing opt-in message rendered"
);
ok(
!content.document.getElementById("multi-stage-message-root").hidden,
"message is shown"
);
ok(content.document.querySelector(className), "Rendered correct message");
});
}
/**
* Checks that we're rendering the expected onboarding UI in the
* Review Checker sidebar panel. Works differently from assertEmptyStateType by
* utilizing the helper function withReviewCheckerSidebar.
*
* @param {string} emptyStateClassName class name of the expected onboarding UI
*/
async function assertEmptyStateTypeWithRC(emptyStateClassName) {
await withReviewCheckerSidebar(
async className => {
let shoppingContainer = await ContentTaskUtils.waitForCondition(
() =>
content.document.querySelector("shopping-container")?.wrappedJSObject,
"Review Checker is loaded."
);
await shoppingContainer.updateComplete;
let containerElem =
shoppingContainer.shadowRoot.getElementById("shopping-container");
let messageSlot = containerElem.getElementsByTagName("slot");
// Check multi-stage-message-slot used to show opt-In message is
// rendered inside shopping container when user optedIn pref value is 0
ok(messageSlot.length, `message slot element exists`);
is(
messageSlot[0].name,
"multi-stage-message-slot",
"multi-stage-message-slot showing opt-in message rendered"
);
ok(
!content.document.getElementById("multi-stage-message-root").hidden,
"message is shown"
);
ok(
content.document.querySelector(`${className}`),
`Rendered correct message ${className}`
);
},
[emptyStateClassName]
);
}
add_setup(async function setup() {
// Block on testFlushAllChildren to ensure Glean is initialized before
// running tests.
@ -117,35 +199,7 @@ add_task(async function test_showOnboarding_notOptedIn() {
);
actor.updateCurrentURL(PRODUCT_URI);
await SpecialPowers.spawn(browser, [], async () => {
let shoppingContainer = await ContentTaskUtils.waitForCondition(
() => content.document.querySelector("shopping-container"),
"shopping-container"
);
let containerElem =
shoppingContainer.shadowRoot.getElementById("shopping-container");
let messageSlot = containerElem.getElementsByTagName("slot");
// Check multi-stage-message-slot used to show opt-In message is
// rendered inside shopping container when user optedIn pref value is 0
ok(messageSlot.length, `message slot element exists`);
is(
messageSlot[0].name,
"multi-stage-message-slot",
"multi-stage-message-slot showing opt-in message rendered"
);
ok(
!content.document.getElementById("multi-stage-message-root").hidden,
"message is shown"
);
ok(
content.document.querySelector(".FS_OPT_IN_SIDEBAR_VARIANT"),
"Rendered correct message"
);
});
await assertEmptyStateType(browser, EMPTY_STATE_CLASS_NAME);
}
);
@ -188,33 +242,7 @@ add_task(async function test_showOnboarding_notOptedIn_supported() {
);
actor.updateCurrentURL(CONTENT_PAGE_URI);
await SpecialPowers.spawn(browser, [], async () => {
let shoppingContainer = await ContentTaskUtils.waitForCondition(
() => content.document.querySelector("shopping-container"),
"shopping-container"
);
let containerElem =
shoppingContainer.shadowRoot.getElementById("shopping-container");
let messageSlot = containerElem.getElementsByTagName("slot");
// Check multi-stage-message-slot used to show opt-In message is
// rendered inside shopping container when user optedIn pref value is 0
ok(messageSlot.length, `message slot element exists`);
is(
messageSlot[0].name,
"multi-stage-message-slot",
"multi-stage-message-slot showing opt-in message rendered"
);
ok(
!content.document.getElementById("multi-stage-message-root").hidden,
"message is shown"
);
ok(
content.document.querySelector(".FS_OPT_IN_SIDEBAR_VARIANT"),
"Rendered correct message"
);
});
await assertEmptyStateType(browser, EMPTY_STATE_CLASS_NAME);
}
);
@ -260,35 +288,10 @@ add_task(
);
actor.updateCurrentURL(UNSUPPORTED_NON_PDP_URI);
await SpecialPowers.spawn(browser, [], async () => {
let shoppingContainer = await ContentTaskUtils.waitForCondition(
() => content.document.querySelector("shopping-container"),
"shopping-container"
);
let containerElem =
shoppingContainer.shadowRoot.getElementById("shopping-container");
let messageSlot = containerElem.getElementsByTagName("slot");
// Check multi-stage-message-slot used to show opt-In message is
// rendered inside shopping container when user optedIn pref value is 0
ok(messageSlot.length, `message slot element exists`);
is(
messageSlot[0].name,
"multi-stage-message-slot",
"multi-stage-message-slot showing opt-in message rendered"
);
ok(
!content.document.getElementById("multi-stage-message-root").hidden,
"message is shown"
);
ok(
content.document.querySelector(
".FS_OPT_IN_SIDEBAR_VARIANT_UNSUPPORTED_NON_PDP"
),
"Rendered correct message"
);
});
await assertEmptyStateType(
browser,
EMPTY_STATE_UNSUPPORTED_PDP_CLASS_NAME
);
}
);
@ -404,3 +407,80 @@ add_task(async function test_hideOnboarding_OptIn_AfterSurveySeen() {
);
await SpecialPowers.popPrefEnv();
});
/**
* Tests that the onboarding UI type is consistent between PDPs and unsupported sites after
* reload, open/close, and tab switch.
*/
add_task(
async function test_showOnboarding_integrated_sidebar_correct_ui_on_rerender() {
// OptedIn pref Value is 0 when a user hasn't opted-in
setOnboardingPrefs({ optedIn: 0, telemetryEnabled: true });
await SpecialPowers.pushPrefEnv({
set: [
["browser.shopping.experience2023.autoClose.userEnabled", false],
["browser.shopping.experience2023.autoOpen.userEnabled", true],
["toolkit.shopping.ohttpRelayURL", ""],
["toolkit.shopping.ohttpConfigURL", ""],
],
});
await BrowserTestUtils.withNewTab(PRODUCT_TEST_URL, async browser => {
await SidebarController.show("viewReviewCheckerSidebar");
await assertEmptyStateTypeWithRC(EMPTY_STATE_CLASS_NAME);
// Verify UI after tab reload.
let loadedPromise = BrowserTestUtils.browserLoaded(browser);
await BrowserTestUtils.reloadTab(gBrowser.selectedTab);
await loadedPromise;
Assert.ok(true, "Promise resolved after reloading tab");
info("Verifying UI for PDP after reload");
await assertEmptyStateTypeWithRC(EMPTY_STATE_CLASS_NAME);
// Verify UI after closing and reopening the RC panel.
SidebarController.hide();
await SidebarController.show("viewReviewCheckerSidebar");
info("Verifying UI for PDP after closing then reopening the RC panel");
await assertEmptyStateTypeWithRC(EMPTY_STATE_CLASS_NAME);
// Verify UI after navigating to an unsupported site tab.
let unsupportedSiteTab = BrowserTestUtils.addTab(
gBrowser,
UNSUPPORTED_NON_PDP_URL
);
let nonPDPBrowser = unsupportedSiteTab.linkedBrowser;
await BrowserTestUtils.browserLoaded(
nonPDPBrowser,
false,
UNSUPPORTED_NON_PDP_URL
);
await BrowserTestUtils.switchTab(gBrowser, unsupportedSiteTab);
Assert.ok(true, "Browser is loaded after switching tabs");
Assert.ok(SidebarController.isOpen, "Sidebar is open now");
info(
"Verifying UI for unsupported sites after switching to an unsupported site tab"
);
await assertEmptyStateTypeWithRC(EMPTY_STATE_UNSUPPORTED_PDP_CLASS_NAME);
// Verify UI after closing and reopening the RC panel.
SidebarController.hide();
await SidebarController.show("viewReviewCheckerSidebar");
info(
"Verifying UI for unsupported sites after closing then reopening the RC panel"
);
await assertEmptyStateTypeWithRC(EMPTY_STATE_UNSUPPORTED_PDP_CLASS_NAME);
await BrowserTestUtils.removeTab(unsupportedSiteTab);
});
SidebarController.hide();
await SpecialPowers.popPrefEnv();
await SpecialPowers.popPrefEnv();
}
);

View file

@ -10,11 +10,11 @@ const PRODUCT_PAGE = "https://example.com/product/Y4YM0Z1LL4";
let verifySidebarPanelNotAdded = async win => {
const { document } = win;
let sidebar = document.querySelector("sidebar-main");
await TestUtils.waitForCondition(
() => sidebar.toolButtons,
"Sidebar tools have been added."
);
let sidebar;
await TestUtils.waitForCondition(() => {
sidebar = document.querySelector("sidebar-main");
return sidebar.toolButtons;
}, "Sidebar tools have been added.");
let reviewCheckerButton = sidebar.shadowRoot.querySelector(
"moz-button[view=viewReviewCheckerSidebar]"
);
@ -36,8 +36,14 @@ add_task(async function test_bug_1901979_pref_toggle_private_windows() {
verifySidebarPanelNotAdded(privateWindow);
// Flip the prefs to trigger the bug.
Services.prefs.setBoolPref("browser.shopping.experience2023.enabled", false);
Services.prefs.setBoolPref("browser.shopping.experience2023.enabled", true);
Services.prefs.setBoolPref(
"browser.shopping.experience2023.integratedSidebar",
false
);
Services.prefs.setBoolPref(
"browser.shopping.experience2023.integratedSidebar",
true
);
// Verify we still haven't displayed the sidebar.
verifySidebarPanelNotAdded(privateWindow);

View file

@ -131,7 +131,7 @@ add_setup(async function setup() {
set: [
["sidebar.revamp", true],
["browser.shopping.experience2023.integratedSidebar", true],
["browser.shopping.experience2023.shoppingSidebar", false],
["browser.shopping.experience2023.enabled", false],
["browser.shopping.experience2023.autoOpen.enabled", true],
["browser.shopping.experience2023.autoOpen.userEnabled", true],
["sidebar.main.tools", "aichat,reviewchecker,syncedtabs,history"],

View file

@ -17,7 +17,7 @@ add_setup(async function setup() {
set: [
["sidebar.revamp", true],
["browser.shopping.experience2023.integratedSidebar", true],
["browser.shopping.experience2023.shoppingSidebar", false],
["browser.shopping.experience2023.enabled", false],
["sidebar.main.tools", "aichat,reviewchecker,syncedtabs,history"],
["toolkit.shopping.ohttpRelayURL", ""],
["toolkit.shopping.ohttpConfigURL", ""],

View file

@ -16,7 +16,7 @@ add_setup(async function setup() {
set: [
["sidebar.revamp", true],
["browser.shopping.experience2023.integratedSidebar", true],
["browser.shopping.experience2023.shoppingSidebar", false],
["browser.shopping.experience2023.enabled", false],
["sidebar.main.tools", "aichat,reviewchecker,syncedtabs,history"],
["toolkit.shopping.ohttpRelayURL", ""],
["toolkit.shopping.ohttpConfigURL", ""],

View file

@ -103,19 +103,44 @@ add_task(async function test_onOptIn() {
gBrowser,
},
async browser => {
await SpecialPowers.spawn(browser, [], async () => {
await ContentTaskUtils.waitForMutationCondition(
content.document,
{ childList: true, subtree: true },
() => !!content.document.querySelector("shopping-container .primary")
);
const REVIEW_CHECKER_ACTOR = "ReviewChecker";
// "Yes, try it" button
let primary = content.document.querySelector(
"shopping-container .primary"
);
primary.click();
});
await SpecialPowers.spawn(
browser,
[{ REVIEW_CHECKER_ACTOR, PRODUCT_TEST_URL }],
async args => {
if (
Services.prefs.getBoolPref(
"browser.shopping.experience2023.integratedSidebar"
)
) {
let actor = content.windowGlobalChild.getExistingActor(
args.REVIEW_CHECKER_ACTOR
);
Assert.ok(actor, "ReviewCheckerChild found");
/**
* We normally exit early on location change if the URL is about:shoppingsidebar.
* For this test, make sure the onboarding UI renders by directly calling
* ReviewCheckerChild.locationChanged and passing in a mock PDP URL.
*/
actor.locationChanged({ url: args.PRODUCT_TEST_URL });
}
await ContentTaskUtils.waitForMutationCondition(
content.document,
{ childList: true, subtree: true },
() =>
!!content.document.querySelector("shopping-container .primary")
);
// "Yes, try it" button
let primary = content.document.querySelector(
"shopping-container .primary"
);
primary.click();
}
);
}
);
@ -136,6 +161,7 @@ add_task(async function test_onOptIn() {
* Helper function to click the links in the Link Paragraph.
*/
async function linkParagraphClickLinks() {
const REVIEW_CHECKER_ACTOR = "ReviewChecker";
const sandbox = sinon.createSandbox();
let handleActionStub = sandbox
@ -152,22 +178,44 @@ async function linkParagraphClickLinks() {
gBrowser,
},
async browser => {
await SpecialPowers.spawn(browser, [], async () => {
await ContentTaskUtils.waitForMutationCondition(
content.document,
{ childList: true, subtree: true },
// Can safely assume that if one of the link exists, they both do.
() =>
!!content.document.querySelector(
".legal-paragraph a[value='terms_of_use']"
await SpecialPowers.spawn(
browser,
[{ REVIEW_CHECKER_ACTOR, PRODUCT_TEST_URL }],
async args => {
if (
Services.prefs.getBoolPref(
"browser.shopping.experience2023.integratedSidebar"
)
);
) {
let actor = content.windowGlobalChild.getExistingActor(
args.REVIEW_CHECKER_ACTOR
);
Assert.ok(actor, "ReviewCheckerChild found");
let termsOfUse = content.document.querySelector(
"shopping-container .legal-paragraph a[value='terms_of_use']"
);
termsOfUse.click();
});
/**
* We normally exit early on location change if the URL is about:shoppingsidebar.
* For this test, make sure the onboarding UI renders by directly calling
* ReviewCheckerChild.locationChanged and passing in a mock PDP URL.
*/
actor.locationChanged({ url: args.PRODUCT_TEST_URL });
}
await ContentTaskUtils.waitForMutationCondition(
content.document,
{ childList: true, subtree: true },
// Can safely assume that if one of the link exists, they both do.
() =>
!!content.document.querySelector(
".legal-paragraph a[value='terms_of_use']"
)
);
let termsOfUse = content.document.querySelector(
"shopping-container .legal-paragraph a[value='terms_of_use']"
);
termsOfUse.click();
}
);
}
);
@ -185,21 +233,43 @@ async function linkParagraphClickLinks() {
gBrowser,
},
async browser => {
await SpecialPowers.spawn(browser, [], async () => {
await ContentTaskUtils.waitForMutationCondition(
content.document,
{ childList: true, subtree: true },
// Can safely assume that if one of the link exists, they both do.
() =>
!!content.document.querySelector(
".legal-paragraph a[value='terms_of_use']"
await SpecialPowers.spawn(
browser,
[{ REVIEW_CHECKER_ACTOR, PRODUCT_TEST_URL }],
async args => {
if (
Services.prefs.getBoolPref(
"browser.shopping.experience2023.integratedSidebar"
)
);
let privacyPolicy = content.document.querySelector(
"shopping-container .legal-paragraph a[value='privacy_policy']"
);
privacyPolicy.click();
});
) {
let actor = content.windowGlobalChild.getExistingActor(
args.REVIEW_CHECKER_ACTOR
);
Assert.ok(actor, "ReviewCheckerChild found");
/**
* We normally exit early on location change if the URL is about:shoppingsidebar.
* For this test, make sure the onboarding UI renders by directly calling
* ReviewCheckerChild.locationChanged and passing in a mock PDP URL.
*/
actor.locationChanged({ url: args.PRODUCT_TEST_URL });
}
await ContentTaskUtils.waitForMutationCondition(
content.document,
{ childList: true, subtree: true },
// Can safely assume that if one of the link exists, they both do.
() =>
!!content.document.querySelector(
".legal-paragraph a[value='terms_of_use']"
)
);
let privacyPolicy = content.document.querySelector(
"shopping-container .legal-paragraph a[value='privacy_policy']"
);
privacyPolicy.click();
}
);
}
);
await handleActionStubCalled;
@ -216,18 +286,39 @@ async function linkParagraphClickLinks() {
gBrowser,
},
async browser => {
await SpecialPowers.spawn(browser, [], async () => {
await ContentTaskUtils.waitForMutationCondition(
content.document,
{ childList: true, subtree: true },
() => content.document.querySelector(".link-paragraph a")
);
let learnMore = content.document.querySelector(
"shopping-container .link-paragraph a[value='learn_more']"
);
// Learn More link button.
learnMore.click();
});
await SpecialPowers.spawn(
browser,
[{ REVIEW_CHECKER_ACTOR, PRODUCT_TEST_URL }],
async args => {
if (
Services.prefs.getBoolPref(
"browser.shopping.experience2023.integratedSidebar"
)
) {
let actor = content.windowGlobalChild.getExistingActor(
args.REVIEW_CHECKER_ACTOR
);
Assert.ok(actor, "ReviewCheckerChild found");
/**
* We normally exit early on location change if the URL is about:shoppingsidebar.
* For this test, make sure the onboarding UI renders by directly calling
* ReviewCheckerChild.locationChanged and passing in a mock PDP URL.
*/
actor.locationChanged({ url: args.PRODUCT_TEST_URL });
}
await ContentTaskUtils.waitForMutationCondition(
content.document,
{ childList: true, subtree: true },
() => content.document.querySelector(".link-paragraph a")
);
let learnMore = content.document.querySelector(
"shopping-container .link-paragraph a[value='learn_more']"
);
// Learn More link button.
learnMore.click();
}
);
}
);
await handleActionStubCalled;

View file

@ -357,6 +357,7 @@ add_task(async function test_onboarding_resets_after_opt_out() {
["browser.shopping.experience2023.survey.pdpVisits", 5],
["browser.shopping.experience2023.survey.optedInTime", time25HrsAgo],
["browser.shopping.experience2023.integratedSidebar", false],
["browser.shopping.experience2023.enabled", true],
],
});
await BrowserTestUtils.withNewTab(
@ -463,7 +464,7 @@ add_task(
["browser.shopping.experience2023.survey.pdpVisits", 5],
["browser.shopping.experience2023.survey.optedInTime", time25HrsAgo],
["browser.shopping.experience2023.integratedSidebar", true],
["browser.shopping.experience2023.shoppingSidebar", false],
["browser.shopping.experience2023.enabled", false],
],
});
await BrowserTestUtils.withNewTab(

View file

@ -211,7 +211,7 @@ add_task(async function test_hideOnboarding_onClose() {
await SpecialPowers.pushPrefEnv({
set: [
["browser.shopping.experience2023.integratedSidebar", false],
["browser.shopping.experience2023.shoppingSidebar", true],
["browser.shopping.experience2023.enabled", true],
],
});

View file

@ -23,19 +23,6 @@ let verifySidebarNotShown = win => {
);
};
let verifySidebarPanelNotAdded = async win => {
const { document } = win;
let sidebar = document.querySelector("sidebar-main");
await TestUtils.waitForCondition(
() => sidebar.toolButtons,
"Sidebar tools have been added."
);
let reviewCheckerButton = sidebar.shadowRoot.querySelector(
"moz-button[view=viewReviewCheckerSidebar]"
);
Assert.equal(reviewCheckerButton, null, "Review Checker should not exist.");
};
add_task(async function test_private_window_disabled() {
let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
private: true,
@ -76,27 +63,3 @@ add_task(async function test_bug_1901979_pref_toggle_private_windows() {
await BrowserTestUtils.closeWindow(privateWindow);
});
add_task(async function test_private_window_does_not_have_integrated_sidebar() {
await SpecialPowers.pushPrefEnv({
set: [
["sidebar.revamp", true],
["sidebar.verticalTabs", true],
["browser.shopping.experience2023.integratedSidebar", true],
],
});
let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
private: true,
});
let browser = privateWindow.gBrowser.selectedBrowser;
BrowserTestUtils.startLoadingURIString(browser, PRODUCT_PAGE);
await BrowserTestUtils.browserLoaded(browser);
verifySidebarPanelNotAdded(privateWindow);
await BrowserTestUtils.closeWindow(privateWindow);
await SpecialPowers.popPrefEnv();
});

View file

@ -369,7 +369,7 @@ export class SidebarState {
mainEl.toggleAttribute("sidebar-launcher-expanded", expanded);
}
splitterEl?.toggleAttribute("sidebar-launcher-expanded", expanded);
boxEl.toggleAttribute("sidebar-launcher-expanded", expanded);
boxEl?.toggleAttribute("sidebar-launcher-expanded", expanded);
contentAreaEl.toggleAttribute("sidebar-launcher-expanded", expanded);
this.#controller.updateToolbarButton();
if (!this.launcherDragActive) {

View file

@ -25,6 +25,7 @@ const toolsNameMap = {
};
const EXPAND_ON_HOVER_DEBOUNCE_RATE_MS = 200;
const EXPAND_ON_HOVER_DEBOUNCE_TIMEOUT_MS = 1000;
const LAUNCHER_SPLITTER_WIDTH = 4;
var SidebarController = {
makeSidebar({ elementId, ...rest }) {
@ -46,8 +47,12 @@ var SidebarController = {
let switcherMenuitem;
const updateMenus = visible => {
// Hide the sidebar if it is open and should not be visible.
if (!visible && this.isOpen && this.currentID == commandID) {
// Hide the sidebar if it is open and should not be visible,
// and unset the current command and lastOpenedId so they do not
// re-open the next time the sidebar does.
if (!visible && this._state.command == commandID) {
this._state.command = "";
this.lastOpenedId = null;
this.hide();
}
@ -266,8 +271,6 @@ var SidebarController = {
_mainResizeObserverAdded: false,
_mainResizeObserver: null,
_ongoingAnimations: [],
_launcherMouseOverListenerAdded: false,
_launcherMouseOutListenerAdded: false,
/**
* @type {MutationObserver | null}
@ -714,9 +717,6 @@ var SidebarController = {
*/
reversePosition() {
Services.prefs.setBoolPref(this.POSITION_START_PREF, !this._positionStart);
if (this.sidebarRevampVisibility === "expand-on-hover") {
this.setLauncherCollapsedWidth();
}
},
/**
@ -1006,15 +1006,6 @@ var SidebarController = {
]);
},
async _waitForOngoingAnimations() {
// Wait for any ongoing animations to finish
return new Promise(resolve => {
if (!this._ongoingAnimations.length) {
resolve();
}
});
},
/**
* Wait for Lit updates and ongoing animations to complete.
*
@ -1025,10 +1016,12 @@ var SidebarController = {
// Legacy sidebar doesn't have animations, nothing to await.
return null;
}
const tasks = [
this.sidebarMain.updateComplete,
...this._ongoingAnimations.map(animation => animation.finished),
];
const tasks = [this.sidebarMain.updateComplete];
if (this._ongoingAnimations?.length) {
tasks.push(
...this._ongoingAnimations.map(animation => animation.finished)
);
}
return Promise.allSettled(tasks);
},
@ -1906,95 +1899,43 @@ var SidebarController = {
this.sidebarMain.requestUpdate();
},
onMouseOver() {
debouncedMouseEnter() {
const contentArea = document.getElementById("tabbrowser-tabbox");
SidebarController._box.toggleAttribute("sidebar-launcher-hovered", true);
this._box.toggleAttribute("sidebar-launcher-hovered", true);
contentArea.toggleAttribute("sidebar-launcher-hovered", true);
SidebarController._state.launcherHoverActive = true;
if (SidebarController._animationEnabled && !window.gReduceMotion) {
if (SidebarController._ongoingAnimations.length) {
SidebarController._ongoingAnimations.forEach(a => a.cancel());
SidebarController._ongoingAnimations = [];
}
SidebarController._animateSidebarMain();
this._state.launcherHoverActive = true;
if (this._animationEnabled && !window.gReduceMotion) {
this._animateSidebarMain();
}
SidebarController._state.launcherExpanded = true;
SidebarController.sidebarContainer.removeEventListener(
"mouseover",
SidebarController.handleMouseOver
);
SidebarController._launcherMouseOverListenerAdded = false;
this._state.launcherExpanded = true;
},
onMouseOut(e) {
if (SidebarController.sidebarContainer.matches(":hover")) {
// Don't fire mouseout if hovered over launcher child element
e?.stopPropagation();
} else {
const contentArea = document.getElementById("tabbrowser-tabbox");
SidebarController._box.toggleAttribute("sidebar-launcher-hovered", false);
contentArea.toggleAttribute("sidebar-launcher-hovered", false);
SidebarController._state.launcherHoverActive = false;
if (!SidebarController._launcherMouseOverListenerAdded) {
SidebarController.sidebarContainer.addEventListener(
"mouseover",
SidebarController.handleMouseOver
);
SidebarController._launcherMouseOverListenerAdded = true;
}
if (SidebarController._animationEnabled && !window.gReduceMotion) {
if (SidebarController._ongoingAnimations.length) {
SidebarController._ongoingAnimations.forEach(a => a.cancel());
SidebarController._ongoingAnimations = [];
}
SidebarController._animateSidebarMain();
}
SidebarController._state.launcherExpanded = false;
SidebarController.sidebarContainer.removeEventListener(
"mouseout",
SidebarController.onMouseOut
);
SidebarController._launcherMouseOutListenerAdded = false;
onMouseLeave() {
this.mouseEnterTask.disarm();
const contentArea = document.getElementById("tabbrowser-tabbox");
this._box.toggleAttribute("sidebar-launcher-hovered", false);
contentArea.toggleAttribute("sidebar-launcher-hovered", false);
this._state.launcherHoverActive = false;
if (this._animationEnabled && !window.gReduceMotion) {
this._animateSidebarMain();
}
this._state.launcherExpanded = false;
},
handleMouseOver(e) {
SidebarController.mouseOverTask = new DeferredTask(
onMouseEnter() {
this.mouseEnterTask = new DeferredTask(
() => {
if (!SidebarController.sidebarContainer.matches(":hover")) {
// Don't fire mouseout if hovered over element outside of the launcher
e?.stopPropagation();
} else {
if (!SidebarController._launcherMouseOutListenerAdded) {
SidebarController.sidebarContainer.addEventListener(
"mouseout",
SidebarController.onMouseOut
);
SidebarController._launcherMouseOutListenerAdded = true;
}
SidebarController.onMouseOver();
}
this.debouncedMouseEnter();
},
EXPAND_ON_HOVER_DEBOUNCE_RATE_MS,
EXPAND_ON_HOVER_DEBOUNCE_TIMEOUT_MS
);
SidebarController.mouseOverTask?.arm();
this.mouseEnterTask?.arm();
},
async setLauncherCollapsedWidth() {
let browserEl = document.getElementById("browser");
let collapsedWidth;
if (this.getUIState().launcherExpanded) {
this._state.launcherExpanded = false;
await this.sidebarMain.updateComplete;
collapsedWidth = await new Promise(resolve => {
requestAnimationFrame(() => {
resolve(this._getRects([this.sidebarMain])[0][1].width);
});
});
} else {
collapsedWidth = this._getRects([this.sidebarMain])[0][1].width;
}
let collapsedWidth = this._getRects([this.sidebarMain])[0][1].width;
browserEl.style.setProperty(
"--sidebar-launcher-collapsed-width",
@ -2002,6 +1943,42 @@ var SidebarController = {
);
},
handleEvent(e) {
switch (e.type) {
case "mouseout":
if (
(this._positionStart && e.x < 0) ||
(!this._positionStart && e.x > window.outerWidth)
) {
this.mouseEnterTask?.disarm();
// Only collapse sidebar if not moused over the window
if (this.getUIState().launcherExpanded) {
if (this._animationEnabled && !window.gReduceMotion) {
this._animateSidebarMain();
}
this._state.launcherExpanded = false;
}
}
break;
}
},
getMouseTargetRect() {
let launcherRect = window.windowUtils.getBoundsWithoutFlushing(
SidebarController.sidebarContainer
);
return {
top: launcherRect.top,
bottom: launcherRect.bottom,
left: this._positionStart
? launcherRect.left
: launcherRect.left + LAUNCHER_SPLITTER_WIDTH,
right: this._positionStart
? launcherRect.right - LAUNCHER_SPLITTER_WIDTH
: launcherRect.right,
};
},
async toggleExpandOnHover(isEnabled, isDragEnded) {
document.documentElement.toggleAttribute(
"sidebar-expand-on-hover",
@ -2011,17 +1988,18 @@ var SidebarController = {
if (!this._state) {
this._state = new this.SidebarState(this);
}
this.sidebarContainer.addEventListener("mouseover", this.handleMouseOver);
await this.sidebarMain.updateComplete;
await this._waitForOngoingAnimations();
if (this.getUIState().launcherExpanded && !isDragEnded) {
this._state.launcherExpanded = false;
await this.waitUntilStable();
}
MousePosTracker.addListener(this);
window.addEventListener("mouseout", this);
if (!isDragEnded) {
await this.setLauncherCollapsedWidth();
}
} else {
this.sidebarContainer.removeEventListener(
"mouseover",
this.handleMouseOver
);
MousePosTracker.removeListener(this);
window.removeEventListener("mouseout", this);
if (!this.mouseOverTask?.isFinalized) {
this.mouseOverTask?.finalize();
}

View file

@ -30,7 +30,7 @@ async function mouseOverSidebarToExpand() {
SidebarController.sidebarContainer,
{ attributes: true },
async () => {
await SidebarController._waitForOngoingAnimations();
await SidebarController.waitUntilStable();
return (
SidebarController.sidebarContainer.hasAttribute(
"sidebar-launcher-expanded"
@ -61,7 +61,7 @@ async function mouseOutSidebarToCollapse() {
SidebarController.sidebarContainer,
{ attributes: true },
async () => {
await SidebarController._waitForOngoingAnimations();
await SidebarController.waitUntilStable();
return (
!SidebarController.sidebarContainer.hasAttribute(
"sidebar-launcher-expanded"
@ -129,6 +129,7 @@ add_task(async function test_enable_expand_on_hover() {
await mouseOverSidebarToExpand();
await mouseOutSidebarToCollapse();
await SidebarController.waitUntilStable();
panel.positionInput.click();
await BrowserTestUtils.waitForMutationCondition(
@ -152,6 +153,15 @@ add_task(async function test_enable_expand_on_hover() {
"The sidebar is positioned on the left"
);
await mouseOutSidebarToCollapse();
await SidebarController.toggleExpandOnHover(false);
await SidebarController.waitUntilStable();
});
add_task(async function test_expand_on_hover_pinned_tabs() {
await SidebarController.toggleExpandOnHover(true);
await SidebarController.waitUntilStable();
let newTabButton = document.getElementById("tabs-newtab-button");
info("Open 2 new tabs using the new tab button.");
newTabButton.click();
@ -162,8 +172,6 @@ add_task(async function test_enable_expand_on_hover() {
let unpinnedTabs = gBrowser.visibleTabs.filter(tab => !tab.pinned);
gBrowser.pinTab(unpinnedTabs[0]);
let pinnedTabs = gBrowser.visibleTabs.filter(tab => tab.pinned);
await mouseOverSidebarToExpand();
let verticalPinnedTabsContainer = document.getElementById(
"vertical-pinned-tabs-container"
);
@ -173,9 +181,17 @@ add_task(async function test_enable_expand_on_hover() {
let inlineMuteButton =
gBrowser.selectedTab.querySelector(".tab-audio-button");
let muteButtonComputedStyle = window.getComputedStyle(inlineMuteButton);
await mouseOutSidebarToCollapse();
await mouseOverSidebarToExpand();
let pinnedTabComputedStyle = window.getComputedStyle(pinnedTabs[0]);
await mouseOverSidebarToExpand();
await SidebarController.waitUntilStable();
await BrowserTestUtils.waitForMutationCondition(
SidebarController.sidebarContainer,
{ attributes: true },
() =>
SidebarController._state.launcherExpanded &&
SidebarController.sidebarMain.hasAttribute("expanded"),
"The launcher is expanded"
);
is(
Math.round(parseInt(verticalTabsComputedStyle.width)),
Math.round(parseInt(pinnedTabComputedStyle.width)),
@ -189,5 +205,38 @@ add_task(async function test_enable_expand_on_hover() {
);
await mouseOutSidebarToCollapse();
SidebarController.hide();
await SidebarController.toggleExpandOnHover(false);
await SidebarController.waitUntilStable();
});
add_task(async function test_expand_on_hover_context_menu() {
await SidebarController.toggleExpandOnHover(true);
await SidebarController.waitUntilStable();
await mouseOverSidebarToExpand();
await SidebarController.waitUntilStable();
await BrowserTestUtils.waitForMutationCondition(
SidebarController.sidebarContainer,
{ attributes: true },
() => SidebarController._state.launcherExpanded,
"The launcher is expanded"
);
const toolbarContextMenu = document.getElementById("toolbar-context-menu");
await openAndWaitForContextMenu(
toolbarContextMenu,
SidebarController.sidebarMain,
() => {
ok(
!document.getElementById("toolbar-context-customize-sidebar").hidden,
"The sidebar context menu is loaded"
);
ok(
SidebarController._state.launcherExpanded,
"The sidebar launcher is still expanded with the context menu open"
);
}
);
await mouseOutSidebarToCollapse();
await SidebarController.toggleExpandOnHover(false);
await SidebarController.waitUntilStable();
});

View file

@ -836,13 +836,20 @@ export class SmartTabGroupingManager {
* @param {number} numTabsInGroup Number of tabs used to generate the label
* @param {string} mlLabel ML generated label for the tab group
* @param {string} userLabel User saved label for the tab group
* @param {string} id The id of the group
*/
async handleLabelTelemetry({ action, numTabsInGroup, mlLabel, userLabel }) {
async handleLabelTelemetry({
action,
numTabsInGroup,
mlLabel,
userLabel,
id = "",
}) {
const { [ML_TASK_TEXT2TEXT]: topicEngineConfig } =
await this.getEngineConfigs();
Glean.browserMlInteraction.smartTabTopic.record({
Glean.tabgroup.smartTabTopic.record({
action,
num_tabs_in_group: numTabsInGroup,
tabs_in_group: numTabsInGroup,
ml_label_length: (mlLabel || "").length,
user_label_length: (userLabel || "").length,
levenshtein_distance: lazy.NLP.levenshtein(
@ -850,6 +857,7 @@ export class SmartTabGroupingManager {
mlLabel || ""
),
model_revision: topicEngineConfig.modelRevision || "",
id,
});
}
@ -863,6 +871,7 @@ export class SmartTabGroupingManager {
* @param {number} numTabsSuggested Number of tabs suggested by the model
* @param {number} numTabsApproved Number of tabs approved by the user
* @param {number} numTabsRemoved Number of tabs removed by the user
* @param {string} id The id of the group
*/
async handleSuggestTelemetry({
action,
@ -871,17 +880,19 @@ export class SmartTabGroupingManager {
numTabsSuggested,
numTabsApproved,
numTabsRemoved,
id = "",
}) {
const { [ML_TASK_FEATURE_EXTRACTION]: embeddingEngineConfig } =
await this.getEngineConfigs();
Glean.browserMlInteraction.smartTabSuggest.record({
Glean.tabgroup.smartTabSuggest.record({
action,
num_tabs_in_window: numTabsInWindow,
num_tabs_in_group: numTabsInGroup,
num_tabs_suggested: numTabsSuggested,
num_tabs_approved: numTabsApproved,
num_tabs_removed: numTabsRemoved,
tabs_in_window: numTabsInWindow,
tabs_in_group: numTabsInGroup,
tabs_suggested: numTabsSuggested,
tabs_approved: numTabsApproved,
tabs_removed: numTabsRemoved,
model_revision: embeddingEngineConfig.modelRevision || "",
id,
});
}
@ -893,14 +904,14 @@ export class SmartTabGroupingManager {
async getEngineConfigs() {
if (!this.topicEngineConfig) {
this.topicEngineConfig = await lazy.MLEngineParent.getInferenceOptions(
this.config.topicGeneration.engineId,
this.config.topicGeneration.featureId,
this.config.topicGeneration.taskName
);
}
if (!this.embeddingEngineConfig) {
this.embeddingEngineConfig =
await lazy.MLEngineParent.getInferenceOptions(
this.config.embedding.engineId,
this.config.embedding.featureId,
this.config.embedding.taskName
);
}

View file

@ -1,18 +0,0 @@
/* 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/. */
/**
* A common list of systems, surfaces, controls, etc. from which user
* interactions with tab groups could originate. These "source" values
* should be sent as extra data with tab group-related metrics events.
*/
const METRIC_SOURCE = Object.freeze({
TAB_OVERFLOW_MENU: "tab_overflow",
TAB_GROUP_MENU: "tab_group",
UNKNOWN: "unknown",
});
export const TabGroupMetrics = {
METRIC_SOURCE,
};

View file

@ -110,8 +110,6 @@
PictureInPicture: "resource://gre/modules/PictureInPicture.sys.mjs",
SmartTabGroupingManager:
"moz-src:///browser/components/tabbrowser/SmartTabGrouping.sys.mjs",
TabGroupMetrics:
"moz-src:///browser/components/tabbrowser/TabGroupMetrics.sys.mjs",
UrlbarProviderOpenTabs:
"resource:///modules/UrlbarProviderOpenTabs.sys.mjs",
});
@ -2218,16 +2216,18 @@
browserSidebarContainer.className = "browserSidebarContainer";
browserSidebarContainer.appendChild(browserContainer);
let visibility = Services.prefs.getStringPref(
"sidebar.visibility",
"always-show"
);
let expandOnHover = Services.prefs.getBoolPref(
"sidebar.expandOnHover",
false
);
if (visibility === "expand-on-hover" && expandOnHover) {
SidebarController.toggleExpandOnHover(true);
if (!isPreloadBrowser) {
let visibility = Services.prefs.getStringPref(
"sidebar.visibility",
"always-show"
);
let expandOnHover = Services.prefs.getBoolPref(
"sidebar.expandOnHover",
false
);
if (visibility === "expand-on-hover" && expandOnHover) {
SidebarController.toggleExpandOnHover(true);
}
}
// Prevent the superfluous initial load of a blank document
@ -2588,6 +2588,7 @@
index: tab._tPos + 1,
userContextId: tab.userContextId,
tabGroup: tab.group,
focusUrlBar: true,
});
resolve(this.selectedBrowser);
}),
@ -2966,9 +2967,7 @@
* switches windows).
* Causes the group create UI to be displayed and telemetry events to be fired.
* @param {string} [options.telemetryUserCreateSource]
* The means by which the tab group was created.
* @see TabGroupMetrics.METRIC_SOURCE for possible values.
* Defaults to "unknown".
* The means by which the tab group was created. Defaults to "unknown".
*/
addTabGroup(
tabs,
@ -3027,23 +3026,8 @@
* The tab group to remove.
* @param {object} [options]
* Options to use when removing tabs. @see removeTabs for more info.
* @param {boolean} [options.isUserTriggered=false]
* Should be true if this group is being removed by an explicit
* request from the user (as opposed to a group being removed
* for technical reasons, such as when an already existing group
* switches windows). This causes telemetry events to fire.
* @param {string} [options.telemetrySource="unknown"]
* The means by which the tab group was removed.
* @see TabGroupMetrics.METRIC_SOURCE for possible values.
* Defaults to "unknown".
*/
async removeTabGroup(
group,
options = {
isUserTriggered: false,
telemetrySource: this.TabGroupMetrics.METRIC_SOURCE.UNKNOWN,
}
) {
async removeTabGroup(group, options = {}) {
if (this.tabGroupMenu.panel.state != "closed") {
this.tabGroupMenu.panel.hidePopup(options.animate);
}
@ -3077,8 +3061,6 @@
bubbles: true,
detail: {
skipSessionStore: options.skipSessionStore,
isUserTriggered: options.isUserTriggered,
telemetrySource: options.telemetrySource,
},
})
);

View file

@ -18,10 +18,6 @@
}
);
const { TabGroupMetrics } = ChromeUtils.importESModule(
"moz-src:///browser/components/tabbrowser/TabGroupMetrics.sys.mjs"
);
class MozTabbrowserTabGroupMenu extends MozXULElement {
static COLORS = [
"blue",
@ -50,17 +46,19 @@
static AI_ICON = "chrome://global/skin/icons/highlights.svg";
static headerSection = /*html*/ `
<html:div class="panel-header">
<html:h1
id="tab-group-editor-title-create"
class="tab-group-create-mode-only"
data-l10n-id="tab-group-editor-title-create">
</html:h1>
<html:h1
id="tab-group-editor-title-edit"
class="tab-group-edit-mode-only"
data-l10n-id="tab-group-editor-title-edit">
</html:h1>
<html:div id="tab-group-default-header">
<html:div class="panel-header" >
<html:h1
id="tab-group-editor-title-create"
class="tab-group-create-mode-only"
data-l10n-id="tab-group-editor-title-create">
</html:h1>
<html:h1
id="tab-group-editor-title-edit"
class="tab-group-edit-mode-only"
data-l10n-id="tab-group-editor-title-edit">
</html:h1>
</html:div>
</html:div>
`;
@ -110,7 +108,6 @@
<html:div class="panel-header">
<html:h1 data-l10n-id="tab-group-editor-title-suggest"></html:h1>
</html:div>
<toolbarseparator />
</html:div>
`;
@ -169,7 +166,7 @@
static defaultActions = /*html*/ `
<html:moz-button-group
class="panel-body tab-group-create-actions tab-group-create-mode-only"
class="tab-group-create-actions tab-group-create-mode-only"
id="tab-group-default-actions">
<html:moz-button
id="tab-group-editor-button-cancel"
@ -207,8 +204,8 @@
ignorekeys="true"
norolluponanchor="true">
<html:div id="tab-group-main">
${this.headerSection}
${this.suggestionsHeader}
<toolbarseparator />
@ -228,6 +225,8 @@
/>
</html:div>
<html:div id="tab-group-main">
<html:div
class="panel-body tab-group-editor-swatches"
role="radiogroup"
@ -242,26 +241,22 @@
${this.suggestionsButton}
<html:p
hidden="true"
id="tab-group-suggestions-disclaimer"
data-l10n-id="tab-group-suggestions-disclaimer">
<a data-l10n-name="support" href="#"></a>
</html:p>
<html:moz-button
hidden="true"
disabled="true"
type="icon ghost"
id="tab-group-suggestions-message"
data-l10n-id="tab-group-editor-no-tabs-found">
</html:moz-button>
<html:div id="tab-group-suggestions-message-container" hidden="true">
<html:moz-button
disabled="true"
type="icon ghost"
id="tab-group-suggestions-message"
data-l10n-id="tab-group-editor-no-tabs-found-title">
</html:moz-button>
<html:p
data-l10n-id="tab-group-editor-no-tabs-found-message">
</html:p>
</html:div>
${this.defaultActions}
</html:div>
${this.suggestionsHeader}
${this.loadingSection}
${this.loadingActions}
${this.suggestionsSection}
@ -311,6 +306,7 @@
#suggestionState = MozTabbrowserTabGroupMenu.State.CREATE_STANDARD_INITIAL;
#suggestionsHeading;
#suggestionsHeader;
#defaultHeader;
#suggestionsContainer;
#suggestions;
#suggestionButton;
@ -319,7 +315,7 @@
#suggestionsLoading;
#selectSuggestionsToggle;
#suggestionsMessage;
#suggestionsDisclaimer;
#suggestionsMessageContainer;
#selectedSuggestedTabs = [];
#suggestedMlLabel;
#hasSuggestedMlTabs = false;
@ -379,7 +375,7 @@
this.#swatchesContainer = this.querySelector(
".tab-group-editor-swatches"
);
this.#defaultHeader = this.querySelector("#tab-group-default-header");
this.#defaultActions = this.querySelector("#tab-group-default-actions");
this.#tabGroupMain = this.querySelector("#tab-group-main");
this.#initSuggestions();
@ -445,10 +441,7 @@
document
.getElementById("tabGroupEditor_deleteGroup")
.addEventListener("command", () => {
gBrowser.removeTabGroup(this.activeGroup, {
isUserTriggered: true,
telemetrySource: TabGroupMetrics.METRIC_SOURCE.TAB_GROUP_MENU,
});
gBrowser.removeTabGroup(this.activeGroup);
});
this.panel.addEventListener("popupshown", this);
@ -474,6 +467,7 @@
}
#initSmartTabGroupsOptin() {
this.#handleMLOptinTelemetry("step0-optin-shown");
this.suggestionState = MozTabbrowserTabGroupMenu.State.OPTIN;
// Init optin component
@ -482,23 +476,48 @@
"tab-group-suggestions-optin-title";
this.#suggestionsOptin.messageL10nId =
"tab-group-suggestions-optin-message";
this.#suggestionsOptin.footerMessageL10nId =
"tab-group-suggestions-optin-message-footer";
this.#suggestionsOptin.headingIcon = MozTabbrowserTabGroupMenu.AI_ICON;
// On Confirm
this.#suggestionsOptin.addEventListener("MlModelOptinConfirm", () => {
this.#handleMLOptinTelemetry("step1-optin-confirmed");
Services.prefs.setBoolPref("browser.tabs.groups.smart.optin", true);
this.#handleFirstDownloadAndSuggest();
});
// On Deny
this.#suggestionsOptin.addEventListener("MlModelOptinDeny", () => {
this.SmartTabGroupingManager.terminateProcess();
this.#handleMLOptinTelemetry("step1-optin-denied");
this.#smartTabGroupingManager.terminateProcess();
this.suggestionState = this.createMode
? MozTabbrowserTabGroupMenu.State.CREATE_AI_INITIAL
: MozTabbrowserTabGroupMenu.State.EDIT_AI_INITIAL;
this.#setFormToDisabled(false);
});
// On Cancel Model Download
this.#suggestionsOptin.addEventListener(
"MlModelOptinCancelDownload",
() => {
this.#handleMLOptinTelemetry("step2-optin-cancel-download");
this.#smartTabGroupingManager.terminateProcess();
this.suggestionState = this.createMode
? MozTabbrowserTabGroupMenu.State.CREATE_AI_INITIAL
: MozTabbrowserTabGroupMenu.State.EDIT_AI_INITIAL;
this.#setFormToDisabled(false);
}
);
// On Footer link click
this.#suggestionsOptin.addEventListener(
"MlModelOptinFooterLinkClick",
() => {
openTrustedLinkIn("about:preferences", "tab");
}
);
this.#suggestionsOptinContainer.appendChild(this.#suggestionsOptin);
}
@ -540,15 +559,15 @@
this.#selectSuggestionsToggle.addEventListener("click", () => {
this.#handleSelectToggle();
});
this.#suggestionsMessageContainer = this.querySelector(
"#tab-group-suggestions-message-container"
);
this.#suggestionsMessage = this.querySelector(
"#tab-group-suggestions-message"
);
this.#suggestionsMessage.iconSrc = this.smartTabGroupsEnabled
? MozTabbrowserTabGroupMenu.AI_ICON
: "";
this.#suggestionsDisclaimer = this.querySelector(
"#tab-group-suggestions-disclaimer"
);
this.#createSuggestionsButton = this.querySelector(
"#tab-group-create-suggestions-button"
);
@ -726,6 +745,7 @@
this.#panel.openPopup(group.firstChild, {
position: this.#panelPosition,
});
// If user has opted in kick off label generation
this.smartTabGroupsOptin && this.#initMlGroupLabel();
}
@ -806,10 +826,19 @@
this.dispatchEvent(
new CustomEvent("TabGroupCreateDone", { bubbles: true })
);
if (
this.smartTabGroupsEnabled &&
(this.#suggestedMlLabel || this.#hasSuggestedMlTabs)
) {
this.#handleMlTelemetry("save-popup-hidden");
}
} else {
this.activeGroup.ungroupTabs();
}
}
if (this.#nameField.disabled) {
this.#setFormToDisabled(false);
}
this.activeGroup = null;
this.#smartTabGroupingManager.terminateProcess();
}
@ -922,12 +951,12 @@
async #handleFirstDownloadAndSuggest() {
this.#setFormToDisabled(true);
this.#suggestionsOptin.isLoading = true;
this.#suggestionsOptin.headingL10nId =
"tab-group-suggestions-optin-title-download";
this.#suggestionsOptin.messageL10nId =
"tab-group-suggestions-optin-message-download";
this.#suggestionsOptin.headingIcon = "";
this.#suggestionsOptin.isLoading = true;
// Init progress with value to show determiniate progress
this.#suggestionsOptin.progressStatus = 0;
@ -938,8 +967,10 @@
// Clean up optin UI
this.#setFormToDisabled(false);
this.#suggestionsOptin.isHidden = true;
this.#suggestionsOptin.isLoading = false;
// Continue on with the suggest flow
this.#handleMLOptinTelemetry("step3-optin-completed");
this.#initMlGroupLabel();
this.#handleSmartSuggest();
}
@ -957,6 +988,13 @@
this.suggestionState = this.#createMode
? MozTabbrowserTabGroupMenu.State.CREATE_AI_WITH_NO_SUGGESTIONS
: MozTabbrowserTabGroupMenu.State.EDIT_AI_WITH_NO_SUGGESTIONS;
// there's no "save" button from the edit ai interaction with
// no tab suggestions, so we need to capture here
if (!this.#createMode) {
this.#hasSuggestedMlTabs = true;
this.#handleMlTelemetry("save");
}
return;
}
@ -975,7 +1013,7 @@
/**
* Sends Glean metrics if smart tab grouping is enabled
* @param {string} action "save" or "cancel"
* @param {string} action "save", "save-popup-hidden" or "cancel"
*/
#handleMlTelemetry(action) {
if (!this.smartTabGroupsEnabled) {
@ -987,6 +1025,7 @@
numTabsInGroup: this.#activeGroup.tabs.length,
mlLabel: this.#suggestedMlLabel,
userLabel: this.#nameField.value,
id: this.#activeGroup.id,
});
this.#suggestedMlLabel = "";
}
@ -999,11 +1038,22 @@
numTabsApproved: this.#selectedSuggestedTabs.length,
numTabsRemoved:
this.#suggestedTabs.length - this.#selectedSuggestedTabs.length,
id: this.#activeGroup.id,
});
this.#hasSuggestedMlTabs = false;
}
}
/**
* Sends Glean metrics for opt-in UI flow
* @param {string} step contains step number and description of flow
*/
#handleMLOptinTelemetry(step) {
Glean.tabgroup.smartTabOptin.record({
step,
});
}
#createRow(tab, index) {
// Create Row
let row = document.createXULElement("toolbaritem");
@ -1084,12 +1134,8 @@
this.#setElementVisibility(this.#suggestionButton, value);
}
#showSuggestionMessage(value) {
this.#setElementVisibility(this.#suggestionsMessage, value);
}
#showSuggestionsDisclaimer(value) {
this.#setElementVisibility(this.#suggestionsDisclaimer, value);
#showSuggestionMessageContainer(value) {
this.#setElementVisibility(this.#suggestionsMessageContainer, value);
}
#showSuggestionsSeparator(value) {
@ -1110,19 +1156,21 @@
}
/**
* Unique state setter for a "3rd" panel state while in Edit Mode
* Unique state setter for a "3rd" panel state while in suggest Mode
* that just shows suggestions and hides the majority of the panel
* @param {boolean} value
*/
#setEditModeSuggestionState(value) {
#setSuggestModeSuggestionState(value) {
this.#setElementVisibility(this.#suggestionsHeader, !value);
this.#setElementVisibility(this.#tabGroupMain, !value);
this.#setElementVisibility(this.#suggestionsHeading, value);
this.#setElementVisibility(this.#defaultHeader, !value);
this.#panel.classList.toggle("tab-group-editor-panel-expanded", value);
}
#resetCommonUI() {
this.#setLoadingState(false);
this.#setEditModeSuggestionState(false);
this.#setSuggestModeSuggestionState(false);
this.#suggestedTabs = [];
this.#selectedSuggestedTabs = [];
this.#suggestions.innerHTML = "";
@ -1137,8 +1185,7 @@
this.#resetCommonUI();
this.#showDefaultTabGroupActions(true);
this.#showSuggestionButton(false);
this.#showSuggestionMessage(false);
this.#showSuggestionsDisclaimer(false);
this.#showSuggestionMessageContainer(false);
this.#showSuggestionsSeparator(false);
break;
@ -1147,9 +1194,8 @@
this.#resetCommonUI();
this.#showSuggestionButton(true);
this.#showDefaultTabGroupActions(true);
this.#showSuggestionMessage(false);
this.#showSuggestionMessageContainer(false);
this.#setSelectToggleState("deselect");
this.#showSuggestionsDisclaimer(true);
this.#setSuggestionsButtonCreateModeState(true);
this.#showSuggestionsSeparator(true);
break;
@ -1159,8 +1205,7 @@
.CREATE_AI_INITIAL_SUGGESTIONS_DISABLED:
this.#resetCommonUI();
this.#showSuggestionButton(false);
this.#showSuggestionsDisclaimer(false);
this.#showSuggestionMessage(true);
this.#showSuggestionMessageContainer(true);
this.#showDefaultTabGroupActions(true);
this.#showSuggestionsSeparator(true);
break;
@ -1172,12 +1217,13 @@
this.#showSuggestionButton(false);
this.#showSuggestionsSeparator(true);
this.#showDefaultTabGroupActions(false);
this.#setSuggestModeSuggestionState(true);
break;
// CREATE AI WITH NO SUGGESTIONS
case MozTabbrowserTabGroupMenu.State.CREATE_AI_WITH_NO_SUGGESTIONS:
this.#setLoadingState(false);
this.#showSuggestionMessage(true);
this.#showSuggestionMessageContainer(true);
this.#showDefaultTabGroupActions(true);
this.#showSuggestionButton(false);
this.#showSuggestionsSeparator(true);
@ -1187,20 +1233,18 @@
case MozTabbrowserTabGroupMenu.State.EDIT_STANDARD_INITIAL:
this.#resetCommonUI();
this.#showSuggestionButton(false);
this.#showSuggestionMessage(false);
this.#showSuggestionMessageContainer(false);
this.#showDefaultTabGroupActions(false);
this.#showSuggestionsDisclaimer(false);
this.#showSuggestionsSeparator(false);
break;
// EDIT AI INITIAL
case MozTabbrowserTabGroupMenu.State.EDIT_AI_INITIAL:
this.#resetCommonUI();
this.#showSuggestionMessage(false);
this.#showSuggestionMessageContainer(false);
this.#setSelectToggleState("deselect");
this.#showSuggestionButton(true);
this.#showDefaultTabGroupActions(false);
this.#showSuggestionsDisclaimer(false);
this.#setSuggestionsButtonCreateModeState(false);
this.#showSuggestionsSeparator(true);
break;
@ -1209,10 +1253,9 @@
case MozTabbrowserTabGroupMenu.State
.EDIT_AI_INITIAL_SUGGESTIONS_DISABLED:
this.#resetCommonUI();
this.#showSuggestionMessage(true);
this.#showSuggestionMessageContainer(true);
this.#showSuggestionButton(false);
this.#showDefaultTabGroupActions(false);
this.#showSuggestionsDisclaimer(false);
this.#showSuggestionsSeparator(true);
break;
@ -1220,14 +1263,14 @@
case MozTabbrowserTabGroupMenu.State.EDIT_AI_WITH_SUGGESTIONS:
this.#setLoadingState(false);
this.#showSmartSuggestionsContainer(true);
this.#setEditModeSuggestionState(true);
this.#setSuggestModeSuggestionState(true);
this.#showSuggestionsSeparator(false);
break;
// EDIT AI WITH NO SUGGESTIONS
case MozTabbrowserTabGroupMenu.State.EDIT_AI_WITH_NO_SUGGESTIONS:
this.#setLoadingState(false);
this.#showSuggestionMessage(true);
this.#showSuggestionMessageContainer(true);
this.#showSuggestionsSeparator(true);
break;
@ -1235,8 +1278,7 @@
case MozTabbrowserTabGroupMenu.State.LOADING:
this.#showDefaultTabGroupActions(false);
this.#showSuggestionButton(false);
this.#showSuggestionsDisclaimer(false);
this.#showSuggestionMessage(false);
this.#showSuggestionMessageContainer(false);
this.#setLoadingState(true);
this.#showSuggestionsSeparator(true);
this.#showDefaultTabGroupActions(false);
@ -1249,7 +1291,6 @@
case MozTabbrowserTabGroupMenu.State.OPTIN:
this.#showSuggestionButton(false);
this.#showSuggestionsDisclaimer(false);
this.#showDefaultTabGroupActions(false);
break;
}

Some files were not shown because too many files have changed in this diff Show more