Merge autoland to mozilla-central. a=merge

This commit is contained in:
Iulian Moraru 2023-05-09 00:40:09 +03:00
Родитель 35afddde8a a1284cc213
Коммит 373d05f4ea
224 изменённых файлов: 11249 добавлений и 3424 удалений

Просмотреть файл

@ -625,6 +625,33 @@ int32_t HyperTextAccessibleBase::CaretOffset() const {
return htOffset;
}
int32_t HyperTextAccessibleBase::CaretLineNumber() {
TextLeafPoint point = TextLeafPoint::GetCaret(const_cast<Accessible*>(Acc()))
.ActualizeCaret(/* aAdjustAtEndOfLine */ false);
if (point.mOffset == 0 && point.mAcc == Acc()) {
MOZ_ASSERT(CharacterCount() == 0);
// If a text box is empty, there will be no children, so point.mAcc will be
// this HyperText.
return 1;
}
if (!point.mAcc ||
(point.mAcc != Acc() && !Acc()->IsAncestorOf(point.mAcc))) {
// The caret is not within this HyperText.
return -1;
}
TextLeafPoint firstPointInThis = TextLeafPoint(Acc(), 0);
int32_t lineNumber = 1;
for (TextLeafPoint line = point; line && firstPointInThis < line;
line = line.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_START,
eDirPrevious)) {
lineNumber++;
}
return lineNumber;
}
bool HyperTextAccessibleBase::IsValidOffset(int32_t aOffset) {
index_t offset = ConvertMagicOffset(aOffset);
return offset.IsValid() && offset <= CharacterCount();

Просмотреть файл

@ -83,6 +83,12 @@ class HyperTextAccessibleBase {
virtual int32_t CaretOffset() const;
virtual void SetCaretOffset(int32_t aOffset) = 0;
/**
* Provide the line number for the caret.
* @return 1-based index for the line number with the caret
*/
virtual int32_t CaretLineNumber();
/**
* Transform magic offset into text offset.
*/

Просмотреть файл

@ -183,11 +183,7 @@ class HyperTextAccessible : public AccessibleWrap,
virtual int32_t CaretOffset() const override;
virtual void SetCaretOffset(int32_t aOffset) override;
/**
* Provide the line number for the caret.
* @return 1-based index for the line number with the caret
*/
int32_t CaretLineNumber();
virtual int32_t CaretLineNumber() override;
/**
* Return the caret rect and the widget containing the caret within this

Просмотреть файл

@ -55,7 +55,6 @@ void ScrollToPoint(uint32_t aScrollType, int32_t aX, int32_t aY);
void Announce(const nsString& aAnnouncement, uint16_t aPriority);
int32_t CaretLineNumber();
virtual int32_t CaretOffset() const override;
virtual void TextSubstring(int32_t aStartOffset, int32_t aEndOfset,
@ -176,7 +175,6 @@ virtual nsIntRect BoundsInCSSPixels() const override;
virtual void Language(nsAString& aLocale) override;
void DocType(nsString& aType);
void Title(nsString& aTitle);
void MimeType(nsString aMime);
void URLDocTypeMimeType(nsString& aURL, nsString& aDocType,
nsString& aMimeType);

Просмотреть файл

@ -1331,17 +1331,6 @@ mozilla::ipc::IPCResult DocAccessibleChild::RecvDocType(const uint64_t& aID,
return IPC_OK();
}
mozilla::ipc::IPCResult DocAccessibleChild::RecvTitle(const uint64_t& aID,
nsString* aTitle) {
LocalAccessible* acc = IdToAccessible(aID);
if (acc) {
mozilla::ErrorResult rv;
acc->GetContent()->GetTextContent(*aTitle, rv);
}
return IPC_OK();
}
mozilla::ipc::IPCResult DocAccessibleChild::RecvMimeType(const uint64_t& aID,
nsString* aMime) {
LocalAccessible* acc = IdToAccessible(aID);

Просмотреть файл

@ -395,8 +395,6 @@ class DocAccessibleChild : public DocAccessibleChildBase {
nsString* aLocale) override;
virtual mozilla::ipc::IPCResult RecvDocType(const uint64_t& aID,
nsString* aType) override;
virtual mozilla::ipc::IPCResult RecvTitle(const uint64_t& aID,
nsString* aTitle) override;
virtual mozilla::ipc::IPCResult RecvMimeType(const uint64_t& aID,
nsString* aMime) override;
virtual mozilla::ipc::IPCResult RecvURLDocTypeMimeType(

Просмотреть файл

@ -318,7 +318,6 @@ child:
[Nested=inside_sync] sync Language(uint64_t aID) returns(nsString aLocale);
[Nested=inside_sync] sync DocType(uint64_t aID) returns(nsString aType);
[Nested=inside_sync] sync Title(uint64_t aID) returns(nsString aTitle);
[Nested=inside_sync] sync MimeType(uint64_t aID) returns(nsString aMime);
[Nested=inside_sync] sync URLDocTypeMimeType(uint64_t aID) returns(nsString aURL, nsString aDocType, nsString aMimeType);

Просмотреть файл

@ -159,8 +159,13 @@ void RemoteAccessible::Announce(const nsString& aAnnouncement,
}
int32_t RemoteAccessible::CaretLineNumber() {
if (StaticPrefs::accessibility_cache_enabled_AtStartup()) {
MOZ_ASSERT(IsHyperText(), "is not hypertext?");
return RemoteAccessibleBase<RemoteAccessible>::CaretLineNumber();
}
int32_t line = -1;
Unused << mDoc->SendCaretOffset(mID, &line);
Unused << mDoc->SendCaretLineNumber(mID, &line);
return line;
}
@ -951,10 +956,6 @@ void RemoteAccessible::DocType(nsString& aType) {
Unused << mDoc->SendDocType(mID, &aType);
}
void RemoteAccessible::Title(nsString& aTitle) {
Unused << mDoc->SendTitle(mID, &aTitle);
}
void RemoteAccessible::MimeType(nsString aMime) {
Unused << mDoc->SendMimeType(mID, &aMime);
}

Просмотреть файл

@ -79,6 +79,8 @@ class RemoteAccessible : public RemoteAccessibleBase<RemoteAccessible> {
virtual int32_t SelectionCount() override;
virtual int32_t CaretLineNumber() override;
using RemoteAccessibleBase<RemoteAccessible>::SelectionBoundsAt;
bool SelectionBoundsAt(int32_t aSelectionNum, nsString& aData,
int32_t* aStartOffset, int32_t* aEndOffset);

Просмотреть файл

@ -18,16 +18,13 @@ using namespace mozilla::a11y;
- (NSString*)moxTitle {
nsAutoString title;
if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
mozilla::ErrorResult rv;
// XXX use the flattening API when there are available
// see bug 768298
acc->GetContent()->GetTextContent(title, rv);
} else if (RemoteAccessible* proxy = mGeckoAccessible->AsRemote()) {
proxy->Title(title);
}
title.CompressWhitespace();
ENameValueFlag flag = mGeckoAccessible->Name(title);
if (flag != eNameFromSubtree) {
// If this is a name via relation or attribute (eg. aria-label)
// it will be provided via AXDescription.
return nil;
}
return nsCocoaUtils::ToNSString(title);
}

Просмотреть файл

@ -98,13 +98,8 @@ inline NSString* ToNSString(id aValue) {
MOZ_ASSERT(mGeckoAccessible);
int32_t lineNumber = -1;
if (mGeckoAccessible->IsLocal()) {
if (HyperTextAccessible* textAcc =
mGeckoAccessible->AsLocal()->AsHyperText()) {
lineNumber = textAcc->CaretLineNumber() - 1;
}
} else {
lineNumber = mGeckoAccessible->AsRemote()->CaretLineNumber() - 1;
if (HyperTextAccessibleBase* textAcc = mGeckoAccessible->AsHyperTextBase()) {
lineNumber = textAcc->CaretLineNumber() - 1;
}
return (lineNumber >= 0) ? [NSNumber numberWithInt:lineNumber] : nil;

Просмотреть файл

@ -13,7 +13,7 @@ addAccessibleTask(
<h1 id="single-line-content">Were building a richer search experience</h1>
<h1 id="multi-lines-content">
Were building a
richer
richest
search experience
</h1>
`,
@ -33,7 +33,45 @@ search experience
);
is(
multiLinesContentHeading.getAttributeValue("AXTitle"),
"Were building a richer search experience"
"Were building a richest search experience"
);
}
);
/**
* Test AXTitle/AXDescription attributes of heading elements
*/
addAccessibleTask(
`
<h1 id="a">Hello <a href="#">world</a></h1>
<h1 id="b">Hello</h1>
<h1 id="c" aria-label="Goodbye">Hello</h1>
`,
async (browser, accDoc) => {
const a = getNativeInterface(accDoc, "a");
is(
a.getAttributeValue("AXTitle"),
"Hello world",
"Correct AXTitle for 'a'"
);
ok(
!a.getAttributeValue("AXDescription"),
"'a' Should not have AXDescription"
);
const b = getNativeInterface(accDoc, "b");
is(b.getAttributeValue("AXTitle"), "Hello", "Correct AXTitle for 'b'");
ok(
!b.getAttributeValue("AXDescription"),
"'b' Should not have AXDescription"
);
const c = getNativeInterface(accDoc, "c");
is(
c.getAttributeValue("AXDescription"),
"Goodbye",
"Correct AXDescription for 'c'"
);
ok(!c.getAttributeValue("AXTitle"), "'c' Should not have AXTitle");
}
);

Просмотреть файл

@ -200,7 +200,7 @@ addAccessibleTask(
link4
.getAttributeValue("AXLinkedUIElements")[0]
.getAttributeValue("AXTitle"),
"",
null,
"Link 4 is linked to the heading"
);
is(

Просмотреть файл

@ -549,10 +549,12 @@ addAccessibleTask(
<textarea id="hard">ab
cd
ef</textarea>
<div id="wrapped" contenteditable style="width: 1ch;">a b c</div>
<div role="textbox" id="wrapped" contenteditable style="width: 1ch;">a b c</div>
`,
async function(browser, docAcc) {
let hard = getNativeInterface(docAcc, "hard");
await focusIntoInput(docAcc, "hard");
is(hard.getAttributeValue("AXInsertionPointLineNumber"), 0);
let event = await synthKeyAndTestSelectionChanged(
"KEY_ArrowDown",
null,
@ -565,6 +567,7 @@ ef</textarea>
}
);
testSelectionEventLine(event, "cd");
is(hard.getAttributeValue("AXInsertionPointLineNumber"), 1);
event = await synthKeyAndTestSelectionChanged(
"KEY_ArrowDown",
null,
@ -577,8 +580,11 @@ ef</textarea>
}
);
testSelectionEventLine(event, "ef");
is(hard.getAttributeValue("AXInsertionPointLineNumber"), 2);
let wrapped = getNativeInterface(docAcc, "wrapped");
await focusIntoInput(docAcc, "wrapped");
is(wrapped.getAttributeValue("AXInsertionPointLineNumber"), 0);
event = await synthKeyAndTestSelectionChanged(
"KEY_ArrowDown",
null,
@ -591,6 +597,7 @@ ef</textarea>
}
);
testSelectionEventLine(event, "b ");
is(wrapped.getAttributeValue("AXInsertionPointLineNumber"), 1);
event = await synthKeyAndTestSelectionChanged(
"KEY_ArrowDown",
null,
@ -603,6 +610,7 @@ ef</textarea>
}
);
testSelectionEventLine(event, "c");
is(wrapped.getAttributeValue("AXInsertionPointLineNumber"), 2);
},
{ chrome: true, topLevel: true }
);

Просмотреть файл

@ -410,6 +410,15 @@ pref("browser.urlbar.suggest.calculator", false);
// Feature gate pref for weather suggestions in the urlbar.
pref("browser.urlbar.weather.featureGate", false);
// When false, the weather suggestion will not be fetched when a VPN is
// detected. When true, it will be fetched anyway.
pref("browser.urlbar.weather.ignoreVPN", false);
// The minimum prefix length of a weather keyword the user must type to trigger
// the suggestion. 0 means the min length should be taken from Nimbus or remote
// settings.
pref("browser.urlbar.weather.minKeywordLength", 0);
// If `browser.urlbar.weather.featureGate` is true, this controls whether
// weather suggestions are turned on.
pref("browser.urlbar.suggest.weather", true);

Просмотреть файл

@ -175,6 +175,12 @@
</popupnotification>
<popupnotification id="identity-credential-notification" hidden="true">
<popupnotificationheader id="identity-credential-header" orient="horizontal" hidden="true">
<html:div class="identity-credential-header-container">
<html:img class="identity-credential-header-icon"></html:img>
<span id="identity-credential-header-text"></span>
</html:div>
</popupnotificationheader>
<popupnotificationcontent id="identity-credential-provider" orient="vertical">
<html:div id="identity-credential-provider-selector-container">
</html:div>
@ -210,7 +216,7 @@
<popupnotification id="relay-integration-offer-notification" hidden="true">
<popupnotificationcontent orient="vertical">
<html:div>
<html:div>
<html:p data-l10n-id="firefox-relay-offer-why-relay"></html:p>
<html:p data-l10n-id="firefox-relay-offer-how-we-integrate"></html:p>
<html:p id="firefox-relay-offer-what-relay-does" data-l10n-id="firefox-relay-offer-what-relay-does" data-l10n-args='{"sitename": "", "useremail": ""}'></html:p>

Просмотреть файл

@ -3,6 +3,14 @@
"use strict";
// See bug 1831731. This test should not actually try to create a connection to
// the real DoH endpoint. But that may happen when clearing the proxy type, and
// sometimes even in the next test.
// To prevent that we override the IP to a local address.
Cc["@mozilla.org/network/native-dns-override;1"]
.getService(Ci.nsINativeDNSResolverOverride)
.addIPOverride("mozilla.cloudflare-dns.com", "127.0.0.1");
let oldProxyType = Services.prefs.getIntPref("network.proxy.type");
function resetPrefs() {
Services.prefs.clearUserPref("network.trr.mode");

Просмотреть файл

@ -138,9 +138,11 @@ var tests = [
// test security delay - too early
{
id: "Test#4",
run() {
async run() {
// Set the security delay to 100s
PopupNotifications.buttonDelay = 100000;
await SpecialPowers.pushPrefEnv({
set: [["security.notification_enable_delay", 100000]],
});
this.notifyObj = new BasicNotification(this.id);
showNotification(this.notifyObj);
@ -168,9 +170,12 @@ var tests = [
// test security delay - after delay
{
id: "Test#5",
run() {
async run() {
// Set the security delay to 10ms
PopupNotifications.buttonDelay = 10;
await SpecialPowers.pushPrefEnv({
set: [["security.notification_enable_delay", 10]],
});
this.notifyObj = new BasicNotification(this.id);
showNotification(this.notifyObj);
@ -192,7 +197,6 @@ var tests = [
!this.notifyObj.dismissalCallbackTriggered,
"dismissal callback was not triggered"
);
PopupNotifications.buttonDelay = PREF_SECURITY_DELAY_INITIAL;
},
},
// reload removes notification

Просмотреть файл

@ -36,17 +36,6 @@ add_setup(async function() {
await SpecialPowers.pushPrefEnv({
set: [["security.notification_enable_delay", TEST_SECURITY_DELAY]],
});
// The delay pref is stored on the PopupNotifications on init. Since a
// previous test may have already called PopupNotifications we need to update
// its internal field to account for the pref change.
// This can be removed once we switch PopupNotifications over to use a lazy
// pref getter, see Bug 1830925.
let originalButtonDelay = PopupNotifications.buttonDelay;
PopupNotifications.buttonDelay = TEST_SECURITY_DELAY;
// After the test revert buttonDelay to its original value.
registerCleanupFunction(async function() {
PopupNotifications.buttonDelay = originalButtonDelay;
});
});
/**
@ -62,7 +51,7 @@ add_task(async function test_timeShownMultipleNotifications() {
* We need to wait for the value performance.now() to be larger than the
* security delay in order to observe the bug. Only then does the
* timeSinceShown check in PopupNotifications.sys.mjs lead to a timeSinceShown
* value that is unconditionally greater than this.buttonDelay for
* value that is unconditionally greater than lazy.buttonDelay for
* notification.timeShown = null = 0.
* See: https://searchfox.org/mozilla-central/rev/f32d5f3949a3f4f185122142b29f2e3ab776836e/toolkit/modules/PopupNotifications.sys.mjs#1870-1872
*

Просмотреть файл

@ -41,10 +41,6 @@ function promiseTabLoadEvent(tab, url) {
return BrowserTestUtils.browserLoaded(browser, false, url);
}
const PREF_SECURITY_DELAY_INITIAL = Services.prefs.getIntPref(
"security.notification_enable_delay"
);
// Tests that call setup() should have a `tests` array defined for the actual
// tests to be run.
/* global tests */
@ -55,7 +51,6 @@ function setup() {
);
registerCleanupFunction(() => {
gBrowser.removeTab(gBrowser.selectedTab);
PopupNotifications.buttonDelay = PREF_SECURITY_DELAY_INITIAL;
});
}

Просмотреть файл

@ -18,8 +18,9 @@ function imageHandler(metadata, response) {
response.setHeader("Cache-Control", "max-age=10000", false);
response.setStatusLine(metadata.httpVersion, 200, "OK");
response.setHeader("Content-Type", "image/png", false);
var body =
"iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAEUlEQVQImWP4z8AAQTAamQkAhpcI+DeMzFcAAAAASUVORK5CYII=";
var body = atob(
"iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAEUlEQVQImWP4z8AAQTAamQkAhpcI+DeMzFcAAAAASUVORK5CYII="
);
response.bodyOutputStream.write(body, body.length);
}

Просмотреть файл

@ -283,6 +283,7 @@
background-size: 16px;
padding-inline-start: 21px;
margin-bottom: 20px;
text-decoration: underline;
@media (forced-colors: active) {
padding: 8px 10px;
@ -295,7 +296,7 @@
}
.external-link:hover {
text-decoration: underline;
text-decoration: none;
}
}

Просмотреть файл

@ -59,7 +59,7 @@ $ds-width: 936px;
font-weight: 500;
&:is(:focus, :hover) {
text-decoration: underline;
text-decoration: none;
}
}
}

Просмотреть файл

@ -127,6 +127,7 @@ $ds-card-image-gradient-solid: rgba(0, 0, 0, 1);
height: 100%;
display: flex;
flex-direction: column;
text-decoration: none;
&:hover {
header {

Просмотреть файл

@ -30,6 +30,10 @@
line-height: $line-height;
max-height: $line-height;
}
a {
text-decoration: none;
}
}
}
}

Просмотреть файл

@ -39,7 +39,7 @@
a {
&:hover,
&:active {
text-decoration: underline;
text-decoration: none;
}
&:active {
@ -57,7 +57,7 @@
float: inline-end;
&:hover {
text-decoration: underline;
text-decoration: none;
}
}

Просмотреть файл

@ -5,6 +5,6 @@
line-height: 24px;
a:hover {
text-decoration: underline;
text-decoration: none;
}
}

Просмотреть файл

@ -16,6 +16,10 @@ $letter-fallback-color: $white;
margin: 0 (-$half-base-gutter);
padding: 0;
a {
text-decoration: none;
}
&:not(.dnd-active) {
.top-site-outer:is(.active, :focus, :hover) {
background: var(--newtab-element-hover-color);

Просмотреть файл

@ -32,10 +32,6 @@ h2 {
font-weight: normal;
}
a {
text-decoration: none;
}
.inner-border {
border: $border-secondary;
border-radius: $border-radius;

Просмотреть файл

@ -286,10 +286,6 @@ h2 {
font-weight: normal;
}
a {
text-decoration: none;
}
.inner-border {
border: 1px solid var(--newtab-border-color);
border-radius: 3px;
@ -528,6 +524,9 @@ main.has-snippet {
margin: 0 -16px;
padding: 0;
}
.top-sites-list a {
text-decoration: none;
}
.top-sites-list:not(.dnd-active) .top-site-outer:is(.active, :focus, :hover) {
background: var(--newtab-element-hover-color);
}
@ -1859,6 +1858,7 @@ main.has-snippet {
background-size: 16px;
padding-inline-start: 21px;
margin-bottom: 20px;
text-decoration: underline;
}
@media (forced-colors: active) {
.home-section .external-link {
@ -1870,7 +1870,7 @@ main.has-snippet {
background-position-x: right;
}
.home-section .external-link:hover {
text-decoration: underline;
text-decoration: none;
}
.home-section .section .sponsored-checkbox:focus-visible,
@ -2622,7 +2622,7 @@ main.has-snippet {
font-weight: 500;
}
.collapsible-section.ds-layout .section-top-bar .learn-more-link a:is(:focus, :hover) {
text-decoration: underline;
text-decoration: none;
}
.ds-onboarding,
@ -3063,6 +3063,9 @@ main.has-snippet {
line-height: 20px;
max-height: 20px;
}
.ds-highlights .section .section-list .card-outer a {
text-decoration: none;
}
.ds-highlights .hide-for-narrow {
display: block;
}
@ -3111,7 +3114,7 @@ main.has-snippet {
content: none;
}
.ds-navigation ul li a:hover, .ds-navigation ul li a:active {
text-decoration: underline;
text-decoration: none;
}
.ds-navigation ul li a:active {
color: var(--newtab-primary-element-active-color);
@ -3124,7 +3127,7 @@ main.has-snippet {
float: inline-end;
}
.ds-navigation .ds-navigation-privacy:hover {
text-decoration: underline;
text-decoration: none;
}
.ds-navigation.ds-navigation-new-topics {
display: block;
@ -3474,6 +3477,7 @@ main.has-snippet {
height: 100%;
display: flex;
flex-direction: column;
text-decoration: none;
}
.ds-card .ds-card-link:hover header {
color: var(--newtab-primary-action-background);
@ -4016,7 +4020,7 @@ main.has-snippet {
line-height: 24px;
}
.ds-privacy-link a:hover {
text-decoration: underline;
text-decoration: none;
}
.ds-topics-widget {

Просмотреть файл

@ -290,10 +290,6 @@ h2 {
font-weight: normal;
}
a {
text-decoration: none;
}
.inner-border {
border: 1px solid var(--newtab-border-color);
border-radius: 3px;
@ -532,6 +528,9 @@ main.has-snippet {
margin: 0 -16px;
padding: 0;
}
.top-sites-list a {
text-decoration: none;
}
.top-sites-list:not(.dnd-active) .top-site-outer:is(.active, :focus, :hover) {
background: var(--newtab-element-hover-color);
}
@ -1863,6 +1862,7 @@ main.has-snippet {
background-size: 16px;
padding-inline-start: 21px;
margin-bottom: 20px;
text-decoration: underline;
}
@media (forced-colors: active) {
.home-section .external-link {
@ -1874,7 +1874,7 @@ main.has-snippet {
background-position-x: right;
}
.home-section .external-link:hover {
text-decoration: underline;
text-decoration: none;
}
.home-section .section .sponsored-checkbox:focus-visible,
@ -2626,7 +2626,7 @@ main.has-snippet {
font-weight: 500;
}
.collapsible-section.ds-layout .section-top-bar .learn-more-link a:is(:focus, :hover) {
text-decoration: underline;
text-decoration: none;
}
.ds-onboarding,
@ -3067,6 +3067,9 @@ main.has-snippet {
line-height: 20px;
max-height: 20px;
}
.ds-highlights .section .section-list .card-outer a {
text-decoration: none;
}
.ds-highlights .hide-for-narrow {
display: block;
}
@ -3115,7 +3118,7 @@ main.has-snippet {
content: none;
}
.ds-navigation ul li a:hover, .ds-navigation ul li a:active {
text-decoration: underline;
text-decoration: none;
}
.ds-navigation ul li a:active {
color: var(--newtab-primary-element-active-color);
@ -3128,7 +3131,7 @@ main.has-snippet {
float: inline-end;
}
.ds-navigation .ds-navigation-privacy:hover {
text-decoration: underline;
text-decoration: none;
}
.ds-navigation.ds-navigation-new-topics {
display: block;
@ -3478,6 +3481,7 @@ main.has-snippet {
height: 100%;
display: flex;
flex-direction: column;
text-decoration: none;
}
.ds-card .ds-card-link:hover header {
color: var(--newtab-primary-action-background);
@ -4020,7 +4024,7 @@ main.has-snippet {
line-height: 24px;
}
.ds-privacy-link a:hover {
text-decoration: underline;
text-decoration: none;
}
.ds-topics-widget {

Просмотреть файл

@ -286,10 +286,6 @@ h2 {
font-weight: normal;
}
a {
text-decoration: none;
}
.inner-border {
border: 1px solid var(--newtab-border-color);
border-radius: 3px;
@ -528,6 +524,9 @@ main.has-snippet {
margin: 0 -16px;
padding: 0;
}
.top-sites-list a {
text-decoration: none;
}
.top-sites-list:not(.dnd-active) .top-site-outer:is(.active, :focus, :hover) {
background: var(--newtab-element-hover-color);
}
@ -1859,6 +1858,7 @@ main.has-snippet {
background-size: 16px;
padding-inline-start: 21px;
margin-bottom: 20px;
text-decoration: underline;
}
@media (forced-colors: active) {
.home-section .external-link {
@ -1870,7 +1870,7 @@ main.has-snippet {
background-position-x: right;
}
.home-section .external-link:hover {
text-decoration: underline;
text-decoration: none;
}
.home-section .section .sponsored-checkbox:focus-visible,
@ -2622,7 +2622,7 @@ main.has-snippet {
font-weight: 500;
}
.collapsible-section.ds-layout .section-top-bar .learn-more-link a:is(:focus, :hover) {
text-decoration: underline;
text-decoration: none;
}
.ds-onboarding,
@ -3063,6 +3063,9 @@ main.has-snippet {
line-height: 20px;
max-height: 20px;
}
.ds-highlights .section .section-list .card-outer a {
text-decoration: none;
}
.ds-highlights .hide-for-narrow {
display: block;
}
@ -3111,7 +3114,7 @@ main.has-snippet {
content: none;
}
.ds-navigation ul li a:hover, .ds-navigation ul li a:active {
text-decoration: underline;
text-decoration: none;
}
.ds-navigation ul li a:active {
color: var(--newtab-primary-element-active-color);
@ -3124,7 +3127,7 @@ main.has-snippet {
float: inline-end;
}
.ds-navigation .ds-navigation-privacy:hover {
text-decoration: underline;
text-decoration: none;
}
.ds-navigation.ds-navigation-new-topics {
display: block;
@ -3474,6 +3477,7 @@ main.has-snippet {
height: 100%;
display: flex;
flex-direction: column;
text-decoration: none;
}
.ds-card .ds-card-link:hover header {
color: var(--newtab-primary-action-background);
@ -4016,7 +4020,7 @@ main.has-snippet {
line-height: 24px;
}
.ds-privacy-link a:hover {
text-decoration: underline;
text-decoration: none;
}
.ds-topics-widget {

Просмотреть файл

@ -38,8 +38,9 @@ function imageHandler(metadata, response) {
response.setHeader("Cache-Control", "max-age=10000", false);
response.setStatusLine(metadata.httpVersion, 200, "OK");
response.setHeader("Content-Type", "image/png", false);
var body =
"iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAEUlEQVQImWP4z8AAQTAamQkAhpcI+DeMzFcAAAAASUVORK5CYII=";
var body = atob(
"iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAEUlEQVQImWP4z8AAQTAamQkAhpcI+DeMzFcAAAAASUVORK5CYII="
);
response.bodyOutputStream.write(body, body.length);
}

Просмотреть файл

@ -16,6 +16,50 @@ re-rendering logic. All new components are being documented in Storybook in an
effort to create a catalog that engineers and designers can use to see which
components can be easily lifted off the shelf for use throughout Firefox.
## Designing new reusable widgets
Widgets that live at the global level, "UI Widgets", should be created in collaboration with the Design System team.
This ensures consistency with the rest of the elements in the Design System and the existing UI elements.
Otherwise, you should consult with your team and the appropriate designer to create domain-specific UI widgets.
Ideally, these domain widgets should be consistent with the rest of the UI patterns established in Firefox.
### Does an existing widget cover the use case you need?
Before creating a new reusable widget, make sure there isn't a widget you could use already.
When designing a new reusable widget, ensure it is designed for all users.
Here are some questions you can use to help include all users: how will people perceive, operate, and understand this widget? Will the widget use standards proven technology.
[Please refer to the "General Considerations" section of the Mozilla Accessibility Release Guidelines document](https://wiki.mozilla.org/Accessibility/Guidelines#General_Considerations) for more details to ensure your widget adheres to accessibility standards.
### Supporting widget use in different processes
A newly designed widget may need to work in the parent process, the content process, or both depending on your use case.
[See the Process Model document for more information about these different processes](https://firefox-source-docs.mozilla.org/dom/ipc/process_model.html).
You will likely be using your widget in a privileged process (such as the parent or privileged content) with access to `Services`, `XPCOMUtils`, and other globals.
Storybook and other web content do not have access to these privileged globals, so you will need to write workarounds for `Services`, `XPCOMUtils`, chrome URIs for CSS files and assets, etc.
[Check out moz-support-link.mjs and moz-support-link.stories.mjs for an example of a widget being used in the parent/chrome and needing to handle `XPCOMUtils` in Storybook](https://searchfox.org/mozilla-central/search?q=moz-support-link&path=&case=false&regexp=false).
[See moz-toggle.mjs for handling chrome URIs for CSS in Storybook](https://searchfox.org/mozilla-central/source/toolkit/content/widgets/moz-toggle/moz-toggle.mjs).
[See moz-label.mjs for an example of handling `Services` in Storybook](https://searchfox.org/mozilla-central/source/toolkit/content/widgets/moz-label/moz-label.mjs).
### Autonomous or Customized built-in Custom Elements
There are two types of custom elements, autonomous elements that extend `HTMLElement` and customized built-in elements that extend basic HTML elements.
If you use autonomous elements, you can use Shadow DOM and/or the Lit library.
[Lit does not support customized built-in custom elements](https://github.com/lit/lit-element/issues/879).
In some cases, you may want to provide some functionality on top of a built-in HTML element, [like how `moz-support-link` prepares the `href` value for anchor elements](https://searchfox.org/mozilla-central/rev/3563da061ca2b32f7f77f5f68088dbf9b5332a9f/toolkit/content/widgets/moz-support-link/moz-support-link.mjs#83-89).
In other cases, you may want to focus on creating markup and reacting to changes on the element.
This is where Lit can be useful for declaritively defining the markup and reacting to changes when attributes are updated.
### How will developers use your widget?
What does the interface to your widget look like?
Do you expect developers to use reactive attributes or [slots](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_templates_and_slots#adding_flexibility_with_slots)?
If there are many ways to accomplish the same end result, this could result in future confusion and increase the maintainance cost.
You should write stories for your widget to demonstrate how it can be used.
These stories can be used as guides for new use cases that may appear in the future.
This can also help draw the line for the responsibilities of your widget.
## Adding new design system components
We have a `./mach addwidget` scaffold command to make it easier to create new
@ -23,6 +67,7 @@ reusable components and hook them up to Storybook. Currently this command can
only be used to add a new Lit based web component to `toolkit/content/widgets`.
In the future we may expand it to support options for creating components
without using Lit and for adding components to different directories.
See [Bug 1803677](https://bugzilla.mozilla.org/show_bug.cgi?id=1803677) for more details on these future use cases.
To create a new component, you run:
@ -46,7 +91,7 @@ The scaffold command will generate the following files:
└── component-name.stories.mjs # component stories
```
It will also make modifications to `toolkit/content/jar.mn` to add chrome://
It will also make modifications to `toolkit/content/jar.mn` to add `chrome://`
URLs for the new files, and to `toolkit/content/tests/widgets/chrome.ini` to
enable running the newly added test.
@ -54,29 +99,69 @@ After running the scaffold command you can start Storybook and you will see
placeholder content that has been generated for your component. You can then
start altering the generated files and see your changes reflected in Storybook.
Unfortunately for now the
[`browser_all_files_referenced.js`](https://searchfox.org/mozilla-central/source/browser/base/content/test/static/browser_all_files_referenced.js)
test will fail unless your new component is immediately used somewhere outside
of Storybook. We have plans to fix this issue, but for now you can get around it
by updating [this array](https://searchfox.org/mozilla-central/source/browser/base/content/test/static/browser_all_files_referenced.js#107) to include your new chrome filepath.
### Known `browser_all_files_referenced.js` issue
## Using new components
Unfortunately for now [the
browser_all_files_referenced.js test](https://searchfox.org/mozilla-central/source/browser/base/content/test/static/browser_all_files_referenced.js)
will fail unless your new component is immediately used somewhere outside
of Storybook. We have plans to fix this issue, [see Bug 1806002 for more details](https://bugzilla.mozilla.org/show_bug.cgi?id=1806002), but for now you can get around it
by updating [this array](https://searchfox.org/mozilla-central/rev/5c922d8b93b43c18bf65539bfc72a30f84989003/browser/base/content/test/static/browser_all_files_referenced.js#113) to include your new chrome filepath.
### Using new design system components
Once you've added a new component to `toolkit/content/widgets` and created
chrome:// URLs via `toolkit/content/jar.mn` you should be able to start using it
`chrome://` URLs via `toolkit/content/jar.mn` you should be able to start using it
throughout Firefox. You can import the component into `html`/`xhtml` files via a
`script` tag with `type="module"`:
```html
<script type="module" src="chrome://global/content/elements/your-component-name.mjs"/>
<script type="module" src="chrome://global/content/elements/your-component-name.mjs"></script>
```
### Common pitfalls
If you are unable to import the new component in html, you can use [`ensureCustomElements()` in customElements.js](https://searchfox.org/mozilla-central/rev/31f5847a4494b3646edabbdd7ea39cb88509afe2/toolkit/content/customElements.js#865) in the relevant JS file.
For example, [we use `window.ensureCustomElements("moz-button-group")` in `browser-siteProtections.js`](https://searchfox.org/mozilla-central/rev/31f5847a4494b3646edabbdd7ea39cb88509afe2/browser/base/content/browser-siteProtections.js#1749).
**Note** you will need to add your new widget to [the switch in importCustomElementFromESModule](https://searchfox.org/mozilla-central/rev/85b4f7363292b272eb9b606e00de2c37a6be73f0/toolkit/content/customElements.js#845-859) for `ensureCustomElements()` to work as expected.
Once [Bug 1803810](https://bugzilla.mozilla.org/show_bug.cgi?id=1803810) lands, this process will be simplified: you won't need to use `ensureCustomElements()` and you will [add your widget to the appropriate array in customElements.js instead of the switch statement](https://searchfox.org/mozilla-central/rev/85b4f7363292b272eb9b606e00de2c37a6be73f0/toolkit/content/customElements.js#818-841).
If you're trying to use a reusable component but nothing is appearing on the
## Adding new domain-specific widgets
While we do have the `./mach addwidget` command, as noted in the [adding new design system components](#adding-new-design-system-components), this does not currently support the domain-specific widget case.
[See Bug 1828181 for more details on supporting this case](https://bugzilla.mozilla.org/show_bug.cgi?id=1828181).
Instead, you will need to do two things to have your new story appear in Storybook:
1. Create `<your-team-or-project>/<your-widget>.stories.mjs` in `browser/components/storybook/stories`
2. In this newly created story file, add the following default export:
```js
export default {
title: "Domain-specific UI Widgets/<your-team-or-project>/<your-widget>"
component: "<your-widget>"
};
```
The next time you run `./mach storybook`, a blank story entry for your widget should appear in your local Storybook.
Since Storybook is unaware of how the actual code is added or built, we won't go into detail about adding your new widget to the codebase.
It's recommended to have a `.html` test for your new widget since this lets you unit test the component directly rather than using integration tests with the domain.
To see what kind of files you may need for your widget, please refer back to [the output of the `./mach addwidget` command](#adding-new-design-system-components).
Just like with the UI widgets, [the `browser_all_files_referenced.js` will fail unless you use your component immediately outside of Storybook.](#known-browser_all_files_referencedjs-issue)
### Using new domain-specific widgets
This is effectively the same as [using new design system components](#using-new-design-system-components).
You will need to import your widget into the relevant `html`/`xhtml` files via a `script` tag with `type="module"`:
```html
<script type="module" src="chrome://browser/content/<domain-directory>/<your-widget>.mjs"></script>
```
Or use `window.ensureCustomElements("<your-widget>")` as previously stated in [the using new design system components section.](#using-new-design-system-components)
## Common pitfalls
If you're trying to use a reusable widget but nothing is appearing on the
page it may be due to one of the following issues:
- Omitting the `type="module"` in your `script` tag.
- Wrong file path for the `src` of your imported module.
- Widget is not declared or incorrectly declared in the correct `jar.mn` file.
- Not specifying the `html:` namespace when using a custom HTML element in an
`xhtml` file. For example the tag should look something like this:
@ -86,14 +171,13 @@ page it may be due to one of the following issues:
- Adding a `script` tag to an `inc.xhtml` file. For example when using a new
component in the privacy section of `about:preferences` the `script` tag needs
to be added to `preferences.xhtml` rather than to `privacy.inc.xhtml`.
- Trying to extend a built-in HTML element in Lit. Because Webkit never
implemented support for customized built-ins Lit doesn't support it either.
- Trying to extend a built-in HTML element in Lit. [Because Webkit never
implemented support for customized built-ins, Lit doesn't support it either.](https://github.com/lit/lit-element/issues/879#issuecomment-1061892879)
That means if you want to do something like:
```js
customElements.define("cool-button", CoolButton, { extends: "button" });
```
you will need to make a vanilla custom element, you cannot use Lit. For an
example of how this works in practice, see
[`moz-support-link`](https://searchfox.org/mozilla-central/source/toolkit/content/widgets/moz-support-link/moz-support-link.mjs).
you will need to make a vanilla custom element, you cannot use Lit.
[For an example of extending an HTML element, see `moz-support-link`](https://searchfox.org/mozilla-central/source/toolkit/content/widgets/moz-support-link/moz-support-link.mjs).

Просмотреть файл

@ -8,13 +8,13 @@ to test with dummy data. [Take a look at our Storybook instance!](https://firefo
Storybook lists components that can be reused, and helps document
what common elements we have. It can also list implementation specific
components, but they should not be added to the "Design System" section.
components, but they should be added to the "Domain-Specific UI Widgets" section.
Changes to files directly referenced from Storybook (so basically non-chrome://
paths) should automatically reflect changes in the opened browser. If you make a
change to a chrome:// referenced file then you'll need to do a hard refresh
change to a `chrome://` referenced file then you'll need to do a hard refresh
(Cmd+Shift+R/Ctrl+Shift+R) to notice the changes. If you're on Windows you may
need to `./mach build faster` to have the chrome:// URL show the latest version.
need to `./mach build faster` to have the `chrome://` URL show the latest version.
## Running Storybook
@ -36,7 +36,7 @@ To install dependencies and start Storybook, just run:
This single command will first install any missing dependencies then start the
local Storybook server. You should run your local build to test in Storybook
since chrome:// URLs are currently being pulled from the running browser, so any
since `chrome://` URLs are currently being pulled from the running browser, so any
changes to common-shared.css for example will come from your build.
The Storybook server will continue running and will watch for component file
@ -52,7 +52,7 @@ This will run your local browser and point it at `http://localhost:5703`. The
`launch` subcommand will also enable SVG context-properties so the `fill` CSS
property works in storybook.
Alternatively, you can simply navigate to http://localhost:5703/ or run:
Alternatively, you can simply navigate to `http://localhost:5703/` or run:
```sh
# In another terminal:
@ -87,12 +87,18 @@ cd browser/components/storybook && ../../../mach npm i -D your-package
## Adding new stories
Storybook is currently configured to search for story files (any file with a
`.stories.(js|mjs)` extension) in `toolkit/content/widgets` and
`.stories.(js|mjs|md)` extension) in `toolkit/content/widgets` and
`browser/components/storybook/stories`.
Stories in `toolkit/content/widgets` are used to document design system
components. The easiest way to use Storybook for non-design system elements is
components, also known as UI widgets.
As long as you used `./mach addwidget` correctly, there is no additional setup needed to view your newly created story in Storybook.
Stories in `browser/components/storybook/stories` are used for non-design system components, also called domain-specific UI widgets.
The easiest way to use Storybook for non-design system elements is
to add a new `.stories.mjs` file to `browser/components/storybook/stories`.
You will also need to set the title of your widget to be: `Domain-specific UI Widgets/<team-or-project-name>/<widget-name>` in the default exported object.
[See the Credential Management/Timeline widget for an example.](https://searchfox.org/mozilla-central/rev/2c11f18f89056a806c299a9d06bfa808718c2e84/browser/components/storybook/stories/credential-management.stories.mjs#11)
If you want to colocate your story with the code it is documenting you will need
to add to the `stories` array in the `.storybook/main.js` [configuration
@ -104,3 +110,18 @@ overview](https://storybook.js.org/docs/web-components/get-started/whats-a-story
of what's involved in writing a new story. For convenience you can use the [Lit
library](https://lit.dev/) to define the template code for your story, but this
is not a requirement.
### UI Widgets versus Domain-Specific UI Widgets
Widgets that are part of [our design system](https://acorn.firefox.com/latest/acorn.html) and intended to be used across the Mozilla suite of products live under the "UI Widgets" category in Storybook and under `toolkit/content/widgets/` in Firefox.
These global widgets are denoted in code by the `moz-` prefix in their name.
For example, the name `moz-support-link` informs us that this widget is design system compliant and can be used anywhere in Firefox.
Storybook can also be used to help document and prototype widgets that are specific to a part of the codebase and not intended for more global use.
Stories for these types of widgets live under the "Domain-Specific UI Widgets" category, while the code can live in any appropriate folder in `mozilla-central`.
[See the Credential Management folder as an example of a domain specific folder](https://firefoxux.github.io/firefox-desktop-components/?path=/docs/domain-specific-ui-widgets-credential-management-timeline--empty-timeline) and [see the credential-management.stories.mjs for how to make a domain specific folder in Storybook](https://searchfox.org/mozilla-central/source/browser/components/storybook/stories/credential-management.stories.mjs).
[To add a non-team specific widget to the "Domain-specific UI Widgets" section, see the migration-wizard.stories.mjs file](https://searchfox.org/mozilla-central/source/browser/components/storybook/stories/migration-wizard.stories.mjs).
Creating and documenting domain specific UI widgets allows other teams to be aware of and take inspiration from existing UI patterns.
With these widgets, **there is no guarantee that the element will work for your domain.**
If you need to use a domain-specific widget outside of its intended domain, it may be worth discussing how to convert this domain specific widget into a global UI widget.

Просмотреть файл

@ -19,11 +19,10 @@ XPCOMUtils.defineLazyModuleGetters(lazy, {
// Quick suggest features. On init, QuickSuggest creates an instance of each and
// keeps it in the `#features` map. See `BaseFeature`.
const FEATURES = {
AdmWikipedia: "resource:///modules/urlbar/private/AdmWikipedia.sys.mjs",
BlockedSuggestions:
"resource:///modules/urlbar/private/BlockedSuggestions.sys.mjs",
ImpressionCaps: "resource:///modules/urlbar/private/ImpressionCaps.sys.mjs",
RemoteSettingsClient:
"resource:///modules/urlbar/private/RemoteSettingsClient.sys.mjs",
Weather: "resource:///modules/urlbar/private/Weather.sys.mjs",
};
@ -97,15 +96,6 @@ class _QuickSuggest {
return ONBOARDING_URI;
}
/**
* @returns {RemoteSettingsClient}
* Quick suggest's remote settings client, which manages configuration and
* suggestions stored in remote settings.
*/
get remoteSettings() {
return this.#features.RemoteSettingsClient;
}
/**
* @returns {BlockedSuggestions}
* The blocked suggestions feature.
@ -172,6 +162,18 @@ class _QuickSuggest {
lazy.UrlbarPrefs.addObserver(this);
}
/**
* Returns a quick suggest feature by name.
*
* @param {string} name
* The name of the feature's JS class.
* @returns {BaseFeature}
* The feature object, an instance of a subclass of `BaseFeature`.
*/
getFeature(name) {
return this.#features[name];
}
/**
* Called when a urlbar pref changes.
*

Просмотреть файл

@ -402,6 +402,15 @@ const PREF_URLBAR_DEFAULTS = new Map([
// Feature gate pref for weather suggestions in the urlbar.
["weather.featureGate", false],
// When false, the weather suggestion will not be fetched when a VPN is
// detected. When true, it will be fetched anyway.
["weather.ignoreVPN", false],
// The minimum prefix length of a weather keyword the user must type to
// trigger the suggestion. 0 means the min length should be taken from Nimbus
// or remote settings.
["weather.minKeywordLength", 0],
// Feature gate pref for trending suggestions in the urlbar.
["trending.featureGate", false],
@ -436,6 +445,7 @@ const NIMBUS_DEFAULTS = {
recordNavigationalSuggestionTelemetry: false,
weatherKeywords: null,
weatherKeywordsMinimumLength: 0,
weatherKeywordsMinimumLengthCap: 0,
};
// Maps preferences under browser.urlbar.suggest to behavior names, as defined
@ -1242,6 +1252,22 @@ class Preferences {
this._observerWeakRefs.push(Cu.getWeakReference(observer));
}
/**
* Removes a preference observer.
*
* @param {object} observer
* An observer previously added with `addObserver()`.
*/
removeObserver(observer) {
for (let i = 0; i < this._observerWeakRefs.length; i++) {
let obs = this._observerWeakRefs[i].get();
if (obs && obs == observer) {
this._observerWeakRefs.splice(i, 1);
break;
}
}
}
/**
* Observes preference changes.
*

Просмотреть файл

@ -15,6 +15,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
MerinoClient: "resource:///modules/MerinoClient.sys.mjs",
PartnerLinkAttribution: "resource:///modules/PartnerLinkAttribution.sys.mjs",
QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
QuickSuggestRemoteSettings:
"resource:///modules/urlbar/private/QuickSuggestRemoteSettings.sys.mjs",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
});
@ -137,7 +139,7 @@ class ProviderQuickSuggest extends UrlbarProvider {
// There are two sources for quick suggest: remote settings and Merino.
let promises = [];
if (lazy.UrlbarPrefs.get("quickSuggestRemoteSettingsEnabled")) {
promises.push(lazy.QuickSuggest.remoteSettings.fetch(searchString));
promises.push(lazy.QuickSuggestRemoteSettings.query(searchString));
}
if (
lazy.UrlbarPrefs.get("merinoEnabled") &&
@ -228,14 +230,15 @@ class ProviderQuickSuggest extends UrlbarProvider {
#makeResult(queryContext, suggestion) {
let result;
switch (suggestion.provider) {
case "adm":
result = this.#makeAdmResult(queryContext, suggestion);
case "adm": // Merino
case "AdmWikipedia": // remote settings
result = lazy.QuickSuggest.getFeature("AdmWikipedia").makeResult(
queryContext,
suggestion,
this._trimmedSearchString
);
break;
default:
if (!suggestion.provider && suggestion.source == "remote-settings") {
result = this.#makeAdmResult(queryContext, suggestion);
break;
}
result = this.#makeDefaultResult(queryContext, suggestion);
break;
}
@ -262,88 +265,6 @@ class ProviderQuickSuggest extends UrlbarProvider {
return result;
}
#makeAdmResult(queryContext, suggestion) {
// Replace the suggestion's template substrings, but first save the original
// URL before its timestamp template is replaced.
let originalUrl = suggestion.url;
lazy.QuickSuggest.replaceSuggestionTemplates(suggestion);
let payload = {
originalUrl,
url: suggestion.url,
icon: suggestion.icon,
isSponsored: suggestion.is_sponsored,
source: suggestion.source,
telemetryType: suggestion.is_sponsored
? "adm_sponsored"
: "adm_nonsponsored",
requestId: suggestion.request_id,
urlTimestampIndex: suggestion.urlTimestampIndex,
sponsoredImpressionUrl: suggestion.impression_url,
sponsoredClickUrl: suggestion.click_url,
sponsoredBlockId: suggestion.block_id,
sponsoredAdvertiser: suggestion.advertiser,
sponsoredIabCategory: suggestion.iab_category,
helpUrl: lazy.QuickSuggest.HELP_URL,
helpL10n: {
id: "urlbar-result-menu-learn-more-about-firefox-suggest",
},
blockL10n: {
id: "urlbar-result-menu-dismiss-firefox-suggest",
},
};
// Determine if the suggestion itself is a best match.
let isSuggestionBestMatch = false;
if (lazy.QuickSuggest.remoteSettings.config.best_match) {
let { best_match } = lazy.QuickSuggest.remoteSettings.config;
let searchString = this._trimmedSearchString;
isSuggestionBestMatch =
best_match.min_search_string_length <= searchString.length &&
!best_match.blocked_suggestion_ids.includes(suggestion.block_id);
}
// Determine if the urlbar result should be a best match.
let isResultBestMatch =
isSuggestionBestMatch &&
lazy.UrlbarPrefs.get("bestMatchEnabled") &&
lazy.UrlbarPrefs.get("suggest.bestmatch");
if (isResultBestMatch) {
// Show the result as a best match. Best match titles don't include the
// `full_keyword`, and the user's search string is highlighted.
payload.title = [suggestion.title, UrlbarUtils.HIGHLIGHT.TYPED];
} else {
// Show the result as a usual quick suggest. Include the `full_keyword`
// and highlight the parts that aren't in the search string.
payload.title = suggestion.title;
payload.qsSuggestion = [
suggestion.full_keyword,
UrlbarUtils.HIGHLIGHT.SUGGESTED,
];
}
payload.isBlockable = lazy.UrlbarPrefs.get(
isResultBestMatch
? "bestMatchBlockingEnabled"
: "quickSuggestBlockingEnabled"
);
let result = new lazy.UrlbarResult(
UrlbarUtils.RESULT_TYPE.URL,
UrlbarUtils.RESULT_SOURCE.SEARCH,
...lazy.UrlbarResult.payloadAndSimpleHighlights(
queryContext.tokens,
payload
)
);
if (isResultBestMatch) {
result.isBestMatch = true;
result.suggestedIndex = 1;
}
return result;
}
#makeDefaultResult(queryContext, suggestion) {
let payload = {
url: suggestion.url,

Просмотреть файл

@ -279,19 +279,25 @@ class ProviderWeather extends UrlbarProvider {
}
getResultCommands(result) {
return [
let commands = [
{
name: RESULT_MENU_COMMAND.INACCURATE_LOCATION,
l10n: {
id: "firefox-suggest-weather-command-inaccurate-location",
},
},
{
];
if (lazy.QuickSuggest.weather.canIncrementMinKeywordLength) {
commands.push({
name: RESULT_MENU_COMMAND.SHOW_LESS_FREQUENTLY,
l10n: {
id: "firefox-suggest-weather-command-show-less-frequently",
},
},
});
}
commands.push(
{
l10n: {
id: "firefox-suggest-weather-command-dont-show-this",
@ -317,8 +323,10 @@ class ProviderWeather extends UrlbarProvider {
l10n: {
id: "urlbar-result-menu-learn-more-about-firefox-suggest",
},
},
];
}
);
return commands;
}
/**
@ -567,7 +575,6 @@ class ProviderWeather extends UrlbarProvider {
case RESULT_MENU_COMMAND.NOT_RELEVANT:
this.logger.info("Dismissing weather result");
lazy.UrlbarPrefs.set("suggest.weather", false);
queryContext.view.controller.removeResult(result);
queryContext.view.acknowledgeDismissal(result);
break;
case RESULT_MENU_COMMAND.INACCURATE_LOCATION:
@ -578,8 +585,8 @@ class ProviderWeather extends UrlbarProvider {
queryContext.view.acknowledgeFeedback(result);
break;
case RESULT_MENU_COMMAND.SHOW_LESS_FREQUENTLY:
// TODO: Increment required keyword length
queryContext.view.acknowledgeFeedback(result);
lazy.QuickSuggest.weather.incrementMinKeywordLength();
break;
}
}

Просмотреть файл

@ -436,7 +436,7 @@ export class UrlbarView {
acknowledgeDismissal(result) {
let row = this.#rows.children[result.rowIndex];
if (!row) {
if (!row || row.result != result) {
return;
}
@ -1836,6 +1836,9 @@ export class UrlbarView {
// Get the view update from the result's provider.
let provider = lazy.UrlbarProvidersManager.getProvider(result.providerName);
let viewUpdate = await provider.getViewUpdate(result, idsByName);
if (item.result != result) {
return;
}
// Update each node in the view by name.
for (let [nodeName, update] of Object.entries(viewUpdate)) {
@ -1849,6 +1852,9 @@ export class UrlbarView {
if (update.l10n) {
if (update.l10n.cacheable) {
await this.#l10nCache.ensureAll([update.l10n]);
if (item.result != result) {
return;
}
}
this.#setElementL10n(node, update.l10n);
} else if (update.textContent) {

Просмотреть файл

@ -57,10 +57,11 @@ EXTRA_JS_MODULES += [
]
EXTRA_JS_MODULES["urlbar/private"] += [
"private/AdmWikipedia.sys.mjs",
"private/BaseFeature.sys.mjs",
"private/BlockedSuggestions.sys.mjs",
"private/ImpressionCaps.sys.mjs",
"private/RemoteSettingsClient.sys.mjs",
"private/QuickSuggestRemoteSettings.sys.mjs",
"private/Weather.sys.mjs",
]

Просмотреть файл

@ -0,0 +1,284 @@
/* 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/. */
import { BaseFeature } from "resource:///modules/urlbar/private/BaseFeature.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
QuickSuggestRemoteSettings:
"resource:///modules/urlbar/private/QuickSuggestRemoteSettings.sys.mjs",
SuggestionsMap:
"resource:///modules/urlbar/private/QuickSuggestRemoteSettings.sys.mjs",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
});
const NONSPONSORED_IAB_CATEGORIES = new Set(["5 - Education"]);
/**
* A feature that manages sponsored adM and non-sponsored Wikpedia (sometimes
* called "expanded Wikipedia") suggestions in remote settings.
*/
export class AdmWikipedia extends BaseFeature {
constructor() {
super();
this.#suggestionsMap = new lazy.SuggestionsMap();
}
get shouldEnable() {
return (
lazy.UrlbarPrefs.get("quickSuggestRemoteSettingsEnabled") &&
(lazy.UrlbarPrefs.get("suggest.quicksuggest.nonsponsored") ||
lazy.UrlbarPrefs.get("suggest.quicksuggest.sponsored"))
);
}
get enablingPreferences() {
return [
"suggest.quicksuggest.nonsponsored",
"suggest.quicksuggest.sponsored",
];
}
enable(enabled) {
if (enabled) {
lazy.QuickSuggestRemoteSettings.register(this);
} else {
lazy.QuickSuggestRemoteSettings.unregister(this);
}
}
async queryRemoteSettings(searchString) {
let suggestions = this.#suggestionsMap.get(searchString);
if (!suggestions) {
return [];
}
// Start each icon fetch at the same time and wait for them all to finish.
let icons = await Promise.all(
suggestions.map(({ icon }) => this.#fetchIcon(icon))
);
return suggestions.map(suggestion => ({
full_keyword: this.#getFullKeyword(searchString, suggestion.keywords),
title: suggestion.title,
url: suggestion.url,
click_url: suggestion.click_url,
impression_url: suggestion.impression_url,
block_id: suggestion.id,
advertiser: suggestion.advertiser,
iab_category: suggestion.iab_category,
is_sponsored: !NONSPONSORED_IAB_CATEGORIES.has(suggestion.iab_category),
score: suggestion.score,
position: suggestion.position,
icon: icons.shift(),
}));
}
async onRemoteSettingsSync(rs) {
let dataType = lazy.UrlbarPrefs.get("quickSuggestRemoteSettingsDataType");
this.logger.debug("Loading remote settings with type: " + dataType);
let [data] = await Promise.all([
rs.get({ filters: { type: dataType } }),
rs
.get({ filters: { type: "icon" } })
.then(icons =>
Promise.all(icons.map(i => rs.attachments.downloadToDisk(i)))
),
]);
if (rs != lazy.QuickSuggestRemoteSettings.rs) {
return;
}
let suggestionsMap = new lazy.SuggestionsMap();
this.logger.debug(`Got data with ${data.length} records`);
for (let record of data) {
let { buffer } = await rs.attachments.download(record);
if (rs != lazy.QuickSuggestRemoteSettings.rs) {
return;
}
let results = JSON.parse(new TextDecoder("utf-8").decode(buffer));
this.logger.debug(`Adding ${results.length} results`);
await suggestionsMap.add(results);
if (rs != lazy.QuickSuggestRemoteSettings.rs) {
return;
}
}
this.#suggestionsMap = suggestionsMap;
}
makeResult(queryContext, suggestion, searchString) {
// Replace the suggestion's template substrings, but first save the original
// URL before its timestamp template is replaced.
let originalUrl = suggestion.url;
lazy.QuickSuggest.replaceSuggestionTemplates(suggestion);
let payload = {
originalUrl,
url: suggestion.url,
icon: suggestion.icon,
isSponsored: suggestion.is_sponsored,
source: suggestion.source,
telemetryType: suggestion.is_sponsored
? "adm_sponsored"
: "adm_nonsponsored",
requestId: suggestion.request_id,
urlTimestampIndex: suggestion.urlTimestampIndex,
sponsoredImpressionUrl: suggestion.impression_url,
sponsoredClickUrl: suggestion.click_url,
sponsoredBlockId: suggestion.block_id,
sponsoredAdvertiser: suggestion.advertiser,
sponsoredIabCategory: suggestion.iab_category,
helpUrl: lazy.QuickSuggest.HELP_URL,
helpL10n: {
id: "urlbar-result-menu-learn-more-about-firefox-suggest",
},
blockL10n: {
id: "urlbar-result-menu-dismiss-firefox-suggest",
},
};
// Determine if the suggestion itself is a best match.
let isSuggestionBestMatch = false;
if (lazy.QuickSuggestRemoteSettings.config.best_match) {
let { best_match } = lazy.QuickSuggestRemoteSettings.config;
isSuggestionBestMatch =
best_match.min_search_string_length <= searchString.length &&
!best_match.blocked_suggestion_ids.includes(suggestion.block_id);
}
// Determine if the urlbar result should be a best match.
let isResultBestMatch =
isSuggestionBestMatch &&
lazy.UrlbarPrefs.get("bestMatchEnabled") &&
lazy.UrlbarPrefs.get("suggest.bestmatch");
if (isResultBestMatch) {
// Show the result as a best match. Best match titles don't include the
// `full_keyword`, and the user's search string is highlighted.
payload.title = [suggestion.title, lazy.UrlbarUtils.HIGHLIGHT.TYPED];
} else {
// Show the result as a usual quick suggest. Include the `full_keyword`
// and highlight the parts that aren't in the search string.
payload.title = suggestion.title;
payload.qsSuggestion = [
suggestion.full_keyword,
lazy.UrlbarUtils.HIGHLIGHT.SUGGESTED,
];
}
payload.isBlockable = lazy.UrlbarPrefs.get(
isResultBestMatch
? "bestMatchBlockingEnabled"
: "quickSuggestBlockingEnabled"
);
let result = new lazy.UrlbarResult(
lazy.UrlbarUtils.RESULT_TYPE.URL,
lazy.UrlbarUtils.RESULT_SOURCE.SEARCH,
...lazy.UrlbarResult.payloadAndSimpleHighlights(
queryContext.tokens,
payload
)
);
if (isResultBestMatch) {
result.isBestMatch = true;
result.suggestedIndex = 1;
}
return result;
}
/**
* Gets the "full keyword" (i.e., suggestion) for a query from a list of
* keywords. The suggestions data doesn't include full keywords, so we make
* our own based on the result's keyword phrases and a particular query. We
* use two heuristics:
*
* (1) Find the first keyword phrase that has more words than the query. Use
* its first `queryWords.length` words as the full keyword. e.g., if the
* query is "moz" and `keywords` is ["moz", "mozi", "mozil", "mozill",
* "mozilla", "mozilla firefox"], pick "mozilla firefox", pop off the
* "firefox" and use "mozilla" as the full keyword.
* (2) If there isn't any keyword phrase with more words, then pick the
* longest phrase. e.g., pick "mozilla" in the previous example (assuming
* the "mozilla firefox" phrase isn't there). That might be the query
* itself.
*
* @param {string} query
* The query string.
* @param {Array} keywords
* An array of suggestion keywords.
* @returns {string}
* The full keyword.
*/
#getFullKeyword(query, keywords) {
let longerPhrase;
let trimmedQuery = query.toLocaleLowerCase().trim();
let queryWords = trimmedQuery.split(" ");
for (let phrase of keywords) {
if (phrase.startsWith(query)) {
let trimmedPhrase = phrase.trim();
let phraseWords = trimmedPhrase.split(" ");
// As an exception to (1), if the query ends with a space, then look for
// phrases with one more word so that the suggestion includes a word
// following the space.
let extra = query.endsWith(" ") ? 1 : 0;
let len = queryWords.length + extra;
if (len < phraseWords.length) {
// We found a phrase with more words.
return phraseWords.slice(0, len).join(" ");
}
if (
query.length < phrase.length &&
(!longerPhrase || longerPhrase.length < trimmedPhrase.length)
) {
// We found a longer phrase with the same number of words.
longerPhrase = trimmedPhrase;
}
}
}
return longerPhrase || trimmedQuery;
}
/**
* Fetch the icon from RemoteSettings attachments.
*
* @param {string} path
* The icon's remote settings path.
*/
async #fetchIcon(path) {
if (!path) {
return null;
}
let { rs } = lazy.QuickSuggestRemoteSettings;
if (!rs) {
return null;
}
let record = (
await rs.get({
filters: { id: `icon-${path}` },
})
).pop();
if (!record) {
return null;
}
return rs.attachments.downloadToDisk(record);
}
get _test_suggestionsMap() {
return this.#suggestionsMap;
}
#suggestionsMap;
}

Просмотреть файл

@ -26,10 +26,15 @@ ChromeUtils.defineESModuleGetters(lazy, {
* place, without mixing it with unrelated code and cluttering up
* `QuickSuggest`. You can also test it in isolation from `QuickSuggest`.
*
* - Your feature will automatically get its own logger.
* - Remote settings management. You can register your feature with
* `QuickSuggestRemoteSettings` and it will be called at appropriate times to
* sync from remote settings.
*
* If your feature can't benefit from these advantages, especially the first,
* feel free to implement it directly in `QuickSuggest`.
* - If your feature also serves suggestions from remote settings, you can
* implement one method, `queryRemoteSettings()`, to hook into
* `UrlbarProviderQuickSuggest`.
*
* - Your feature will automatically get its own logger.
*
* To register your subclass with `QuickSuggest`, add it to the `FEATURES` const
* in QuickSuggest.sys.mjs.
@ -69,6 +74,53 @@ export class BaseFeature {
throw new Error("`enable()` must be overridden");
}
/**
* If the feature manages suggestions from remote settings that should be
* returned by UrlbarProviderQuickSuggest, the subclass should override this
* method. It should return remote settings suggestions matching the given
* search string.
*
* @param {string} searchString
* The search string.
* @returns {Array}
* An array of matching suggestions, or null if not implemented.
*/
async queryRemoteSettings(searchString) {
return null;
}
/**
* If the feature manages data in remote settings, the subclass should
* override this method. It should fetch the data and build whatever data
* structures are necessary to support the feature.
*
* @param {RemoteSettings} rs
* The `RemoteSettings` client object.
*/
async onRemoteSettingsSync(rs) {}
/**
* If the feature corresponds to a type of suggestion, the subclass should
* override this method. It should return a new `UrlbarResult` for a given
* suggestion, which can come from either remote settings or Merino.
*
* @param {UrlbarQueryContext} queryContext
* The query context.
* @param {object} suggestion
* The suggestion from either remote settings or Merino.
* @param {string} searchString
* The search string that was used to fetch the suggestion. It may be
* different from `queryContext.searchString` due to trimming, lower-casing,
* etc. This is included as a param in case it's useful.
* @returns {UrlbarResult}
* A new result for the suggestion.
*/
makeResult(queryContext, suggestion, searchString) {
return null;
}
// Methods not designed for overriding below
/**
* @returns {Logger}
* The feature's logger.
@ -76,7 +128,7 @@ export class BaseFeature {
get logger() {
if (!this._logger) {
this._logger = lazy.UrlbarUtils.getLogger({
prefix: `QuickSuggest.${this.constructor.name}`,
prefix: `QuickSuggest.${this.name}`,
});
}
return this._logger;
@ -91,6 +143,14 @@ export class BaseFeature {
return this.#isEnabled;
}
/**
* @returns {string}
* The feature's name.
*/
get name() {
return this.constructor.name;
}
/**
* Enables or disables the feature according to `shouldEnable` and whether
* quick suggest is enabled. If the feature is already enabled appropriately,

Просмотреть файл

@ -9,6 +9,8 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
QuickSuggestRemoteSettings:
"resource:///modules/urlbar/private/QuickSuggestRemoteSettings.sys.mjs",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
clearInterval: "resource://gre/modules/Timer.sys.mjs",
setInterval: "resource://gre/modules/Timer.sys.mjs",
@ -69,8 +71,7 @@ export class ImpressionCaps extends BaseFeature {
JSON.stringify({
type,
currentStats: this.#stats,
impression_caps:
lazy.QuickSuggest.remoteSettings.config.impression_caps,
impression_caps: lazy.QuickSuggestRemoteSettings.config.impression_caps,
})
);
@ -175,10 +176,7 @@ export class ImpressionCaps extends BaseFeature {
// Validate stats against any changes to the impression caps in the config.
this._onConfigSet = () => this.#validateStats();
lazy.QuickSuggest.remoteSettings.emitter.on(
"config-set",
this._onConfigSet
);
lazy.QuickSuggestRemoteSettings.emitter.on("config-set", this._onConfigSet);
// Periodically record impression counters reset telemetry.
this.#setCountersResetInterval();
@ -192,7 +190,7 @@ export class ImpressionCaps extends BaseFeature {
}
#uninit() {
lazy.QuickSuggest.remoteSettings.emitter.off(
lazy.QuickSuggestRemoteSettings.emitter.off(
"config-set",
this._onConfigSet
);
@ -237,7 +235,7 @@ export class ImpressionCaps extends BaseFeature {
* corresponding to each impression cap. See the `#stats` comment for info.
*/
#validateStats() {
let { impression_caps } = lazy.QuickSuggest.remoteSettings.config;
let { impression_caps } = lazy.QuickSuggestRemoteSettings.config;
this.logger.info("Validating impression stats");
this.logger.debug(
@ -358,8 +356,7 @@ export class ImpressionCaps extends BaseFeature {
this.logger.debug(
JSON.stringify({
currentStats: this.#stats,
impression_caps:
lazy.QuickSuggest.remoteSettings.config.impression_caps,
impression_caps: lazy.QuickSuggestRemoteSettings.config.impression_caps,
})
);

Просмотреть файл

@ -0,0 +1,387 @@
/* 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 lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs",
RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
});
const RS_COLLECTION = "quicksuggest";
// Default score for remote settings suggestions.
const DEFAULT_SUGGESTION_SCORE = 0.2;
// Entries are added to `SuggestionsMap` map in chunks, and each chunk will add
// at most this many entries.
const SUGGESTIONS_MAP_CHUNK_SIZE = 1000;
const TELEMETRY_LATENCY = "FX_URLBAR_QUICK_SUGGEST_REMOTE_SETTINGS_LATENCY_MS";
/**
* Manages quick suggest remote settings data.
*/
class _QuickSuggestRemoteSettings {
/**
* @returns {number}
* The default score for remote settings suggestions, a value in the range
* [0, 1]. All suggestions require a score that can be used for comparison,
* so if a remote settings suggestion does not have one, it's assigned this
* value.
*/
get DEFAULT_SUGGESTION_SCORE() {
return DEFAULT_SUGGESTION_SCORE;
}
constructor() {
this.#emitter = new lazy.EventEmitter();
}
/**
* @returns {RemoteSettings}
* The underlying `RemoteSettings` client object.
*/
get rs() {
return this.#rs;
}
/**
* @returns {EventEmitter}
* The client will emit events on this object.
*/
get emitter() {
return this.#emitter;
}
/**
* @returns {object}
* Global quick suggest configuration stored in remote settings. When the
* config changes the `emitter` property will emit a "config-set" event. The
* config is an object that looks like this:
*
* {
* best_match: {
* min_search_string_length,
* blocked_suggestion_ids,
* },
* impression_caps: {
* nonsponsored: {
* lifetime,
* custom: [
* { interval_s, max_count },
* ],
* },
* sponsored: {
* lifetime,
* custom: [
* { interval_s, max_count },
* ],
* },
* },
* }
*/
get config() {
return this.#config;
}
get logger() {
if (!this.#logger) {
this.#logger = lazy.UrlbarUtils.getLogger({
prefix: "QuickSuggestRemoteSettings",
});
}
return this.#logger;
}
/**
* Registers a quick suggest feature that uses remote settings.
*
* @param {BaseFeature} feature
* An instance of a `BaseFeature` subclass. See `BaseFeature` for methods
* that the subclass must implement.
*/
register(feature) {
this.logger.debug("Registering feature: " + feature.name);
this.#features.add(feature);
if (this.#features.size == 1) {
this.#enableSettings(true);
}
this.#syncFeature(feature);
}
/**
* Unregisters a quick suggest feature that uses remote settings.
*
* @param {BaseFeature} feature
* An instance of a `BaseFeature` subclass.
*/
unregister(feature) {
this.logger.debug("Unregistering feature: " + feature.name);
this.#features.delete(feature);
if (!this.#features.size) {
this.#enableSettings(false);
}
}
/**
* Queries remote settings suggestions from all registered features.
*
* @param {string} searchString
* The search string.
* @returns {Array}
* The remote settings suggestions. If there are no matches, an empty array
* is returned.
*/
async query(searchString) {
let suggestions;
let stopwatchInstance = {};
TelemetryStopwatch.start(TELEMETRY_LATENCY, stopwatchInstance);
try {
suggestions = await this.#queryHelper(searchString);
TelemetryStopwatch.finish(TELEMETRY_LATENCY, stopwatchInstance);
} catch (error) {
TelemetryStopwatch.cancel(TELEMETRY_LATENCY, stopwatchInstance);
this.logger.error("Query error: " + error);
}
return suggestions || [];
}
async #queryHelper(searchString) {
this.logger.info("Handling query: " + JSON.stringify(searchString));
let results = await Promise.all(
[...this.#features].map(async feature => {
let suggestions = await feature.queryRemoteSettings(searchString);
return [feature, suggestions ?? []];
})
);
let allSuggestions = [];
for (let [feature, suggestions] of results) {
for (let suggestion of suggestions) {
suggestion.source = "remote-settings";
suggestion.provider = feature.name;
if (typeof suggestion.score != "number") {
suggestion.score = DEFAULT_SUGGESTION_SCORE;
}
allSuggestions.push(suggestion);
}
}
return allSuggestions;
}
async #enableSettings(enabled) {
if (enabled && !this.#rs) {
this.logger.debug("Creating RemoteSettings client");
this.#onSettingsSync = event => this.#syncAll({ event });
this.#rs = lazy.RemoteSettings(RS_COLLECTION);
this.#rs.on("sync", this.#onSettingsSync);
await this.#syncConfig();
} else if (!enabled && this.#rs) {
this.logger.debug("Destroying RemoteSettings client");
this.#rs.off("sync", this.#onSettingsSync);
this.#rs = null;
this.#onSettingsSync = null;
}
}
async #syncConfig() {
if (this._test_ignoreSettingsSync) {
return;
}
this.logger.debug("Syncing config");
let rs = this.#rs;
let configArray = await rs.get({ filters: { type: "configuration" } });
if (rs != this.#rs || this._test_ignoreSettingsSync) {
return;
}
this.logger.debug("Got config array: " + JSON.stringify(configArray));
this.#setConfig(configArray?.[0]?.configuration || {});
}
async #syncFeature(feature) {
if (this._test_ignoreSettingsSync) {
return;
}
this.logger.debug("Syncing feature: " + feature.name);
await feature.onRemoteSettingsSync(this.#rs);
}
async #syncAll({ event = null } = {}) {
if (this._test_ignoreSettingsSync) {
return;
}
this.logger.debug("Syncing all");
let rs = this.#rs;
// Remove local files of deleted records
if (event?.data?.deleted) {
await Promise.all(
event.data.deleted
.filter(d => d.attachment)
.map(entry =>
Promise.all([
this.#rs.attachments.deleteDownloaded(entry), // type: data
this.#rs.attachments.deleteFromDisk(entry), // type: icon
])
)
);
if (rs != this.#rs || this._test_ignoreSettingsSync) {
return;
}
}
let promises = [this.#syncConfig()];
for (let feature of this.#features) {
promises.push(this.#syncFeature(feature));
}
await Promise.all(promises);
}
/**
* Sets the quick suggest config and emits a "config-set" event.
*
* @param {object} config
* The config object.
*/
#setConfig(config) {
config ??= {};
this.logger.debug("Setting config: " + JSON.stringify(config));
this.#config = config;
this.#emitter.emit("config-set");
}
get _test_rs() {
return this.#rs;
}
_test_setConfig(config) {
this.#setConfig(config);
}
// The `RemoteSettings` client.
#rs = null;
// Registered `BaseFeature` instances.
#features = new Set();
// Configuration data synced from remote settings. See the `config` getter.
#config = {};
#emitter = null;
#logger = null;
#onSettingsSync = null;
}
export var QuickSuggestRemoteSettings = new _QuickSuggestRemoteSettings();
/**
* A wrapper around `Map` that handles quick suggest suggestions from remote
* settings. It maps keywords to suggestions. It has two benefits over `Map`:
*
* - The main benefit is that map entries are added in batches on idle to avoid
* blocking the main thread for too long, since there can be many suggestions
* and keywords.
* - A secondary benefit is that the interface is tailored to quick suggest
* suggestions, which have a `keywords` property.
*/
export class SuggestionsMap {
/**
* Returns the list of suggestions for a keyword.
*
* @param {string} keyword
* The keyword.
* @returns {Array}
* The array of suggestions for the keyword. If the keyword isn't in the
* map, the array will be empty.
*/
get(keyword) {
let object = this.#suggestionsByKeyword.get(keyword.toLocaleLowerCase());
if (!object) {
return [];
}
return Array.isArray(object) ? object : [object];
}
/**
* Adds a list of suggestion objects to the results map. Each suggestion must
* have a `keywords` property.
*
* @param {Array} suggestions
* Array of suggestion objects.
*/
async add(suggestions) {
// There can be many suggestions, and each suggestion can have many
// keywords. To avoid blocking the main thread for too long, update the map
// in chunks, and to avoid blocking the UI and other higher priority work,
// do each chunk only when the main thread is idle. During each chunk, we'll
// add at most `chunkSize` entries to the map.
let suggestionIndex = 0;
let keywordIndex = 0;
// Keep adding chunks until all suggestions have been fully added.
while (suggestionIndex < suggestions.length) {
await new Promise(resolve => {
Services.tm.idleDispatchToMainThread(() => {
// Keep updating the map until the current chunk is done.
let indexInChunk = 0;
while (
indexInChunk < SuggestionsMap.chunkSize &&
suggestionIndex < suggestions.length
) {
let suggestion = suggestions[suggestionIndex];
if (keywordIndex == suggestion.keywords.length) {
suggestionIndex++;
keywordIndex = 0;
continue;
}
// If the keyword's only suggestion is `suggestion`, store it
// directly as the value. Otherwise store an array of suggestions.
// For details, see the `#suggestionsByKeyword` comment.
let keyword = suggestion.keywords[keywordIndex];
let object = this.#suggestionsByKeyword.get(keyword);
if (!object) {
this.#suggestionsByKeyword.set(keyword, suggestion);
} else if (!Array.isArray(object)) {
this.#suggestionsByKeyword.set(keyword, [object, suggestion]);
} else {
object.push(suggestion);
}
keywordIndex++;
indexInChunk++;
}
// The current chunk is done.
resolve();
});
});
}
}
clear() {
this.#suggestionsByKeyword.clear();
}
// Maps each keyword in the dataset to one or more suggestions for the
// keyword. If only one suggestion uses a keyword, the keyword's value in the
// map will be the suggestion object. If more than one suggestion uses the
// keyword, the value will be an array of the suggestions. The reason for not
// always using an array is that we expect the vast majority of keywords to be
// used by only one suggestion, and since there are potentially very many
// keywords and suggestions and we keep them in memory all the time, we want
// to save as much memory as possible.
#suggestionsByKeyword = new Map();
// This is only defined as a property so that tests can override it.
static chunkSize = SUGGESTIONS_MAP_CHUNK_SIZE;
}

Просмотреть файл

@ -1,449 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { BaseFeature } from "resource:///modules/urlbar/private/BaseFeature.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs",
RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
TaskQueue: "resource:///modules/UrlbarUtils.sys.mjs",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
});
const RS_COLLECTION = "quicksuggest";
// Categories that should show "Firefox Suggest" instead of "Sponsored"
const NONSPONSORED_IAB_CATEGORIES = new Set(["5 - Education"]);
// Default score for remote settings suggestions.
const DEFAULT_SUGGESTION_SCORE = 0.2;
// Entries are added to the `#resultsByKeyword` map in chunks, and each chunk
// will add at most this many entries.
const ADD_RESULTS_CHUNK_SIZE = 1000;
const TELEMETRY_LATENCY = "FX_URLBAR_QUICK_SUGGEST_REMOTE_SETTINGS_LATENCY_MS";
/**
* Fetches the suggestions data from RemoteSettings and builds the structures
* to provide suggestions for UrlbarProviderQuickSuggest.
*/
export class RemoteSettingsClient extends BaseFeature {
/**
* @returns {number}
* The default score for remote settings suggestions, a value in the range
* [0, 1]. All suggestions require a score that can be used for comparison,
* so if a remote settings suggestion does not have one, it's assigned this
* value.
*/
static get DEFAULT_SUGGESTION_SCORE() {
return DEFAULT_SUGGESTION_SCORE;
}
constructor() {
super();
this.#taskQueue = new lazy.TaskQueue();
this.#emitter = new lazy.EventEmitter();
}
get shouldEnable() {
return (
lazy.UrlbarPrefs.get("suggest.quicksuggest.nonsponsored") ||
lazy.UrlbarPrefs.get("suggest.quicksuggest.sponsored")
);
}
get enablingPreferences() {
return [
"suggest.quicksuggest.nonsponsored",
"suggest.quicksuggest.sponsored",
];
}
/**
* @returns {EventEmitter}
* The client will emit events on this object.
*/
get emitter() {
return this.#emitter;
}
/**
* @returns {Promise}
* Resolves when any ongoing updates to the suggestions data are done.
*/
get readyPromise() {
return this.#taskQueue.emptyPromise;
}
/**
* @returns {object}
* Global quick suggest configuration stored in remote settings. When the
* config changes the `emitter` property will emit a "config-set" event. The
* config is an object that looks like this:
*
* {
* best_match: {
* min_search_string_length,
* blocked_suggestion_ids,
* },
* impression_caps: {
* nonsponsored: {
* lifetime,
* custom: [
* { interval_s, max_count },
* ],
* },
* sponsored: {
* lifetime,
* custom: [
* { interval_s, max_count },
* ],
* },
* },
* }
*/
get config() {
return this.#config;
}
enable(enabled) {
this.#queueSettingsSetup(enabled);
}
/**
* Fetches remote settings suggestions.
*
* @param {string} searchString
* The search string.
* @returns {Array}
* The remote settings suggestions. If there are no matches, an empty array
* is returned.
*/
async fetch(searchString) {
let suggestions;
let stopwatchInstance = {};
TelemetryStopwatch.start(TELEMETRY_LATENCY, stopwatchInstance);
try {
suggestions = await this.#fetchHelper(searchString);
TelemetryStopwatch.finish(TELEMETRY_LATENCY, stopwatchInstance);
} catch (error) {
TelemetryStopwatch.cancel(TELEMETRY_LATENCY, stopwatchInstance);
this.logger.error("Error fetching suggestions: " + error);
}
return suggestions || [];
}
/**
* Helper for `fetch()` that actually looks up the matching suggestions.
*
* @param {string} phrase
* The search string.
* @returns {Array}
* The matched suggestion objects. If there are no matches, an empty array
* is returned.
*/
async #fetchHelper(phrase) {
this.logger.info("Handling query: " + JSON.stringify(phrase));
phrase = phrase.toLowerCase();
let object = this.#resultsByKeyword.get(phrase);
if (!object) {
return [];
}
// `object` will be a single result object if there's only one match or an
// array of result objects if there's more than one match.
let results = [object].flat();
// Start each icon fetch at the same time and wait for them all to finish.
let icons = await Promise.all(
results.map(({ icon }) => this.#fetchIcon(icon))
);
return results.map(result => ({
full_keyword: this.getFullKeyword(phrase, result.keywords),
title: result.title,
url: result.url,
click_url: result.click_url,
impression_url: result.impression_url,
block_id: result.id,
advertiser: result.advertiser,
iab_category: result.iab_category,
is_sponsored: !NONSPONSORED_IAB_CATEGORIES.has(result.iab_category),
score:
typeof result.score == "number"
? result.score
: DEFAULT_SUGGESTION_SCORE,
source: "remote-settings",
icon: icons.shift(),
position: result.position,
}));
}
/**
* Gets the full keyword (i.e., suggestion) for a result and query. The data
* doesn't include full keywords, so we make our own based on the result's
* keyword phrases and a particular query. We use two heuristics:
*
* (1) Find the first keyword phrase that has more words than the query. Use
* its first `queryWords.length` words as the full keyword. e.g., if the
* query is "moz" and `result.keywords` is ["moz", "mozi", "mozil",
* "mozill", "mozilla", "mozilla firefox"], pick "mozilla firefox", pop
* off the "firefox" and use "mozilla" as the full keyword.
* (2) If there isn't any keyword phrase with more words, then pick the
* longest phrase. e.g., pick "mozilla" in the previous example (assuming
* the "mozilla firefox" phrase isn't there). That might be the query
* itself.
*
* @param {string} query
* The query string that matched `result`.
* @param {Array} keywords
* An array of result keywords.
* @returns {string}
* The full keyword.
*/
getFullKeyword(query, keywords) {
let longerPhrase;
let trimmedQuery = query.trim();
let queryWords = trimmedQuery.split(" ");
for (let phrase of keywords) {
if (phrase.startsWith(query)) {
let trimmedPhrase = phrase.trim();
let phraseWords = trimmedPhrase.split(" ");
// As an exception to (1), if the query ends with a space, then look for
// phrases with one more word so that the suggestion includes a word
// following the space.
let extra = query.endsWith(" ") ? 1 : 0;
let len = queryWords.length + extra;
if (len < phraseWords.length) {
// We found a phrase with more words.
return phraseWords.slice(0, len).join(" ");
}
if (
query.length < phrase.length &&
(!longerPhrase || longerPhrase.length < trimmedPhrase.length)
) {
// We found a longer phrase with the same number of words.
longerPhrase = trimmedPhrase;
}
}
}
return longerPhrase || trimmedQuery;
}
/**
* Queues a task to ensure our remote settings client is initialized or torn
* down as appropriate.
*
* @param {boolean} enabled
* Whether the feature should be enabled.
*/
#queueSettingsSetup(enabled) {
this.#taskQueue.queue(() => {
if (enabled && !this.#rs) {
this.#onSettingsSync = (...args) => this.#queueSettingsSync(...args);
this.#rs = lazy.RemoteSettings(RS_COLLECTION);
this.#rs.on("sync", this.#onSettingsSync);
this.#queueSettingsSync();
} else if (!enabled && this.#rs) {
this.#rs.off("sync", this.#onSettingsSync);
this.#rs = null;
this.#onSettingsSync = null;
}
});
}
/**
* Queues a task to populate the results map from the remote settings data
* plus any other work that needs to be done on sync.
*
* @param {object} [event]
* The event object passed to the "sync" event listener if you're calling
* this from the listener.
*/
async #queueSettingsSync(event = null) {
await this.#taskQueue.queue(async () => {
if (!this.#rs || this._test_ignoreSettingsSync) {
return;
}
// Remove local files of deleted records
if (event?.data?.deleted) {
await Promise.all(
event.data.deleted
.filter(d => d.attachment)
.map(entry =>
Promise.all([
this.#rs.attachments.deleteDownloaded(entry), // type: data
this.#rs.attachments.deleteFromDisk(entry), // type: icon
])
)
);
}
let dataType = lazy.UrlbarPrefs.get("quickSuggestRemoteSettingsDataType");
this.logger.debug("Loading data with type: " + dataType);
let [configArray, data] = await Promise.all([
this.#rs.get({ filters: { type: "configuration" } }),
this.#rs.get({ filters: { type: dataType } }),
this.#rs
.get({ filters: { type: "icon" } })
.then(icons =>
Promise.all(icons.map(i => this.#rs.attachments.downloadToDisk(i)))
),
]);
this.logger.debug("Got configuration: " + JSON.stringify(configArray));
this.#setConfig(configArray?.[0]?.configuration || {});
this.#resultsByKeyword.clear();
this.logger.debug(`Got data with ${data.length} records`);
for (let record of data) {
let { buffer } = await this.#rs.attachments.download(record);
let results = JSON.parse(new TextDecoder("utf-8").decode(buffer));
this.logger.debug(`Adding ${results.length} results`);
await this.#addResults(results);
}
});
}
/**
* Sets the quick suggest config and emits a "config-set" event.
*
* @param {object} config
* The config object.
*/
#setConfig(config) {
this.#config = config || {};
this.#emitter.emit("config-set");
}
/**
* Adds a list of result objects to the results map. This method is also used
* by tests to set up mock suggestions.
*
* @param {Array} results
* Array of result objects.
*/
async #addResults(results) {
// There can be many results, and each result can have many keywords. To
// avoid blocking the main thread for too long, update the map in chunks,
// and to avoid blocking the UI and other higher priority work, do each
// chunk only when the main thread is idle. During each chunk, we'll add at
// most `_addResultsChunkSize` entries to the map.
let resultIndex = 0;
let keywordIndex = 0;
// Keep adding chunks until all results have been fully added.
while (resultIndex < results.length) {
await new Promise(resolve => {
Services.tm.idleDispatchToMainThread(() => {
// Keep updating the map until the current chunk is done.
let indexInChunk = 0;
while (
indexInChunk < this._addResultsChunkSize &&
resultIndex < results.length
) {
let result = results[resultIndex];
if (keywordIndex == result.keywords.length) {
resultIndex++;
keywordIndex = 0;
continue;
}
// If the keyword's only result is `result`, store it directly as
// the value. Otherwise store an array of results. For details, see
// the `#resultsByKeyword` comment.
let keyword = result.keywords[keywordIndex];
let object = this.#resultsByKeyword.get(keyword);
if (!object) {
this.#resultsByKeyword.set(keyword, result);
} else if (!Array.isArray(object)) {
this.#resultsByKeyword.set(keyword, [object, result]);
} else {
object.push(result);
}
keywordIndex++;
indexInChunk++;
}
// The current chunk is done.
resolve();
});
});
}
}
/**
* Fetch the icon from RemoteSettings attachments.
*
* @param {string} path
* The icon's remote settings path.
*/
async #fetchIcon(path) {
if (!path || !this.#rs) {
return null;
}
let record = (
await this.#rs.get({
filters: { id: `icon-${path}` },
})
).pop();
if (!record) {
return null;
}
return this.#rs.attachments.downloadToDisk(record);
}
get _test_rs() {
return this.#rs;
}
get _test_resultsByKeyword() {
return this.#resultsByKeyword;
}
_test_setConfig(config) {
this.#setConfig(config);
}
async _test_addResults(results) {
await this.#addResults(results);
}
// The RemoteSettings client.
#rs = null;
// Task queue for serializing access to remote settings and related data.
// Methods in this class should use this when they need to to modify or access
// the settings client. It ensures settings accesses are serialized, do not
// overlap, and happen only one at a time. It also lets clients, especially
// tests, use this class without having to worry about whether a settings sync
// or initialization is ongoing; see `readyPromise`.
#taskQueue = null;
// Configuration data synced from remote settings. See the `config` getter.
#config = {};
// Maps each keyword in the dataset to one or more results for the keyword. If
// only one result uses a keyword, the keyword's value in the map will be the
// result object. If more than one result uses the keyword, the value will be
// an array of the results. The reason for not always using an array is that
// we expect the vast majority of keywords to be used by only one result, and
// since there are potentially very many keywords and results and we keep them
// in memory all the time, we want to save as much memory as possible.
#resultsByKeyword = new Map();
// This is only defined as a property so that tests can override it.
_addResultsChunkSize = ADD_RESULTS_CHUNK_SIZE;
#onSettingsSync = null;
#emitter = null;
}

Просмотреть файл

@ -10,6 +10,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
clearTimeout: "resource://gre/modules/Timer.sys.mjs",
MerinoClient: "resource:///modules/MerinoClient.sys.mjs",
PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
QuickSuggestRemoteSettings:
"resource:///modules/urlbar/private/QuickSuggestRemoteSettings.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
});
@ -62,8 +64,49 @@ export class Weather extends BaseFeature {
return this.#keywords;
}
/**
* @returns {number}
* The minimum prefix length of a weather keyword the user must type to
* trigger the suggestion. Note that the strings returned from `keywords`
* already take this into account. The min length is determined from the
* first source below whose value is non-zero. If no source has a non-zero
* value, zero will be returned and `this.keywords` will be null, which
* means the suggestion should be shown on zero prefix.
*
* 1. The `weather.minKeywordLength` pref, which is set when the user
* increments the min length
* 2. `weatherKeywordsMinimumLength` in Nimbus
* 3. `min_keyword_length` in remote settings
*/
get minKeywordLength() {
let minLength =
lazy.UrlbarPrefs.get("weather.minKeywordLength") ||
lazy.UrlbarPrefs.get("weatherKeywordsMinimumLength") ||
this.#rsData?.min_keyword_length ||
0;
return Math.max(minLength, 0);
}
/**
* @returns {boolean}
* Weather the min keyword length can be incremented. A cap on the min
* length can be set in remote settings and Nimbus.
*/
get canIncrementMinKeywordLength() {
let cap =
lazy.UrlbarPrefs.get("weatherKeywordsMinimumLengthCap") ||
this.#rsData?.min_keyword_length_cap ||
0;
return !cap || this.minKeywordLength < cap;
}
update() {
super.update();
// This method is called by `QuickSuggest` in a
// `NimbusFeatures.urlbar.onUpdate()` callback, when a change occurs to a
// Nimbus variable or to a pref that's a fallback for a Nimbus variable.
// A keyword-related variable or pref may have changed, so update keywords.
if (this.isEnabled) {
this.#updateKeywords();
}
@ -77,6 +120,21 @@ export class Weather extends BaseFeature {
}
}
/**
* Increments the minimum prefix length of a weather keyword the user must
* type to trigger the suggestion, if possible. A cap on the min length can be
* set in remote settings and Nimbus, and if the cap has been reached, the
* length is not incremented.
*/
incrementMinKeywordLength() {
if (this.canIncrementMinKeywordLength) {
lazy.UrlbarPrefs.set(
"weather.minKeywordLength",
this.minKeywordLength + 1
);
}
}
/**
* Returns a promise that resolves when all pending fetches finish, if there
* are pending fetches. If there aren't, the promise resolves when all pending
@ -91,7 +149,23 @@ export class Weather extends BaseFeature {
return this.#waitForFetchesDeferred.promise;
}
async onRemoteSettingsSync(rs) {
this.logger.debug("Loading weather remote settings");
let records = await rs.get({ filters: { type: "weather" } });
if (rs != lazy.QuickSuggestRemoteSettings.rs) {
return;
}
this.logger.debug("Got weather records: " + JSON.stringify(records));
this.#rsData = records?.[0]?.weather;
this.#updateKeywords();
}
get #vpnDetected() {
if (lazy.UrlbarPrefs.get("weather.ignoreVPN")) {
return false;
}
let linkService =
this._test_linkService ||
Cc["@mozilla.org/network/network-link-service;1"].getService(
@ -113,6 +187,8 @@ export class Weather extends BaseFeature {
this.#merino = new lazy.MerinoClient(this.constructor.name);
this.#fetch();
this.#updateKeywords();
lazy.UrlbarPrefs.addObserver(this);
lazy.QuickSuggestRemoteSettings.register(this);
for (let notif of Object.values(NOTIFICATIONS)) {
Services.obs.addObserver(this, notif);
}
@ -123,6 +199,8 @@ export class Weather extends BaseFeature {
Services.obs.removeObserver(this, notif);
}
lazy.clearTimeout(this.#fetchTimer);
lazy.QuickSuggestRemoteSettings.unregister(this);
lazy.UrlbarPrefs.removeObserver(this);
this.#merino = null;
this.#suggestion = null;
this.#fetchTimer = 0;
@ -262,25 +340,40 @@ export class Weather extends BaseFeature {
}
#updateKeywords() {
let fullKeywords = lazy.UrlbarPrefs.get("weatherKeywords");
let minLength = lazy.UrlbarPrefs.get("weatherKeywordsMinimumLength");
// Get the full keywords and minimum keyword length, preferring Nimbus over
// remote settings.
let fullKeywords =
lazy.UrlbarPrefs.get("weatherKeywords") ?? this.#rsData?.keywords;
let minLength = this.minKeywordLength;
if (!fullKeywords || !minLength) {
this.logger.debug(
"Keywords or min length not defined, using zero prefix"
);
this.#keywords = null;
return;
}
this.#keywords = new Set();
this.logger.debug(
"Updating keywords: " + JSON.stringify({ fullKeywords, minLength })
);
// Create keywords that are prefixes of the full keywords starting at the
// specified minimum length.
this.#keywords = new Set();
for (let full of fullKeywords) {
this.#keywords.add(full);
for (let i = minLength; i < full.length; i++) {
for (let i = minLength; i <= full.length; i++) {
this.#keywords.add(full.substring(0, i));
}
}
}
onPrefChanged(pref) {
if (pref == "weather.minKeywordLength") {
this.#updateKeywords();
}
}
observe(subject, topic, data) {
this.logger.debug(
"Observed notification: " + JSON.stringify({ topic, data })
@ -345,6 +438,11 @@ export class Weather extends BaseFeature {
await this.#fetch();
}
_test_setRsData(data) {
this.#rsData = data;
this.#updateKeywords();
}
_test_setSuggestionToNull() {
this.#suggestion = null;
}
@ -361,6 +459,7 @@ export class Weather extends BaseFeature {
#lastFetchTimeMs = 0;
#merino = null;
#pendingFetchCount = 0;
#rsData = null;
#suggestion = null;
#timeoutMs = MERINO_TIMEOUT_MS;
#waitForFetchesDeferred = null;

Просмотреть файл

@ -332,6 +332,77 @@ export var UrlbarTestUtils = {
this._testScope?.info("got the command event");
},
/**
* Opens the result menu of a specific result and clicks a menu item with a
* specified command name.
*
* @param {object} win
* The window containing the urlbar.
* @param {string|Array} commandOrArray
* If the command is in the top-level result menu, set this to the command
* name. If it's in a submenu, set this to an array where each element i is
* a selector that can be used to click the i'th menu item that opens a
* submenu, and the last element is the command name.
* @param {object} options
* The options object.
* @param {number} options.resultIndex
* The index of the result. Defaults to the current selected index.
* @param {boolean} options.openByMouse
* Whether to open the menu by mouse or keyboard.
*/
async openResultMenuAndClickItem(
win,
commandOrArray,
{
resultIndex = win.gURLBar.view.selectedRowIndex,
openByMouse = false,
} = {}
) {
await this.openResultMenu(win, { resultIndex, byMouse: openByMouse });
let selectors = Array.isArray(commandOrArray)
? commandOrArray
: [commandOrArray];
let command = selectors.pop();
// Open the sequence of submenus that contains the command.
for (let selector of selectors) {
let menuitem = win.gURLBar.view.resultMenu.querySelector(selector);
if (!menuitem) {
throw new Error("Menu item not found for selector: " + selector);
}
this._testScope?.info("Clicking menu item with selector: " + selector);
let promisePopup = lazy.BrowserTestUtils.waitForEvent(
win.gURLBar.view.resultMenu,
"popupshown"
);
this.EventUtils.synthesizeMouseAtCenter(menuitem, {}, win);
this._testScope?.info("Waiting for submenu popupshown event");
await promisePopup;
this._testScope?.info("Got the submenu popupshown event");
}
// Now click the command.
let menuitem = win.gURLBar.view.resultMenu.querySelector(
`menuitem[data-command=${command}]`
);
if (!menuitem) {
throw new Error("Menu item not found for command: " + command);
}
this._testScope?.info("Clicking menu item with command: " + command);
let promiseCommand = lazy.BrowserTestUtils.waitForEvent(
win.gURLBar.view.resultMenu,
"command"
);
this.EventUtils.synthesizeMouseAtCenter(menuitem, {}, win);
this._testScope?.info("Waiting for command event");
await promiseCommand;
this._testScope?.info("Got the command event");
},
/**
* Returns true if the oneOffSearchButtons are visible.
*

Просмотреть файл

@ -15,12 +15,15 @@ ChromeUtils.defineESModuleGetters(lazy, {
ExperimentFakes: "resource://testing-common/NimbusTestUtils.sys.mjs",
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
QuickSuggestRemoteSettings:
"resource:///modules/urlbar/private/QuickSuggestRemoteSettings.sys.mjs",
SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs",
TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
TestUtils: "resource://testing-common/TestUtils.sys.mjs",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
UrlbarProviderQuickSuggest:
"resource:///modules/UrlbarProviderQuickSuggest.sys.mjs",
UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs",
UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
sinon: "resource://testing-common/Sinon.sys.mjs",
@ -201,29 +204,23 @@ class _QuickSuggestTestUtils {
* otherwise.
*/
async ensureQuickSuggestInit({
remoteSettingsResults = null,
remoteSettingsResults = [],
merinoSuggestions = null,
config = DEFAULT_CONFIG,
} = {}) {
lazy.QuickSuggestRemoteSettings._test_ignoreSettingsSync = true;
this.info?.("ensureQuickSuggestInit calling QuickSuggest.init()");
lazy.QuickSuggest.init();
this.info?.("ensureQuickSuggestInit awaiting remoteSettings.readyPromise");
let { remoteSettings } = lazy.QuickSuggest;
await remoteSettings.readyPromise;
this.info?.(
"ensureQuickSuggestInit done awaiting remoteSettings.readyPromise"
);
this.setConfig(config);
// Set up the remote settings client. Ignore remote settings syncs that
// occur during the test. Clear its results and add the test results.
remoteSettings._test_ignoreSettingsSync = true;
remoteSettings._test_resultsByKeyword.clear();
// Clear remote settings suggestions and add the test suggestions.
let admWikipedia = lazy.QuickSuggest.getFeature("AdmWikipedia");
admWikipedia._test_suggestionsMap.clear();
if (remoteSettingsResults) {
this.info?.("ensureQuickSuggestInit adding remote settings results");
await remoteSettings._test_addResults(remoteSettingsResults);
await admWikipedia._test_suggestionsMap.add(remoteSettingsResults);
this.info?.("ensureQuickSuggestInit done adding remote settings results");
}
@ -239,8 +236,8 @@ class _QuickSuggestTestUtils {
let cleanup = async () => {
this.info?.("ensureQuickSuggestInit starting cleanup");
this.setConfig(DEFAULT_CONFIG);
delete remoteSettings._test_ignoreSettingsSync;
remoteSettings._test_resultsByKeyword.clear();
delete lazy.QuickSuggestRemoteSettings._test_ignoreSettingsSync;
admWikipedia._test_suggestionsMap.clear();
if (merinoSuggestions) {
lazy.UrlbarPrefs.clear("quicksuggest.dataCollection.enabled");
}
@ -260,9 +257,9 @@ class _QuickSuggestTestUtils {
* Array of remote settings result objects.
*/
async setRemoteSettingsResults(results) {
let { remoteSettings } = lazy.QuickSuggest;
remoteSettings._test_resultsByKeyword.clear();
await remoteSettings._test_addResults(results);
let admWikipedia = lazy.QuickSuggest.getFeature("AdmWikipedia");
admWikipedia._test_suggestionsMap.clear();
await admWikipedia._test_suggestionsMap.add(results);
}
/**
@ -271,10 +268,10 @@ class _QuickSuggestTestUtils {
*
* @param {object} config
* The config to be applied. See
* {@link QuickSuggestRemoteSettingsClient._setConfig}
* {@link QuickSuggestRemoteSettings._test_setConfig}
*/
setConfig(config) {
lazy.QuickSuggest.remoteSettings._test_setConfig(config);
lazy.QuickSuggestRemoteSettings._test_setConfig(config);
}
/**
@ -291,7 +288,7 @@ class _QuickSuggestTestUtils {
* @see {@link setConfig}
*/
async withConfig({ config, callback }) {
let original = lazy.QuickSuggest.remoteSettings.config;
let original = lazy.QuickSuggestRemoteSettings.config;
this.setConfig(config);
await callback();
this.setConfig(original);

Просмотреть файл

@ -36,7 +36,6 @@ add_task(async function test_updateFeatureState_pref() {
let spy = sandbox.spy(QuickSuggest, "_updateFeatureState");
UrlbarPrefs.set("quicksuggest.enabled", false);
await QuickSuggest.remoteSettings.readyPromise;
Assert.equal(
spy.callCount,
1,
@ -44,7 +43,6 @@ add_task(async function test_updateFeatureState_pref() {
);
UrlbarPrefs.clear("quicksuggest.enabled");
await QuickSuggest.remoteSettings.readyPromise;
Assert.equal(
spy.callCount,
2,

Просмотреть файл

@ -2,21 +2,22 @@
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* This test ensures the browser navigates to the weather webpage after
* the weather result is selected.
* Browser test for the weather suggestion.
*/
"use strict";
add_setup(async function() {
registerCleanupFunction(async () => {
await PlacesUtils.history.clear();
});
ChromeUtils.defineESModuleGetters(this, {
UrlbarProviderWeather: "resource:///modules/UrlbarProviderWeather.sys.mjs",
});
add_task(async function test_weather_result_selection() {
add_setup(async function() {
await MerinoTestUtils.initWeather();
});
// This test ensures the browser navigates to the weather webpage after
// the weather result is selected.
add_task(async function test_weather_result_selection() {
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
let browserLoadedPromise = BrowserTestUtils.browserLoaded(
tab.linkedBrowser,
@ -43,4 +44,185 @@ add_task(async function test_weather_result_selection() {
);
BrowserTestUtils.removeTab(tab);
await PlacesUtils.history.clear();
});
// Tests the "Show less frequently" result menu command.
add_task(async function showLessFrequently() {
// Set up a min keyword length and cap.
QuickSuggest.weather._test_setRsData({
keywords: ["weather"],
min_keyword_length: 3,
min_keyword_length_cap: 4,
});
// Trigger the suggestion.
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: "wea",
});
let resultIndex = 1;
let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex);
Assert.equal(
details.result.providerName,
UrlbarProviderWeather.name,
"Weather suggestion should be present at expected index after 'wea' search"
);
// Click the command.
let command = "show_less_frequently";
await UrlbarTestUtils.openResultMenuAndClickItem(window, command, {
resultIndex,
});
Assert.ok(
gURLBar.view.isOpen,
"The view should remain open clicking the command"
);
Assert.ok(
details.element.row.hasAttribute("feedback-acknowledgment"),
"Row should have feedback acknowledgment after clicking command"
);
// Do the same search again. The suggestion should not appear.
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: "wea",
});
for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
details = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
Assert.notEqual(
details.result.providerName,
UrlbarProviderWeather.name,
`Weather suggestion should be absent (checking index ${i})`
);
}
// Do a search using one more character. The suggestion should appear.
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: "weat",
});
details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex);
Assert.equal(
details.result.providerName,
UrlbarProviderWeather.name,
"Weather suggestion should be present at expected index after 'weat' search"
);
Assert.ok(
!details.element.row.hasAttribute("feedback-acknowledgment"),
"Row should not have feedback acknowledgment after 'weat' search"
);
// Since the cap has been reached, the command should no longer appear in the
// result menu.
await UrlbarTestUtils.openResultMenu(window, { resultIndex });
let menuitem = gURLBar.view.resultMenu.querySelector(
`menuitem[data-command=${command}]`
);
Assert.ok(!menuitem, "Menuitem should be absent");
gURLBar.view.resultMenu.hidePopup(true);
await UrlbarTestUtils.promisePopupClose(window);
QuickSuggest.weather._test_setRsData(null);
UrlbarPrefs.clear("weather.minKeywordLength");
});
// Tests the "Not interested" result menu dismissal command.
add_task(async function notInterested() {
await doDismissTest("not_interested");
});
// Tests the "Not relevant" result menu dismissal command.
add_task(async function notRelevant() {
await doDismissTest("not_relevant");
});
async function doDismissTest(command) {
// Trigger the suggestion.
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: "",
});
let resultCount = UrlbarTestUtils.getResultCount(window);
let resultIndex = 0;
let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex);
Assert.equal(
details.result.providerName,
UrlbarProviderWeather.name,
"Weather suggestion should be present"
);
// Click the command.
await UrlbarTestUtils.openResultMenuAndClickItem(
window,
["[data-l10n-id=firefox-suggest-weather-command-dont-show-this]", command],
{ resultIndex }
);
Assert.ok(
!UrlbarPrefs.get("suggest.weather"),
"suggest.weather pref should be set to false after dismissal"
);
// The row should be a tip now.
Assert.ok(gURLBar.view.isOpen, "The view should remain open after dismissal");
Assert.equal(
UrlbarTestUtils.getResultCount(window),
resultCount,
"The result count should not haved changed after dismissal"
);
details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex);
Assert.equal(
details.type,
UrlbarUtils.RESULT_TYPE.TIP,
"Row should be a tip after dismissal"
);
Assert.equal(
details.result.payload.type,
"dismissalAcknowledgment",
"Tip type should be dismissalAcknowledgment"
);
// Get the dismissal acknowledgment's "Got it" button and click it.
let gotItButton = UrlbarTestUtils.getButtonForResultIndex(
window,
"0",
resultIndex
);
Assert.ok(gotItButton, "Row should have a 'Got it' button");
EventUtils.synthesizeMouseAtCenter(gotItButton, {}, window);
// The view should remain open and the tip row should be gone.
Assert.ok(
gURLBar.view.isOpen,
"The view should remain open clicking the 'Got it' button"
);
Assert.equal(
UrlbarTestUtils.getResultCount(window),
resultCount - 1,
"The result count should be one less after clicking 'Got it' button"
);
for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
details = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
Assert.ok(
details.type != UrlbarUtils.RESULT_TYPE.TIP &&
details.result.providerName != UrlbarProviderWeather.name,
"Tip result and weather result should not be present"
);
}
await UrlbarTestUtils.promisePopupClose(window);
// Enable the weather suggestion again and wait for it to be fetched.
let fetchPromise = QuickSuggest.weather.waitForFetches();
UrlbarPrefs.clear("suggest.weather");
info("Waiting for weather fetch after re-enabling the suggestion");
await fetchPromise;
info("Got weather fetch");
}

Просмотреть файл

@ -5,8 +5,8 @@
ChromeUtils.defineESModuleGetters(this, {
QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
RemoteSettingsClient:
"resource:///modules/urlbar/private/RemoteSettingsClient.sys.mjs",
QuickSuggestRemoteSettings:
"resource:///modules/urlbar/private/QuickSuggestRemoteSettings.sys.mjs",
TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
UrlbarProviderAutofill: "resource:///modules/UrlbarProviderAutofill.sys.mjs",
UrlbarProviderQuickSuggest:

Просмотреть файл

@ -803,9 +803,8 @@ add_task(async function setupAndTeardown() {
// Disable the suggest prefs so the settings client starts out torn down.
UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false);
UrlbarPrefs.set("suggest.quicksuggest.sponsored", false);
await QuickSuggest.remoteSettings.readyPromise;
Assert.ok(
!QuickSuggest.remoteSettings._test_rs,
!QuickSuggestRemoteSettings._test_rs,
"Settings client is null after disabling suggest prefs"
);
@ -813,58 +812,50 @@ add_task(async function setupAndTeardown() {
// assume all previous tasks left `quicksuggest.enabled` true (from the init
// task).
UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
await QuickSuggest.remoteSettings.readyPromise;
Assert.ok(
QuickSuggest.remoteSettings._test_rs,
QuickSuggestRemoteSettings._test_rs,
"Settings client is non-null after enabling suggest.quicksuggest.nonsponsored"
);
UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false);
await QuickSuggest.remoteSettings.readyPromise;
Assert.ok(
!QuickSuggest.remoteSettings._test_rs,
!QuickSuggestRemoteSettings._test_rs,
"Settings client is null after disabling suggest.quicksuggest.nonsponsored"
);
UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
await QuickSuggest.remoteSettings.readyPromise;
Assert.ok(
QuickSuggest.remoteSettings._test_rs,
QuickSuggestRemoteSettings._test_rs,
"Settings client is non-null after enabling suggest.quicksuggest.sponsored"
);
UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
await QuickSuggest.remoteSettings.readyPromise;
Assert.ok(
QuickSuggest.remoteSettings._test_rs,
QuickSuggestRemoteSettings._test_rs,
"Settings client remains non-null after enabling suggest.quicksuggest.nonsponsored"
);
UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", false);
await QuickSuggest.remoteSettings.readyPromise;
Assert.ok(
QuickSuggest.remoteSettings._test_rs,
QuickSuggestRemoteSettings._test_rs,
"Settings client remains non-null after disabling suggest.quicksuggest.nonsponsored"
);
UrlbarPrefs.set("suggest.quicksuggest.sponsored", false);
await QuickSuggest.remoteSettings.readyPromise;
Assert.ok(
!QuickSuggest.remoteSettings._test_rs,
!QuickSuggestRemoteSettings._test_rs,
"Settings client is null after disabling suggest.quicksuggest.sponsored"
);
UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
await QuickSuggest.remoteSettings.readyPromise;
Assert.ok(
QuickSuggest.remoteSettings._test_rs,
QuickSuggestRemoteSettings._test_rs,
"Settings client is non-null after enabling suggest.quicksuggest.nonsponsored"
);
UrlbarPrefs.set("quicksuggest.enabled", false);
await QuickSuggest.remoteSettings.readyPromise;
Assert.ok(
!QuickSuggest.remoteSettings._test_rs,
!QuickSuggestRemoteSettings._test_rs,
"Settings client is null after disabling quicksuggest.enabled"
);
@ -872,9 +863,8 @@ add_task(async function setupAndTeardown() {
UrlbarPrefs.clear("suggest.quicksuggest.nonsponsored");
UrlbarPrefs.clear("suggest.quicksuggest.sponsored");
UrlbarPrefs.set("quicksuggest.enabled", true);
await QuickSuggest.remoteSettings.readyPromise;
Assert.ok(
!QuickSuggest.remoteSettings._test_rs,
!QuickSuggestRemoteSettings._test_rs,
"Settings client remains null at end of task"
);
});
@ -1306,18 +1296,17 @@ add_task(async function block() {
// the value of the `quickSuggestRemoteSettingsDataType` Nimbus variable.
add_task(async function remoteSettingsDataType() {
// `QuickSuggestTestUtils.ensureQuickSuggestInit()` stubs
// `QuickSuggest.remoteSettings._queueSettingsSync()`, which we want to test
// `QuickSuggestRemoteSettings._queueSettingsSync()`, which we want to test
// below, so remove the stub by calling the cleanup function it returned.
await cleanUpQuickSuggest();
// We need to spy on `QuickSuggest.remoteSettings.#rs.get()`, but `#rs` is
// We need to spy on `QuickSuggestRemoteSettings.#rs.get()`, but `#rs` is
// created lazily. Set `suggest.quicksuggest.sponsored` to trigger its
// creation.
UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
await QuickSuggest.remoteSettings.readyPromise;
let sandbox = sinon.createSandbox();
let spy = sandbox.spy(QuickSuggest.remoteSettings._test_rs, "get");
let spy = sandbox.spy(QuickSuggestRemoteSettings._test_rs, "get");
for (let dataType of [undefined, "test-data-type"]) {
// Set up a mock Nimbus rollout with the data type.
@ -1327,10 +1316,9 @@ add_task(async function remoteSettingsDataType() {
}
let cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature(value);
// Re-enable remote settings to trigger `remoteSettings.#rs.get()`.
await QuickSuggest.remoteSettings.enable(false);
await QuickSuggest.remoteSettings.enable(true);
await QuickSuggest.remoteSettings.readyPromise;
// Re-enable to trigger sync from remote settings.
UrlbarPrefs.set("quicksuggest.remoteSettings.enabled", false);
UrlbarPrefs.set("quicksuggest.remoteSettings.enabled", true);
let expectedDataType = dataType || "data";
Assert.ok(

Просмотреть файл

@ -6,87 +6,86 @@
"use strict";
ChromeUtils.defineESModuleGetters(this, {
SuggestionsMap:
"resource:///modules/urlbar/private/QuickSuggestRemoteSettings.sys.mjs",
});
XPCOMUtils.defineLazyModuleGetters(this, {
ObjectUtils: "resource://gre/modules/ObjectUtils.jsm",
});
// This overrides `QuickSuggest.remoteSettings._addResultsChunkSize`. Testing
// the actual value can make the test run too long. This is OK because the
// correctness of the chunking behavior doesn't depend on the chunk size.
const TEST_ADD_RESULTS_CHUNK_SIZE = 100;
// This overrides `SuggestionsMap.chunkSize`. Testing the actual value can make
// the test run too long. This is OK because the correctness of the chunking
// behavior doesn't depend on the chunk size.
const TEST_CHUNK_SIZE = 100;
add_task(async function init() {
UrlbarPrefs.set("quicksuggest.enabled", true);
await QuickSuggestTestUtils.ensureQuickSuggestInit();
// Sanity check the actual `_addResultsChunkSize` value.
// Sanity check the actual `chunkSize` value.
Assert.equal(
typeof QuickSuggest.remoteSettings._addResultsChunkSize,
typeof SuggestionsMap.chunkSize,
"number",
"Sanity check: _addResultsChunkSize is a number"
);
Assert.greater(
QuickSuggest.remoteSettings._addResultsChunkSize,
0,
"Sanity check: _addResultsChunkSize > 0"
"Sanity check: chunkSize is a number"
);
Assert.greater(SuggestionsMap.chunkSize, 0, "Sanity check: chunkSize > 0");
// Set our test value.
QuickSuggest.remoteSettings._addResultsChunkSize = TEST_ADD_RESULTS_CHUNK_SIZE;
SuggestionsMap.chunkSize = TEST_CHUNK_SIZE;
});
// Tests many results with one keyword each.
// Tests many suggestions with one keyword each.
add_task(async function chunking_singleKeyword() {
let resultCounts = [
1 * QuickSuggest.remoteSettings._addResultsChunkSize - 1,
1 * QuickSuggest.remoteSettings._addResultsChunkSize,
1 * QuickSuggest.remoteSettings._addResultsChunkSize + 1,
2 * QuickSuggest.remoteSettings._addResultsChunkSize - 1,
2 * QuickSuggest.remoteSettings._addResultsChunkSize,
2 * QuickSuggest.remoteSettings._addResultsChunkSize + 1,
3 * QuickSuggest.remoteSettings._addResultsChunkSize - 1,
3 * QuickSuggest.remoteSettings._addResultsChunkSize,
3 * QuickSuggest.remoteSettings._addResultsChunkSize + 1,
let suggestionCounts = [
1 * SuggestionsMap.chunkSize - 1,
1 * SuggestionsMap.chunkSize,
1 * SuggestionsMap.chunkSize + 1,
2 * SuggestionsMap.chunkSize - 1,
2 * SuggestionsMap.chunkSize,
2 * SuggestionsMap.chunkSize + 1,
3 * SuggestionsMap.chunkSize - 1,
3 * SuggestionsMap.chunkSize,
3 * SuggestionsMap.chunkSize + 1,
];
for (let count of resultCounts) {
for (let count of suggestionCounts) {
await doChunkingTest(count, 1);
}
});
// Tests a small number of results with many keywords each.
// Tests a small number of suggestions with many keywords each.
add_task(async function chunking_manyKeywords() {
let keywordCounts = [
1 * QuickSuggest.remoteSettings._addResultsChunkSize - 1,
1 * QuickSuggest.remoteSettings._addResultsChunkSize,
1 * QuickSuggest.remoteSettings._addResultsChunkSize + 1,
2 * QuickSuggest.remoteSettings._addResultsChunkSize - 1,
2 * QuickSuggest.remoteSettings._addResultsChunkSize,
2 * QuickSuggest.remoteSettings._addResultsChunkSize + 1,
3 * QuickSuggest.remoteSettings._addResultsChunkSize - 1,
3 * QuickSuggest.remoteSettings._addResultsChunkSize,
3 * QuickSuggest.remoteSettings._addResultsChunkSize + 1,
1 * SuggestionsMap.chunkSize - 1,
1 * SuggestionsMap.chunkSize,
1 * SuggestionsMap.chunkSize + 1,
2 * SuggestionsMap.chunkSize - 1,
2 * SuggestionsMap.chunkSize,
2 * SuggestionsMap.chunkSize + 1,
3 * SuggestionsMap.chunkSize - 1,
3 * SuggestionsMap.chunkSize,
3 * SuggestionsMap.chunkSize + 1,
];
for (let resultCount = 1; resultCount <= 3; resultCount++) {
for (let suggestionCount = 1; suggestionCount <= 3; suggestionCount++) {
for (let keywordCount of keywordCounts) {
await doChunkingTest(resultCount, keywordCount);
await doChunkingTest(suggestionCount, keywordCount);
}
}
});
async function doChunkingTest(resultCount, keywordCountPerResult) {
async function doChunkingTest(suggestionCount, keywordCountPerSuggestion) {
info(
"Running chunking test: " +
JSON.stringify({ resultCount, keywordCountPerResult })
JSON.stringify({ suggestionCount, keywordCountPerSuggestion })
);
// Create `resultCount` results, each with `keywordCountPerResult` keywords.
let results = [];
for (let i = 0; i < resultCount; i++) {
// Create `suggestionCount` suggestions, each with `keywordCountPerSuggestion`
// keywords.
let suggestions = [];
for (let i = 0; i < suggestionCount; i++) {
let keywords = [];
for (let k = 0; k < keywordCountPerResult; k++) {
for (let k = 0; k < keywordCountPerSuggestion; k++) {
keywords.push(`keyword-${i}-${k}`);
}
results.push({
suggestions.push({
keywords,
id: i,
url: "http://example.com/" + i,
@ -98,61 +97,67 @@ async function doChunkingTest(resultCount, keywordCountPerResult) {
});
}
// Add the results.
QuickSuggest.remoteSettings._test_resultsByKeyword.clear();
await QuickSuggest.remoteSettings._test_addResults(results);
// Add the suggestions.
let map = new SuggestionsMap();
await map.add(suggestions);
// Make sure all keyword-result pairs have been added.
for (let i = 0; i < resultCount; i++) {
for (let k = 0; k < keywordCountPerResult; k++) {
// Make sure all keyword-suggestion pairs have been added.
for (let i = 0; i < suggestionCount; i++) {
for (let k = 0; k < keywordCountPerSuggestion; k++) {
let keyword = `keyword-${i}-${k}`;
// Check the resultsByKeyword map. Logging all assertions takes a ton of
// time and makes the test run much longer than it otherwise would,
// especially if `_addResultsChunkSize` is large, so only log failing
// assertions.
let actualResult = QuickSuggest.remoteSettings._test_resultsByKeyword.get(
keyword
);
if (!ObjectUtils.deepEqual(actualResult, results[i])) {
Assert.deepEqual(
actualResult,
results[i],
`Result ${i} is in _test_resultsByKeyword for keyword ${keyword}`
);
}
// Call `query()` and make sure a suggestion is returned for the result.
// Computing the expected value of `full_keyword` is kind of a pain and
// it's not important to check it, so first delete it from the returned
// suggestion.
let actualSuggestions = await QuickSuggest.remoteSettings.fetch(keyword);
for (let s of actualSuggestions) {
delete s.full_keyword;
}
let expectedSuggestions = [
{
block_id: i,
url: "http://example.com/" + i,
title: "Suggestion " + i,
click_url: "http://example.com/click",
impression_url: "http://example.com/impression",
advertiser: "TestAdvertiser",
iab_category: "22 - Shopping",
is_sponsored: true,
score: RemoteSettingsClient.DEFAULT_SUGGESTION_SCORE,
source: "remote-settings",
icon: null,
position: undefined,
},
];
if (!ObjectUtils.deepEqual(actualSuggestions, expectedSuggestions)) {
// Check the map. Logging all assertions takes a ton of time and makes the
// test run much longer than it otherwise would, especially if `chunkSize`
// is large, so only log failing assertions.
let actualSuggestions = map.get(keyword);
if (!ObjectUtils.deepEqual(actualSuggestions, [suggestions[i]])) {
Assert.deepEqual(
actualSuggestions,
expectedSuggestions,
`query() returns a suggestion for result ${i} with keyword ${keyword}`
[suggestions[i]],
`Suggestion ${i} is present for keyword ${keyword}`
);
}
}
}
}
add_task(async function duplicateKeywords() {
let suggestions = [
{
title: "suggestion 0",
keywords: ["a", "b", "c"],
},
{
title: "suggestion 1",
keywords: ["b", "c", "d"],
},
{
title: "suggestion 2",
keywords: ["c", "d", "e"],
},
{
title: "suggestion 3",
keywords: ["f"],
},
];
let expectedIndexesByKeyword = {
a: [0],
b: [0, 1],
c: [0, 1, 2],
d: [1, 2],
e: [2],
f: [3],
};
let map = new SuggestionsMap();
await map.add(suggestions);
for (let [keyword, indexes] of Object.entries(expectedIndexesByKeyword)) {
Assert.deepEqual(
map.get(keyword),
indexes.map(i => suggestions[i]),
"get() with keyword: " + keyword
);
}
});

Просмотреть файл

@ -114,7 +114,7 @@ add_task(async function init() {
});
Assert.equal(
typeof RemoteSettingsClient.DEFAULT_SUGGESTION_SCORE,
typeof QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE,
"number",
"Sanity check: DEFAULT_SUGGESTION_SCORE is defined"
);
@ -131,7 +131,7 @@ add_task(async function oneEnabled_merino() {
// Use a score lower than the remote settings score to make sure the
// suggestion is included regardless.
MerinoTestUtils.server.response.body.suggestions[0].score =
RemoteSettingsClient.DEFAULT_SUGGESTION_SCORE / 2;
QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE / 2;
let context = createContext(SEARCH_STRING, {
providers: [UrlbarProviderQuickSuggest.name],
@ -206,7 +206,7 @@ add_task(async function higherScore() {
let histograms = MerinoTestUtils.getAndClearHistograms();
MerinoTestUtils.server.response.body.suggestions[0].score =
2 * RemoteSettingsClient.DEFAULT_SUGGESTION_SCORE;
2 * QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE;
let context = createContext(SEARCH_STRING, {
providers: [UrlbarProviderQuickSuggest.name],
@ -238,7 +238,7 @@ add_task(async function lowerScore() {
let histograms = MerinoTestUtils.getAndClearHistograms();
MerinoTestUtils.server.response.body.suggestions[0].score =
RemoteSettingsClient.DEFAULT_SUGGESTION_SCORE / 2;
QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE / 2;
let context = createContext(SEARCH_STRING, {
providers: [UrlbarProviderQuickSuggest.name],
@ -270,7 +270,7 @@ add_task(async function sameScore() {
let histograms = MerinoTestUtils.getAndClearHistograms();
MerinoTestUtils.server.response.body.suggestions[0].score =
RemoteSettingsClient.DEFAULT_SUGGESTION_SCORE;
QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE;
let context = createContext(SEARCH_STRING, {
providers: [UrlbarProviderQuickSuggest.name],

Просмотреть файл

@ -19,17 +19,17 @@ let SUGGESTIONS_DATA = [
{
keywords: ["aaa", "bbb"],
isSponsored: false,
score: 2 * RemoteSettingsClient.DEFAULT_SUGGESTION_SCORE,
score: 2 * QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE,
},
{
keywords: ["bbb"],
isSponsored: true,
score: 4 * RemoteSettingsClient.DEFAULT_SUGGESTION_SCORE,
score: 4 * QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE,
},
{
keywords: ["bbb"],
isSponsored: false,
score: 3 * RemoteSettingsClient.DEFAULT_SUGGESTION_SCORE,
score: 3 * QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE,
},
{
keywords: ["ccc"],
@ -38,7 +38,7 @@ let SUGGESTIONS_DATA = [
];
// Test cases. In this object, keywords map to subtest cases. For each keyword,
// the test calls `fetch(keyword)` and checks that the indexes (relative to
// the test calls `query(keyword)` and checks that the indexes (relative to
// `SUGGESTIONS_DATA`) of the returned quick suggest results are the ones in
// `expectedIndexes`. Then the test does a series of urlbar searches using the
// keyword as the search string, one search per object in `searches`. Sponsored
@ -131,6 +131,8 @@ let TESTS = {
add_task(async function() {
UrlbarPrefs.set("quicksuggest.enabled", true);
UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
// Create results and suggestions based on `SUGGESTIONS_DATA`.
let qsResults = [];
@ -161,10 +163,11 @@ add_task(async function() {
score:
typeof score == "number"
? score
: RemoteSettingsClient.DEFAULT_SUGGESTION_SCORE,
: QuickSuggestRemoteSettings.DEFAULT_SUGGESTION_SCORE,
source: "remote-settings",
icon: null,
position: undefined,
provider: "AdmWikipedia",
};
delete qsSuggestion.keywords;
delete qsSuggestion.id;
@ -215,37 +218,16 @@ add_task(async function() {
let { expectedIndexes, searches } = test;
// Call `fetch()`.
// Call `query()`.
Assert.deepEqual(
await QuickSuggest.remoteSettings.fetch(keyword),
await QuickSuggestRemoteSettings.query(keyword),
expectedIndexes.map(i => ({
...qsSuggestions[i],
full_keyword: keyword,
})),
`fetch() for ${keyword}`
`query() for keyword ${keyword}`
);
// Make sure the expected result object(s) are stored correctly.
let mapValue = QuickSuggest.remoteSettings._test_resultsByKeyword.get(
keyword
);
if (expectedIndexes.length == 1) {
Assert.ok(!Array.isArray(mapValue), "The map value is not an array");
Assert.deepEqual(
mapValue,
qsResults[expectedIndexes[0]],
"The map value is the expected result object"
);
} else {
Assert.ok(Array.isArray(mapValue), "The map value is an array");
Assert.greater(mapValue.length, 0, "The array is not empty");
Assert.deepEqual(
mapValue,
expectedIndexes.map(i => qsResults[i]),
"The map value is the expected array of result objects"
);
}
// Now do a urlbar search for the keyword with all possible combinations of
// sponsored and non-sponsored suggestions enabled and disabled.
for (let sponsored of [true, false]) {
@ -288,5 +270,8 @@ add_task(async function() {
await check_results({ context, matches });
}
}
UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true);
}
});

Просмотреть файл

@ -614,282 +614,6 @@ async function doLocaleTest({ shouldRunTask, osUnit, unitsByLocale }) {
}
}
// When no keyword-related Nimbus variables are defined, the suggestion should
// be triggered on zero prefix.
add_task(async function noKeywordVariables() {
await doKeywordsTest({
nimbusValues: {},
tests: {
"": true,
w: false,
we: false,
wea: false,
weat: false,
weath: false,
weathe: false,
weather: false,
f: false,
fo: false,
for: false,
fore: false,
forec: false,
foreca: false,
forecas: false,
forecast: false,
},
});
});
// When `weatherKeywords` is non-null and `weatherKeywordsMinimumLength` is
// absent from the Nimbus recipe, the suggestion should be triggered on zero
// prefix.
add_task(async function minLength_absent() {
await doKeywordsTest({
nimbusValues: {
weatherKeywords: ["weather"],
},
tests: {
"": true,
w: false,
we: false,
wea: false,
weat: false,
weath: false,
weathe: false,
weather: false,
" weather": false,
"weather ": false,
" weather ": false,
},
});
});
// When `weatherKeywords` is non-null and `weatherKeywordsMinimumLength` is
// zero, the suggestion should be triggered on zero prefix.
add_task(async function minLength_zero() {
await doKeywordsTest({
nimbusValues: {
weatherKeywords: ["weather"],
weatherKeywordsMinimumLength: 0,
},
tests: {
"": true,
w: false,
we: false,
wea: false,
weat: false,
weath: false,
weathe: false,
weather: false,
" weather": false,
"weather ": false,
" weather ": false,
},
});
});
// When `weatherKeywords` is non-null and `weatherKeywordsMinimumLength` is
// larger than the length of all keywords, the suggestion should be triggered by
// typing a full keyword.
add_task(async function minLength_large() {
await doKeywordsTest({
nimbusValues: {
weatherKeywords: ["weather", "forecast"],
weatherKeywordsMinimumLength: 999,
},
tests: {
"": false,
w: false,
we: false,
wea: false,
weat: false,
weath: false,
weathe: false,
weather: true,
f: false,
fo: false,
for: false,
fore: false,
forec: false,
foreca: false,
forecas: false,
forecast: true,
},
});
});
// When `weatherKeywords` is non-null and `weatherKeywordsMinimumLength` is a
// typical value like 4, the suggestion should be triggered by typing the first
// 4 or more characters of a keyword.
add_task(async function minLength_typical() {
await doKeywordsTest({
nimbusValues: {
weatherKeywords: ["weather", "forecast"],
weatherKeywordsMinimumLength: 4,
},
tests: {
"": false,
w: false,
we: false,
wea: false,
weat: true,
weath: true,
weathe: true,
weather: true,
f: false,
fo: false,
for: false,
fore: true,
forec: true,
foreca: true,
forecas: true,
forecast: true,
" wea": false,
" wea": false,
"wea ": false,
"wea ": false,
" weat": true,
" weat": true,
"weat ": true,
"weat ": true,
},
});
});
async function doKeywordsTest({ nimbusValues, tests }) {
// Sanity check initial state.
assertEnabled({
message: "Sanity check initial state",
hasSuggestion: true,
pendingFetchCount: 0,
});
let cleanup = await UrlbarTestUtils.initNimbusFeature(nimbusValues);
for (let [searchString, expected] of Object.entries(tests)) {
info("Doing search: " + JSON.stringify({ nimbusValues, searchString }));
let suggestedIndex = searchString ? 1 : 0;
await check_results({
context: createContext(searchString, {
providers: [UrlbarProviderWeather.name],
isPrivate: false,
}),
matches: expected ? [makeExpectedResult({ suggestedIndex })] : [],
});
}
await cleanup();
}
// When a Nimbus experiment isn't active, the suggestion should be triggered on
// zero prefix. Installing and uninstalling an experiment should update the
// keywords.
add_task(async function zeroPrefix_withoutNimbus() {
// Sanity check initial state.
assertEnabled({
message: "Sanity check initial state",
hasSuggestion: true,
pendingFetchCount: 0,
});
info("1. Doing searches before installing experiment and setting keyword");
await check_results({
context: createContext("", {
providers: [UrlbarProviderWeather.name],
isPrivate: false,
}),
matches: [makeExpectedResult()],
});
await check_results({
context: createContext("weather", {
providers: [UrlbarProviderWeather.name],
isPrivate: false,
}),
matches: [],
});
let cleanup = await UrlbarTestUtils.initNimbusFeature({
weatherKeywords: ["weather"],
weatherKeywordsMinimumLength: 1,
});
info("2. Doing searches after installing experiment and setting keyword");
await check_results({
context: createContext("", {
providers: [UrlbarProviderWeather.name],
isPrivate: false,
}),
matches: [],
});
await check_results({
context: createContext("weather", {
providers: [UrlbarProviderWeather.name],
isPrivate: false,
}),
matches: [makeExpectedResult({ suggestedIndex: 1 })],
});
await cleanup();
info("3. Doing searches after uninstalling experiment");
await check_results({
context: createContext("", {
providers: [UrlbarProviderWeather.name],
isPrivate: false,
}),
matches: [makeExpectedResult()],
});
await check_results({
context: createContext("weather", {
providers: [UrlbarProviderWeather.name],
isPrivate: false,
}),
matches: [],
});
});
// When the zero-prefix suggestion is enabled, search strings with only spaces
// or that start with spaces should not trigger a weather suggestion.
add_task(async function zeroPrefix_spacesInSearchString() {
// Sanity check initial state.
assertEnabled({
message: "Sanity check initial state",
hasSuggestion: true,
pendingFetchCount: 0,
});
for (let searchString of [" ", " ", " ", " doesn't match anything"]) {
await check_results({
context: createContext(searchString, {
providers: [UrlbarProviderWeather.name],
isPrivate: false,
}),
matches: [],
});
}
});
// When the zero-prefix suggestion is enabled, a weather suggestion should not
// be returned for a non-empty search string.
add_task(async function zeroPrefix_nonEmptySearchString() {
assertEnabled({
message: "Sanity check initial state",
hasSuggestion: true,
pendingFetchCount: 0,
});
// Do a search.
let context = createContext("this shouldn't match anything", {
providers: [UrlbarProviderWeather.name],
isPrivate: false,
});
await check_results({
context,
matches: [],
});
});
// Blocks a result and makes sure the weather pref is disabled.
add_task(async function block() {
// Sanity check initial state.
@ -945,109 +669,6 @@ add_task(async function block() {
});
});
// When a sponsored quick suggest result matches the same keyword as the weather
// result, the weather result should be shown and the quick suggest result
// should not be shown.
add_task(async function matchingQuickSuggest_sponsored() {
await doMatchingQuickSuggestTest("suggest.quicksuggest.sponsored", true);
});
// When a non-sponsored quick suggest result matches the same keyword as the
// weather result, the weather result should be shown and the quick suggest
// result should not be shown.
add_task(async function matchingQuickSuggest_nonsponsored() {
await doMatchingQuickSuggestTest("suggest.quicksuggest.nonsponsored", false);
});
async function doMatchingQuickSuggestTest(pref, isSponsored) {
// Sanity check initial state.
assertEnabled({
message: "Sanity check initial state",
hasSuggestion: true,
pendingFetchCount: 0,
});
let keyword = "test";
let iab_category = isSponsored ? "22 - Shopping" : "5 - Education";
// Add a remote settings result to quick suggest.
UrlbarPrefs.set(pref, true);
await QuickSuggestTestUtils.setRemoteSettingsResults([
{
id: 1,
url: "http://example.com/",
title: "Suggestion",
keywords: [keyword],
click_url: "http://example.com/click",
impression_url: "http://example.com/impression",
advertiser: "TestAdvertiser",
iab_category,
},
]);
// First do a search to verify the quick suggest result matches the keyword.
info("Doing first search for quick suggest result");
await check_results({
context: createContext(keyword, {
providers: [UrlbarProviderQuickSuggest.name, UrlbarProviderWeather.name],
isPrivate: false,
}),
matches: [
{
type: UrlbarUtils.RESULT_TYPE.URL,
source: UrlbarUtils.RESULT_SOURCE.SEARCH,
heuristic: false,
payload: {
telemetryType: isSponsored ? "adm_sponsored" : "adm_nonsponsored",
qsSuggestion: keyword,
title: "Suggestion",
url: "http://example.com/",
displayUrl: "http://example.com",
originalUrl: "http://example.com/",
icon: null,
sponsoredImpressionUrl: "http://example.com/impression",
sponsoredClickUrl: "http://example.com/click",
sponsoredBlockId: 1,
sponsoredAdvertiser: "TestAdvertiser",
sponsoredIabCategory: iab_category,
isSponsored,
helpUrl: QuickSuggest.HELP_URL,
helpL10n: {
id: UrlbarPrefs.get("resultMenu")
? "urlbar-result-menu-learn-more-about-firefox-suggest"
: "firefox-suggest-urlbar-learn-more",
},
isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"),
blockL10n: {
id: UrlbarPrefs.get("resultMenu")
? "urlbar-result-menu-dismiss-firefox-suggest"
: "firefox-suggest-urlbar-block",
},
source: "remote-settings",
},
},
],
});
// Set up the keyword for the weather suggestion and do a second search to
// verify only the weather result matches.
info("Doing second search for weather suggestion");
let cleanup = await UrlbarTestUtils.initNimbusFeature({
weatherKeywords: [keyword],
weatherKeywordsMinimumLength: 1,
});
await check_results({
context: createContext(keyword, {
providers: [UrlbarProviderQuickSuggest.name, UrlbarProviderWeather.name],
isPrivate: false,
}),
matches: [makeExpectedResult({ suggestedIndex: 1 })],
});
await cleanup();
UrlbarPrefs.clear(pref);
}
// Simulates wake 100ms before the start of the next fetch period. A new fetch
// should not start.
add_task(async function wakeBeforeNextFetchPeriod() {
@ -1485,6 +1106,16 @@ add_task(async function vpn() {
await QuickSuggest.weather._test_fetch();
Assert.ok(!QuickSuggest.weather.suggestion, "Suggestion should be null");
// Set `weather.ignoreVPN` and fetch again. It should complete successfully.
UrlbarPrefs.set("weather.ignoreVPN", true);
await QuickSuggest.weather._test_fetch();
Assert.ok(QuickSuggest.weather.suggestion, "Suggestion should be fetched");
// Clear the pref and fetch again. It should set the suggestion back to null.
UrlbarPrefs.clear("weather.ignoreVPN");
await QuickSuggest.weather._test_fetch();
Assert.ok(!QuickSuggest.weather.suggestion, "Suggestion should be null");
// Simulate the link status changing. Since the mock link service still
// indicates a VPN is detected, the suggestion should remain null.
let fetchPromise = QuickSuggest.weather.waitForFetches();

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -17,3 +17,4 @@ firefox-appdir = browser
[test_quicksuggest_offlineDefault.js]
[test_quicksuggest_positionInSuggestions.js]
[test_weather.js]
[test_weather_keywords.js]

Просмотреть файл

@ -206,7 +206,13 @@ let AVAILABLE_PIP_OVERRIDES;
},
tubi: {
"https://*.tubitv.com/*": {
"https://*.tubitv.com/live*": {
videoWrapperScriptPath: "video-wrappers/tubilive.js",
},
"https://*.tubitv.com/movies*": {
videoWrapperScriptPath: "video-wrappers/tubi.js",
},
"https://*.tubitv.com/tv-shows*": {
videoWrapperScriptPath: "video-wrappers/tubi.js",
},
},

Просмотреть файл

@ -43,6 +43,7 @@ FINAL_TARGET_FILES.features["pictureinpicture@mozilla.org"]["video-wrappers"] +=
"video-wrappers/primeVideo.js",
"video-wrappers/radiocanada.js",
"video-wrappers/tubi.js",
"video-wrappers/tubilive.js",
"video-wrappers/twitch.js",
"video-wrappers/videojsWrapper.js",
"video-wrappers/voot.js",

Просмотреть файл

@ -0,0 +1,35 @@
/* 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";
class PictureInPictureVideoWrapper {
setCaptionContainerObserver(video, updateCaptionsFunction) {
let container = video.parentElement;
if (container) {
updateCaptionsFunction("");
const callback = function(mutationsList, observer) {
let text =
container.querySelector(`.tubi-text-track-container`)?.innerText ||
container.querySelector(`.subtitleWindow`)?.innerText;
updateCaptionsFunction(text);
};
// immediately invoke the callback function to add subtitles to the PiP window
callback([1], null);
let captionsObserver = new MutationObserver(callback);
captionsObserver.observe(container, {
attributes: true,
childList: true,
subtree: true,
});
}
}
}
this.PictureInPictureVideoWrapper = PictureInPictureVideoWrapper;

Просмотреть файл

@ -1 +1,2 @@
firefox/* ${DEB_PKG_INSTALL_PATH}
debian/${DEB_PKG_NAME}.desktop usr/share/applications

Просмотреть файл

@ -1 +1,6 @@
usr/lib/${DEB_PKG_NAME}/firefox usr/bin/${DEB_PKG_NAME}
${DEB_PKG_INSTALL_PATH}/firefox usr/bin/${DEB_PKG_NAME}
${DEB_PKG_INSTALL_PATH}/browser/chrome/icons/default/default16.png usr/share/icons/hicolor/16x16/apps/${DEB_PKG_NAME}.png
${DEB_PKG_INSTALL_PATH}/browser/chrome/icons/default/default32.png usr/share/icons/hicolor/32x32/apps/${DEB_PKG_NAME}.png
${DEB_PKG_INSTALL_PATH}/browser/chrome/icons/default/default48.png usr/share/icons/hicolor/48x48/apps/${DEB_PKG_NAME}.png
${DEB_PKG_INSTALL_PATH}/browser/chrome/icons/default/default64.png usr/share/icons/hicolor/64x64/apps/${DEB_PKG_NAME}.png
${DEB_PKG_INSTALL_PATH}/browser/chrome/icons/default/default128.png usr/share/icons/hicolor/128x128/apps/${DEB_PKG_NAME}.png

Просмотреть файл

@ -10,6 +10,8 @@ desktop-entry-name = { -brand-shortcut-name }
# The comment usually appears as a tooltip when hovering over application menu entry.
desktop-entry-comment = Browse the World Wide Web
desktop-entry-generic-name = Web Browser
# Combine Name and GenericName. This string is specific to GNOME.
desktop-entry-x-gnome-full-name = { -brand-shortcut-name } Web Browser
# Keywords are search terms used to find this application.
# The string is a list of keywords separated by semicolons:
# - Do NOT replace semicolons with other punctuation signs.
@ -21,3 +23,4 @@ desktop-entry-keywords = Internet;WWW;Browser;Web;Explorer;
desktop-action-new-window-name = New Window
desktop-action-new-private-window-name = New Private Window
desktop-action-open-profile-manager = Open Profile Manager

Просмотреть файл

@ -25,6 +25,21 @@
overflow: auto;
}
#identity-credential-header-text {
font-weight: 600;
}
.identity-credential-header-container {
margin: 16px 16px -16px 16px;
display: flex;
}
.identity-credential-header-icon {
width: 16px;
height: 16px;
margin-inline-end: 8px;
}
.identity-credential-list-item {
display: flex;
gap: 10px;

Просмотреть файл

@ -450,7 +450,7 @@
}
.urlbarView-row[type=tip][tip-type=dismissalAcknowledgment] {
padding-block: 0 6px;
padding-block: 6px;
}
/* Row label (a.k.a. group label) */

Просмотреть файл

@ -15,7 +15,7 @@ interoperability.
The [`uniffi-bindgen-gecko-js`](https://searchfox.org/mozilla-central/source/toolkit/components/uniffi-bindgen-gecko-js)
tool, which lives in the Firefox source tree, generates 2 things:
- A JS interface for the scaffolding code, which uses [WebIDL](/dom/bindings/webidl/index.rst)
- A JSM module that uses the scaffolding to provide the bindings API.
- A module that uses the scaffolding to provide the bindings API.
Currently, this generated code gets checked in to source control. We are working on a system to avoid this and
auto-generate it at build time instead (see [bugzilla 1756214](https://bugzilla.mozilla.org/show_bug.cgi?id=1756214)).
@ -55,15 +55,15 @@ Here's how you can create a new set of bindings using UniFFI:
3. Generate bindings code for your crate
- Add the path of your UDL (that you made in step 1) in `toolkit/components/uniffi-bindgen-gecko-js/mach_commands.py`
- Run `./mach uniffi generate`
- add your newly generated `Rust{udl-name}.jsm` file to `toolkit/components/uniffi-bindgen-gecko-js/components/moz.build`
- Then simply import your `jsm` module to the file you want to use it in and start using your APIs!
- add your newly generated `Rust{udl-name}.sys.mjs` file to `toolkit/components/uniffi-bindgen-gecko-js/components/moz.build`
- Then simply import your module to the file you want to use it in and start using your APIs!
Example from tabs module:
``` js
XPCOMUtils.defineLazyModuleGetters(lazy, {
ChromeUtils.defineESModuleGetters(lazy, {
...
TabsStore: "resource://gre/modules/RustTabs.jsm",
TabsStore: "resource://gre/modules/RustTabs.sys.mjs",
});
...
this._rustStore = await lazy.TabsStore.init(path);

Просмотреть файл

@ -275,16 +275,6 @@ void BodyStream::WriteIntoReadRequestBuffer(JSContext* aCx,
return;
}
// Subscribe WAIT_CLOSURE_ONLY so that OnInputStreamReady can be called when
// mInputStream is closed.
rv = mInputStream->AsyncWait(this, nsIAsyncInputStream::WAIT_CLOSURE_ONLY, 0,
mOwningEventTarget);
if (NS_WARN_IF(NS_FAILED(rv))) {
ErrorPropagation(aCx, aStream, rv);
return;
}
mAsyncWaitWorkerRef = mWorkerRef;
// All good.
}
@ -412,6 +402,19 @@ void BodyStream::EnqueueChunkWithSizeIntoStream(JSContext* aCx,
JS::Rooted<JS::Value> chunkValue(aCx);
chunkValue.setObject(*chunk);
aStream->EnqueueNative(aCx, chunkValue, aRv);
if (aRv.Failed()) {
return;
}
// Subscribe WAIT_CLOSURE_ONLY so that OnInputStreamReady can be called when
// mInputStream is closed.
nsresult rv = mInputStream->AsyncWait(
this, nsIAsyncInputStream::WAIT_CLOSURE_ONLY, 0, mOwningEventTarget);
if (NS_WARN_IF(NS_FAILED(rv))) {
aRv.Throw(rv);
return;
}
mAsyncWaitWorkerRef = mWorkerRef;
}
NS_IMETHODIMP
@ -469,11 +472,15 @@ BodyStream::OnInputStreamReady(nsIAsyncInputStream* aStream) {
return NS_OK;
}
// The previous call can execute JS (even up to running a nested event
// loop, including calling this OnInputStreamReady again before it ends, see
// the above nsAutoMicroTask), so |mPullPromise| can't be asserted to have any
// particular value, even if the previous call succeeds.
MOZ_ASSERT_IF(!mPullPromise, IsClosed());
// Enqueuing triggers read request chunk steps which may execute JS, but:
// 1. The nsIAsyncInputStream should hold the reference of `this` so it should
// be safe from cycle collection
// 2. AsyncWait is called after enqueuing and thus OnInputStreamReady can't be
// synchronously called again
//
// That said, it's generally good to be cautious as there's no guarantee that
// the interface is implemented in a safest way.
MOZ_DIAGNOSTIC_ASSERT(mPullPromise);
if (mPullPromise) {
mPullPromise->MaybeResolveWithUndefined();
mPullPromise = nullptr;

Просмотреть файл

@ -9,7 +9,7 @@
// access the uri. This prevents, for example, a source document from tricking
// the user into dragging a chrome url.
function ContentAreaDropListener() {}
export function ContentAreaDropListener() {}
ContentAreaDropListener.prototype = {
classID: Components.ID("{1f34bc80-1bc7-11d6-a384-d705dd0746fc}"),
@ -327,5 +327,3 @@ ContentAreaDropListener.prototype = {
);
},
};
var EXPORTED_SYMBOLS = ["ContentAreaDropListener"];

Просмотреть файл

@ -36,7 +36,7 @@ class DOMArena {
friend class DocGroup;
DOMArena() {
arena_params_t params;
params.mMaxDirtyIncreaseOverride = 5;
params.mMaxDirtyIncreaseOverride = 7;
mArenaId = moz_create_arena_with_params(&params);
}

Просмотреть файл

@ -16,9 +16,7 @@
* to the child side of frame and process message manager and removing them
* when needed.
*/
var EXPORTED_SYMBOLS = ["DOMRequestIpcHelper"];
function DOMRequestIpcHelper() {
export function DOMRequestIpcHelper() {
// _listeners keeps a list of messages for which we added a listener and the
// kind of listener that we added (strong or weak). It's an object of this
// form:

Просмотреть файл

@ -2,10 +2,6 @@
* 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";
const EXPORTED_SYMBOLS = ["LocationHelper"];
function isPublic(ap) {
let mask = "_nomap";
let result = ap.ssid.indexOf(mask, ap.ssid.length - mask.length);
@ -25,7 +21,7 @@ function encode(ap) {
* Location Services.
*/
class LocationHelper {
export class LocationHelper {
static formatWifiAccessPoints(accessPoints) {
return accessPoints
.filter(isPublic)

Просмотреть файл

@ -4,7 +4,7 @@
// Fills up aProcesses until max and then selects randomly from the available
// ones.
function RandomSelector() {}
export function RandomSelector() {}
RandomSelector.prototype = {
classID: Components.ID("{c616fcfd-9737-41f1-aa74-cee72a38f91b}"),
@ -21,7 +21,7 @@ RandomSelector.prototype = {
// Fills up aProcesses until max and then selects one from the available
// ones that host the least number of tabs.
function MinTabSelector() {}
export function MinTabSelector() {}
MinTabSelector.prototype = {
classID: Components.ID("{2dc08eaf-6eef-4394-b1df-a3a927c1290b}"),
@ -56,5 +56,3 @@ MinTabSelector.prototype = {
return candidate;
},
};
var EXPORTED_SYMBOLS = ["RandomSelector", "MinTabSelector"];

Просмотреть файл

@ -2,9 +2,7 @@
* 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";
function SlowScriptDebug() {}
export function SlowScriptDebug() {}
SlowScriptDebug.prototype = {
classDescription: "Slow script debug handler",
@ -24,5 +22,3 @@ SlowScriptDebug.prototype = {
this._remoteActivationHandler = cb;
},
};
var EXPORTED_SYMBOLS = ["SlowScriptDebug"];

Просмотреть файл

@ -10,24 +10,24 @@ Classes = [
'cid': '{1f34bc80-1bc7-11d6-a384-d705dd0746fc}',
'contract_ids': ['@mozilla.org/content/dropped-link-handler;1'],
'interfaces': ['nsIDroppedLinkHandler'],
'jsm': 'resource://gre/modules/ContentAreaDropListener.jsm',
'esModule': 'resource://gre/modules/ContentAreaDropListener.sys.mjs',
'constructor': 'ContentAreaDropListener',
},
{
'cid': '{c616fcfd-9737-41f1-aa74-cee72a38f91b}',
'jsm': 'resource://gre/modules/ProcessSelector.jsm',
'esModule': 'resource://gre/modules/ProcessSelector.sys.mjs',
'constructor': 'RandomSelector',
},
{
'cid': '{2dc08eaf-6eef-4394-b1df-a3a927c1290b}',
'contract_ids': ['@mozilla.org/ipc/processselector;1'],
'jsm': 'resource://gre/modules/ProcessSelector.jsm',
'esModule': 'resource://gre/modules/ProcessSelector.sys.mjs',
'constructor': 'MinTabSelector',
},
{
'cid': '{e740ddb4-18b4-4aac-8ae1-9b0f4320769d}',
'contract_ids': ['@mozilla.org/dom/slow-script-debug;1'],
'jsm': 'resource://gre/modules/SlowScriptDebug.jsm',
'esModule': 'resource://gre/modules/SlowScriptDebug.sys.mjs',
'constructor': 'SlowScriptDebug',
},
]

Просмотреть файл

@ -553,12 +553,12 @@ if CONFIG["CPU_ARCH"].startswith("ppc"):
SOURCES["nsTextFragmentVMX.cpp"].flags += CONFIG["PPC_VMX_FLAGS"]
EXTRA_JS_MODULES += [
"ContentAreaDropListener.jsm",
"DOMRequestHelper.jsm",
"ContentAreaDropListener.sys.mjs",
"DOMRequestHelper.sys.mjs",
"IndexedDBHelper.sys.mjs",
"LocationHelper.jsm",
"ProcessSelector.jsm",
"SlowScriptDebug.jsm",
"LocationHelper.sys.mjs",
"ProcessSelector.sys.mjs",
"SlowScriptDebug.sys.mjs",
]
XPCOM_MANIFESTS += [

Просмотреть файл

@ -2387,7 +2387,7 @@ bool nsContentUtils::ShouldResistFingerprinting(
// will check the parent's principal
nsIPrincipal* principal = aLoadInfo->GetLoadingPrincipal();
MOZ_ASSERT_IF(principal,
MOZ_ASSERT_IF(principal && !principal->IsSystemPrincipal(),
BasePrincipal::Cast(principal)->OriginAttributesRef() ==
aLoadInfo->GetOriginAttributes());
return ShouldResistFingerprinting_dangerous(principal, "Internal Call",

Просмотреть файл

@ -94,3 +94,4 @@ MSG_DEF(MSG_URL_NOT_LOADABLE, 2, true, JSEXN_TYPEERR, "{0}Access to '{1}' from s
MSG_DEF(MSG_ONE_OFF_TYPEERR, 2, true, JSEXN_TYPEERR, "{0}{1}")
MSG_DEF(MSG_ONE_OFF_RANGEERR, 2, true, JSEXN_RANGEERR, "{0}{1}")
MSG_DEF(MSG_NO_CODECS_PARAMETER, 2, true, JSEXN_TYPEERR, "{0}The provided type '{1}' does not have a 'codecs' parameter.")
MSG_DEF(MSG_JSON_INVALID_VALUE, 1, true, JSEXN_TYPEERR, "{0}Value could not be serialized as JSON.")

Просмотреть файл

@ -33,6 +33,7 @@
#include "nsPrintfCString.h"
#include "nsRFPService.h"
#include "nsReadableUtils.h"
#include "nsColor.h"
@ -1867,6 +1868,12 @@ UniquePtr<uint8_t[]> CanvasRenderingContext2D::GetImageBuffer(
mBufferProvider->ReturnSnapshot(snapshot.forget());
if (ShouldResistFingerprinting(RFPTarget::CanvasRandomization)) {
nsRFPService::RandomizePixels(GetCookieJarSettings(), ret.get(),
GetWidth() * GetHeight() * 4,
SurfaceFormat::A8R8G8B8_UINT32);
}
return ret;
}
@ -4659,8 +4666,8 @@ bool CanvasRenderingContext2D::IsPointInPath(
aSubjectPrincipal)) {
return false;
}
} else if (mOffscreenCanvas &&
mOffscreenCanvas->ShouldResistFingerprinting()) {
} else if (mOffscreenCanvas && mOffscreenCanvas->ShouldResistFingerprinting(
RFPTarget::CanvasImageExtractionPrompt)) {
return false;
}
@ -4722,8 +4729,8 @@ bool CanvasRenderingContext2D::IsPointInStroke(
aSubjectPrincipal)) {
return false;
}
} else if (mOffscreenCanvas &&
mOffscreenCanvas->ShouldResistFingerprinting()) {
} else if (mOffscreenCanvas && mOffscreenCanvas->ShouldResistFingerprinting(
RFPTarget::CanvasImageExtractionPrompt)) {
return false;
}
@ -4741,6 +4748,7 @@ bool CanvasRenderingContext2D::IsPointInStroke(
if (mPathTransformWillUpdate) {
return mPath->StrokeContainsPoint(strokeOptions, Point(aX, aY), mPathToDS);
}
return mPath->StrokeContainsPoint(strokeOptions, Point(aX, aY),
mTarget->GetTransform());
}
@ -5644,7 +5652,8 @@ nsresult CanvasRenderingContext2D::GetImageDataArray(
usePlaceholder = !CanvasUtils::IsImageExtractionAllowed(ownerDoc, aCx,
aSubjectPrincipal);
} else if (mOffscreenCanvas) {
usePlaceholder = mOffscreenCanvas->ShouldResistFingerprinting();
usePlaceholder = mOffscreenCanvas->ShouldResistFingerprinting(
RFPTarget::CanvasImageExtractionPrompt);
}
do {
@ -5670,6 +5679,13 @@ nsresult CanvasRenderingContext2D::GetImageDataArray(
uint8_t* src =
rawData.mData + srcReadRect.y * srcStride + srcReadRect.x * 4;
// Apply the random noises if canvan randomization is enabled.
if (ShouldResistFingerprinting(RFPTarget::CanvasRandomization)) {
nsRFPService::RandomizePixels(GetCookieJarSettings(), src,
GetWidth() * GetHeight() * 4,
SurfaceFormat::A8R8G8B8_UINT32);
}
uint8_t* dst = data + dstWriteRect.y * (aWidth * 4) + dstWriteRect.x * 4;
if (mOpaque) {

Просмотреть файл

@ -56,7 +56,8 @@ bool IsImageExtractionAllowed(dom::Document* aDocument, JSContext* aCx,
return false;
}
if (!aDocument->ShouldResistFingerprinting()) {
if (!aDocument->ShouldResistFingerprinting(
RFPTarget::CanvasImageExtractionPrompt)) {
return true;
}

Просмотреть файл

@ -1252,6 +1252,12 @@ UniquePtr<uint8_t[]> ClientWebGLContext::GetImageBuffer(
const auto& premultAlpha = mNotLost->info.options.premultipliedAlpha;
*out_imageSize = dataSurface->GetSize();
if (ShouldResistFingerprinting(RFPTarget::CanvasRandomization)) {
return gfxUtils::GetImageBufferWithRandomNoise(
dataSurface, premultAlpha, GetCookieJarSettings(), out_format);
}
return gfxUtils::GetImageBuffer(dataSurface, premultAlpha, out_format);
}
@ -1266,6 +1272,13 @@ ClientWebGLContext::GetInputStream(const char* mimeType,
RefPtr<gfx::DataSourceSurface> dataSurface = snapshot->GetDataSurface();
const auto& premultAlpha = mNotLost->info.options.premultipliedAlpha;
if (ShouldResistFingerprinting(RFPTarget::CanvasRandomization)) {
return gfxUtils::GetInputStreamWithRandomNoise(
dataSurface, premultAlpha, mimeType, encoderOptions,
GetCookieJarSettings(), out_stream);
}
return gfxUtils::GetInputStream(dataSurface, premultAlpha, mimeType,
encoderOptions, out_stream);
}
@ -5758,17 +5771,6 @@ void ClientWebGLContext::ProvokingVertex(const GLenum rawMode) const {
// -
bool ClientWebGLContext::ShouldResistFingerprinting() const {
if (mCanvasElement) {
return mCanvasElement->OwnerDoc()->ShouldResistFingerprinting();
}
if (mOffscreenCanvas) {
return mOffscreenCanvas->ShouldResistFingerprinting();
}
// Last resort, just check the global preference
return nsContentUtils::ShouldResistFingerprinting("Fallback");
}
uint32_t ClientWebGLContext::GetPrincipalHashValue() const {
if (mCanvasElement) {
return mCanvasElement->NodePrincipal()->GetHashValue();

Просмотреть файл

@ -2225,8 +2225,6 @@ class ClientWebGLContext final : public nsICanvasRenderingContextInternal,
already_AddRefed<dom::Promise> MakeXRCompatible(ErrorResult& aRv);
protected:
bool ShouldResistFingerprinting() const;
uint32_t GetPrincipalHashValue() const;
// Prepare the context for capture before compositing

Просмотреть файл

@ -7,6 +7,7 @@
#include "gfxPlatform.h"
#include "gfx2DGlue.h"
#include "mozilla/dom/ImageBitmapRenderingContextBinding.h"
#include "mozilla/gfx/Types.h"
#include "nsComponentManagerUtils.h"
#include "nsRegion.h"
#include "ImageContainer.h"
@ -167,7 +168,15 @@ mozilla::UniquePtr<uint8_t[]> ImageBitmapRenderingContext::GetImageBuffer(
*aFormat = imgIEncoder::INPUT_FORMAT_HOSTARGB;
*aImageSize = data->GetSize();
return gfx::SurfaceToPackedBGRA(data);
UniquePtr<uint8_t[]> ret = gfx::SurfaceToPackedBGRA(data);
if (ShouldResistFingerprinting(RFPTarget::CanvasRandomization)) {
nsRFPService::RandomizePixels(GetCookieJarSettings(), ret.get(),
GetWidth() * GetHeight() * 4,
gfx::SurfaceFormat::A8R8G8B8_UINT32);
}
return ret;
}
NS_IMETHODIMP

Просмотреть файл

@ -404,7 +404,8 @@ already_AddRefed<Promise> OffscreenCanvas::ConvertToBlob(
RefPtr<EncodeCompleteCallback> callback =
CreateEncodeCompleteCallback(promise);
bool usePlaceholder = ShouldResistFingerprinting();
bool usePlaceholder =
ShouldResistFingerprinting(RFPTarget::CanvasImageExtractionPrompt);
CanvasRenderingContextHelper::ToBlob(callback, type, encodeOptions,
/* aUsingCustomOptions */ false,
usePlaceholder, aRv);
@ -445,7 +446,8 @@ already_AddRefed<Promise> OffscreenCanvas::ToBlob(JSContext* aCx,
RefPtr<EncodeCompleteCallback> callback =
CreateEncodeCompleteCallback(promise);
bool usePlaceholder = ShouldResistFingerprinting();
bool usePlaceholder =
ShouldResistFingerprinting(RFPTarget::CanvasImageExtractionPrompt);
CanvasRenderingContextHelper::ToBlob(aCx, callback, aType, aParams,
usePlaceholder, aRv);
@ -461,8 +463,8 @@ already_AddRefed<gfx::SourceSurface> OffscreenCanvas::GetSurfaceSnapshot(
return mCurrentContext->GetSurfaceSnapshot(aOutAlphaType);
}
bool OffscreenCanvas::ShouldResistFingerprinting() const {
return nsContentUtils::ShouldResistFingerprinting(GetOwnerGlobal());
bool OffscreenCanvas::ShouldResistFingerprinting(RFPTarget aTarget) const {
return nsContentUtils::ShouldResistFingerprinting(GetOwnerGlobal(), aTarget);
}
/* static */

Просмотреть файл

@ -22,6 +22,7 @@ struct JSContext;
namespace mozilla {
class CancelableRunnable;
class ErrorResult;
enum class RFPTarget : unsigned;
namespace gfx {
class SourceSurface;
@ -153,7 +154,7 @@ class OffscreenCanvas final : public DOMEventTargetHelper,
return mCompositorBackendType;
}
bool ShouldResistFingerprinting() const;
bool ShouldResistFingerprinting(mozilla::RFPTarget aTarget) const;
bool IsTransferredFromElement() const { return !!mDisplay; }

Просмотреть файл

@ -7,7 +7,10 @@
#include "mozilla/dom/CanvasUtils.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/WorkerCommon.h"
#include "mozilla/dom/WorkerPrivate.h"
#include "mozilla/PresShell.h"
#include "nsPIDOMWindow.h"
#include "nsRefreshDriver.h"
nsICanvasRenderingContextInternal::nsICanvasRenderingContextInternal() =
@ -46,6 +49,35 @@ nsIPrincipal* nsICanvasRenderingContextInternal::PrincipalOrNull() const {
return nullptr;
}
nsICookieJarSettings* nsICanvasRenderingContextInternal::GetCookieJarSettings()
const {
if (mCanvasElement) {
return mCanvasElement->OwnerDoc()->CookieJarSettings();
}
// If there is an offscreen canvas, attempt to retrieve its owner window
// and return the cookieJarSettings for the window's document, if available.
if (mOffscreenCanvas) {
nsCOMPtr<nsPIDOMWindowInner> win =
do_QueryInterface(mOffscreenCanvas->GetOwnerGlobal());
if (win) {
return win->GetExtantDoc()->CookieJarSettings();
}
// If the owner window cannot be retrieved, check if there is a current
// worker and return its cookie jar settings if available.
mozilla::dom::WorkerPrivate* worker =
mozilla::dom::GetCurrentThreadWorkerPrivate();
if (worker) {
return worker->CookieJarSettings();
}
}
return nullptr;
}
void nsICanvasRenderingContextInternal::RemovePostRefreshObserver() {
if (mRefreshDriver) {
mRefreshDriver->RemovePostRefreshObserver(this);
@ -72,3 +104,15 @@ void nsICanvasRenderingContextInternal::DoSecurityCheck(
aForceWriteOnly, aCORSUsed);
}
}
bool nsICanvasRenderingContextInternal::ShouldResistFingerprinting(
mozilla::RFPTarget aTarget) const {
if (mCanvasElement) {
return mCanvasElement->OwnerDoc()->ShouldResistFingerprinting(aTarget);
}
if (mOffscreenCanvas) {
return mOffscreenCanvas->ShouldResistFingerprinting(aTarget);
}
// Last resort, just check the global preference
return nsContentUtils::ShouldResistFingerprinting("Fallback", aTarget);
}

Просмотреть файл

@ -12,6 +12,7 @@
#include "nsIInputStream.h"
#include "nsIDocShell.h"
#include "nsRefreshObservers.h"
#include "nsRFPService.h"
#include "mozilla/dom/HTMLCanvasElement.h"
#include "mozilla/dom/OffscreenCanvas.h"
#include "mozilla/Maybe.h"
@ -29,6 +30,7 @@
} \
}
class nsICookieJarSettings;
class nsIDocShell;
class nsIPrincipal;
class nsRefreshDriver;
@ -82,6 +84,8 @@ class nsICanvasRenderingContextInternal : public nsISupports,
nsIGlobalObject* GetParentObject() const;
nsICookieJarSettings* GetCookieJarSettings() const;
nsIPrincipal* PrincipalOrNull() const;
void SetOffscreenCanvas(mozilla::dom::OffscreenCanvas* aOffscreenCanvas) {
@ -211,6 +215,14 @@ class nsICanvasRenderingContextInternal : public nsISupports,
void DoSecurityCheck(nsIPrincipal* aPrincipal, bool forceWriteOnly,
bool CORSUsed);
// Checking if fingerprinting protection is enable for the given target. Note
// that we need to use unknown target as the default value for the WebGL
// callsites that haven't cut over to use RFPTarget.
//
// The default unknown target should be removed in Bug 1829635.
bool ShouldResistFingerprinting(
mozilla::RFPTarget aTarget = mozilla::RFPTarget::Unknown) const;
protected:
RefPtr<mozilla::dom::HTMLCanvasElement> mCanvasElement;
RefPtr<mozilla::dom::OffscreenCanvas> mOffscreenCanvas;

Просмотреть файл

@ -166,10 +166,10 @@ already_AddRefed<Response> Response::Redirect(const GlobalObject& aGlobal,
return r.forget();
}
/*static*/
already_AddRefed<Response> Response::Constructor(
/* static */ already_AddRefed<Response> Response::CreateAndInitializeAResponse(
const GlobalObject& aGlobal, const Nullable<fetch::ResponseBodyInit>& aBody,
const ResponseInit& aInit, ErrorResult& aRv) {
const nsACString& aDefaultContentType, const ResponseInit& aInit,
ErrorResult& aRv) {
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
if (NS_WARN_IF(!global)) {
@ -177,12 +177,14 @@ already_AddRefed<Response> Response::Constructor(
return nullptr;
}
// Initialize a response, Step 1.
if (aInit.mStatus < 200 || aInit.mStatus > 599) {
aRv.ThrowRangeError("Invalid response status code.");
return nullptr;
}
// Check if the status text contains illegal characters
// Initialize a response, Step 2: Check if the status text contains illegal
// characters
nsACString::const_iterator start, end;
aInit.mStatusText.BeginReading(start);
aInit.mStatusText.EndReading(end);
@ -197,6 +199,7 @@ already_AddRefed<Response> Response::Constructor(
return nullptr;
}
// Initialize a response, Step 3-4.
SafeRefPtr<InternalResponse> internalResponse =
MakeSafeRefPtr<InternalResponse>(aInit.mStatus, aInit.mStatusText);
@ -317,6 +320,10 @@ already_AddRefed<Response> Response::Constructor(
return nullptr;
}
if (!aDefaultContentType.IsVoid()) {
contentTypeWithCharset = aDefaultContentType;
}
bodySize = size;
}
@ -339,6 +346,37 @@ already_AddRefed<Response> Response::Constructor(
return r.forget();
}
/* static */
already_AddRefed<Response> Response::CreateFromJson(const GlobalObject& aGlobal,
JSContext* aCx,
JS::Handle<JS::Value> aData,
const ResponseInit& aInit,
ErrorResult& aRv) {
aRv.MightThrowJSException();
nsAutoString serializedValue;
if (!nsContentUtils::StringifyJSON(aCx, aData, serializedValue,
UndefinedIsVoidString)) {
aRv.StealExceptionFromJSContext(aCx);
return nullptr;
}
if (serializedValue.IsVoid()) {
aRv.ThrowTypeError<MSG_JSON_INVALID_VALUE>();
return nullptr;
}
Nullable<fetch::ResponseBodyInit> body;
body.SetValue().SetAsUSVString().ShareOrDependUpon(serializedValue);
return CreateAndInitializeAResponse(aGlobal, body, "application/json"_ns,
aInit, aRv);
}
/*static*/
already_AddRefed<Response> Response::Constructor(
const GlobalObject& aGlobal, const Nullable<fetch::ResponseBodyInit>& aBody,
const ResponseInit& aInit, ErrorResult& aRv) {
return CreateAndInitializeAResponse(aGlobal, aBody, VoidCString(), aInit,
aRv);
}
already_AddRefed<Response> Response::Clone(JSContext* aCx, ErrorResult& aRv) {
bool bodyUsed = GetBodyUsed(aRv);
if (NS_WARN_IF(aRv.Failed())) {

Просмотреть файл

@ -105,6 +105,12 @@ class Response final : public FetchBody<Response>, public nsWrapperCache {
uint16_t aStatus,
ErrorResult& aRv);
static already_AddRefed<Response> CreateFromJson(const GlobalObject&,
JSContext*,
JS::Handle<JS::Value>,
const ResponseInit&,
ErrorResult&);
static already_AddRefed<Response> Constructor(
const GlobalObject& aGlobal,
const Nullable<fetch::ResponseBodyInit>& aBody, const ResponseInit& aInit,
@ -135,6 +141,12 @@ class Response final : public FetchBody<Response>, public nsWrapperCache {
}
private:
static already_AddRefed<Response> CreateAndInitializeAResponse(
const GlobalObject& aGlobal,
const Nullable<fetch::ResponseBodyInit>& aBody,
const nsACString& aDefaultContentType, const ResponseInit& aInit,
ErrorResult& aRv);
~Response();
SafeRefPtr<InternalResponse> mInternalResponse;

Просмотреть файл

@ -34,6 +34,7 @@
#include "mozilla/MouseEvents.h"
#include "mozilla/Preferences.h"
#include "mozilla/ProfilerLabels.h"
#include "mozilla/StaticPrefs_privacy.h"
#include "mozilla/Telemetry.h"
#include "mozilla/webgpu/CanvasContext.h"
#include "nsAttrValueInlines.h"

Просмотреть файл

@ -653,6 +653,39 @@ void nsGenericHTMLElement::BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName,
aNotify);
}
void nsGenericHTMLElement::AfterSetPopoverAttr() {
const nsAttrValue* newValue = GetParsedAttr(nsGkAtoms::popover);
// https://html.spec.whatwg.org/multipage/popover.html#attr-popover
PopoverState newState;
if (newValue) {
if (newValue->Type() == nsAttrValue::eEnum) {
newState = static_cast<dom::PopoverState>(newValue->GetEnumValue());
} else {
// The invalid value default is the manual state
newState = PopoverState::Manual;
}
} else {
// The missing value default is the no popover state.
newState = PopoverState::None;
}
PopoverState oldState = GetPopoverState();
if (newState != oldState) {
if (oldState != PopoverState::None) {
HidePopoverInternal(/* aFocusPreviousElement = */ true,
/* aFireEvents = */ true, IgnoreErrors());
}
// Bug 1831081: `newState` might here differ from `GetPopoverState()`.
if (newState != PopoverState::None) {
EnsurePopoverData().SetPopoverState(newState);
PopoverPseudoStateUpdate(false, true);
} else {
ClearPopoverData();
RemoveStates(ElementState::POPOVER_OPEN);
}
}
}
void nsGenericHTMLElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName,
const nsAttrValue* aValue,
const nsAttrValue* aOldValue,
@ -667,33 +700,9 @@ void nsGenericHTMLElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName,
SyncEditorsOnSubtree(this);
} else if (aName == nsGkAtoms::popover &&
StaticPrefs::dom_element_popover_enabled()) {
// https://html.spec.whatwg.org/multipage/popover.html#attr-popover
PopoverState newState;
if (aValue) {
if (aValue->Type() == nsAttrValue::eEnum) {
newState = static_cast<dom::PopoverState>(aValue->GetEnumValue());
} else {
// The invalid value default is the manual state
newState = PopoverState::Manual;
}
} else {
// The missing value default is the no popover state.
newState = PopoverState::None;
}
PopoverState oldState = GetPopoverState();
if (newState != oldState) {
if (oldState != PopoverState::None) {
HidePopoverInternal(/* aFocusPreviousElement = */ true,
/* aFireEvents = */ false, IgnoreErrors());
}
if (newState != PopoverState::None) {
EnsurePopoverData().SetPopoverState(newState);
PopoverPseudoStateUpdate(false, true);
} else {
ClearPopoverData();
RemoveStates(ElementState::POPOVER_OPEN);
}
}
nsContentUtils::AddScriptRunner(
NewRunnableMethod("nsGenericHTMLElement::AfterSetPopoverAttr", this,
&nsGenericHTMLElement::AfterSetPopoverAttr));
} else if (aName == nsGkAtoms::dir) {
Directionality dir = eDir_LTR;
// A boolean tracking whether we need to recompute our directionality.

Просмотреть файл

@ -759,6 +759,8 @@ class nsGenericHTMLElement : public nsGenericHTMLElementBase {
const nsAttrValue* aOldValue, nsIPrincipal* aMaybeScriptedPrincipal,
bool aNotify) override;
MOZ_CAN_RUN_SCRIPT void AfterSetPopoverAttr();
mozilla::EventListenerManager* GetEventListenerManagerForAttr(
nsAtom* aAttrName, bool* aDefer) override;

Просмотреть файл

@ -2850,9 +2850,9 @@ mozilla::ipc::IPCResult ContentChild::RecvNotifyProcessPriorityChanged(
dom_memory_foreground_content_processes_have_larger_page_cache()) {
if (mProcessPriority >= hal::PROCESS_PRIORITY_FOREGROUND) {
// Note: keep this in sync with the JS shell (js/src/shell/js.cpp).
moz_set_max_dirty_page_modifier(3);
moz_set_max_dirty_page_modifier(4);
} else if (mProcessPriority == hal::PROCESS_PRIORITY_BACKGROUND) {
moz_set_max_dirty_page_modifier(-1);
moz_set_max_dirty_page_modifier(-2);
} else {
moz_set_max_dirty_page_modifier(0);
}

Просмотреть файл

@ -54,6 +54,7 @@
#include "nsITransportSecurityInfo.h"
#include "nsISharePicker.h"
#include "nsIURIMutator.h"
#include "nsScriptSecurityManager.h"
#include "mozilla/dom/DOMException.h"
#include "mozilla/dom/DOMExceptionBinding.h"
@ -65,6 +66,7 @@
#include "SessionStoreFunctions.h"
#include "nsIXPConnect.h"
#include "nsImportModule.h"
#include "nsIOService.h"
#include "mozilla/dom/PBackgroundSessionStorageCache.h"
@ -81,7 +83,6 @@ WindowGlobalParent::WindowGlobalParent(
uint64_t aOuterWindowId, FieldValues&& aInit)
: WindowContext(aBrowsingContext, aInnerWindowId, aOuterWindowId,
std::move(aInit)),
mIsInitialDocument(false),
mSandboxFlags(0),
mDocumentHasLoaded(false),
mDocumentHasUserInteracted(false),
@ -108,7 +109,7 @@ already_AddRefed<WindowGlobalParent> WindowGlobalParent::CreateDisconnected(
aInit.context().mOuterWindowId, std::move(fields));
wgp->mDocumentPrincipal = aInit.principal();
wgp->mDocumentURI = aInit.documentURI();
wgp->mIsInitialDocument = aInit.isInitialDocument();
wgp->mIsInitialDocument = Some(aInit.isInitialDocument());
wgp->mBlockAllMixedContent = aInit.blockAllMixedContent();
wgp->mUpgradeInsecureRequests = aInit.upgradeInsecureRequests();
wgp->mSandboxFlags = aInit.sandboxFlags();
@ -371,7 +372,44 @@ mozilla::ipc::IPCResult WindowGlobalParent::RecvInternalLoad(
IPCResult WindowGlobalParent::RecvUpdateDocumentURI(nsIURI* aURI) {
// XXX(nika): Assert that the URI change was one which makes sense (either
// about:blank -> a real URI, or a legal push/popstate URI change?)
// about:blank -> a real URI, or a legal push/popstate URI change):
nsAutoCString scheme;
if (NS_FAILED(aURI->GetScheme(scheme))) {
return IPC_FAIL(this, "Setting DocumentURI without scheme.");
}
nsIIOService* ios = nsContentUtils::GetIOService();
if (!ios) {
return IPC_FAIL(this, "Cannot get IOService");
}
nsCOMPtr<nsIProtocolHandler> handler;
ios->GetProtocolHandler(scheme.get(), getter_AddRefs(handler));
if (!handler) {
return IPC_FAIL(this, "Setting DocumentURI with unknown protocol.");
}
auto isLoadableViaInternet = [](nsIURI* uri) {
return (uri && (net::SchemeIsHTTP(uri) || net::SchemeIsHTTPS(uri)));
};
if (isLoadableViaInternet(aURI)) {
nsCOMPtr<nsIURI> principalURI = mDocumentPrincipal->GetURI();
if (mDocumentPrincipal->GetIsNullPrincipal()) {
nsCOMPtr<nsIPrincipal> precursor =
mDocumentPrincipal->GetPrecursorPrincipal();
if (precursor) {
principalURI = precursor->GetURI();
}
}
if (isLoadableViaInternet(principalURI) &&
!nsScriptSecurityManager::SecurityCompareURIs(principalURI, aURI)) {
return IPC_FAIL(this,
"Setting DocumentURI with a different Origin than "
"principal URI");
}
}
mDocumentURI = aURI;
return IPC_OK();
}

Просмотреть файл

@ -151,7 +151,9 @@ class WindowGlobalParent final : public WindowContext,
void GetContentBlockingLog(nsAString& aLog);
bool IsInitialDocument() { return mIsInitialDocument; }
bool IsInitialDocument() {
return mIsInitialDocument.isSome() && mIsInitialDocument.value();
}
already_AddRefed<mozilla::dom::Promise> PermitUnload(
PermitUnloadAction aAction, uint32_t aTimeout, mozilla::ErrorResult& aRv);
@ -252,7 +254,12 @@ class WindowGlobalParent final : public WindowContext,
mozilla::ipc::IPCResult RecvUpdateDocumentTitle(const nsString& aTitle);
mozilla::ipc::IPCResult RecvUpdateHttpsOnlyStatus(uint32_t aHttpsOnlyStatus);
mozilla::ipc::IPCResult RecvSetIsInitialDocument(bool aIsInitialDocument) {
mIsInitialDocument = aIsInitialDocument;
if (aIsInitialDocument && mIsInitialDocument.isSome() &&
(mIsInitialDocument.value() != aIsInitialDocument)) {
return IPC_FAIL_NO_REASON(this);
}
mIsInitialDocument = Some(aIsInitialDocument);
return IPC_OK();
}
mozilla::ipc::IPCResult RecvUpdateDocumentSecurityInfo(
@ -337,7 +344,7 @@ class WindowGlobalParent final : public WindowContext,
nsCOMPtr<nsIURI> mDocumentURI;
Maybe<nsString> mDocumentTitle;
bool mIsInitialDocument;
Maybe<bool> mIsInitialDocument;
// True if this window has a "beforeunload" event listener.
bool mHasBeforeUnload;

Просмотреть файл

@ -175,6 +175,26 @@ void DOMLocalization::GetAttributes(Element& aElement, L10nIdArgs& aResult,
}
}
void DOMLocalization::SetArgs(JSContext* aCx, Element& aElement,
const Optional<JS::Handle<JSObject*>>& aArgs,
ErrorResult& aRv) {
if (aArgs.WasPassed() && aArgs.Value()) {
nsAutoString data;
JS::Rooted<JS::Value> val(aCx, JS::ObjectValue(*aArgs.Value()));
if (!nsContentUtils::StringifyJSON(aCx, val, data,
UndefinedIsNullStringLiteral)) {
aRv.NoteJSContextException(aCx);
return;
}
if (!aElement.AttrValueIs(kNameSpaceID_None, nsGkAtoms::datal10nargs, data,
eCaseMatters)) {
aElement.SetAttr(kNameSpaceID_None, nsGkAtoms::datal10nargs, data, true);
}
} else {
aElement.UnsetAttr(kNameSpaceID_None, nsGkAtoms::datal10nargs, true);
}
}
already_AddRefed<Promise> DOMLocalization::TranslateFragment(nsINode& aNode,
ErrorResult& aRv) {
Sequence<OwningNonNull<Element>> elements;

Просмотреть файл

@ -61,6 +61,9 @@ class DOMLocalization : public intl::Localization {
ErrorResult& aRv);
void GetAttributes(Element& aElement, L10nIdArgs& aResult, ErrorResult& aRv);
void SetArgs(JSContext* aCx, Element& aElement,
const Optional<JS::Handle<JSObject*>>& aArgs, ErrorResult& aRv);
already_AddRefed<Promise> TranslateFragment(nsINode& aNode, ErrorResult& aRv);
already_AddRefed<Promise> TranslateElements(

Просмотреть файл

@ -2,7 +2,7 @@
<html>
<head>
<meta charset="utf-8">
<title>Test DOMLocalization.prototype.setAttributes</title>
<title>Test DOMLocalization.prototype.setAttributes and DOMLocalization.prototype.setArgs</title>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript">
@ -21,11 +21,53 @@
const p1 = document.querySelectorAll("p")[0];
domLoc.setAttributes(p1, "title");
is(p1.getAttribute("data-l10n-id"), "title");
is(
p1.getAttribute("data-l10n-id"),
"title",
"The data-l10n-id can be set by setAttributes."
);
is(
p1.getAttribute("data-l10n-args"),
null,
"The data-l10n-args is unset."
);
domLoc.setAttributes(p1, "title2", {userName: "John"});
is(p1.getAttribute("data-l10n-id"), "title2");
is(p1.getAttribute("data-l10n-args"), JSON.stringify({userName: "John"}));
is(
p1.getAttribute("data-l10n-id"),
"title2",
"The data-l10n-id can be set by setAttributes."
);
is(
p1.getAttribute("data-l10n-args"),
JSON.stringify({userName: "John"}),
"The data-l10n-args can be set by setAttributes."
);
domLoc.setArgs(p1, {userName: "Jane"});
is(
p1.getAttribute("data-l10n-id"),
"title2",
"The data-l10n-id is unchanged by setArgs."
);
is(
p1.getAttribute("data-l10n-args"),
JSON.stringify({userName: "Jane"}),
"The data-l10n-args can by set by setArgs."
);
domLoc.setArgs(p1);
is(
p1.getAttribute("data-l10n-id"),
"title2",
"The data-l10n-id is unchanged by setArgs."
);
is(
p1.getAttribute("data-l10n-args"),
null,
"The data-l10n-args be unset by setArgs."
);
SimpleTest.finish();
};

Просмотреть файл

@ -4,9 +4,7 @@
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const { DOMRequestIpcHelper } = ChromeUtils.import(
"resource://gre/modules/DOMRequestHelper.jsm"
);
import { DOMRequestIpcHelper } from "resource://gre/modules/DOMRequestHelper.sys.mjs";
const lazy = {};

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше