Update On Tue Jan 28 19:21:08 CET 2025

This commit is contained in:
github-action[bot] 2025-01-28 19:21:09 +01:00
parent ef3e750c74
commit 8f8e53da7d
830 changed files with 17558 additions and 9034 deletions

View file

@ -21,7 +21,7 @@
#include "mozilla/PresShell.h"
#include "mozilla/ProfilerMarkers.h"
#include "nsAccessibilityService.h"
#include "mozilla/Telemetry.h"
#include "mozilla/glean/AccessibleMetrics.h"
using namespace mozilla;
using namespace mozilla::a11y;
@ -679,7 +679,7 @@ void NotificationController::ProcessMutationEvents() {
void NotificationController::WillRefresh(mozilla::TimeStamp aTime) {
AUTO_PROFILER_MARKER_TEXT("NotificationController::WillRefresh", A11Y, {},
""_ns);
Telemetry::AutoTimer<Telemetry::A11Y_TREE_UPDATE_TIMING_MS> timer;
auto timer = glean::a11y::tree_update_timing.Measure();
// DO NOT ADD CODE ABOVE THIS BLOCK: THIS CODE IS MEASURING TIMINGS.
AUTO_PROFILER_LABEL("NotificationController::WillRefresh", A11Y);

View file

@ -7,14 +7,14 @@
#ifndef A11Y_STATISTICS_H_
#define A11Y_STATISTICS_H_
#include "mozilla/Telemetry.h"
#include "mozilla/glean/AccessibleMetrics.h"
namespace mozilla {
namespace a11y {
namespace statistics {
inline void A11yConsumers(uint32_t aConsumer) {
Telemetry::Accumulate(Telemetry::A11Y_CONSUMERS, aConsumer);
glean::a11y::consumers.AccumulateSingleSample(aConsumer);
}
} // namespace statistics

View file

@ -154,3 +154,41 @@ a11y:
- always
- never
telemetry_mirror: A11Y_THEME
consumers:
type: custom_distribution
description: |
A list of known accessibility clients that inject into Firefox process space (see https://searchfox.org/mozilla-central/source/accessible/windows/msaa/Compatibility.h).
This metric was generated to correspond to the Legacy Telemetry enumerated histogram A11Y_CONSUMERS.
range_min: 0
range_max: 11
bucket_count: 12
histogram_type: linear
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1382820
- https://bugzilla.mozilla.org/show_bug.cgi?id=1462238
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1382820
- https://bugzilla.mozilla.org/show_bug.cgi?id=1462238
notification_emails:
- accessibility@mozilla.com
- jteh@mozilla.com
expires: never
telemetry_mirror: A11Y_CONSUMERS
tree_update_timing:
type: timing_distribution
description: >
The amount of time taken to update the accessibility tree (ms)
This metric was generated to correspond to the Legacy Telemetry
exponential histogram A11Y_TREE_UPDATE_TIMING_MS.
time_unit: millisecond
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1424768
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1424768
notification_emails:
- asurkov@mozilla.com
expires: never
telemetry_mirror: A11Y_TREE_UPDATE_TIMING_MS

View file

@ -46,18 +46,18 @@ export class BlockedSiteParent extends JSWindowActorParent {
switch (elementId) {
case "goBackButton":
if (sendTelemetry) {
Services.telemetry
.getHistogramById("URLCLASSIFIER_UI_EVENTS")
.add(nsISecTel[bucketName + "GET_ME_OUT_OF_HERE"]);
Glean.urlclassifier.uiEvents.accumulateSingleSample(
nsISecTel[bucketName + "GET_ME_OUT_OF_HERE"]
);
}
browser.ownerGlobal.getMeOutOfHere(this.browsingContext);
break;
case "ignore_warning_link":
if (Services.prefs.getBoolPref("browser.safebrowsing.allowOverride")) {
if (sendTelemetry) {
Services.telemetry
.getHistogramById("URLCLASSIFIER_UI_EVENTS")
.add(nsISecTel[bucketName + "IGNORE_WARNING"]);
Glean.urlclassifier.uiEvents.accumulateSingleSample(
nsISecTel[bucketName + "IGNORE_WARNING"]
);
}
BrowserOnClick.ignoreWarningLink(
reason,

View file

@ -8,12 +8,12 @@
# See PermissionManager.cpp for more...
# UITour
# Bug 1557153: www.mozilla.org gets a special workaround in UITourChild.sys.mjs
# Bug 1837407: support.mozilla.org gets a special workaround for similar reasons.
origin uitour 1 https://www.mozilla.org
origin uitour 1 https://support.mozilla.org
origin uitour 1 about:home
origin uitour 1 about:newtab
# Bug 1942328: firefox.com needs the same privileges as mozilla.org
origin uitour 1 https://www.firefox.com
# XPInstall
origin install 1 https://addons.mozilla.org

View file

@ -458,7 +458,7 @@ pref("browser.urlbar.suggest.recentsearches", true);
pref("browser.urlbar.suggest.quickactions", true);
pref("browser.urlbar.deduplication.enabled", false);
pref("browser.urlbar.deduplication.thresholdDays", 7);
pref("browser.urlbar.deduplication.thresholdDays", 0);
#ifdef NIGHTLY_BUILD
pref("browser.urlbar.scotchBonnet.enableOverride", true);
@ -1354,13 +1354,6 @@ pref("browser.backspace_action", 2);
pref("intl.regional_prefs.use_os_locales", false);
// this will automatically enable inline spellchecking (if it is available) for
// editable elements in HTML
// 0 = spellcheck nothing
// 1 = check multi-line controls [default]
// 2 = check multi/single line controls
pref("layout.spellcheckDefault", 1);
pref("browser.send_pings", false);
pref("browser.geolocation.warning.infoURL", "https://www.mozilla.org/%LOCALE%/firefox/geolocation/");
@ -1851,7 +1844,7 @@ pref("browser.newtabpage.activity-stream.newNewtabExperience.colors", "#0090ED,#
// Default layout experimentation
pref("browser.newtabpage.activity-stream.newtabLayouts.variant-a", false);
pref("browser.newtabpage.activity-stream.newtabLayouts.variant-b", false);
pref("browser.newtabpage.activity-stream.newtabLayouts.variant-b", true);
// Discovery stream ad size experiment
pref("browser.newtabpage.activity-stream.newtabAdSize.variant-a", false);
@ -1968,6 +1961,8 @@ pref("browser.newtabpage.activity-stream.discoverystream.sections.locale-content
// List of regions that get section layout by default
pref("browser.newtabpage.activity-stream.discoverystream.sections.region-content-config", "");
pref("browser.newtabpage.activity-stream.discoverystream.sections.cards.enabled", true);
pref("browser.newtabpage.activity-stream.discoverystream.merino-provider.endpoint", "merino.services.mozilla.com");
// List of regions that get spocs by default.
@ -3142,11 +3137,11 @@ pref("devtools.debugger.features.async-live-stacks", false);
pref("devtools.debugger.show-content-scripts", false);
pref("devtools.debugger.hide-ignored-sources", false);
#if defined(NIGHTLY_BUILD)
pref("devtools.debugger.features.codemirror-next", true);
#else
pref("devtools.debugger.features.codemirror-next", false);
#endif
// When `true` the debugger editor uses Codemirror v6
// and when `false` the debugger editor uses Codemirror v5
// This should be removed once the CM5 code is cleaned up. See Bug 1943909
pref("devtools.debugger.features.codemirror-next", true);
// Disable autohide for DevTools popups and tooltips.
// This is currently not exposed by any UI to avoid making

View file

@ -388,7 +388,7 @@ customElements.define(
}
#createPrivateBrowsingCheckbox() {
const { onPrivateBrowsingAllowedChanged, grantPrivateBrowsingAllowed } =
const { grantPrivateBrowsingAllowed } =
this.notification.options.customElementOptions;
const doc = this.ownerDocument;
@ -396,6 +396,12 @@ customElements.define(
let checkboxEl = doc.createXULElement("checkbox");
checkboxEl.checked = grantPrivateBrowsingAllowed;
checkboxEl.addEventListener("CheckboxStateChange", () => {
// NOTE: the popupnotification instances will be reused
// and so the callback function is destructured here to
// avoid this custom element to prevent it from being
// garbage collected.
const { onPrivateBrowsingAllowedChanged } =
this.notification.options.customElementOptions;
onPrivateBrowsingAllowedChanged?.(checkboxEl.checked);
});
doc.l10n.setAttributes(

View file

@ -1377,11 +1377,16 @@ var gProtectionsHandler = {
/**
* Contains an array of smartblock compatible sites and information on the corresponding shim
* site is the compatible site
* sites is a list of compatible sites
* shimId is the id of the shim blocking content from the origin
* toggleDisplayName is the name shown for the toggle used for blocking/unblocking the origin
* displayName is the name shown for the toggle used for blocking/unblocking the origin
*/
smartblockEmbedInfo: [
{
sites: ["https://itisatracker.org"],
shimId: "EmbedTestShim",
displayName: "Test",
},
{
sites: ["https://www.instagram.com", "https://platform.instagram.com"],
shimId: "InstagramEmbed",
@ -1394,6 +1399,18 @@ var gProtectionsHandler = {
},
],
/**
* Keeps track of if a smartblock toggle has been clicked since the panel was opened. Resets
* everytime the panel is closed. Used for telemetry purposes.
*/
_hasClickedSmartBlockEmbedToggle: false,
/**
* Keeps track of what was responsible for opening the protections panel popup. Used for
* telemetry purposes.
*/
_protectionsPopupOpeningReason: null,
_protectionsPopup: null,
_initializePopup() {
if (!this._protectionsPopup) {
@ -1771,21 +1788,17 @@ var gProtectionsHandler = {
if (PrivateBrowsingUtils.isWindowPrivate(window)) {
return;
}
Services.telemetry
.getHistogramById("TRACKING_PROTECTION_SHIELD")
.add(value);
Glean.contentblocking.trackingProtectionShield.accumulateSingleSample(
value
);
},
cryptominersHistogramAdd(value) {
Services.telemetry
.getHistogramById("CRYPTOMINERS_BLOCKED_COUNT")
.add(value);
Glean.contentblocking.cryptominersBlockedCount[value].add(1);
},
fingerprintersHistogramAdd(value) {
Services.telemetry
.getHistogramById("FINGERPRINTERS_BLOCKED_COUNT")
.add(value);
Glean.contentblocking.fingerprintersBlockedCount[value].add(1);
},
handleProtectionsButtonEvent(event) {
@ -1799,7 +1812,7 @@ var gProtectionsHandler = {
return; // Left click, space or enter only
}
this.showProtectionsPopup({ event });
this.showProtectionsPopup({ event, openingReason: "shieldButtonClicked" });
},
onPopupShown(event) {
@ -1813,8 +1826,13 @@ var gProtectionsHandler = {
// remain collapsed.
this._insertProtectionsPanelInfoMessage(event);
// Record telemetry for open, don't record if the panel open is only a toast
if (!event.target.hasAttribute("toast")) {
Glean.securityUiProtectionspopup.openProtectionsPopup.record();
Glean.securityUiProtectionspopup.openProtectionsPopup.record({
openingReason: this._protectionsPopupOpeningReason,
smartblockEmbedTogglesShown:
!this._protectionsPopupSmartblockContainer.hidden,
});
}
ReportBrokenSite.updateParentMenu(event);
@ -1826,6 +1844,17 @@ var gProtectionsHandler = {
window.removeEventListener("focus", this, true);
this._protectionsPopupTPSwitch.removeEventListener("toggle", this);
}
// Record close telemetry, don't record for toasts
if (!event.target.hasAttribute("toast")) {
Glean.securityUiProtectionspopup.closeProtectionsPopup.record({
openingReason: this._protectionsPopupOpeningReason,
smartblockToggleClicked: this._hasClickedSmartBlockEmbedToggle,
});
}
this._hasClickedSmartBlockEmbedToggle = false;
this._protectionsPopupOpeningReason = null;
},
async onTrackingProtectionIconHoveredOrFocused() {
@ -2162,7 +2191,10 @@ var gProtectionsHandler = {
PanelMultiView.hidePopup(this._protectionsPopup);
// Open the full protections panel.
this.showProtectionsPopup({ event });
this.showProtectionsPopup({
event,
openingReason: "toastButtonClicked",
});
break;
}
},
@ -2229,7 +2261,10 @@ var gProtectionsHandler = {
if (gBrowser.selectedBrowser.browserId !== subject.browserId) {
break;
}
this.showProtectionsPopup();
this.showProtectionsPopup({
openingReason: "embedPlaceholderButton",
});
break;
}
},
@ -2467,11 +2502,19 @@ var gProtectionsHandler = {
// add functionality to toggle
toggle.addEventListener("toggle", event => {
let newToggleState = event.target.pressed;
if (newToggleState) {
this._sendUnblockMessageToSmartblock(shimId);
} else {
this._sendReblockMessageToSmartblock(shimId);
}
Glean.securityUiProtectionspopup.clickSmartblockembedsToggle.record({
isBlock: !newToggleState,
openingReason: this._protectionsPopupOpeningReason,
});
this._hasClickedSmartBlockEmbedToggle = true;
});
this._protectionsPopupSmartblockToggleContainer.insertAdjacentElement(
@ -2663,12 +2706,18 @@ var gProtectionsHandler = {
* A boolean to indicate if we need to open the protections
* popup as a toast. A toast only has a header section and
* will be hidden after a certain amount of time.
* openingReason:
* A string indicating why the panel was opened. Used for
* telemetry purposes.
*/
showProtectionsPopup(options = {}) {
const { event, toast } = options;
const { event, toast, openingReason } = options;
this._initializePopup();
// Set opening reason variable for telemetry
this._protectionsPopupOpeningReason = openingReason;
// Ensure we've updated category state based on the last blocking event:
if (this.hasOwnProperty("_lastEvent")) {
this.updatePanelForBlockingEvent(this._lastEvent);

View file

@ -28,7 +28,7 @@
persist="screenX screenY width height sizemode"
data-l10n-sync="true">
<head>
#if defined(NIGHTLY_BUILD) || defined(DEBUG)
#if defined(EARLY_BETA_OR_EARLIER) || defined(DEBUG)
<meta http-equiv="Content-Security-Policy" content="script-src-attr 'none' 'report-sample'" />
#endif

View file

@ -191,7 +191,7 @@ We use a few tricks and optimizations to help improve the perceived performance
2. When a tab hasnt ever been seen before, and is still in the process of loading (right now, dubiously checked by looking for the “busy” attribute on the ``<xul:tab>``) we show a blank content area until its layers are finally ready. The idea here is to shift perceived lag from the async tab switcher to the network by showing the blank space instead of the tab switch spinner.
3. “Warming” is a nascent optimization that will allow us to pre-emptively render and cache the layers for tabs that we think the user is likely to switch to soon. After a timeout (``browser.tabs.remote.warmup.unloadDelayMs``), “warmed” tabs that arent switched to have their layers unloaded and cleared from the cache.
3. “Warming” is a nascent optimization that will allow us to preemptively render and cache the layers for tabs that we think the user is likely to switch to soon. After a timeout (``browser.tabs.remote.warmup.unloadDelayMs``), “warmed” tabs that arent switched to have their layers unloaded and cleared from the cache.
4. On platforms that support ``occlusionstatechange`` events (as of this writing, only macOS) and ``sizemodechange`` events (Windows, macOS and Linux), we stop rendering the layers for the currently selected tab when the window is minimized or fully occluded by another window.

View file

@ -103,8 +103,16 @@ security.ui.protectionspopup:
type: event
description: >
How many times the protections panel was opened.
This event was generated to correspond to the Legacy Telemetry event
security.ui.protectionspopup.open#protections_popup.
extra_keys:
openingReason:
description: >
string representing how the protections panel was opened,
one of ["shieldButtonClicked", "embedPlaceholderButton", "toastButtonClicked"]
type: string
smartblockEmbedTogglesShown:
description: >
boolean representing if smartblock toggles were shown to the user
type: boolean
bugs: &security_ui_protectionspopup_open_bugs
- https://bugzil.la/1560327
- https://bugzil.la/1607488
@ -112,6 +120,7 @@ security.ui.protectionspopup:
- https://bugzil.la/1678201
- https://bugzil.la/1739287
- https://bugzil.la/1787249
- https://bugzil.la/1920735
data_reviews: &security_ui_protectionspopup_open_data_reviews
- https://bugzil.la/1560327
- https://bugzil.la/1607488
@ -119,18 +128,41 @@ security.ui.protectionspopup:
- https://bugzil.la/1678201
- https://bugzil.la/1739287
- https://bugzil.la/1787249
- https://bugzil.la/1920735
notification_emails: &security_ui_protectionspopup_open_emails
- emz@mozilla.com
- seceng-telemetry@mozilla.com
expires: never
telemetry_mirror: SecurityUiProtectionspopup_Open_ProtectionsPopup
close_protections_popup:
type: event
description: >
Triggered when the protections panel is closed. Records how the panel was opened and if
the SmartBlock section had any interactions
extra_keys:
openingReason:
description: >
string representing how the protections panel was opened,
one of ["shieldButtonClicked", "embedPlaceholderButton", "toastButtonClicked"]
type: string
smartblockToggleClicked:
description: >
boolean representing if the user interacted with the toggle anytime before it was closed
type: boolean
bugs:
- https://bugzil.la/1920735
data_reviews:
- https://bugzil.la/1920735
notification_emails:
- wwen@mozilla.com
- emz@mozilla.com
expires:
140
open_protectionspopup_cfr:
type: event
description: >
How many times the protections panel was opened.
This event was generated to correspond to the Legacy Telemetry event
security.ui.protectionspopup.open#protectionspopup_cfr.
bugs: *security_ui_protectionspopup_open_bugs
data_reviews: *security_ui_protectionspopup_open_data_reviews
notification_emails: *security_ui_protectionspopup_open_emails
@ -146,7 +178,6 @@ security.ui.protectionspopup:
For protectionspopup_cfr, the message ID.
type: string
telemetry_mirror: SecurityUiProtectionspopup_Open_ProtectionspopupCfr
click_etp_toggle_on:
type: event
@ -342,6 +373,44 @@ security.ui.protectionspopup:
extra_keys: *security_ui_protectionspopup_open_extra
telemetry_mirror: SecurityUiProtectionspopup_Click_ProtectionspopupCfr
click_smartblockembeds_toggle:
type: event
description: >
Triggered when SmartBlock embed toggles are clicked by the user
extra_keys:
isBlock:
description: >
boolean representing if this was a block or an unblock
type: boolean
openingReason:
description: >
string representing how the protections panel was opened,
one of ["shieldButtonClicked", "embedPlaceholderButton", "toastButtonClicked"]
type: string
bugs:
- https://bugzil.la/1920735
data_reviews:
- https://bugzil.la/1920735
notification_emails:
- wwen@mozilla.com
- emz@mozilla.com
expires:
140
smartblockembeds_shown:
type: counter
description: >
How many times the SmartBlock placeholders are shown on the page
bugs:
- https://bugzil.la/1920735
data_reviews:
- https://bugzil.la/1920735
notification_emails:
- wwen@mozilla.com
- emz@mozilla.com
expires:
140
browser.engagement:
bookmarks_toolbar_bookmark_added:
type: counter

View file

@ -0,0 +1,9 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
function handleRequest(request, response) {
response.setStatusLine(request.httpVersion, 401, "Unauthorized");
response.setHeader("WWW-Authenticate", 'Basic realm="foo"', false);
}

View file

@ -0,0 +1,38 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
function decode(str) {
return decodeURIComponent(str.replace(/\+/g, encodeURIComponent(" ")));
}
function handleRequest(request, response) {
const queryString = request.queryString;
let params = queryString.split("&").reduce((memo, pair) => {
let [key, val] = pair.split("=");
if (!val) {
val = key;
key = "query";
}
try {
memo[decode(key)] = decode(val);
} catch (e) {
memo[key] = val;
}
return memo;
}, {});
const status = parseInt(params.status);
const message = params.message;
// Set default if missing parameters
if (!status || !message) {
response.setStatusLine(request.httpVersion, 400, "Bad Request");
response.setHeader("Content-Length", "0", false);
return;
}
response.setStatusLine(request.httpVersion, status, message);
response.setHeader("Content-Length", "0", false);
}

View file

@ -56,6 +56,16 @@ skip-if = [
["browser_aboutNetError.js"]
["browser_aboutNetError_basicHttpAuth.js"]
support-files = [
"basic_auth_route.sjs",
]
["browser_aboutNetError_blank_page.js"]
support-files = [
"blank_page.sjs",
]
["browser_aboutNetError_csp_iframe.js"]
https_first_disabled = true
support-files = [

View file

@ -0,0 +1,75 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const AUTH_ROUTE =
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
"http://example.com/browser/browser/base/content/test/about/basic_auth_route.sjs";
// From appstrings.properties
const EXPECTED_SHORT_DESC =
"Someone pretending to be the site could try to steal things like your username, password, or email.";
add_task(async function test_basicHttpAuth() {
await SpecialPowers.pushPrefEnv({
set: [
// https first is disabled to enforce the scheme as http
["dom.security.https_first", false],
["network.http.basic_http_auth.enabled", false],
],
});
let browser;
let pageLoaded;
await BrowserTestUtils.openNewForegroundTab(
gBrowser,
() => {
gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, AUTH_ROUTE);
browser = gBrowser.selectedBrowser;
pageLoaded = BrowserTestUtils.waitForErrorPage(browser);
},
false
);
info("Loading and waiting for the net error");
await pageLoaded;
await SpecialPowers.spawn(
browser,
[EXPECTED_SHORT_DESC],
function (expectedShortDesc) {
const doc = content.document;
ok(
doc.documentURI.startsWith("about:neterror"),
"Should be showing error page"
);
const titleEl = doc.querySelector(".title-text");
const actualDataL10nID = titleEl.getAttribute("data-l10n-id");
is(
actualDataL10nID,
"general-body-title",
"Correct error page title is set"
);
// We use startsWith to account for the error code portion
const shortDesc = doc.getElementById("errorShortDesc");
ok(
shortDesc.textContent.startsWith(expectedShortDesc),
"Correct error page title is set"
);
const anchor = doc.querySelector("a");
const actualAnchorl10nID = anchor.getAttribute("data-l10n-id");
is(
actualAnchorl10nID,
"neterror-learn-more-link",
"Correct error link is set"
);
}
);
BrowserTestUtils.removeTab(gBrowser.selectedTab);
await SpecialPowers.popPrefEnv();
});

View file

@ -0,0 +1,76 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const BLANK_PAGE =
"https://example.com/browser/browser/base/content/test/about/blank_page.sjs";
async function test_blankPage(
page,
expectedL10nID,
responseStatus,
responseStatusText
) {
await SpecialPowers.pushPrefEnv({
set: [["browser.http.blank_page_with_error_response.enabled", false]],
});
let browser;
let pageLoaded;
const uri = `${page}?status=${encodeURIComponent(
responseStatus
)}&message=${encodeURIComponent(responseStatusText)}`;
// Simulating loading the page
await BrowserTestUtils.openNewForegroundTab(
gBrowser,
() => {
gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, uri);
browser = gBrowser.selectedBrowser;
pageLoaded = BrowserTestUtils.waitForErrorPage(browser);
},
false
);
info("Loading and waiting for the net error");
await pageLoaded;
await SpecialPowers.spawn(
browser,
[expectedL10nID, responseStatus, responseStatusText],
function (l10nID, expectedStatus, expectedText) {
const doc = content.document;
ok(
doc.documentURI.startsWith("about:neterror"),
"Should be showing error page"
);
const titleEl = doc.querySelector(".title-text");
const actualDataL10nID = titleEl.getAttribute("data-l10n-id");
is(actualDataL10nID, l10nID, "Correct error page title is set");
const expectedLabel =
"Error code: " + expectedStatus.toString() + " " + expectedText;
const actualLabel = doc.getElementById(
"response-status-label"
).textContent;
is(actualLabel, expectedLabel, "Correct response status message is set");
}
);
BrowserTestUtils.removeTab(gBrowser.selectedTab);
}
add_task(async function test_blankPage_4xx() {
await test_blankPage(BLANK_PAGE, "httpErrorPage-title", 400, "Bad Request");
});
add_task(async function test_blankPage_5xx() {
await test_blankPage(
BLANK_PAGE,
"serverError-title",
503,
"Service Unavailable"
);
});

View file

@ -187,6 +187,7 @@ add_task(async function testReloadButtonPress() {
// Test activation of the Sidebars button from the keyboard.
// This is a toolbarbutton with a command handler.
add_task(async function testSidebarsButtonPress() {
const { SidebarController } = window;
let sidebarRevampEnabled = Services.prefs.getBoolPref(
"sidebar.revamp",
false
@ -194,6 +195,12 @@ add_task(async function testSidebarsButtonPress() {
let sidebar, sidebarBox;
if (!sidebarRevampEnabled) {
CustomizableUI.addWidgetToArea("sidebar-button", "nav-bar");
} else {
// Expanded is only available with vertical tabs enabled
await SpecialPowers.pushPrefEnv({
set: [["sidebar.verticalTabs", true]],
});
await SidebarController.initializeUIState({ launcherExpanded: false });
}
let button = document.getElementById("sidebar-button");
ok(!button.checked, "Sidebars button not checked at start of test");
@ -238,6 +245,7 @@ add_task(async function testSidebarsButtonPress() {
"Sidebar not expanded after press"
);
ok(!sidebar.expanded, "Sidebar not expanded after press");
await SpecialPowers.popPrefEnv();
}
});

View file

@ -43,6 +43,8 @@ add_setup(async function () {
Services.telemetry.canRecordExtended = oldCanRecord;
Services.telemetry.clearEvents();
});
Services.fog.testResetFOG();
});
async function clickToggle(toggle) {
@ -63,16 +65,9 @@ add_task(async function testToggleSwitch() {
return gProtectionsHandler._protectionsPopup.hasAttribute("blocking");
});
let events = Services.telemetry.snapshotEvents(
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS
).parent;
let buttonEvents = events.filter(
e =>
e[1] == "security.ui.protectionspopup" &&
e[2] == "open" &&
e[3] == "protections_popup"
);
console.log(buttonEvents);
let buttonEvents =
Glean.securityUiProtectionspopup.openProtectionsPopup.testGetValue();
is(buttonEvents.length, 1, "recorded telemetry for opening the popup");
let browserLoadedPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);

View file

@ -34,6 +34,8 @@ add_setup(async function () {
Services.telemetry.canRecordExtended = oldCanRecord;
Services.telemetry.clearEvents();
});
Services.fog.testResetFOG();
});
add_task(async function testPanelInfoMessage() {
@ -68,17 +70,8 @@ add_task(async function testPanelInfoMessage() {
ok(BrowserTestUtils.isVisible(learnMoreLink), "The link should be visible.");
// Check telemetry for the info message
let events = Services.telemetry.snapshotEvents(
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
false
).parent;
let messageEvents = events.filter(
e =>
e[1] == "security.ui.protectionspopup" &&
e[2] == "open" &&
e[3] == "protectionspopup_cfr" &&
e[4] == "impression"
);
let messageEvents =
Glean.securityUiProtectionspopup.openProtectionspopupCfr.testGetValue();
is(
messageEvents.length,
1,

View file

@ -45,6 +45,10 @@ add_task(async function test_sidebar_in_customize_mode() {
});
}
if (Services.prefs.getBoolPref("sidebar.revamp", false)) {
Services.prefs.setBoolPref("sidebar.verticalTabs", true);
}
let widgetIcon = CustomizableUI.getWidget("sidebar-button")
.forWindow(window)
.node?.querySelector(".toolbarbutton-icon");
@ -109,4 +113,8 @@ add_task(async function test_sidebar_in_customize_mode() {
0,
"Sidebar widget background should appear unchecked"
);
if (Services.prefs.getBoolPref("sidebar.verticalTabs", false)) {
Services.prefs.clearUserPref("sidebar.verticalTabs");
}
});

View file

@ -2060,21 +2060,21 @@ BrowserGlue.prototype = {
let tpEnabled = Services.prefs.getBoolPref(
"privacy.trackingprotection.enabled"
);
Services.telemetry
.getHistogramById("TRACKING_PROTECTION_ENABLED")
.add(tpEnabled);
Glean.contentblocking.trackingProtectionEnabled[
tpEnabled ? "true" : "false"
].add();
let tpPBDisabled = Services.prefs.getBoolPref(
let tpPBEnabled = Services.prefs.getBoolPref(
"privacy.trackingprotection.pbmode.enabled"
);
Services.telemetry
.getHistogramById("TRACKING_PROTECTION_PBM_DISABLED")
.add(!tpPBDisabled);
Glean.contentblocking.trackingProtectionPbmDisabled[
!tpPBEnabled ? "true" : "false"
].add();
let cookieBehavior = Services.prefs.getIntPref(
"network.cookie.cookieBehavior"
);
Services.telemetry.getHistogramById("COOKIE_BEHAVIOR").add(cookieBehavior);
Glean.contentblocking.cookieBehavior.accumulateSingleSample(cookieBehavior);
let fpEnabled = Services.prefs.getBoolPref(
"privacy.trackingprotection.fingerprinting.enabled"

View file

@ -316,9 +316,9 @@ export var DownloadsCommon = {
download.error?.becauseBlockedByReputationCheck &&
download.hasBlockedData
) {
Services.telemetry
.getKeyedHistogramById("DOWNLOADS_USER_ACTION_ON_BLOCKED_DOWNLOAD")
.add(download.error.reputationCheckVerdict, 1); // confirm block
Glean.downloads.userActionOnBlockedDownload[
download.error.reputationCheckVerdict
].accumulateSingleSample(1); // confirm block
}
// Remove the associated history element first, if any, so that the views

View file

@ -608,9 +608,7 @@ class MigrationUtils {
);
let entrypoint = aOptions.entrypoint || this.MIGRATION_ENTRYPOINTS.UNKNOWN;
Services.telemetry
.getHistogramById("FX_MIGRATION_ENTRY_POINT_CATEGORICAL")
.add(entrypoint);
Glean.browserMigration.entryPointCategorical[entrypoint].add(1);
let openStandaloneWindow = blocking => {
let features = "dialog,centerscreen,resizable=no";

View file

@ -339,9 +339,9 @@ export class MigrationWizardParent extends JSWindowActorParent {
* updated.
*/
async #doBrowserMigration(migrationDetails, extraArgs) {
Services.telemetry
.getHistogramById("FX_MIGRATION_SOURCE_BROWSER")
.add(MigrationUtils.getSourceIdForTelemetry(migrationDetails.key));
Glean.browserMigration.sourceBrowser.accumulateSingleSample(
MigrationUtils.getSourceIdForTelemetry(migrationDetails.key)
);
let migrator = await MigrationUtils.getMigrator(migrationDetails.key);
let availableResourceTypes = await migrator.getMigrateData(
@ -349,8 +349,7 @@ export class MigrationWizardParent extends JSWindowActorParent {
);
let resourceTypesToMigrate = 0;
let progress = {};
let migrationUsageHist =
Services.telemetry.getKeyedHistogramById("FX_MIGRATION_USAGE");
let gleanMigrationUsage = Glean.browserMigration.usage;
for (let resourceTypeName of migrationDetails.resourceTypes) {
let resourceType = MigrationUtils.resourceTypes[resourceTypeName];
@ -362,7 +361,9 @@ export class MigrationWizardParent extends JSWindowActorParent {
};
if (!migrationDetails.autoMigration) {
migrationUsageHist.add(migrationDetails.key, Math.log2(resourceType));
gleanMigrationUsage[migrationDetails.key].accumulateSingleSample(
Math.log2(resourceType)
);
}
}
}
@ -455,9 +456,9 @@ export class MigrationWizardParent extends JSWindowActorParent {
);
} else {
if (!success) {
Services.telemetry
.getKeyedHistogramById("FX_MIGRATION_ERRORS")
.add(migrationDetails.key, Math.log2(resourceTypeNum));
Glean.browserMigration.errors[
migrationDetails.key
].accumulateSingleSample(Math.log2(resourceTypeNum));
}
if (
foundResourceTypeName ==

View file

@ -355,17 +355,13 @@ export class MigratorBase {
for (let resourceType of Object.keys(
lazy.MigrationUtils._importQuantities
)) {
let histogramId =
"FX_MIGRATION_" + resourceType.toUpperCase() + "_QUANTITY";
let metricName = resourceType + "Quantity";
try {
Services.telemetry
.getKeyedHistogramById(histogramId)
.add(
browserKey,
lazy.MigrationUtils._importQuantities[resourceType]
);
Glean.browserMigration[metricName][browserKey].accumulateSingleSample(
lazy.MigrationUtils._importQuantities[resourceType]
);
} catch (ex) {
console.error(histogramId, ": ", ex);
console.error(metricName, ": ", ex);
}
}
};

View file

@ -340,6 +340,248 @@ browser.migration:
type: quantity
telemetry_mirror: BrowserMigration_MigrationFinished_Wizard
entry_point_categorical:
type: labeled_counter
description: >
Where the migration wizard was entered from.
This metric was generated to correspond to the Legacy Telemetry
categorical histogram FX_MIGRATION_ENTRY_POINT_CATEGORICAL.
labels:
- unknown
- firstrun
- fxrefresh
- places
- passwords
- newtab
- file_menu
- help_menu
- bookmarks_toolbar
- preferences
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1822692
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1822692
notification_emails:
- mconley@mozilla.com
- gijs@mozilla.com
- mak@mozilla.com
expires: never
telemetry_mirror: h#FX_MIGRATION_ENTRY_POINT_CATEGORICAL
source_browser:
type: custom_distribution
description: >
The browser that data is pulled from. The values correspond to the
internal browser ID (see MigrationUtils.sys.mjs)
This metric was generated to correspond to the Legacy Telemetry enumerated
histogram FX_MIGRATION_SOURCE_BROWSER.
range_min: 0
range_max: 15
bucket_count: 16
histogram_type: linear
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=731025
- https://bugzilla.mozilla.org/show_bug.cgi?id=1523179
- https://bugzilla.mozilla.org/show_bug.cgi?id=1643431
- https://bugzilla.mozilla.org/show_bug.cgi?id=1678204
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=731025
- https://bugzilla.mozilla.org/show_bug.cgi?id=1523179
- https://bugzilla.mozilla.org/show_bug.cgi?id=1643431
- https://bugzilla.mozilla.org/show_bug.cgi?id=1678204
notification_emails:
- gijs@mozilla.com
- mak@mozilla.com
expires: never
telemetry_mirror: FX_MIGRATION_SOURCE_BROWSER
errors:
type: labeled_custom_distribution
description: >
Errors encountered during migration in buckets defined by the datatype,
keyed by the string description of the browser.
This metric was generated to correspond to the Legacy Telemetry enumerated
histogram FX_MIGRATION_ERRORS.
range_min: 0
range_max: 12
bucket_count: 13
histogram_type: linear
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=731025
- https://bugzilla.mozilla.org/show_bug.cgi?id=1584261
- https://bugzilla.mozilla.org/show_bug.cgi?id=1643431
- https://bugzilla.mozilla.org/show_bug.cgi?id=1678204
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=731025
- https://bugzilla.mozilla.org/show_bug.cgi?id=1584261
- https://bugzilla.mozilla.org/show_bug.cgi?id=1643431
- https://bugzilla.mozilla.org/show_bug.cgi?id=1678204
notification_emails:
- gijs@mozilla.com
- mak@mozilla.com
expires: never
telemetry_mirror: FX_MIGRATION_ERRORS
usage:
type: labeled_custom_distribution
description: >
Usage of migration for each datatype when migration is run through the
post-firstrun flow which allows individual datatypes, keyed by the string
description of the browser.
This metric was generated to correspond to the Legacy Telemetry enumerated
histogram FX_MIGRATION_USAGE.
range_min: 0
range_max: 12
bucket_count: 13
histogram_type: linear
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=731025
- https://bugzilla.mozilla.org/show_bug.cgi?id=1584261
- https://bugzilla.mozilla.org/show_bug.cgi?id=1643431
- https://bugzilla.mozilla.org/show_bug.cgi?id=1678204
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=731025
- https://bugzilla.mozilla.org/show_bug.cgi?id=1584261
- https://bugzilla.mozilla.org/show_bug.cgi?id=1643431
- https://bugzilla.mozilla.org/show_bug.cgi?id=1678204
notification_emails:
- gijs@mozilla.com
- mak@mozilla.com
expires: never
telemetry_mirror: FX_MIGRATION_USAGE
bookmarks_quantity:
type: labeled_custom_distribution
description: >
How many bookmarks we imported from another browser, keyed by the name of
the browser.
This metric was generated to correspond to the Legacy Telemetry
exponential histogram FX_MIGRATION_BOOKMARKS_QUANTITY.
range_min: 1
range_max: 1000
bucket_count: 20
histogram_type: exponential
unit: bookmarks
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1279501
- https://bugzilla.mozilla.org/show_bug.cgi?id=1643431
- https://bugzilla.mozilla.org/show_bug.cgi?id=1678204
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1279501
- https://bugzilla.mozilla.org/show_bug.cgi?id=1643431
- https://bugzilla.mozilla.org/show_bug.cgi?id=1678204
notification_emails:
- gijs@mozilla.com
- mak@mozilla.com
expires: never
telemetry_mirror: FX_MIGRATION_BOOKMARKS_QUANTITY
history_quantity:
type: labeled_custom_distribution
description: >
How many history visits we imported from another browser, keyed by the
name of the browser.
This metric was generated to correspond to the Legacy Telemetry
exponential histogram FX_MIGRATION_HISTORY_QUANTITY.
range_min: 1
range_max: 10000
bucket_count: 40
histogram_type: exponential
unit: history visits
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1279501
- https://bugzilla.mozilla.org/show_bug.cgi?id=1643431
- https://bugzilla.mozilla.org/show_bug.cgi?id=1678204
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1279501
- https://bugzilla.mozilla.org/show_bug.cgi?id=1643431
- https://bugzilla.mozilla.org/show_bug.cgi?id=1678204
notification_emails:
- gijs@mozilla.com
- mak@mozilla.com
expires: never
telemetry_mirror: FX_MIGRATION_HISTORY_QUANTITY
logins_quantity:
type: labeled_custom_distribution
description: >
How many logins (passwords) we imported from another browser, keyed by the
name of the browser.
This metric was generated to correspond to the Legacy Telemetry
exponential histogram FX_MIGRATION_LOGINS_QUANTITY.
range_min: 1
range_max: 1000
bucket_count: 20
histogram_type: exponential
unit: passwords
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1279501
- https://bugzilla.mozilla.org/show_bug.cgi?id=1584261
- https://bugzilla.mozilla.org/show_bug.cgi?id=1643431
- https://bugzilla.mozilla.org/show_bug.cgi?id=1678204
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1279501
- https://bugzilla.mozilla.org/show_bug.cgi?id=1584261
- https://bugzilla.mozilla.org/show_bug.cgi?id=1643431
- https://bugzilla.mozilla.org/show_bug.cgi?id=1678204
notification_emails:
- gijs@mozilla.com
- mak@mozilla.com
- passwords-dev@mozilla.org
expires: never
telemetry_mirror: FX_MIGRATION_LOGINS_QUANTITY
cards_quantity:
type: labeled_custom_distribution
description: >
How many credit card entries we imported from another browser, keyed by
the name of the browser.
This metric was generated to correspond to the Legacy Telemetry
exponential histogram FX_MIGRATION_CARDS_QUANTITY.
range_min: 1
range_max: 1000
bucket_count: 20
histogram_type: exponential
unit: credit card entries
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1834545
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1834545
notification_emails:
- mconley@mozilla.com
expires: never
telemetry_mirror: FX_MIGRATION_CARDS_QUANTITY
extensions_quantity:
type: labeled_custom_distribution
description: >
How many extensions were matched to be imported from another browser,
keyed by the name of the browser.
This metric was generated to correspond to the Legacy Telemetry
exponential histogram FX_MIGRATION_EXTENSIONS_QUANTITY.
range_min: 1
range_max: 1000
bucket_count: 20
histogram_type: exponential
unit: extensions
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1834545
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1834545
notification_emails:
- mconley@mozilla.com
expires: never
telemetry_mirror: FX_MIGRATION_EXTENSIONS_QUANTITY
migration:
uninstaller_profile_refresh:
type: boolean

View file

@ -5,8 +5,11 @@
import { actionTypes as at } from "resource://activity-stream/common/Actions.mjs";
import { Dedupe } from "resource://activity-stream/common/Dedupe.sys.mjs";
export const TOP_SITES_DEFAULT_ROWS = 1;
export const TOP_SITES_MAX_SITES_PER_ROW = 8;
export {
TOP_SITES_DEFAULT_ROWS,
TOP_SITES_MAX_SITES_PER_ROW,
} from "resource:///modules/topsites/constants.mjs";
const PREF_COLLECTION_DISMISSIBLE = "discoverystream.isCollectionDismissible";
const dedupe = new Dedupe(site => site && site.url);
@ -174,43 +177,6 @@ function App(prevState = INITIAL_STATE.App, action) {
}
}
/**
* insertPinned - Inserts pinned links in their specified slots
*
* @param {array} a list of links
* @param {array} a list of pinned links
* @return {array} resulting list of links with pinned links inserted
*/
export function insertPinned(links, pinned) {
// Remove any pinned links
const pinnedUrls = pinned.map(link => link && link.url);
let newLinks = links.filter(link =>
link ? !pinnedUrls.includes(link.url) : false
);
newLinks = newLinks.map(link => {
if (link && link.isPinned) {
delete link.isPinned;
delete link.pinIndex;
}
return link;
});
// Then insert them in their specified location
pinned.forEach((val, index) => {
if (!val) {
return;
}
let link = Object.assign({}, val, { isPinned: true, pinIndex: index });
if (index > newLinks.length) {
newLinks[index] = link;
} else {
newLinks.splice(index, 0, link);
}
});
return newLinks;
}
function TopSites(prevState = INITIAL_STATE.TopSites, action) {
let hasMatch;
let newRows;

View file

@ -209,7 +209,7 @@ function SectionsMgmtPanel({ exitEventFired }) {
? onUnfollowClick(sectionKey, receivedRank)
: onFollowClick(sectionKey, receivedRank)
}
type={following ? "destructive" : "default"}
type={"default"}
index={receivedRank}
section={sectionKey}
id={`follow-topic-${sectionKey}`}

View file

@ -398,13 +398,12 @@
// Typography
h3 {
font-size: inherit;
margin-block: 0 var(--space-small);
}
// List
.topic-list {
@include wallpaper-contrast-fix;
list-style: none;
display: flex;
flex-direction: column;
@ -416,6 +415,7 @@
li {
display: flex;
justify-content: space-between;
align-items: center;
}
}
@ -453,8 +453,6 @@
// Follow / Unfollow and Block / Unblock Buttons
.section-block,
.section-follow {
cursor: pointer;
.section-button-blocked-text,
.section-button-following-text {
display: none;

View file

@ -369,7 +369,7 @@ $calculated-max-width-twice-widest: $break-point-widest + 2 * $card-width;
}
&:not(.sponsored) .sponsored-label {
visibility: hidden;
display: none;
}
}
@ -711,23 +711,25 @@ $calculated-max-width-twice-widest: $break-point-widest + 2 * $card-width;
align-items: center;
}
span[dir='auto'] {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-height: 20px;
width: 100px;
padding: 0 var(--space-xsmall);
.title {
.title-label {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-height: 2lh;
width: 100px;
padding: 0 var(--space-xsmall);
}
&.sponsored .title-label {
min-height: 1lh;
}
}
&:hover,
&:focus-within {
.title:not(.sponsored) {
.sponsored-label {
display: none;
}
.title-label {
-webkit-line-clamp: 2;
white-space: wrap;

View file

@ -1075,7 +1075,7 @@ main section {
font-size: 0.9em;
}
.top-site-outer .title:not(.sponsored) .sponsored-label {
visibility: hidden;
display: none;
}
.top-site-outer.search-shortcut .rich-icon {
background-color: #FFF;
@ -1338,17 +1338,17 @@ main section {
justify-content: center;
align-items: center;
}
.top-sites-list .top-site-outer span[dir=auto] {
.top-sites-list .top-site-outer .title .title-label {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-height: 20px;
min-height: 2lh;
width: 100px;
padding: 0 var(--space-xsmall);
}
.top-sites-list .top-site-outer:hover .title:not(.sponsored) .sponsored-label, .top-sites-list .top-site-outer:focus-within .title:not(.sponsored) .sponsored-label {
display: none;
.top-sites-list .top-site-outer .title.sponsored .title-label {
min-height: 1lh;
}
.top-sites-list .top-site-outer:hover .title:not(.sponsored) .title-label, .top-sites-list .top-site-outer:focus-within .title:not(.sponsored) .title-label {
-webkit-line-clamp: 2;
@ -2317,6 +2317,7 @@ main section {
transform: translateX(0);
}
.sections-mgmt-panel h3 {
font-size: inherit;
margin-block: 0 var(--space-small);
}
.sections-mgmt-panel .topic-list {
@ -2328,15 +2329,10 @@ main section {
padding-inline: 0;
width: 100%;
}
.lightWallpaper .sections-mgmt-panel .topic-list {
color-scheme: light;
}
.darkWallpaper .sections-mgmt-panel .topic-list {
color-scheme: dark;
}
.sections-mgmt-panel .topic-list li {
display: flex;
justify-content: space-between;
align-items: center;
}
.sections-mgmt-panel .topic-list-empty-state {
display: block;
@ -2362,10 +2358,6 @@ main section {
margin-block: 0;
font-weight: var(--button-font-weight);
}
.sections-mgmt-panel .section-block,
.sections-mgmt-panel .section-follow {
cursor: pointer;
}
.sections-mgmt-panel .section-block .section-button-blocked-text,
.sections-mgmt-panel .section-block .section-button-following-text,
.sections-mgmt-panel .section-follow .section-button-blocked-text,

View file

@ -6557,6 +6557,14 @@ class Dedupe {
}
}
;// CONCATENATED MODULE: ../topsites/constants.mjs
/* 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/. */
const TOP_SITES_DEFAULT_ROWS = 1;
const TOP_SITES_MAX_SITES_PER_ROW = 8;
;// CONCATENATED MODULE: ./common/Reducers.sys.mjs
/* 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
@ -6565,8 +6573,8 @@ class Dedupe {
const TOP_SITES_DEFAULT_ROWS = 1;
const TOP_SITES_MAX_SITES_PER_ROW = 8;
const PREF_COLLECTION_DISMISSIBLE = "discoverystream.isCollectionDismissible";
const dedupe = new Dedupe(site => site && site.url);
@ -6734,43 +6742,6 @@ function App(prevState = INITIAL_STATE.App, action) {
}
}
/**
* insertPinned - Inserts pinned links in their specified slots
*
* @param {array} a list of links
* @param {array} a list of pinned links
* @return {array} resulting list of links with pinned links inserted
*/
function insertPinned(links, pinned) {
// Remove any pinned links
const pinnedUrls = pinned.map(link => link && link.url);
let newLinks = links.filter(link =>
link ? !pinnedUrls.includes(link.url) : false
);
newLinks = newLinks.map(link => {
if (link && link.isPinned) {
delete link.isPinned;
delete link.pinIndex;
}
return link;
});
// Then insert them in their specified location
pinned.forEach((val, index) => {
if (!val) {
return;
}
let link = Object.assign({}, val, { isPinned: true, pinIndex: index });
if (index > newLinks.length) {
newLinks[index] = link;
} else {
newLinks.splice(index, 0, link);
}
});
return newLinks;
}
function TopSites(prevState = INITIAL_STATE.TopSites, action) {
let hasMatch;
let newRows;
@ -10777,7 +10748,7 @@ function SectionsMgmtPanel({
className: following ? "section-follow following" : "section-follow"
}, /*#__PURE__*/external_React_default().createElement("moz-button", {
onClick: () => following ? onUnfollowClick(sectionKey, receivedRank) : onFollowClick(sectionKey, receivedRank),
type: following ? "destructive" : "default",
type: "default",
index: receivedRank,
section: sectionKey,
id: `follow-topic-${sectionKey}`

View file

@ -558,6 +558,13 @@ export const PREFS_CONFIG = new Map([
value: "",
},
],
[
"discoverystream.sections.topicSelection.enabled",
{
title: "Boolean flag to enable inline topic selection",
value: false,
},
],
[
"discoverystream.spoc-positions",
{

View file

@ -8,7 +8,7 @@ import { shortURL } from "resource://activity-stream/lib/ShortURL.sys.mjs";
import {
TOP_SITES_DEFAULT_ROWS,
TOP_SITES_MAX_SITES_PER_ROW,
} from "resource://activity-stream/common/Reducers.sys.mjs";
} from "resource:///modules/topsites/constants.mjs";
import { Dedupe } from "resource://activity-stream/common/Dedupe.sys.mjs";
const lazy = {};

View file

@ -7,10 +7,8 @@ import {
actionTypes as at,
} from "resource://activity-stream/common/Actions.mjs";
import { TippyTopProvider } from "resource:///modules/topsites/TippyTopProvider.sys.mjs";
import {
insertPinned,
TOP_SITES_MAX_SITES_PER_ROW,
} from "resource://activity-stream/common/Reducers.sys.mjs";
import { insertPinned } from "resource:///modules/topsites/TopSites.sys.mjs";
import { TOP_SITES_MAX_SITES_PER_ROW } from "resource:///modules/topsites/constants.mjs";
import { Dedupe } from "resource://activity-stream/common/Dedupe.sys.mjs";
import {
shortURL,

View file

@ -1,4 +1,4 @@
import { INITIAL_STATE, insertPinned, reducers } from "common/Reducers.sys.mjs";
import { INITIAL_STATE, reducers } from "common/Reducers.sys.mjs";
const {
TopSites,
App,
@ -735,76 +735,6 @@ describe("Reducers", () => {
assert.equal(oldRow, oldState[0].rows[1]);
});
});
describe("#insertPinned", () => {
let links;
beforeEach(() => {
links = new Array(12).fill(null).map((v, i) => ({ url: `site${i}.com` }));
});
it("should place pinned links where they belong", () => {
const pinned = [
{ url: "http://github.com/mozilla/activity-stream", title: "moz/a-s" },
{ url: "http://example.com", title: "example" },
];
const result = insertPinned(links, pinned);
for (let index of [0, 1]) {
assert.equal(result[index].url, pinned[index].url);
assert.ok(result[index].isPinned);
assert.equal(result[index].pinIndex, index);
}
assert.deepEqual(result.slice(2), links);
});
it("should handle empty slots in the pinned list", () => {
const pinned = [
null,
{ url: "http://github.com/mozilla/activity-stream", title: "moz/a-s" },
null,
null,
{ url: "http://example.com", title: "example" },
];
const result = insertPinned(links, pinned);
for (let index of [1, 4]) {
assert.equal(result[index].url, pinned[index].url);
assert.ok(result[index].isPinned);
assert.equal(result[index].pinIndex, index);
}
result.splice(4, 1);
result.splice(1, 1);
assert.deepEqual(result, links);
});
it("should handle a pinned site past the end of the list of links", () => {
const pinned = [];
pinned[11] = {
url: "http://github.com/mozilla/activity-stream",
title: "moz/a-s",
};
const result = insertPinned([], pinned);
assert.equal(result[11].url, pinned[11].url);
assert.isTrue(result[11].isPinned);
assert.equal(result[11].pinIndex, 11);
});
it("should unpin previously pinned links no longer in the pinned list", () => {
const pinned = [];
links[2].isPinned = true;
links[2].pinIndex = 2;
const result = insertPinned(links, pinned);
assert.notProperty(result[2], "isPinned");
assert.notProperty(result[2], "pinIndex");
});
it("should handle a link present in both the links and pinned list", () => {
const pinned = [links[7]];
const result = insertPinned(links, pinned);
assert.equal(links.length, result.length);
});
it("should not modify the original data", () => {
const pinned = [{ url: "http://example.com" }];
insertPinned(links, pinned);
assert.equal(typeof pinned[0].isPinned, "undefined");
});
});
describe("Pocket", () => {
it("should return INITIAL_STATE by default", () => {
assert.equal(

View file

@ -551,6 +551,9 @@ const TEST_GLOBAL = {
removeExpirationFilter() {},
},
Logger: FakeLogger,
LinksCache: class {},
FaviconFeed: class {},
getFxAccountsSingleton() {},
AboutNewTab: {},
Glean: {

View file

@ -239,11 +239,11 @@ add_task(async function test_cache_worker() {
let root = doc.getElementById("root");
ok(root.childElementCount, "There are children on the root node");
// There should be the 1 top story, and 20 placeholders.
// There should be the 1 top story, and 23 placeholders.
equal(
Array.from(root.querySelectorAll(".ds-card")).length,
21,
"There are 21 DSCards"
24,
"There are 24 DSCards"
);
let cardHostname = doc.querySelector(
"[data-section-id='topstories'] .source"
@ -251,7 +251,7 @@ add_task(async function test_cache_worker() {
equal(cardHostname, "bbc.com", "Card hostname is bbc.com");
let placeholders = doc.querySelectorAll(".ds-card.placeholder");
equal(placeholders.length, 20, "There should be 20 placeholders");
equal(placeholders.length, 23, "There should be 23 placeholders");
});
/**

View file

@ -22,9 +22,8 @@ ChromeUtils.defineESModuleGetters(this, {
Screenshots: "resource://activity-stream/lib/Screenshots.sys.mjs",
Sampling: "resource://gre/modules/components-utils/Sampling.sys.mjs",
SearchService: "resource://gre/modules/SearchService.sys.mjs",
TOP_SITES_DEFAULT_ROWS: "resource://activity-stream/common/Reducers.sys.mjs",
TOP_SITES_MAX_SITES_PER_ROW:
"resource://activity-stream/common/Reducers.sys.mjs",
TOP_SITES_DEFAULT_ROWS: "resource:///modules/topsites/constants.mjs",
TOP_SITES_MAX_SITES_PER_ROW: "resource:///modules/topsites/constants.mjs",
});
const FAKE_FAVICON = "data987";

View file

@ -26,6 +26,10 @@ module.exports = (env = {}) => ({
new RegExp("^resource://activity-stream/"),
path.join(__dirname, "./"),
],
[
new RegExp("^resource:///modules/topsites/"),
path.join(__dirname, "../topsites/"),
],
],
}),
new webpack.BannerPlugin(

View file

@ -20,6 +20,10 @@ var gAppManagerDialog = {
let gMainPane = window.parent.gMainPane;
document
.getElementById("cmd_remove")
.addEventListener("command", () => this.remove());
const appDescElem = document.getElementById("appDescription");
if (this.handlerInfo.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) {
let { typeDescription } = this.handlerInfo;
@ -43,6 +47,7 @@ var gAppManagerDialog = {
}
let list = document.getElementById("appList");
list.addEventListener("select", () => this.onSelect());
let listFragment = document.createDocumentFragment();
for (let app of this.handlerInfo.possibleApplicationHandlers.enumerate()) {
if (!gMainPane.isValidHandlerApp(app)) {
@ -127,3 +132,5 @@ var gAppManagerDialog = {
document.l10n.setAttributes(appTypeElem, l10nId);
},
};
window.addEventListener("load", () => gAppManagerDialog.onLoad());

View file

@ -6,9 +6,9 @@
<window
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
onload="gAppManagerDialog.onLoad();"
data-l10n-id="app-manager-window2"
data-l10n-attrs="title, style"
csp="default-src chrome:; img-src chrome: moz-icon: http: https:; style-src chrome: 'unsafe-inline';"
>
<dialog id="appManager" buttons="accept,cancel">
<linkset>
@ -29,11 +29,7 @@
<script src="chrome://browser/content/preferences/dialogs/applicationManager.js" />
<commandset id="appManagerCommandSet">
<command
id="cmd_remove"
oncommand="gAppManagerDialog.remove();"
disabled="true"
/>
<command id="cmd_remove" disabled="true" />
</commandset>
<keyset id="appManagerKeyset">
@ -43,11 +39,7 @@
<description id="appDescription" />
<separator class="thin" />
<hbox flex="1">
<richlistbox
id="appList"
onselect="gAppManagerDialog.onSelect();"
flex="1"
/>
<richlistbox id="appList" flex="1" />
<vbox>
<button
id="remove"

View file

@ -166,6 +166,8 @@ class TestSessionRestore(SessionStoreTestCase):
self.marionette.execute_script(
"""
Services.prefs.setBoolPref("sidebar.revamp", true);
// Always show is only available with vertical tabs
Services.prefs.setBoolPref("sidebar.verticalTabs", true);
Services.prefs.setBoolPref("sidebar.animation.enabled", false);
Services.prefs.setStringPref("sidebar.visibility", "always-show");
"""
@ -182,7 +184,6 @@ class TestSessionRestore(SessionStoreTestCase):
self.marionette.execute_script(
"""
const window = BrowserWindowTracker.getTopWindow();
window.SidebarController.toolbarButton.click();
return window.SidebarController.sidebarMain.expanded;
"""
),
@ -228,7 +229,6 @@ class TestSessionRestore(SessionStoreTestCase):
self.marionette.execute_script(
"""
const window = BrowserWindowTracker.getTopWindow();
window.SidebarController.toolbarButton.click();
return window.SidebarController.sidebarContainer.hidden;
"""
),

View file

@ -6,6 +6,7 @@ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const BACKUP_STATE_PREF = "sidebar.backupState";
const VISIBILITY_SETTING_PREF = "sidebar.visibility";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
@ -19,6 +20,12 @@ XPCOMUtils.defineLazyPreferenceGetter(
"sidebarBackupState",
BACKUP_STATE_PREF
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"verticalTabsEnabled",
"sidebar.verticalTabs",
false
);
export const SidebarManager = {
/**
@ -66,6 +73,10 @@ export const SidebarManager = {
setPref(pref, lazy.NimbusFeatures[featureId].getVariable(pref))
);
});
if (!lazy.verticalTabsEnabled) {
Services.prefs.setStringPref(VISIBILITY_SETTING_PREF, "hide-sidebar");
}
},
/**

View file

@ -2,6 +2,16 @@
* 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/. */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"verticalTabsEnabled",
"sidebar.verticalTabs"
);
/**
* The properties that make up a sidebar's UI state.
*
@ -43,7 +53,6 @@ export class SidebarState {
launcherDragActive: false,
launcherHoverActive: false,
};
#previousExpandedState = false;
#previousLauncherVisible = undefined;
/**
@ -103,11 +112,7 @@ export class SidebarState {
if (isPopup) {
// Don't show launcher if we're in a popup window.
this.launcherVisible = false;
} else if (this.revampVisibility === "hide-sidebar" && !this.panelOpen) {
// When using "Show and hide sidebar", launcher will start off hidden.
this.launcherVisible = false;
} else if (this.revampVisibility === "always-show") {
// When using "Expand and collapse sidebar", launcher must be visible.
} else {
this.launcherVisible = true;
}
// Ensure that tab container has the updated value of `launcherExpanded`.
@ -188,19 +193,11 @@ export class SidebarState {
// Launcher must be visible to open a panel.
this.#previousLauncherVisible = this.launcherVisible;
this.launcherVisible = true;
// Whenever a panel is shown, the sidebar is collapsed. Upon hiding
// that panel afterwards, `expanded` reverts back to what it was prior
// to calling `show()`. Thus, we store the expanded state at this point.
this.#previousExpandedState = this.launcherExpanded;
this.launcherExpanded = false;
} else if (this.revampVisibility === "hide-sidebar") {
// When visibility is set to "Hide Sidebar", revert back to an expanded state except
// when a panel was opened via keyboard shortcut and the launcher was previously hidden.
this.launcherExpanded = this.#previousLauncherVisible;
this.launcherExpanded = lazy.verticalTabsEnabled
? this.#previousLauncherVisible
: false;
this.launcherVisible = this.#previousLauncherVisible;
} else {
this.launcherExpanded = this.#previousExpandedState;
}
}
@ -229,7 +226,7 @@ export class SidebarState {
// If we are hiding the sidebar because we removed the toolbar button, close everything
this.#previousLauncherVisible = false;
this.launcherVisible = false;
this.launcherExpanded = true;
this.launcherExpanded = false;
if (this.panelOpen) {
this.#controller.hide();
@ -241,8 +238,8 @@ export class SidebarState {
// customize panel, we don't want to close anything on them.
return;
}
// we need this set to true to ensure it has the correct state when toggling the sidebar button
this.launcherExpanded = true;
// we need this set to verticalTabsEnabled to ensure it has the correct state when toggling the sidebar button
this.launcherExpanded = lazy.verticalTabsEnabled;
if (!visible && this.panelOpen) {
// Hiding the launcher should also close out any open panels and resets panelOpen
@ -302,6 +299,8 @@ export class SidebarState {
this.#props.launcherDragActive = active;
if (active) {
this.#launcherEl.toggleAttribute("customWidth", true);
} else if (!lazy.verticalTabsEnabled) {
this.launcherExpanded = false;
} else if (this.launcherWidth < LAUNCHER_MINIMUM_WIDTH) {
// Snap back to collapsed state when the new width is too narrow.
this.launcherExpanded = false;
@ -327,7 +326,11 @@ export class SidebarState {
) {
this.#panelEl.style.maxWidth = `calc(${SIDEBAR_MAXIMUM_WIDTH} - ${width}px)`;
// Expand the launcher when it gets wide enough.
this.launcherExpanded = width >= LAUNCHER_MINIMUM_WIDTH;
if (lazy.verticalTabsEnabled) {
this.launcherExpanded = width >= LAUNCHER_MINIMUM_WIDTH;
} else {
this.launcherExpanded = false;
}
}
}

View file

@ -225,6 +225,7 @@ var SidebarController = {
POSITION_START_PREF: "sidebar.position_start",
DEFAULT_SIDEBAR_ID: "viewBookmarksSidebar",
TOOLS_PREF: "sidebar.main.tools",
VISIBILITY_PREF: "sidebar.visibility",
// lastOpenedId is set in show() but unlike currentID it's not cleared out on hide
// and isn't persisted across windows
@ -1086,7 +1087,11 @@ var SidebarController = {
let sidebarToggleKey = document.getElementById("toggleSidebarKb");
const shortcut = ShortcutUtils.prettifyShortcut(sidebarToggleKey);
toolbarButton.dataset.l10nArgs = JSON.stringify({ shortcut });
toolbarButton.toggleAttribute("expanded", this.sidebarMain.expanded);
if (this.sidebarVerticalTabsEnabled) {
toolbarButton.toggleAttribute("expanded", this.sidebarMain.expanded);
} else {
toolbarButton.toggleAttribute("expanded", false);
}
switch (this.sidebarRevampVisibility) {
case "always-show":
// Toolbar button controls expanded state.
@ -1815,7 +1820,11 @@ XPCOMUtils.defineLazyPreferenceGetter(
SidebarController.updateToolbarButton();
SidebarController.recordVisibilitySetting(newValue);
SidebarController._state.revampVisibility = newValue;
SidebarController._state.updateVisibility(newValue != "hide-sidebar");
SidebarController._state.updateVisibility(
(newValue != "hide-sidebar" &&
SidebarController.sidebarVerticalTabsEnabled) ||
!SidebarController.sidebarVerticalTabsEnabled
);
}
}
);
@ -1826,6 +1835,10 @@ XPCOMUtils.defineLazyPreferenceGetter(
false,
(_aPreference, _previousValue, newValue) => {
if (!SidebarController.uninitializing) {
Services.prefs.setStringPref(
SidebarController.VISIBILITY_PREF,
newValue ? "always-show" : "hide-sidebar"
);
SidebarController.recordTabsLayoutSetting(newValue);
}
}

View file

@ -9,6 +9,12 @@ import { SidebarPage } from "./sidebar-page.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://global/content/elements/moz-radio-group.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs",
});
const l10nMap = new Map([
["viewGenaiChatSidebar", "sidebar-menu-genai-chat-label"],
["viewReviewCheckerSidebar", "sidebar-menu-review-checker-label"],
@ -27,12 +33,14 @@ export class SidebarCustomize extends SidebarPage {
VISIBILITY_SETTING_PREF,
"always-show"
);
this.verticalTabsEnabled = lazy.CustomizableUI.verticalTabsEnabled;
this.boundObserve = (...args) => this.observe(...args);
}
static properties = {
activeExtIndex: { type: Number },
visibility: { type: String },
verticalTabsEnabled: { type: Boolean },
};
static queries = {
@ -49,6 +57,7 @@ export class SidebarCustomize extends SidebarPage {
this.getWindow().addEventListener("SidebarItemChanged", this);
this.getWindow().addEventListener("SidebarItemRemoved", this);
Services.prefs.addObserver(VISIBILITY_SETTING_PREF, this.boundObserve);
Services.obs.addObserver(this.boundObserve, "tabstrip-orientation-change");
}
disconnectedCallback() {
@ -56,6 +65,10 @@ export class SidebarCustomize extends SidebarPage {
this.getWindow().removeEventListener("SidebarItemAdded", this);
this.getWindow().removeEventListener("SidebarItemChanged", this);
this.getWindow().removeEventListener("SidebarItemRemoved", this);
Services.obs.removeObserver(
this.boundObserve,
"tabstrip-orientation-change"
);
Services.prefs.removeObserver(VISIBILITY_SETTING_PREF, this.boundObserve);
}
@ -71,6 +84,9 @@ export class SidebarCustomize extends SidebarPage {
break;
}
break;
case "tabstrip-orientation-change":
this.verticalTabsEnabled = lazy.CustomizableUI.verticalTabsEnabled;
break;
}
}
@ -246,28 +262,32 @@ export class SidebarCustomize extends SidebarPage {
</div>
</div>`
)}
<div class="customize-group">
<moz-radio-group
@change=${this.#handleVisibilityChange}
name="visibility"
data-l10n-id="sidebar-customize-button-header"
>
<moz-radio
class="visibility-setting"
value="always-show"
?checked=${this.visibility === "always-show"}
iconsrc="chrome://browser/skin/sidebar-expanded.svg"
data-l10n-id="sidebar-visibility-setting-always-show"
></moz-radio>
<moz-radio
class="visibility-setting"
value="hide-sidebar"
?checked=${this.visibility === "hide-sidebar"}
iconsrc="chrome://browser/skin/sidebar-hidden.svg"
data-l10n-id="sidebar-visibility-setting-hide-sidebar"
></moz-radio>
</moz-radio-group>
</div>
${when(
this.verticalTabsEnabled,
() =>
html`<div class="customize-group">
<moz-radio-group
@change=${this.#handleVisibilityChange}
name="visibility"
data-l10n-id="sidebar-customize-button-header"
>
<moz-radio
class="visibility-setting"
value="always-show"
?checked=${this.visibility === "always-show"}
iconsrc="chrome://browser/skin/sidebar-expanded.svg"
data-l10n-id="sidebar-visibility-setting-always-show"
></moz-radio>
<moz-radio
class="visibility-setting"
value="hide-sidebar"
?checked=${this.visibility === "hide-sidebar"}
iconsrc="chrome://browser/skin/sidebar-hidden.svg"
data-l10n-id="sidebar-visibility-setting-hide-sidebar"
></moz-radio>
</moz-radio-group>
</div>`
)}
<div class="customize-group">
<moz-radio-group
@change=${this.reversePosition}

View file

@ -205,6 +205,9 @@ add_task(async function test_customize_position_setting() {
});
add_task(async function test_customize_visibility_setting() {
await SpecialPowers.pushPrefEnv({
set: [[TAB_DIRECTION_PREF, true]],
});
const deferredPrefChange = Promise.withResolvers();
const prefObserver = () => deferredPrefChange.resolve();
Services.prefs.addObserver(SIDEBAR_VISIBILITY_PREF, prefObserver);
@ -235,6 +238,7 @@ add_task(async function test_customize_visibility_setting() {
await BrowserTestUtils.closeWindow(newWin);
Services.prefs.clearUserPref(SIDEBAR_VISIBILITY_PREF);
await SpecialPowers.popPrefEnv();
});
add_task(async function test_vertical_tabs_setting() {

View file

@ -6,6 +6,9 @@ const { DOMFullscreenTestUtils } = ChromeUtils.importESModule(
let win;
add_setup(async () => {
await SpecialPowers.pushPrefEnv({
set: [["sidebar.verticalTabs", true]],
});
DOMFullscreenTestUtils.init(this, window);
win = await BrowserTestUtils.openNewBrowserWindow();
await waitForBrowserWindowActive(win);

View file

@ -7,6 +7,8 @@ requestLongerTimeout(10);
const lazy = {};
const TAB_DIRECTION_PREF = "sidebar.verticalTabs";
ChromeUtils.defineESModuleGetters(lazy, {
TabsSetupFlowManager:
"resource:///modules/firefox-view-tabs-setup-manager.sys.mjs",
@ -41,6 +43,10 @@ add_task(async function test_metrics_initialized() {
});
add_task(async function test_sidebar_expand() {
await SpecialPowers.pushPrefEnv({
set: [[TAB_DIRECTION_PREF, true]],
});
// Vertical tabs are expanded by default
await SidebarController.initializeUIState({ launcherExpanded: false });
info("Expand the sidebar.");
@ -58,7 +64,9 @@ add_task(async function test_sidebar_expand() {
);
const events = Glean.sidebar.expand.testGetValue();
Assert.equal(events?.length, 1, "One event was reported.");
Assert.equal(events?.length, 2, "Two events were reported.");
await SpecialPowers.popPrefEnv();
});
async function testSidebarToggle(commandID, gleanEvent, otherCommandID) {
@ -396,6 +404,9 @@ async function testCustomizeSetting(
}
add_task(async function test_customize_sidebar_display() {
await SpecialPowers.pushPrefEnv({
set: [[TAB_DIRECTION_PREF, true]],
});
await testCustomizeSetting(
"visibilityInputs",
Glean.sidebarCustomize.sidebarDisplay,
@ -403,6 +414,7 @@ add_task(async function test_customize_sidebar_display() {
{ preference: "always" },
true
);
await SpecialPowers.popPrefEnv();
});
add_task(async function test_customize_sidebar_position() {
@ -439,6 +451,9 @@ add_task(async function test_customize_firefox_settings_clicked() {
});
add_task(async function test_sidebar_resize() {
await SpecialPowers.pushPrefEnv({
set: [[TAB_DIRECTION_PREF, true]],
});
await SidebarController.show("viewHistorySidebar");
const originalWidth = SidebarController._box.style.width;
SidebarController._box.style.width = "500px";
@ -461,9 +476,13 @@ add_task(async function test_sidebar_resize() {
SidebarController._box.style.width = originalWidth;
SidebarController.hide();
await SpecialPowers.popPrefEnv();
});
add_task(async function test_sidebar_display_settings() {
await SpecialPowers.pushPrefEnv({
set: [[TAB_DIRECTION_PREF, true]],
});
await testCustomizeSetting(
"visibilityInputs",
Glean.sidebar.displaySettings,
@ -471,6 +490,7 @@ add_task(async function test_sidebar_display_settings() {
"always",
true
);
await SpecialPowers.popPrefEnv();
});
add_task(async function test_sidebar_position_settings() {
@ -523,6 +543,7 @@ async function testIconClick(expanded) {
set: [
["browser.ml.chat.enabled", true],
["sidebar.main.tools", "aichat,syncedtabs,history,bookmarks"],
[TAB_DIRECTION_PREF, true],
],
});
@ -539,13 +560,15 @@ async function testIconClick(expanded) {
info(`Click the icon for: ${button.getAttribute("view")}`);
EventUtils.synthesizeMouseAtCenter(button, {});
const events = gleanEvents[i].testGetValue();
Assert.equal(events?.length, 1, "One event was reported.");
Assert.deepEqual(
events?.[0].extra,
{ sidebar_open: `${expanded}` },
`Event indicates the sidebar was ${expanded ? "expanded" : "collapsed"}.`
);
if (gleanEvents[i]) {
const events = gleanEvents[i].testGetValue();
Assert.equal(events?.length, 1, "One event was reported.");
Assert.deepEqual(
events?.[0].extra,
{ sidebar_open: `${expanded}` },
`Event indicates the sidebar was ${expanded ? "expanded" : "collapsed"}.`
);
}
}
info("Load an extension.");
@ -556,7 +579,10 @@ async function testIconClick(expanded) {
await SidebarController.initializeUIState({ launcherExpanded: expanded });
info("Click the icon for the extension.");
const extensionButton = sidebarMain.extensionButtons[0];
const extensionButton = await TestUtils.waitForCondition(
() => sidebarMain.extensionButtons[0],
"Extension button is present"
);
EventUtils.synthesizeMouseAtCenter(extensionButton, {});
const events = Glean.sidebar.addonIconClick.testGetValue();

View file

@ -8,6 +8,7 @@ add_setup(async () => {
set: [
["sidebar.visibility", "always-show"],
["sidebar.position_start", true],
["sidebar.verticalTabs", true],
],
});
await SidebarController.initializeUIState({
@ -16,6 +17,10 @@ add_setup(async () => {
});
});
registerCleanupFunction(async () => {
await SpecialPowers.popPrefEnv();
});
async function dragLauncher(deltaX, shouldExpand) {
AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
@ -101,3 +106,14 @@ add_task(async function test_custom_width_persists() {
);
await BrowserTestUtils.closeWindow(win);
});
add_task(async function test_drag_show_and_hide_for_horizontal_tabs() {
await SidebarController.initializeUIState({
launcherExpanded: false,
launcherVisible: true,
});
await dragLauncher(-200, false);
ok(!SidebarController.sidebarContainer.hidden, "Sidebar is not hidden.");
ok(!SidebarController.sidebarContainer.expanded, "Sidebar is not expanded.");
});

View file

@ -3,7 +3,12 @@
"use strict";
const TAB_DIRECTION_PREF = "sidebar.verticalTabs";
add_task(async function test_customize_sidebar_actions() {
SpecialPowers.pushPrefEnv({
set: [[TAB_DIRECTION_PREF, true]],
});
const win = await BrowserTestUtils.openNewBrowserWindow();
const { document } = win;
const sidebar = document.querySelector("sidebar-main");
@ -54,5 +59,6 @@ add_task(async function test_customize_sidebar_actions() {
"The max-width of the sidebar is approximately 75% of the viewport width."
);
SpecialPowers.popPrefEnv();
await BrowserTestUtils.closeWindow(win);
});

View file

@ -11,10 +11,13 @@ let gAreas = CustomizableUI.getTestOnlyInternalProp("gAreas");
const SIDEBAR_BUTTON_INTRODUCED_PREF =
"browser.toolbarbuttons.introduced.sidebar-button";
const SIDEBAR_VISIBILITY_PREF = "sidebar.visibility";
const SIDEBAR_TAB_DIRECTION_PREF = "sidebar.verticalTabs";
add_setup(async () => {
// Only vertical tabs mode has expanded state
await SpecialPowers.pushPrefEnv({
set: [
[SIDEBAR_TAB_DIRECTION_PREF, true],
[SIDEBAR_BUTTON_INTRODUCED_PREF, false],
[SIDEBAR_VISIBILITY_PREF, "always-show"],
],
@ -142,15 +145,11 @@ add_task(async function test_expanded_state_for_always_show() {
EventUtils.synthesizeMouseAtCenter(toolbarButton, {}, win);
await checkExpandedState(false);
info("Collapse the sidebar by loading a tool.");
info("Don't collapse the sidebar by loading a tool.");
await SidebarController.initializeUIState({ launcherExpanded: true });
await sidebarMain.updateComplete;
const toolButton = sidebarMain.toolButtons[0];
EventUtils.synthesizeMouseAtCenter(toolButton, {}, win);
await checkExpandedState(false);
info("Restore the sidebar back to its previous state.");
EventUtils.synthesizeMouseAtCenter(toolButton, {}, win);
await checkExpandedState(true);
info("Load and unload a tool with the sidebar collapsed to begin with.");
@ -178,7 +177,82 @@ add_task(async function test_expanded_state_for_always_show() {
add_task(async function test_states_for_hide_sidebar() {
await SpecialPowers.pushPrefEnv({
set: [[SIDEBAR_VISIBILITY_PREF, "hide-sidebar"]],
set: [[SIDEBAR_TAB_DIRECTION_PREF, false]],
});
const win = await BrowserTestUtils.openNewBrowserWindow();
const { SidebarController } = win;
const { sidebarContainer, sidebarMain, toolbarButton } = SidebarController;
const checkStates = async (
{ hidden },
container = sidebarContainer,
component = sidebarMain,
button = toolbarButton
) => {
await TestUtils.waitForCondition(
() => container.hidden == hidden,
"Hidden state is correct."
);
await TestUtils.waitForCondition(
() => !component.expanded,
"Expanded state is correct."
);
await TestUtils.waitForCondition(
() => button.checked == !hidden,
"Toolbar button state is correct."
);
Assert.deepEqual(
document.l10n.getAttributes(button),
{
id: hidden
? "sidebar-widget-show-sidebar2"
: "sidebar-widget-hide-sidebar2",
args:
AppConstants.platform === "macosx"
? { shortcut: "⌃Z" }
: { shortcut: "Alt+Ctrl+Z" },
},
"Toolbar button has the correct tooltip."
);
await TestUtils.waitForCondition(
() => !button.hasAttribute("expanded"),
"Toolbar button expanded attribute is absent."
);
};
// Hide the sidebar
info("Check default hidden state.");
await checkStates({ hidden: false });
info("Hide sidebar using the toolbar button.");
EventUtils.synthesizeMouseAtCenter(toolbarButton, {}, win);
await checkStates({ hidden: true });
info("Show sidebar using the toolbar button.");
EventUtils.synthesizeMouseAtCenter(toolbarButton, {}, win);
await checkStates({ hidden: false });
info("Check states on a new window.");
EventUtils.synthesizeMouseAtCenter(toolbarButton, {}, win);
await checkStates({ hidden: true });
const newWin = await BrowserTestUtils.openNewBrowserWindow();
await checkStates(
{ hidden: true },
newWin.SidebarController.sidebarContainer,
newWin.SidebarController.sidebarMain,
newWin.SidebarController.toolbarButton
);
await BrowserTestUtils.closeWindow(win);
await BrowserTestUtils.closeWindow(newWin);
await SpecialPowers.popPrefEnv();
});
add_task(async function test_states_for_hide_sidebar_vertical() {
await SpecialPowers.pushPrefEnv({
set: [
[SIDEBAR_TAB_DIRECTION_PREF, true],
[SIDEBAR_VISIBILITY_PREF, "hide-sidebar"],
],
});
const win = await BrowserTestUtils.openNewBrowserWindow();
const { SidebarController } = win;
@ -224,29 +298,22 @@ add_task(async function test_states_for_hide_sidebar() {
};
// Hide the sidebar
EventUtils.synthesizeMouseAtCenter(toolbarButton, {}, win);
info("Check default hidden state.");
await checkStates({ hidden: true, expanded: false });
info("Show expanded sidebar using the toolbar button.");
EventUtils.synthesizeMouseAtCenter(toolbarButton, {}, win);
await checkStates({ hidden: false, expanded: true });
info("Collapse the sidebar by loading a tool.");
info("Don't collapse the sidebar by loading a tool.");
const toolButton = sidebarMain.toolButtons[0];
EventUtils.synthesizeMouseAtCenter(toolButton, {}, win);
await checkStates({ hidden: false, expanded: false });
info("Restore the sidebar back to its previous state.");
EventUtils.synthesizeMouseAtCenter(toolButton, {}, win);
ok(SidebarController.isOpen, "Panel is open.");
await checkStates({ hidden: false, expanded: true });
info("Close a panel using the toolbar button.");
EventUtils.synthesizeMouseAtCenter(toolButton, {}, win);
ok(SidebarController.isOpen, "Panel is open.");
EventUtils.synthesizeMouseAtCenter(toolbarButton, {}, win);
ok(!SidebarController.isOpen, "Panel is closed.");
await checkStates({ hidden: true, expanded: true });
await checkStates({ hidden: true, expanded: false });
info("Check states on a new window.");
EventUtils.synthesizeMouseAtCenter(toolbarButton, {}, win);
@ -262,7 +329,7 @@ add_task(async function test_states_for_hide_sidebar() {
await BrowserTestUtils.closeWindow(win);
await BrowserTestUtils.closeWindow(newWin);
await SpecialPowers.popPrefEnv();
}).skip(); //bug 1896421
});
add_task(async function test_sidebar_button_runtime_pref_enabled() {
await SpecialPowers.pushPrefEnv({

View file

@ -29,6 +29,7 @@
class="panel tab-group-editor-panel"
orient="vertical"
role="dialog"
ignorekeys="true"
norolluponanchor="true">
<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>
@ -97,11 +98,11 @@
this.#populateSwatches();
this.#cancelButton.addEventListener("click", () => {
this.close();
this.close(false);
});
this.#createButton.addEventListener("click", () => {
this.close(true);
this.close();
});
this.#nameField.addEventListener("input", () => {
@ -274,7 +275,7 @@
});
}
close(keepNewlyCreatedGroup = false) {
close(keepNewlyCreatedGroup = true) {
if (this.createMode) {
this.#keepNewlyCreatedGroup = keepNewlyCreatedGroup;
}
@ -283,7 +284,7 @@
on_popupshown() {
if (this.createMode) {
this.#keepNewlyCreatedGroup = false;
this.#keepNewlyCreatedGroup = true;
}
this.#nameField.focus();
}
@ -302,8 +303,18 @@
}
on_keypress(event) {
if (event.keyCode == KeyEvent.DOM_VK_RETURN) {
this.close(true);
if (event.defaultPrevented) {
// The event has already been consumed inside of the panel.
return;
}
switch (event.keyCode) {
case KeyEvent.DOM_VK_ESCAPE:
this.close(false);
break;
case KeyEvent.DOM_VK_RETURN:
this.close();
break;
}
}

View file

@ -1558,14 +1558,19 @@ add_task(async function test_tabGroupCreatePanel() {
let tabgroupPanel = tabgroupEditor.panel;
let nameField = tabgroupPanel.querySelector("#tab-group-name");
let tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
let group;
let panelShown = BrowserTestUtils.waitForPopupEvent(tabgroupPanel, "shown");
let group = gBrowser.addTabGroup([tab], {
color: "cyan",
label: "Food",
showCreateUI: true,
});
await panelShown;
let openCreatePanel = async () => {
let panelShown = BrowserTestUtils.waitForPopupEvent(tabgroupPanel, "shown");
group = gBrowser.addTabGroup([tab], {
color: "cyan",
label: "Food",
showCreateUI: true,
});
await panelShown;
};
await openCreatePanel();
Assert.equal(tabgroupPanel.state, "open", "Create panel is visible");
Assert.ok(tabgroupEditor.createMode, "Group editor is in create mode");
// Edit panel should be populated with correct group details
@ -1585,27 +1590,31 @@ add_task(async function test_tabGroupCreatePanel() {
"Create panel's colorpicker has correct color pre-selected"
);
// Group should be removed after hitting Cancel
info("New group should be removed after hitting Cancel");
let panelHidden = BrowserTestUtils.waitForPopupEvent(tabgroupPanel, "hidden");
tabgroupPanel.querySelector("#tab-group-editor-button-cancel").click();
await panelHidden;
Assert.ok(!tab.group, "Tab is ungrouped after hitting Cancel");
// Group should be removed after hitting Esc
info("New group should be removed after hitting Esc");
await openCreatePanel();
panelHidden = BrowserTestUtils.waitForPopupEvent(tabgroupPanel, "hidden");
EventUtils.synthesizeKey("KEY_Escape");
await panelHidden;
Assert.ok(!tab.group, "Tab is ungrouped after hitting Esc");
panelShown = BrowserTestUtils.waitForPopupEvent(tabgroupPanel, "shown");
group = gBrowser.addTabGroup([tab], {
color: "cyan",
label: "Food",
showCreateUI: true,
});
await panelShown;
info("New group should remain when dismissing panel");
await openCreatePanel();
panelHidden = BrowserTestUtils.waitForPopupEvent(tabgroupPanel, "hidden");
tabgroupPanel.hidePopup();
await panelHidden;
Assert.equal(tabgroupPanel.state, "closed", "Tabgroup edit panel is closed");
Assert.equal(group.label, "Food");
Assert.equal(group.color, "cyan");
group.ungroupTabs();
// Panel inputs should work correctly
info("Panel inputs should work correctly");
await openCreatePanel();
nameField.focus();
nameField.value = "";
EventUtils.sendString("Shopping");
@ -1626,7 +1635,9 @@ add_task(async function test_tabGroupCreatePanel() {
"Red swatch radio selected after clicking red swatch"
);
// Panel dismissed after clicking Create and group remains
info(
"Panel should be dismissed after clicking Create and new group should remain"
);
panelHidden = BrowserTestUtils.waitForPopupEvent(tabgroupPanel, "hidden");
tabgroupPanel.querySelector("#tab-group-editor-button-create").click();
await panelHidden;
@ -1635,8 +1646,8 @@ add_task(async function test_tabGroupCreatePanel() {
Assert.equal(group.color, "red");
let rightClickGroupLabel = async () => {
// right-clicking on the group label reopens the panel in edit mode
panelShown = BrowserTestUtils.waitForPopupEvent(tabgroupPanel, "shown");
info("right-clicking on the group label should reopen panel in edit mode");
let panelShown = BrowserTestUtils.waitForPopupEvent(tabgroupPanel, "shown");
EventUtils.synthesizeMouseAtCenter(
group.querySelector(".tab-group-label"),
{ type: "contextmenu", button: 2 },
@ -1647,7 +1658,7 @@ add_task(async function test_tabGroupCreatePanel() {
Assert.ok(!tabgroupEditor.createMode, "Group editor is not in create mode");
};
// Panel dismissed after hitting Enter and group remains
info("Panel should be dismissed after hitting Enter and group should remain");
await rightClickGroupLabel();
panelHidden = BrowserTestUtils.waitForPopupEvent(tabgroupPanel, "hidden");
EventUtils.synthesizeKey("VK_RETURN");

View file

@ -50,4 +50,4 @@ run-if = ["os != 'mac'"] # On macOS we can't change browser.quitShortcut.disable
run-if = ["os == 'win'"]
["browser_csp_blocks_event_handlers.js"]
run-if = ["debug", "nightly_build"]
run-if = ["debug", "early_beta_or_earlier"]

View file

@ -3,15 +3,12 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { TippyTopProvider } from "resource:///modules/topsites/TippyTopProvider.sys.mjs";
import {
insertPinned,
TOP_SITES_MAX_SITES_PER_ROW,
} from "resource://activity-stream/common/Reducers.sys.mjs";
import { Dedupe } from "resource://activity-stream/common/Dedupe.sys.mjs";
import {
shortURL,
shortHostname,
} from "resource://activity-stream/lib/ShortURL.sys.mjs";
import { TOP_SITES_MAX_SITES_PER_ROW } from "resource:///modules/topsites/constants.mjs";
import {
CUSTOM_SEARCH_SHORTCUTS,
@ -39,6 +36,7 @@ ChromeUtils.defineLazyGetter(lazy, "log", () => {
});
export const DEFAULT_TOP_SITES = [];
const FRECENCY_THRESHOLD = 100 + 1; // 1 visit (skip first-run/one-time pages)
const MIN_FAVICON_SIZE = 96;
const PINNED_FAVICON_PROPS_TO_MIGRATE = [
@ -1088,4 +1086,41 @@ class _TopSites {
}
}
/**
* insertPinned - Inserts pinned links in their specified slots
*
* @param {Array} links list of links
* @param {Array} pinned list of pinned links
* @returns {Array} resulting list of links with pinned links inserted
*/
export function insertPinned(links, pinned) {
// Remove any pinned links
const pinnedUrls = pinned.map(link => link && link.url);
let newLinks = links.filter(link =>
link ? !pinnedUrls.includes(link.url) : false
);
newLinks = newLinks.map(link => {
if (link && link.isPinned) {
delete link.isPinned;
delete link.pinIndex;
}
return link;
});
// Then insert them in their specified location
pinned.forEach((val, index) => {
if (!val) {
return;
}
let link = Object.assign({}, val, { isPinned: true, pinIndex: index });
if (index > newLinks.length) {
newLinks[index] = link;
} else {
newLinks.splice(index, 0, link);
}
});
return newLinks;
}
export const TopSites = new _TopSites();

View file

@ -0,0 +1,6 @@
/* 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/. */
export const TOP_SITES_DEFAULT_ROWS = 1;
export const TOP_SITES_MAX_SITES_PER_ROW = 8;

View file

@ -5,6 +5,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
EXTRA_JS_MODULES.topsites += [
"constants.mjs",
"TippyTopProvider.sys.mjs",
"TopSites.sys.mjs",
]

View file

@ -3,9 +3,8 @@
"use strict";
const { TopSites, DEFAULT_TOP_SITES } = ChromeUtils.importESModule(
"resource:///modules/topsites/TopSites.sys.mjs"
);
const { TopSites, insertPinned, DEFAULT_TOP_SITES } =
ChromeUtils.importESModule("resource:///modules/topsites/TopSites.sys.mjs");
const { actionTypes: at } = ChromeUtils.importESModule(
"resource://activity-stream/common/Actions.mjs"
@ -22,9 +21,8 @@ ChromeUtils.defineESModuleGetters(this, {
Screenshots: "resource://activity-stream/lib/Screenshots.sys.mjs",
SearchService: "resource://gre/modules/SearchService.sys.mjs",
TestUtils: "resource://testing-common/TestUtils.sys.mjs",
TOP_SITES_DEFAULT_ROWS: "resource://activity-stream/common/Reducers.sys.mjs",
TOP_SITES_MAX_SITES_PER_ROW:
"resource://activity-stream/common/Reducers.sys.mjs",
TOP_SITES_DEFAULT_ROWS: "resource:///modules/topsites/constants.mjs",
TOP_SITES_MAX_SITES_PER_ROW: "resource:///modules/topsites/constants.mjs",
});
const FAKE_FAVICON = "data987";
@ -2504,3 +2502,99 @@ add_task(async function test_updatePinnedSearchShortcuts() {
sandbox.restore();
});
add_task(async function test_insertPinned() {
info("#insertPinned");
function createLinks(count) {
return new Array(count).fill(null).map((v, i) => ({ url: `site${i}.com` }));
}
info("should place pinned links where they belong");
{
let links = createLinks(12);
const pinned = [
{ url: "http://github.com/mozilla/activity-stream", title: "moz/a-s" },
{ url: "http://example.com", title: "example" },
];
const result = insertPinned(links, pinned);
for (let index of [0, 1]) {
Assert.equal(result[index].url, pinned[index].url, "Pinned URL matches");
Assert.ok(result[index].isPinned, "Link is marked as pinned");
Assert.equal(result[index].pinIndex, index, "Pin index is correct");
}
Assert.deepEqual(result.slice(2), links, "Remaining links are unchanged");
}
info("should handle empty slots in the pinned list");
{
let links = createLinks(12);
const pinned = [
null,
{ url: "http://github.com/mozilla/activity-stream", title: "moz/a-s" },
null,
null,
{ url: "http://example.com", title: "example" },
];
const result = insertPinned(links, pinned);
for (let index of [1, 4]) {
Assert.equal(result[index].url, pinned[index].url, "Pinned URL matches");
Assert.ok(result[index].isPinned, "Link is marked as pinned");
Assert.equal(result[index].pinIndex, index, "Pin index is correct");
}
result.splice(4, 1);
result.splice(1, 1);
Assert.deepEqual(result, links, "Remaining links are unchanged");
}
info("should handle a pinned site past the end of the list of links");
{
const pinned = [];
pinned[11] = {
url: "http://github.com/mozilla/activity-stream",
title: "moz/a-s",
};
const result = insertPinned([], pinned);
Assert.equal(result[11].url, pinned[11].url, "Pinned URL matches");
Assert.ok(result[11].isPinned, "Link is marked as pinned");
Assert.equal(result[11].pinIndex, 11, "Pin index is correct");
}
info("should unpin previously pinned links no longer in the pinned list");
{
let links = createLinks(12);
const pinned = [];
links[2].isPinned = true;
links[2].pinIndex = 2;
const result = insertPinned(links, pinned);
Assert.ok(!result[2].isPinned, "isPinned property removed");
Assert.ok(!result[2].pinIndex, "pinIndex property removed");
}
info("should handle a link present in both the links and pinned list");
{
let links = createLinks(12);
const pinned = [links[7]];
const result = insertPinned(links, pinned);
Assert.equal(links.length, result.length, "Length of links is unchanged");
}
info("should not modify the original data");
{
let links = createLinks(12);
const pinned = [{ url: "http://example.com" }];
insertPinned(links, pinned);
Assert.equal(
typeof pinned[0].isPinned,
"undefined",
"Pinned data is not mutated"
);
}
});

View file

@ -498,7 +498,7 @@ const PREF_URLBAR_DEFAULTS = new Map([
["deduplication.enabled", false],
// How old history results have to be to be deduplicated.
["deduplication.thresholdDays", 7],
["deduplication.thresholdDays", 0],
// When using switch to tabs, if set to true this will move the tab into the
// active window.

View file

@ -135,16 +135,16 @@ class ProviderQuickSuggest extends UrlbarProvider {
break;
}
let canAdd = await this.#canAddSuggestion(suggestion);
let result = await this.#makeResult(queryContext, suggestion);
if (instance != this.queryInstance) {
return;
}
if (canAdd) {
let result = await this.#makeResult(queryContext, suggestion);
if (result) {
let canAdd = await this.#canAddResult(result);
if (instance != this.queryInstance) {
return;
}
if (result) {
if (canAdd) {
addCallback(this, result);
if (!result.isHiddenExposure) {
remainingCount--;
@ -163,8 +163,9 @@ class ProviderQuickSuggest extends UrlbarProvider {
for (let i = 0; i < suggestions.length; i++) {
let suggestion = suggestions[i];
// Discard suggestions that don't have the required keys, which are used
// to look up their features. Normally this shouldn't ever happen.
// Discard the suggestion if it doesn't have the properties required to
// get the feature that manages it. Each backend should set these, so this
// should never happen.
if (!requiredKeys.every(key => suggestion[key])) {
this.logger.error("Suggestion is missing one or more required keys", {
requiredKeys,
@ -173,14 +174,7 @@ class ProviderQuickSuggest extends UrlbarProvider {
continue;
}
// Set `is_sponsored` before continuing because
// `#getSuggestionTelemetryType()` and other things depend on it.
let feature = this.#getFeature(suggestion);
if (!suggestion.hasOwnProperty("is_sponsored")) {
suggestion.is_sponsored = !!feature?.isSuggestionSponsored(suggestion);
}
// Ensure all suggestions have scores.
// Ensure the suggestion has a score.
//
// Step 1: Set a default score if the suggestion doesn't have one.
if (typeof suggestion.score != "number" || isNaN(suggestion.score)) {
@ -206,6 +200,8 @@ class ProviderQuickSuggest extends UrlbarProvider {
}
// Save some state used below to build the final list of suggestions.
// `feature` will be null if the suggestion isn't managed by one.
let feature = this.#getFeature(suggestion);
let featureSuggestions = suggestionsByFeature.get(feature);
if (!featureSuggestions) {
featureSuggestions = [];
@ -216,7 +212,7 @@ class ProviderQuickSuggest extends UrlbarProvider {
}
// Let each feature filter its suggestions.
suggestions = (
let filteredSuggestions = (
await Promise.all(
[...suggestionsByFeature].map(([feature, featureSuggestions]) =>
feature
@ -228,14 +224,14 @@ class ProviderQuickSuggest extends UrlbarProvider {
// Sort the suggestions. When scores are equal, sort by original index to
// ensure a stable sort.
suggestions.sort((a, b) => {
filteredSuggestions.sort((a, b) => {
return (
b.score - a.score ||
indexesBySuggestion.get(a) - indexesBySuggestion.get(b)
);
});
return suggestions;
return filteredSuggestions;
}
onImpression(state, queryContext, controller, resultsAndIndexes, details) {
@ -370,35 +366,32 @@ class ProviderQuickSuggest extends UrlbarProvider {
}
async #makeResult(queryContext, suggestion) {
let result;
let result = null;
let feature = this.#getFeature(suggestion);
if (!feature) {
// We specifically allow Merino to serve suggestion types that Firefox
// doesn't know about so that we can experiment with new types without
// requiring changes in Firefox. No other source should return unknown
// suggestion types with the possible exception of the ML backend: Its
// models are stored in remote settings and it may return newer intents
// that aren't recognized by older Firefoxes.
if (suggestion.source != "merino") {
return null;
}
result = this.#makeDefaultResult(queryContext, suggestion);
} else {
result = this.#makeUnmanagedResult(queryContext, suggestion);
} else if (feature.isEnabled) {
result = await feature.makeResult(
queryContext,
suggestion,
this._trimmedSearchString
);
if (!result) {
// Feature might return null, if the feature is disabled and so on.
return null;
}
}
// See `#getFeature()` for possible values of `source` and `provider`.
if (!result) {
return null;
}
// Set important properties that every Suggest result should have. See
// `#getFeature()` for possible values of `source` and `provider`. If the
// suggestion isn't managed by a feature, then it's from Merino and has
// `is_sponsored` set if it's sponsored. (Merino uses snake_case.)
result.payload.source = suggestion.source;
result.payload.provider = suggestion.provider;
result.payload.telemetryType = this.#getSuggestionTelemetryType(suggestion);
result.payload.isSponsored = feature
? feature.isSuggestionSponsored(suggestion)
: !!suggestion.is_sponsored;
// Handle icons here so each feature doesn't have to do it, but use `||=` to
// let them do it if they need to.
@ -420,7 +413,7 @@ class ProviderQuickSuggest extends UrlbarProvider {
result.suggestedIndex = suggestion.position;
} else {
result.isSuggestedIndexRelativeToGroup = true;
if (!suggestion.is_sponsored) {
if (!result.payload.isSponsored) {
result.suggestedIndex = lazy.UrlbarPrefs.get(
"quickSuggestNonSponsoredIndex"
);
@ -431,11 +424,12 @@ class ProviderQuickSuggest extends UrlbarProvider {
queryContext.isPrivate
).supportsResponseType(lazy.SearchUtils.URL_TYPE.SUGGEST_JSON)
) {
// Show sponsored suggestions somewhere other than the bottom of the
// Suggest section only if search suggestions are shown first, the
// search suggestions provider is active for the current context (it
// will not be active if search suggestions are disabled, among other
// reasons), and the default engine supports suggestions.
// Allow sponsored suggestions to be shown somewhere other than the
// bottom of the Suggest section (-1, the `else` branch below) only if
// search suggestions are shown first, the search suggestions provider
// is active for the current context (it will not be active if search
// suggestions are disabled, among other reasons), and the default
// engine supports suggestions.
result.suggestedIndex = lazy.UrlbarPrefs.get(
"quickSuggestSponsoredIndex"
);
@ -448,10 +442,32 @@ class ProviderQuickSuggest extends UrlbarProvider {
return result;
}
#makeDefaultResult(queryContext, suggestion) {
/**
* Returns a new result for an unmanaged suggestion. An "unmanaged" suggestion
* is a suggestion without a feature.
*
* Merino is the only backend allowed to serve unmanaged suggestions, for a
* couple of reasons: (1) Some suggestion types aren't that complicated and
* can be handled in a default manner, for example dynamic Wikipedia
* suggestions. (2) It allows us to experiment with new suggestion types
* without requiring any changes to Firefox.
*
* @param {UrlbarQueryContext} queryContext
* The query context.
* @param {object} suggestion
* The suggestion.
* @returns {UrlbarResult|null}
* A new result for the suggestion or null if the suggestion is not from
* Merino.
*/
#makeUnmanagedResult(queryContext, suggestion) {
if (suggestion.source != "merino") {
return null;
}
// Note that Merino uses snake_case keys.
let payload = {
url: suggestion.url,
isSponsored: suggestion.is_sponsored,
isBlockable: true,
blockL10n: {
id: "urlbar-result-menu-dismiss-firefox-suggest",
@ -554,62 +570,44 @@ class ProviderQuickSuggest extends UrlbarProvider {
}
/**
* Returns whether a given suggestion can be added for a query, assuming the
* Returns whether a given result can be added for a query, assuming the
* provider itself should be active.
*
* @param {object} suggestion
* The suggestion to check.
* @param {UrlbarResult} result
* The result to check.
* @returns {boolean}
* Whether the suggestion can be added.
* Whether the result can be added.
*/
async #canAddSuggestion(suggestion) {
this.logger.debug("Checking if suggestion can be added", suggestion);
// Return false if suggestions are disabled. Always allow Rust exposure
// suggestions.
async #canAddResult(result) {
// Discard the result if it's not managed by a feature and its sponsored
// state isn't allowed.
//
// This isn't necessary when the result is managed because in that case: If
// its feature is disabled, we didn't create a result in the first place; if
// its feature is enabled, we delegate responsibility to it for either
// creating or not creating its results.
//
// Also note that it's possible for suggestion types to be considered
// neither sponsored nor nonsponsored. In other words, the decision to add
// them or not does not depend on the prefs in this conditional. Such types
// should always be managed. Exposure suggestions are an example.
let feature = this.#getFeatureByResult(result);
if (
((suggestion.is_sponsored &&
!feature &&
((result.payload.isSponsored &&
!lazy.UrlbarPrefs.get("suggest.quicksuggest.sponsored")) ||
(!suggestion.is_sponsored &&
!lazy.UrlbarPrefs.get("suggest.quicksuggest.nonsponsored"))) &&
(suggestion.source != "rust" || suggestion.provider != "Exposure")
(!result.payload.isSponsored &&
!lazy.UrlbarPrefs.get("suggest.quicksuggest.nonsponsored")))
) {
this.logger.debug("Suggestions disabled, not adding suggestion");
return false;
}
// Return false if an impression cap has been hit.
if (
(suggestion.is_sponsored &&
lazy.UrlbarPrefs.get("quickSuggestImpressionCapsSponsoredEnabled")) ||
(!suggestion.is_sponsored &&
lazy.UrlbarPrefs.get("quickSuggestImpressionCapsNonSponsoredEnabled"))
) {
let type = suggestion.is_sponsored ? "sponsored" : "nonsponsored";
let hitStats = lazy.QuickSuggest.impressionCaps.getHitStats(type);
if (hitStats) {
this.logger.debug("Impression cap(s) hit, not adding suggestion", {
type,
hitStats,
});
return false;
}
}
// Return false if the suggestion is blocked based on its URL. Suggestions
// from the JS backend define a single `url` property. Suggestions from the
// Rust backend are more complicated: Sponsored suggestions define `rawUrl`,
// which may contain timestamp templates, while non-sponsored suggestions
// define only `url`. Blocking should always be based on URLs with timestamp
// templates, where applicable, so check `rawUrl` and then `url`, in that
// order.
let { blockedSuggestions } = lazy.QuickSuggest;
if (await blockedSuggestions.has(suggestion.rawUrl ?? suggestion.url)) {
// Discard the result if its URL is blocked.
if (await lazy.QuickSuggest.blockedSuggestions.isResultBlocked(result)) {
this.logger.debug("Suggestion blocked, not adding suggestion");
return false;
}
this.logger.debug("Suggestion can be added");
return true;
}

View file

@ -19,9 +19,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs",
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
TopSites: "resource:///modules/topsites/TopSites.sys.mjs",
TOP_SITES_DEFAULT_ROWS: "resource://activity-stream/common/Reducers.sys.mjs",
TOP_SITES_MAX_SITES_PER_ROW:
"resource://activity-stream/common/Reducers.sys.mjs",
TOP_SITES_DEFAULT_ROWS: "resource:///modules/topsites/constants.mjs",
TOP_SITES_MAX_SITES_PER_ROW: "resource:///modules/topsites/constants.mjs",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.sys.mjs",
UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
@ -158,7 +157,7 @@ class ProviderTopSites extends UrlbarProvider {
}
// This is done here, rather than in the global scope, because
// TOP_SITES_DEFAULT_ROWS causes the import of Reducers.sys.mjs, and we want to
// TOP_SITES_DEFAULT_ROWS causes import of topsites constants.mjs, and we want to
// do that only when actually querying for Top Sites.
if (this.topSitesRows === undefined) {
XPCOMUtils.defineLazyPreferenceGetter(

View file

@ -2129,9 +2129,11 @@ export class UrlbarView {
}
#getBlobUrlForResult(result, blob) {
// Blob icons are currently limited to Suggest results, which will define
// a `payload.originalUrl` if the result URL contains timestamp templates
// that are replaced at query time.
// For some Suggest results, `url` is a value that is modified at query time
// and that is potentially unique per query. For example, it might contain
// timestamps or query-related search params. Those results will also have
// an `originalUrl` that is the unmodified URL, and it should be used as the
// map key.
let resultUrl = result.payload.originalUrl || result.payload.url;
if (resultUrl) {
let blobUrl = this.#blobUrlsByResultUrl?.get(resultUrl);

View file

@ -189,7 +189,7 @@ browser.urlbar.searchTips.test.ignoreShowLimits (boolean, default: false)
This is useful for testing purposes.
browser.urlbar.speculativeConnect.enabled (boolean, default: true)
Speculative connections allow to resolve domains pre-emptively when the user
Speculative connections allow to resolve domains preemptively when the user
is likely to pick a result from the Address Bar. This allows for faster
navigation.

View file

@ -174,7 +174,7 @@ export class AddonSuggestions extends SuggestProvider {
// selType == "dismiss" when the user presses the dismiss key shortcut.
case "dismiss":
case RESULT_MENU_COMMAND.NOT_RELEVANT:
lazy.QuickSuggest.blockedSuggestions.add(result.payload.originalUrl);
lazy.QuickSuggest.blockedSuggestions.blockResult(result);
result.acknowledgeDismissalL10n = {
id: "firefox-suggest-dismissal-acknowledgment-one",
};

View file

@ -102,7 +102,6 @@ export class AmpSuggestions extends SuggestProvider {
suggestion = {
title: suggestion.title,
url: suggestion.url,
is_sponsored: suggestion.is_sponsored,
fullKeyword: suggestion.full_keyword,
impressionUrl: suggestion.impression_url,
clickUrl: suggestion.click_url,
@ -117,7 +116,6 @@ export class AmpSuggestions extends SuggestProvider {
originalUrl,
url: suggestion.url,
title: suggestion.title,
isSponsored: suggestion.is_sponsored,
requestId: suggestion.requestId,
urlTimestampIndex: suggestion.urlTimestampIndex,
sponsoredImpressionUrl: suggestion.impressionUrl,
@ -195,7 +193,7 @@ export class AmpSuggestions extends SuggestProvider {
// commands. Dismissal is the only one we need to handle here. `UrlbarInput`
// handles Manage.
if (details.selType == "dismiss") {
lazy.QuickSuggest.blockedSuggestions.add(result.payload.originalUrl);
lazy.QuickSuggest.blockedSuggestions.blockResult(result);
controller.removeResult(result);
}

View file

@ -29,10 +29,43 @@ export class BlockedSuggestions extends SuggestFeature {
}
/**
* Blocks a suggestion.
* Blocks a result's URL.
*
* @param {UrlbarResult} result
* The URL of this result will be blocked.
*/
async blockResult(result) {
// For some Suggest results, `url` is a value that is modified at query time
// and that is potentially unique per query. For example, it might contain
// timestamps or query-related search params. Those results will also have
// an `originalUrl` that is the unmodified URL, and it should be used for
// blocking purposes.
await this.add(result.payload.originalUrl || result.payload.url);
}
/**
* Returns true if a result's URL is blocked.
*
* @param {UrlbarResult} result
* The result to check.
* @returns {boolean}
* Whether the result's URL is blocked.
*/
async isResultBlocked(result) {
// See `blockResult()` for a note on `originalUrl`.
let isBlocked = await this.has(
result.payload.originalUrl || result.payload.url
);
return isBlocked;
}
/**
* Blocks a URL. Callers should use `blockResult()` instead when they have a
* `UrlbarResult`.
*
* @param {string} originalUrl
* The suggestion's original URL with its unreplaced timestamp template.
* The URL to block. In cases where a URL is potentially unique to a query,
* this value should be the original unmodified URL.
*/
async add(originalUrl) {
await this.#taskQueue.queue(async () => {
@ -52,12 +85,14 @@ export class BlockedSuggestions extends SuggestFeature {
}
/**
* Gets whether a suggestion is blocked.
* Returns true if a URL is blocked. Callers should use `isResultBlocked()`
* instead when they have a `UrlbarResult`.
*
* @param {string} originalUrl
* The suggestion's original URL with its unreplaced timestamp template.
* The URL to check. In cases where a URL is potentially unique to a query,
* this value should be the original unmodified URL.
* @returns {boolean}
* Whether the suggestion is blocked.
* Whether the URL is blocked.
*/
async has(originalUrl) {
return this.#taskQueue.queue(async () => {
@ -67,7 +102,7 @@ export class BlockedSuggestions extends SuggestFeature {
}
/**
* Unblocks all suggestions.
* Unblocks all URLs.
*/
async clear() {
await this.#taskQueue.queue(() => {
@ -141,9 +176,12 @@ export class BlockedSuggestions extends SuggestFeature {
return this.#getDigest(string);
}
// Set of digests of the original URLs of blocked suggestions. A suggestion's
// "original URL" is its URL straight from the source with an unreplaced
// timestamp template. For details on the digests, see `#getDigest()`.
// Set of digests of the original URLs of blocked suggestions. For some
// Suggest results, `url` is a value that is modified at query time and that
// is potentially unique per query. For example, it might contain timestamps
// or query-related search params. Those results will also have an
// `originalUrl` that is the unmodified URL, and it should be used for
// blocking purposes. For details on the digests, see `#getDigest()`.
//
// The only reason we use URL digests is that suggestions currently do not
// have persistent IDs. We could use the URLs themselves but SHA-1 digests are

View file

@ -260,7 +260,7 @@ export class FakespotSuggestions extends SuggestProvider {
// selType == "dismiss" when the user presses the dismiss key shortcut.
case "dismiss":
case RESULT_MENU_COMMAND.NOT_RELEVANT:
lazy.QuickSuggest.blockedSuggestions.add(result.payload.originalUrl);
lazy.QuickSuggest.blockedSuggestions.blockResult(result);
result.acknowledgeDismissalL10n = {
id: "firefox-suggest-dismissal-acknowledgment-one-fakespot",
};

View file

@ -122,12 +122,7 @@ export class MDNSuggestions extends SuggestProvider {
// selType == "dismiss" when the user presses the dismiss key shortcut.
case "dismiss":
case RESULT_MENU_COMMAND.NOT_RELEVANT:
// MDNSuggestions adds the UTM parameters to the original URL and
// returns it as payload.url in the result. However, as
// UrlbarProviderQuickSuggest filters suggestions with original URL of
// provided suggestions, need to use the original URL when adding to the
// block list.
lazy.QuickSuggest.blockedSuggestions.add(result.payload.originalUrl);
lazy.QuickSuggest.blockedSuggestions.blockResult(result);
result.acknowledgeDismissalL10n = {
id: "firefox-suggest-dismissal-acknowledgment-one-mdn",
};

View file

@ -40,13 +40,11 @@ export class OfflineWikipediaSuggestions extends SuggestProvider {
lazy.UrlbarUtils.RESULT_SOURCE.SEARCH,
...lazy.UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
url: suggestion.url,
originalUrl: suggestion.url,
title: suggestion.title,
qsSuggestion: [
suggestion.fullKeyword,
lazy.UrlbarUtils.HIGHLIGHT.SUGGESTED,
],
isSponsored: suggestion.is_sponsored,
sponsoredAdvertiser: "Wikipedia",
sponsoredIabCategory: "5 - Education",
isBlockable: true,
@ -65,7 +63,7 @@ export class OfflineWikipediaSuggestions extends SuggestProvider {
// commands. Dismissal is the only one we need to handle here. `UrlbarInput`
// handles Manage.
if (details.selType == "dismiss") {
lazy.QuickSuggest.blockedSuggestions.add(result.payload.originalUrl);
lazy.QuickSuggest.blockedSuggestions.blockResult(result);
controller.removeResult(result);
}
}

View file

@ -147,12 +147,7 @@ export class PocketSuggestions extends SuggestProvider {
// selType == "dismiss" when the user presses the dismiss key shortcut.
case "dismiss":
case RESULT_MENU_COMMAND.NOT_RELEVANT:
// PocketSuggestions adds the UTM parameters to the original URL and
// returns it as payload.url in the result. However, as
// UrlbarProviderQuickSuggest filters suggestions with original URL of
// provided suggestions, need to use the original URL when adding to the
// block list.
lazy.QuickSuggest.blockedSuggestions.add(result.payload.originalUrl);
lazy.QuickSuggest.blockedSuggestions.blockResult(result);
result.acknowledgeDismissalL10n = {
id: "firefox-suggest-dismissal-acknowledgment-one",
};

View file

@ -266,7 +266,7 @@ export class YelpSuggestions extends SuggestProvider {
// selType == "dismiss" when the user presses the dismiss key shortcut.
case "dismiss":
case RESULT_MENU_COMMAND.NOT_RELEVANT:
lazy.QuickSuggest.blockedSuggestions.add(result.payload.originalUrl);
lazy.QuickSuggest.blockedSuggestions.blockResult(result);
result.acknowledgeDismissalL10n = {
id: "firefox-suggest-dismissal-acknowledgment-one-yelp",
};

View file

@ -500,7 +500,6 @@ class _QuickSuggestTestUtils {
fullKeyword = keyword,
title = "Wikipedia Suggestion",
url = "https://example.com/wikipedia",
originalUrl = url,
iconBlob = null,
suggestedIndex = -1,
isSuggestedIndexRelativeToGroup = true,
@ -514,7 +513,6 @@ class _QuickSuggestTestUtils {
payload: {
title,
url,
originalUrl,
iconBlob,
source,
provider,
@ -832,6 +830,7 @@ class _QuickSuggestTestUtils {
originalUrl,
icon,
displayUrl: url.replace(/^https:\/\//, ""),
isSponsored: false,
shouldShowUrl: true,
bottomTextL10n: { id: "firefox-suggest-addons-recommended" },
helpUrl: lazy.QuickSuggest.HELP_URL,
@ -869,6 +868,7 @@ class _QuickSuggestTestUtils {
url: finalUrl.href,
originalUrl: url,
displayUrl: finalUrl.href.replace(/^https:\/\//, ""),
isSponsored: false,
description,
icon: "chrome://global/skin/icons/mdn.svg",
shouldShowUrl: true,
@ -928,6 +928,7 @@ class _QuickSuggestTestUtils {
},
source,
provider,
isSponsored: true,
telemetryType: "weather",
},
};

View file

@ -325,9 +325,7 @@ async function doDismissTest(command, allDismissed) {
"suggest.addons should be true iff all suggestions weren't dismissed"
);
Assert.equal(
await QuickSuggest.blockedSuggestions.has(
details.result.payload.originalUrl
),
await QuickSuggest.blockedSuggestions.isResultBlocked(details.result),
!allDismissed,
"Suggestion URL should be blocked iff all suggestions weren't dismissed"
);

View file

@ -102,7 +102,8 @@ async function doOneBasicBlockTest({ result, block }) {
await QuickSuggestTestUtils.assertIsQuickSuggest({
window,
isSponsored,
originalUrl: result.url,
url: isSponsored ? undefined : result.url,
originalUrl: isSponsored ? result.url : undefined,
});
// Block the suggestion.
@ -134,15 +135,18 @@ async function doOneBasicBlockTest({ result, block }) {
add_task(async function blockMultiple() {
for (let i = 0; i < REMOTE_SETTINGS_RESULTS.length; i++) {
// Do a search that triggers the i'th suggestion.
let { keywords, url } = REMOTE_SETTINGS_RESULTS[i];
let { keywords, url, iab_category } = REMOTE_SETTINGS_RESULTS[i];
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: keywords[0],
});
let isSponsored = iab_category != "5 - Education";
await QuickSuggestTestUtils.assertIsQuickSuggest({
window,
originalUrl: url,
isSponsored: keywords[0] == "sponsored",
isSponsored,
url: isSponsored ? undefined : url,
originalUrl: isSponsored ? url : undefined,
});
// Block it.

View file

@ -387,7 +387,7 @@ add_task(async function resultMenu_not_relevant() {
menu: "not_relevant",
assert: resuilt => {
Assert.ok(
QuickSuggest.blockedSuggestions.has(resuilt.payload.url),
QuickSuggest.blockedSuggestions.isResultBlocked(resuilt),
"The URL should be register as blocked"
);
},

View file

@ -285,7 +285,7 @@ add_task(async function resultMenu_not_relevant() {
menu: "not_relevant",
assert: resuilt => {
Assert.ok(
QuickSuggest.blockedSuggestions.has(resuilt.payload.url),
QuickSuggest.blockedSuggestions.isResultBlocked(resuilt),
"The URL should be register as blocked"
);
},

View file

@ -1336,7 +1336,7 @@ add_task(async function block() {
});
// Block it.
await QuickSuggest.blockedSuggestions.add(context.results[0].payload.url);
await QuickSuggest.blockedSuggestions.blockResult(context.results[0]);
// Do another search. The result shouldn't be added.
await check_results({
@ -1382,7 +1382,7 @@ add_task(async function block_timestamp() {
);
// Block the result.
await QuickSuggest.blockedSuggestions.add(result.payload.originalUrl);
await QuickSuggest.blockedSuggestions.blockResult(result);
// Do another search. The result shouldn't be added.
await check_results({

View file

@ -480,6 +480,7 @@ function makeExpectedResult(exposureSuggestionType) {
dynamicType: "exposure",
provider: "Exposure",
telemetryType: "exposure",
isSponsored: false,
},
};
}

View file

@ -254,6 +254,7 @@ function makeExpectedExposureResult(exposureSuggestionType) {
dynamicType: "exposure",
provider: "Exposure",
telemetryType: "exposure",
isSponsored: false,
},
};
}

View file

@ -315,7 +315,7 @@ add_task(async function notRelevant() {
await QuickSuggest.blockedSuggestions._test_readyPromise;
Assert.ok(
await QuickSuggest.blockedSuggestions.has(result.payload.originalUrl),
await QuickSuggest.blockedSuggestions.isResultBlocked(result),
"The result's URL should be blocked"
);
@ -857,6 +857,7 @@ function makeExpectedResult({
totalReviews,
fakespotGrade,
fakespotProvider,
isSponsored: true,
dynamicType: "fakespot",
icon: null,
},

View file

@ -669,7 +669,7 @@ async function doUnmanagedTest({ pref, suggestion }) {
await QuickSuggest.blockedSuggestions._test_readyPromise;
Assert.ok(
await QuickSuggest.blockedSuggestions.has(suggestion.url),
await QuickSuggest.blockedSuggestions.isResultBlocked(context.results[0]),
"The suggestion URL should be blocked"
);

View file

@ -341,7 +341,7 @@ add_task(async function notRelevant() {
await QuickSuggest.blockedSuggestions._test_readyPromise;
Assert.ok(
await QuickSuggest.blockedSuggestions.has(result.payload.originalUrl),
await QuickSuggest.blockedSuggestions.isResultBlocked(result),
"The result's URL should be blocked"
);
@ -529,6 +529,7 @@ function makeExpectedResult({
icon: isTopPick
? "chrome://global/skin/icons/pocket.svg"
: "chrome://global/skin/icons/pocket-favicon.ico",
isSponsored: false,
helpUrl: QuickSuggest.HELP_URL,
shouldShowUrl: true,
bottomTextL10n: {

View file

@ -572,7 +572,9 @@ async function doTest({
let stub = sandbox
.stub(feature, "makeResult")
.callsFake((queryContext, suggestion, searchString) => {
if (suggestion.url == expectedResult.payload.originalUrl) {
let expectedUrl =
expectedResult.payload.originalUrl || expectedResult.payload.url;
if (suggestion.url == expectedUrl) {
actualScore = suggestion.score;
}
return stub.wrappedMethod.call(
@ -633,7 +635,6 @@ function makeExpectedWikipediaResult({ suggestion, keyword, source }) {
source,
title: suggestion.title,
url: suggestion.url,
originalUrl: suggestion.url,
impressionUrl: suggestion.impression_url,
clickUrl: suggestion.click_url,
blockId: suggestion.id,

View file

@ -549,7 +549,7 @@ add_task(async function notRelevant() {
await QuickSuggest.blockedSuggestions._test_readyPromise;
Assert.ok(
await QuickSuggest.blockedSuggestions.has(result.payload.originalUrl),
await QuickSuggest.blockedSuggestions.isResultBlocked(result),
"The result's URL should be blocked"
);
@ -1128,6 +1128,7 @@ function makeExpectedResult({
title,
displayUrl,
icon: null,
isSponsored: true,
},
};
}

View file

@ -489,7 +489,7 @@ add_task(async function notRelevant() {
await QuickSuggest.blockedSuggestions._test_readyPromise;
Assert.ok(
await QuickSuggest.blockedSuggestions.has(result.payload.originalUrl),
await QuickSuggest.blockedSuggestions.isResultBlocked(result),
"The result's URL should be blocked"
);
@ -588,6 +588,7 @@ function makeExpectedResult({
title,
displayUrl,
icon: null,
isSponsored: true,
},
};
}

View file

@ -401,11 +401,13 @@ export class ManageCreditCards extends ManageRecords {
}
let decryptedCCNumObj = {};
let errorMessage;
if (creditCard && creditCard["cc-number-encrypted"]) {
try {
decryptedCCNumObj["cc-number"] = await lazy.OSKeyStore.decrypt(
creditCard["cc-number-encrypted"]
);
errorMessage = "NO_ERROR";
} catch (ex) {
if (ex.result == Cr.NS_ERROR_ABORT) {
// User shouldn't be ask to reauth here, but it could happen.
@ -417,6 +419,13 @@ export class ManageCreditCards extends ManageRecords {
// unencrypted credit card number.
decryptedCCNumObj["cc-number"] = "";
console.error(ex);
errorMessage = ex.result;
} finally {
Glean.creditcard.osKeystoreDecrypt.record({
isDecryptSuccess: errorMessage === "NO_ERROR",
errorMessage,
trigger: "edit",
});
}
}
let decryptedCreditCard = Object.assign({}, creditCard, decryptedCCNumObj);

View file

@ -104,6 +104,29 @@ const AVAILABLE_SHIMS = [
"*://example.com/browser/browser/extensions/webcompat/tests/browser/shims_test_3.js",
],
},
{
id: "EmbedTestShim",
platform: "desktop",
name: "Test shim for smartblock embed unblocking",
bug: "1892175",
runFirst: "embed-test-shim.js",
// Blank stub file just so we run the script above when the matched script
// files get blocked.
file: "empty-script.js",
matches: [
"https://itisatracker.org/browser/browser/extensions/webcompat/tests/browser/embed_test.js",
],
// Use instagram logo as an example
logos: ["instagram.svg"],
needsShimHelpers: [
"embedClicked",
"smartblockEmbedReplaced",
"smartblockGetFluentString",
],
isSmartblockEmbedShim: true,
onlyIfBlockedByETP: true,
unblocksOnOptIn: ["*://itisatracker.org/*"],
},
{
id: "AddThis",
platform: "all",
@ -908,7 +931,11 @@ const AVAILABLE_SHIMS = [
],
logos: ["instagram.svg"],
webExposedShimHelpers: [],
needsShimHelpers: ["embedClicked", "smartblockGetFluentString"],
needsShimHelpers: [
"embedClicked",
"smartblockEmbedReplaced",
"smartblockGetFluentString",
],
isSmartblockEmbedShim: true,
onlyIfBlockedByETP: true,
unblocksOnOptIn: [
@ -929,7 +956,11 @@ const AVAILABLE_SHIMS = [
matches: ["https://www.tiktok.com/embed.js"],
logos: ["tiktok.svg"],
webExposedShimHelpers: [],
needsShimHelpers: ["embedClicked", "smartblockGetFluentString"],
needsShimHelpers: [
"embedClicked",
"smartblockEmbedReplaced",
"smartblockGetFluentString",
],
isSmartblockEmbedShim: true,
onlyIfBlockedByETP: true,
unblocksOnOptIn: ["*://www.tiktok.com/*"],

View file

@ -44,13 +44,18 @@ class AllowList {
class Manager {
constructor() {
this._allowLists = new Map();
this._PBModeAllowLists = new Map();
}
_getAllowList(id) {
if (!this._allowLists.has(id)) {
this._allowLists.set(id, new AllowList(id));
_getAllowList(id, isPrivateMode) {
const activeAllowLists = isPrivateMode
? this._PBModeAllowLists
: this._allowLists;
if (!activeAllowLists.has(id)) {
activeAllowLists.set(id, new AllowList(id));
}
return this._allowLists.get(id);
return activeAllowLists.get(id);
}
_ensureStarted() {
@ -59,6 +64,7 @@ class Manager {
}
this._unblockedChannelIds = new Set();
this._PBModeUnblockedChannelIds = new Set();
this._channelClassifier = Cc[
"@mozilla.org/url-classifier/channel-classifier-service;1"
].getService(Ci.nsIChannelClassifierService);
@ -67,13 +73,21 @@ class Manager {
switch (topic) {
case "http-on-stop-request": {
const { channelId } = subject.QueryInterface(Ci.nsIIdentChannel);
this._unblockedChannelIds.delete(channelId);
const isPrivateMode =
subject.loadInfo.browsingContext?.originAttributes
?.privateBrowsingId;
if (isPrivateMode) {
this._PBModeUnblockedChannelIds.delete(channelId);
} else {
this._unblockedChannelIds.delete(channelId);
}
break;
}
case "urlclassifier-before-block-channel": {
const channel = subject.QueryInterface(
Ci.nsIUrlClassifierBlockedChannel
);
const isPrivateMode = subject.isPrivateBrowsing;
const { channelId, url } = channel;
let topHost;
try {
@ -81,22 +95,28 @@ class Manager {
} catch (_) {
return;
}
const activeAllowLists = isPrivateMode
? this._PBModeAllowLists
: this._allowLists;
const activeUnblockedChannelIds = isPrivateMode
? this._PBModeUnblockedChannelIds
: this._unblockedChannelIds;
// If anti-tracking webcompat is disabled, we only permit replacing
// channels, not fully unblocking them.
if (Manager.ENABLE_WEBCOMPAT) {
// if any allowlist unblocks the request entirely, we allow it
for (const allowList of this._allowLists.values()) {
for (const allowList of activeAllowLists.values()) {
if (allowList.allows(url, topHost)) {
this._unblockedChannelIds.add(channelId);
activeUnblockedChannelIds.add(channelId);
channel.allow();
return;
}
}
}
// otherwise, if any allowlist shims the request we say it's replaced
for (const allowList of this._allowLists.values()) {
for (const allowList of activeAllowLists.values()) {
if (allowList.shims(url, topHost)) {
this._unblockedChannelIds.add(channelId);
activeUnblockedChannelIds.add(channelId);
channel.replace();
return;
}
@ -123,22 +143,26 @@ class Manager {
delete this._classifierObserver;
}
wasChannelIdUnblocked(channelId) {
return this._unblockedChannelIds?.has(channelId);
wasChannelIdUnblocked(channelId, isPrivateMode) {
const activeUnblockedChannelIds = isPrivateMode
? this._PBModeUnblockedChannelIds
: this._unblockedChannelIds;
return activeUnblockedChannelIds?.has(channelId);
}
allow(allowListId, patterns, hosts) {
allow(allowListId, patterns, isPrivateMode, hosts) {
this._ensureStarted();
this._getAllowList(allowListId).setAllows(patterns, hosts);
this._getAllowList(allowListId, isPrivateMode).setAllows(patterns, hosts);
}
shim(allowListId, patterns, notHosts) {
shim(allowListId, patterns, isPrivateMode, notHosts) {
this._ensureStarted();
this._getAllowList(allowListId).setShims(patterns, notHosts);
this._getAllowList(allowListId, isPrivateMode).setShims(patterns, notHosts);
}
revoke(allowListId) {
this._allowLists.delete(allowListId);
this._PBModeAllowLists.delete(allowListId);
}
}
var manager = new Manager();
@ -202,7 +226,7 @@ this.trackingProtection = class extends ExtensionAPI {
context,
name: "trackingProtection.onSmartBlockEmbedReblock",
register: fire => {
const callback = (subject, topic, data) => {
const callback = (subject, _topic, data) => {
// chrome tab id needs to be converted to extension tab id
let hostname = subject.linkedBrowser.currentURI.host;
let tabId = tabManager.convert(subject).id;
@ -214,16 +238,31 @@ this.trackingProtection = class extends ExtensionAPI {
};
},
}).api(),
onPrivateSessionEnd: new EventManager({
context,
name: "trackingProtection.onPrivateSessionEnd",
register: fire => {
const callback = (_subject, _topic) => {
fire.sync();
};
Services.obs.addObserver(callback, "last-pb-context-exited");
return () => {
Services.obs.removeObserver(callback, "last-pb-context-exited");
};
},
}).api(),
async shim(allowListId, patterns, notHosts) {
manager.shim(allowListId, patterns, notHosts);
// shim for both PB and non-PB modes
manager.shim(allowListId, patterns, true, notHosts);
manager.shim(allowListId, patterns, false, notHosts);
},
async allow(allowListId, patterns, hosts) {
manager.allow(allowListId, patterns, hosts);
async allow(allowListId, patterns, isPrivate, hosts) {
manager.allow(allowListId, patterns, isPrivate, hosts);
},
async revoke(allowListId) {
manager.revoke(allowListId);
},
async wasRequestUnblocked(requestId) {
async wasRequestUnblocked(requestId, isPrivate) {
if (!manager) {
return false;
}
@ -231,7 +270,7 @@ this.trackingProtection = class extends ExtensionAPI {
if (!channelId) {
return false;
}
return manager.wasChannelIdUnblocked(channelId);
return manager.wasChannelIdUnblocked(channelId, isPrivate);
},
async isDFPIActive(isPrivate) {
if (isPrivate) {
@ -241,7 +280,7 @@ this.trackingProtection = class extends ExtensionAPI {
},
openProtectionsPanel(tabId) {
let tab = tabManager.get(tabId);
if (!tab.active) {
if (!tab?.active) {
// break if tab is not the active tab
return;
}
@ -252,6 +291,9 @@ this.trackingProtection = class extends ExtensionAPI {
"smartblock:open-protections-panel"
);
},
incrementSmartblockEmbedShownTelemetry() {
Glean.securityUiProtectionspopup.smartblockembedsShown.add();
},
async getSmartBlockEmbedFluentString(tabId, shimId, websiteHost) {
let win = tabManager.get(tabId).window;
let document = win.document;

View file

@ -46,6 +46,12 @@
"description": "Hostname of the website"
}
]
},
{
"name": "onPrivateSessionEnd",
"type": "function",
"description": "Event used to notify webcompat extension that it's time to clear PB state",
"parameters": []
}
],
"functions": [
@ -108,6 +114,10 @@
"type": "string"
}
},
{
"name": "isPrivate",
"type": "boolean"
},
{
"name": "hosts",
"description": "Hosts to allow the patterns on",
@ -139,6 +149,10 @@
{
"name": "requestId",
"type": "string"
},
{
"name": "isPrivate",
"type": "boolean"
}
],
"async": true
@ -155,6 +169,12 @@
}
]
},
{
"name": "incrementSmartblockEmbedShownTelemetry",
"type": "function",
"description": "Sends an observer message to inform protections panel that a embed was replaced on the window specified by the tab id",
"parameters": []
},
{
"name": "getSmartBlockEmbedFluentString",
"type": "function",

View file

@ -71,6 +71,7 @@ class Shim {
);
this._hostOptIns = new Set();
this._pBModeHostOptIns = new Set();
this._disabledByConfig = opts.disabled;
this._disabledGlobally = false;
@ -350,6 +351,7 @@ class Shim {
async _allowRequestsInETP() {
const matches = this.matches.map(m => m.patterns).flat();
if (matches.length) {
// ensure requests shimmed in both PB and non-PB modes
await browser.trackingProtection.shim(this.id, matches);
}
@ -359,10 +361,23 @@ class Shim {
await browser.trackingProtection.allow(
this.id,
this._optInPatterns,
false,
Array.from(this._hostOptIns)
);
}
}
if (this._pBModeHostOptIns.size) {
const optIns = this.getApplicableOptIns();
if (optIns.length) {
await browser.trackingProtection.allow(
this.id,
this._optInPatterns,
true,
Array.from(this._pBModeHostOptIns)
);
}
}
}
_revokeRequestsInETP() {
@ -456,21 +471,27 @@ class Shim {
return optins;
}
async onUserOptIn(host) {
async onUserOptIn(host, isPrivateMode) {
const optins = await this.getApplicableOptIns();
const activeHostOptIns = isPrivateMode
? this._pBModeHostOptIns
: this._hostOptIns;
if (optins.length) {
this.userHasOptedIn = true;
this._hostOptIns.add(host);
activeHostOptIns.add(host);
await browser.trackingProtection.allow(
this.id,
optins,
Array.from(this._hostOptIns)
isPrivateMode,
Array.from(activeHostOptIns)
);
}
}
hasUserOptedInAlready(host) {
return this._hostOptIns.has(host);
hasUserOptedInAlready(host, isPrivateMode) {
const activeHostOptIns = isPrivateMode
? this._pBModeHostOptIns
: this._hostOptIns;
return activeHostOptIns.has(host);
}
showOptInWarningOnce(tabId, origin) {
@ -489,18 +510,35 @@ class Shim {
.catch(() => {});
}
async onUserOptOut(host) {
async onUserOptOut(host, isPrivateMode) {
const optIns = await this.getApplicableOptIns();
const activeHostOptIns = isPrivateMode
? this._pBModeHostOptIns
: this._hostOptIns;
if (optIns.length) {
this._hostOptIns.delete(host);
activeHostOptIns.delete(host);
await browser.trackingProtection.allow(
this.id,
optIns,
Array.from(this._hostOptIns)
isPrivateMode,
Array.from(activeHostOptIns)
);
}
if (!this._hostOptIns.length) {
this.userHasOptedIn = false;
}
async clearUserOptIns(forPrivateMode) {
const optIns = await this.getApplicableOptIns();
const activeHostOptIns = forPrivateMode
? this._pBModeHostOptIns
: this._hostOptIns;
if (optIns.length) {
activeHostOptIns.clear();
await browser.trackingProtection.allow(
this.id,
optIns,
forPrivateMode,
Array.from(activeHostOptIns)
);
}
}
}
@ -539,13 +577,13 @@ class Shims {
console.warn("Smartblock shim not found", { tabId, shimId });
return;
}
await shim.onUserOptIn(hostname);
const isPB = (await browser.tabs.get(tabId)).incognito;
await shim.onUserOptIn(hostname, isPB);
// send request to shim to remove placeholders and replace with original embeds
await browser.tabs.sendMessage(tabId, {
shimId,
topic: "smartblock:unblock-embed",
data: hostname,
});
}
);
@ -558,14 +596,21 @@ class Shims {
console.warn("Smartblock shim not found", { tabId, shimId });
return;
}
await shim.onUserOptOut(hostname);
const isPB = (await browser.tabs.get(tabId)).incognito;
await shim.onUserOptOut(hostname, isPB);
// a browser reload is required to reload the shim in the case where the shim gets unloaded
// i.e. after user unblocks, then closes and revisits the page while shim is still allowed
browser.tabs.reload(tabId);
}
);
// handles data clearing on private browsing mode end
browser.trackingProtection.onPrivateSessionEnd.addListener(() => {
for (const shim of this.shims.values()) {
shim.clearUserOptIns(true);
}
});
}
bindAboutCompatBroker(broker) {
@ -889,6 +934,7 @@ class Shims {
message !== "getOptions" &&
message !== "optIn" &&
message !== "embedClicked" &&
message !== "smartblockEmbedReplaced" &&
message !== "smartblockGetFluentString"
) {
return undefined;
@ -917,7 +963,7 @@ class Shims {
);
} else if (message === "optIn") {
try {
await shim.onUserOptIn(new URL(url).hostname);
await shim.onUserOptIn(new URL(url).hostname, tab.incognito);
const origin = new URL(tab.url).origin;
warn(
"** User opted in for",
@ -936,6 +982,8 @@ class Shims {
}
} else if (message === "embedClicked") {
browser.trackingProtection.openProtectionsPanel(id);
} else if (message === "smartblockEmbedReplaced") {
browser.trackingProtection.incrementSmartblockEmbedShownTelemetry();
} else if (message === "smartblockGetFluentString") {
return await browser.trackingProtection.getSmartBlockEmbedFluentString(
id,
@ -1094,8 +1142,11 @@ class Shims {
// We need to base our checks not on the frame's host, but the tab's.
const topHost = new URL((await browser.tabs.get(tabId)).url).hostname;
const unblocked =
await browser.trackingProtection.wasRequestUnblocked(requestId);
const isPB = (await browser.tabs.get(details.tabId)).incognito;
const unblocked = await browser.trackingProtection.wasRequestUnblocked(
requestId,
isPB
);
let match;
let shimToApply;
@ -1107,7 +1158,6 @@ class Shims {
}
if (shim.onlyIfDFPIActive || shim.onlyIfPrivateBrowsing) {
const isPB = (await browser.tabs.get(details.tabId)).incognito;
if (!isPB && shim.onlyIfPrivateBrowsing) {
continue;
}
@ -1138,7 +1188,7 @@ class Shims {
// If the user has already opted in for this shim, all requests it covers
// should be allowed; no need for a shim anymore.
if (shim.hasUserOptedInAlready(topHost)) {
if (shim.hasUserOptedInAlready(topHost, isPB)) {
warn(
`Allowing tracking ${type} ${url} on tab ${tabId} frame ${frameId} due to opt-in`
);

View file

@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "Web Compatibility Interventions",
"description": "Urgent post-release fixes for web compatibility.",
"version": "136.3.0",
"version": "136.4.0",
"browser_specific_settings": {
"gecko": {
"id": "webcompat@mozilla.org",
@ -114,6 +114,7 @@
"shims/cxense.js",
"shims/doubleverify.js",
"shims/eluminate.js",
"shims/embed-test-shim.js",
"shims/empty-script.js",
"shims/empty-shim.txt",
"shims/everest.js",

View file

@ -138,6 +138,7 @@ FINAL_TARGET_FILES.features["webcompat@mozilla.org"]["shims"] += [
"shims/cxense.js",
"shims/doubleverify.js",
"shims/eluminate.js",
"shims/embed-test-shim.js",
"shims/empty-script.js",
"shims/empty-shim.txt",
"shims/everest.js",

View file

@ -0,0 +1,179 @@
/* 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 https://mozilla.org/MPL/2.0/. */
/* globals browser */
if (!window.smartblockTestShimInitialized) {
// Guard against this script running multiple times
window.smartblockTestShimInitialized = true;
const SHIM_ID = "EmbedTestShim";
// Original URL of the test embed script.
const ORIGINAL_URL =
"https://itisatracker.org/browser/browser/extensions/webcompat/tests/browser/embed_test.js";
// Use instagram logo as a test logo
const LOGO_URL = "https://smartblock.firefox.etp/instagram.svg";
let originalEmbedContainers = document.querySelectorAll(
".broken-embed-content"
);
let embedPlaceholders = [];
// Bug 1925582: this should be a common snippet for use in multiple shims.
function sendMessageToAddon(message) {
return browser.runtime.sendMessage({ message, shimId: SHIM_ID });
}
function addonMessageHandler(message) {
let { topic, shimId } = message;
// Only react to messages which are targeting this shim.
if (shimId != SHIM_ID) {
return;
}
if (topic === "smartblock:unblock-embed") {
// remove embed placeholders
embedPlaceholders.forEach((p, idx) => {
p.replaceWith(originalEmbedContainers[idx]);
});
// recreate scripts
let scriptElement = document.createElement("script");
// Set the script element's src with the website's principal instead of
// the content script principal to ensure the tracker script is not loaded
// via the content script's expanded principal.
scriptElement.wrappedJSObject.src = ORIGINAL_URL;
document.body.appendChild(scriptElement);
}
}
async function createShimPlaceholders() {
const [titleString, descriptionString, buttonString] =
await sendMessageToAddon("smartblockGetFluentString");
originalEmbedContainers.forEach(originalEmbedContainer => {
// this string has to be defined within this function to avoid linting errors
// see: https://github.com/mozilla/eslint-plugin-no-unsanitized/issues/259
const SMARTBLOCK_PLACEHOLDER_HTML_STRING = `
<style>
#smartblock-placeholder-wrapper {
min-height: 225px;
width: 400px;
padding: 32px 24px;
display: block;
align-content: center;
text-align: center;
background-color: light-dark(rgb(255, 255, 255), rgb(28, 27, 34));
color: light-dark(rgb(43, 42, 51), rgb(251, 251, 254));
border-radius: 8px;
border: 2px dashed #0250bb;
font-size: 14px;
line-height: 1.2;
font-family: system-ui;
}
#smartblock-placeholder-button {
min-height: 32px;
padding: 8px 14px;
border-radius: 4px;
font-weight: 600;
border: 0;
/* Colours match light/dark theme from
https://searchfox.org/mozilla-central/source/browser/themes/addons/light/manifest.json
https://searchfox.org/mozilla-central/source/browser/themes/addons/dark/manifest.json */
background-color: light-dark(rgb(0, 97, 224), rgb(0, 221, 255));
color: light-dark(rgb(251, 251, 254), rgb(43, 42, 51));
}
#smartblock-placeholder-button:hover {
/* Colours match light/dark theme from
https://searchfox.org/mozilla-central/source/browser/themes/addons/light/manifest.json
https://searchfox.org/mozilla-central/source/browser/themes/addons/dark/manifest.json */
background-color: light-dark(rgb(2, 80, 187), rgb(128, 235, 255));
}
#smartblock-placeholder-button:hover:active {
/* Colours match light/dark theme from
https://searchfox.org/mozilla-central/source/browser/themes/addons/light/manifest.json
https://searchfox.org/mozilla-central/source/browser/themes/addons/dark/manifest.json */
background-color: light-dark(rgb(5, 62, 148), rgb(170, 242, 255));
}
#smartblock-placeholder-title {
margin-block: 14px;
font-size: 16px;
font-weight: bold;
}
#smartblock-placeholder-desc {
margin-block: 14px;
}
</style>
<div id="smartblock-placeholder-wrapper">
<img id="smartblock-placeholder-image" width="24" height="24" />
<p id="smartblock-placeholder-title"></p>
<p id="smartblock-placeholder-desc"></p>
<button id="smartblock-placeholder-button"></button>
</div>`;
// Create the placeholder inside a shadow dom
const placeholderDiv = document.createElement("div");
embedPlaceholders.push(placeholderDiv);
// Tag the div with a class to make it easily detectable FOR THE TEST SHIM ONLY
placeholderDiv.classList.add("shimmed-embedded-content");
const shadowRoot = placeholderDiv.attachShadow({ mode: "closed" });
shadowRoot.innerHTML = SMARTBLOCK_PLACEHOLDER_HTML_STRING;
shadowRoot.getElementById("smartblock-placeholder-image").src = LOGO_URL;
shadowRoot.getElementById("smartblock-placeholder-title").textContent =
titleString;
shadowRoot.getElementById("smartblock-placeholder-desc").textContent =
descriptionString;
shadowRoot.getElementById("smartblock-placeholder-button").textContent =
buttonString;
// Wait for user to opt-in.
shadowRoot
.getElementById("smartblock-placeholder-button")
.addEventListener("click", ({ isTrusted }) => {
if (!isTrusted) {
return;
}
// Send a message to the addon to allow loading TikTok tracking resources
// needed by the embed.
sendMessageToAddon("embedClicked");
});
// Replace the embed with the placeholder
originalEmbedContainer.replaceWith(placeholderDiv);
sendMessageToAddon("smartblockEmbedReplaced");
});
// Dispatch event to signal that the script is done replacing FOR TEST SHIM ONLY
const finishedEvent = new CustomEvent("smartblockEmbedScriptFinished", {
bubbles: true,
composed: true,
});
window.dispatchEvent(finishedEvent);
}
// Listen for messages from the background script.
browser.runtime.onMessage.addListener(request => {
addonMessageHandler(request);
});
createShimPlaceholders();
}

View file

@ -23,18 +23,13 @@ if (!window.smartblockInstagramShimInitialized) {
}
function addonMessageHandler(message) {
let { topic, data, shimId } = message;
let { topic, shimId } = message;
// Only react to messages which are targeting this shim.
if (shimId != SHIM_ID) {
return;
}
if (topic === "smartblock:unblock-embed") {
if (data != window.location.hostname) {
// host name does not match the original hostname, user must have navigated
// away, skip replacing embeds
return;
}
// remove embed placeholders
embedPlaceholders.forEach((p, idx) => {
p.replaceWith(originalEmbedContainers[idx]);
@ -155,6 +150,8 @@ if (!window.smartblockInstagramShimInitialized) {
// Replace the embed with the placeholder
originalEmbedContainer.replaceWith(placeholderDiv);
sendMessageToAddon("smartblockEmbedReplaced");
});
}

View file

@ -24,18 +24,13 @@ if (!window.smartblockTikTokShimInitialized) {
}
function addonMessageHandler(message) {
let { topic, data, shimId } = message;
let { topic, shimId } = message;
// Only react to messages which are targeting this shim.
if (shimId != SHIM_ID) {
return;
}
if (topic === "smartblock:unblock-embed") {
if (data != window.location.hostname) {
// host name does not match the original hostname, user must have navigated
// away, skip replacing embeds
return;
}
// remove embed placeholders
embedPlaceholders.forEach((p, idx) => {
p.replaceWith(originalEmbedContainers[idx]);
@ -156,6 +151,8 @@ if (!window.smartblockTikTokShimInitialized) {
// Replace the embed with the placeholder
originalEmbedContainer.replaceWith(placeholderDiv);
sendMessageToAddon("smartblockEmbedReplaced");
});
}

View file

@ -8,6 +8,8 @@ support-files = [
"shims_test.html",
"shims_test_2.html",
"shims_test_3.html",
"smartblock_embed_test.html",
"embed_test.js",
]
["browser_aboutcompat.js"]
@ -15,3 +17,5 @@ support-files = [
["browser_shims.js"]
https_first_disabled = true
skip-if = ["verify"]
["browser_smartblockembeds.js"]

View file

@ -0,0 +1,305 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
add_setup(async function () {
await UrlClassifierTestUtils.addTestTrackers();
registerCleanupFunction(() => {
UrlClassifierTestUtils.cleanupTestTrackers();
Services.prefs.clearUserPref(TRACKING_PREF);
});
});
add_task(async function test_smartblock_embed_replaced() {
Services.prefs.setBoolPref(TRACKING_PREF, true);
Services.fog.testResetFOG();
let clickToggle = async toggle => {
let changed = BrowserTestUtils.waitForEvent(toggle, "toggle");
await EventUtils.synthesizeMouseAtCenter(toggle.buttonEl, {});
await changed;
};
let closeProtectionsPanel = async (win = window) => {
let protectionsPopup = win.document.getElementById("protections-popup");
if (!protectionsPopup) {
return;
}
let popuphiddenPromise = BrowserTestUtils.waitForEvent(
protectionsPopup,
"popuphidden"
);
PanelMultiView.hidePopup(protectionsPopup);
await popuphiddenPromise;
};
let openProtectionsPanel = async (win = window) => {
let popupShownPromise = BrowserTestUtils.waitForEvent(
win,
"popupshown",
true,
e => e.target.id == "protections-popup"
);
win.gProtectionsHandler.showProtectionsPopup();
await popupShownPromise;
};
// Open a site with a test "embed"
const tab = await BrowserTestUtils.openNewForegroundTab({
gBrowser,
waitForLoad: true,
});
let smartblockScriptFinished = BrowserTestUtils.waitForContentEvent(
tab.linkedBrowser,
"smartblockEmbedScriptFinished",
false,
null,
true
);
BrowserTestUtils.startLoadingURIString(
tab.linkedBrowser,
TEST_PAGE_WITH_SMARTBLOCK_COMPATIBLE_EMBED
);
await smartblockScriptFinished;
// Check TP enabled
const TrackingProtection = gProtectionsHandler.blockers.TrackingProtection;
ok(TrackingProtection, "TP is attached to the tab");
ok(TrackingProtection.enabled, "TP is enabled");
// Setup promise for listening for protections panel open
let popupShownPromise = BrowserTestUtils.waitForEvent(
window,
"popupshown",
true,
e => e.target.id == "protections-popup"
);
await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
// Check that the "embed" was replaced with a placeholder
let placeholder = content.document.querySelector(
".shimmed-embedded-content"
);
ok(placeholder, "Embed is replaced with a placeholder");
// Get the button element from the placeholder
let shadowRoot = placeholder.openOrClosedShadowRoot;
ok(shadowRoot, "Shadow root exists");
// Check that all elements are present
let placeholderButton = shadowRoot.querySelector(
"#smartblock-placeholder-button"
);
ok(placeholderButton, "Placeholder button exists");
let placeholderTitle = shadowRoot.querySelector(
"#smartblock-placeholder-title"
);
ok(placeholderTitle, "Placeholder title exists");
let placeholderLabel = shadowRoot.querySelector(
"#smartblock-placeholder-desc"
);
ok(placeholderLabel, "Placeholder description exists");
let placeholderImage = shadowRoot.querySelector(
"#smartblock-placeholder-image"
);
ok(placeholderImage, "Placeholder image exists");
// Click button to open protections panel
await EventUtils.synthesizeMouseAtCenter(placeholderButton, {}, content);
});
// If this await finished, then protections panel is open
await popupShownPromise;
// Check telemetry is triggered
let protectionsPanelOpenEvents =
Glean.securityUiProtectionspopup.openProtectionsPopup.testGetValue();
is(
protectionsPanelOpenEvents.length,
1,
"Protections panel open telemetry has correct embeds shown value"
);
is(
protectionsPanelOpenEvents[0].extra.openingReason,
"embedPlaceholderButton",
"Protections panel open telemetry has correct opening reason"
);
is(
protectionsPanelOpenEvents[0].extra.smartblockEmbedTogglesShown,
"true",
"Protections panel open telemetry has correct toggles shown value"
);
// Check smartblock section is unhidden
ok(
BrowserTestUtils.isVisible(
gProtectionsHandler._protectionsPopupSmartblockContainer
),
"Smartblock section is visible"
);
// Check toggle is present and off
let blockedEmbedToggle =
gProtectionsHandler._protectionsPopupSmartblockToggleContainer
.firstElementChild;
ok(blockedEmbedToggle, "Toggle exists in container");
ok(BrowserTestUtils.isVisible(blockedEmbedToggle), "Toggle is visible");
ok(
!blockedEmbedToggle.hasAttribute("pressed"),
"Unblock toggle should be off"
);
// Setup promise on custom event to wait for placeholders to finish replacing
let embedScriptFinished = BrowserTestUtils.waitForContentEvent(
tab.linkedBrowser,
"testEmbedScriptFinished",
false,
null,
true
);
// Click to toggle to unblock embed and wait for script to finish
await clickToggle(blockedEmbedToggle);
await embedScriptFinished;
await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
let unloadedEmbed = content.document.querySelector(".broken-embed-content");
ok(!unloadedEmbed, "Unloaded embeds should not be on the page");
// Check embed was put back on the page
let loadedEmbed = content.document.querySelector(".loaded-embed-content");
ok(loadedEmbed, "Embed should now be on the page");
});
// Check toggle telemetry is triggered
let toggleEvents =
Glean.securityUiProtectionspopup.clickSmartblockembedsToggle.testGetValue();
is(toggleEvents.length, 1, "Telemetry triggered for toggle press");
is(
toggleEvents[0].extra.isBlock,
"false",
"Toggle press telemetry is an unblock"
);
is(
toggleEvents[0].extra.openingReason,
"embedPlaceholderButton",
"Smartblock shown event has correct reason"
);
// close and open protections panel
await closeProtectionsPanel(window);
// Verify telemetry after close
let protectionsPanelClosedEvents =
Glean.securityUiProtectionspopup.closeProtectionsPopup.testGetValue();
is(
protectionsPanelClosedEvents.length,
1,
"Telemetry triggered for protections panel closed"
);
is(
protectionsPanelClosedEvents[0].extra.smartblockToggleClicked,
"true",
"Protections panel closed telemetry shows toggle was clicked"
);
is(
protectionsPanelClosedEvents[0].extra.openingReason,
"embedPlaceholderButton",
"Protections panel closed event has correct reason"
);
await openProtectionsPanel(window);
// Check if smartblock section is still there after unblock
ok(
BrowserTestUtils.isVisible(
gProtectionsHandler._protectionsPopupSmartblockContainer
),
"Smartblock section is visible"
);
// Check toggle is still there and is on now
blockedEmbedToggle =
gProtectionsHandler._protectionsPopupSmartblockToggleContainer
.firstElementChild;
ok(blockedEmbedToggle, "Toggle exists in container");
ok(BrowserTestUtils.isVisible(blockedEmbedToggle), "Toggle is visible");
ok(blockedEmbedToggle.hasAttribute("pressed"), "Unblock toggle should be on");
// Check protections panel open telemetry
// Check telemetry is triggered
protectionsPanelOpenEvents =
Glean.securityUiProtectionspopup.openProtectionsPopup.testGetValue();
is(
protectionsPanelOpenEvents.length,
2,
"Protections panel open telemetry has correct embeds shown value"
);
// Note: the openingReason shows as undefined since the test opened the
// protections panel directly with a function call"
is(
protectionsPanelOpenEvents[1].extra.openingReason,
undefined,
"Protections panel open telemetry has correct opening reason"
);
is(
protectionsPanelOpenEvents[1].extra.smartblockEmbedTogglesShown,
"true",
"Protections panel open telemetry has correct toggles shown value"
);
// Setup promise on custom event to wait for placeholders to finish replacing
smartblockScriptFinished = BrowserTestUtils.waitForContentEvent(
tab.linkedBrowser,
"smartblockEmbedScriptFinished",
false,
null,
true
);
// click toggle to reblock (this will trigger a reload)
clickToggle(blockedEmbedToggle);
// Wait for smartblock embed script to finish
await smartblockScriptFinished;
await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
// Check that the "embed" was replaced with a placeholder
let placeholder = content.document.querySelector(
".shimmed-embedded-content"
);
ok(placeholder, "Embed replaced with a placeholder after reblock");
});
// Check toggle telemetry is triggered
toggleEvents =
Glean.securityUiProtectionspopup.clickSmartblockembedsToggle.testGetValue();
is(toggleEvents.length, 2, "Telemetry triggered for toggle press");
is(
toggleEvents[1].extra.isBlock,
"true",
"Toggle press telemetry is a block"
);
// Note: the openingReason shows as undefined since the test opened the
// protections panel directly with a function call"
is(
toggleEvents[1].extra.openingReason,
undefined,
"Smartblock shown event has correct reason"
);
await BrowserTestUtils.removeTab(tab);
});

View file

@ -0,0 +1,20 @@
/* 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/. */
document.querySelectorAll(".broken-embed-content").forEach(embedContainer => {
// create the "real" embed content
let contentDiv = document.createElement("div");
contentDiv.classList.add("loaded-embed-content");
let contentContent = document.createTextNode("This is the loaded embed");
contentDiv.appendChild(contentContent);
// replace the embed code with the "real" embed
embedContainer.replaceWith(contentDiv);
});
const finishedEvent = new Event("testEmbedScriptFinished", {
bubbles: true,
});
window.dispatchEvent(finishedEvent);

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