зеркало из https://github.com/mozilla/gecko-dev.git
Merge autoland to mozilla-central. a=merge
This commit is contained in:
Коммит
373d05f4ea
|
@ -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">We’re building a richer search experience</h1>
|
||||
<h1 id="multi-lines-content">
|
||||
We’re building a
|
||||
richer
|
||||
richest
|
||||
search experience
|
||||
</h1>
|
||||
`,
|
||||
|
@ -33,7 +33,45 @@ search experience
|
|||
);
|
||||
is(
|
||||
multiLinesContentHeading.getAttributeValue("AXTitle"),
|
||||
"We’re building a richer search experience"
|
||||
"We’re 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®exp=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(¶ms);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = {};
|
||||
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче