Update On Tue Feb 4 19:53:54 CET 2025

This commit is contained in:
github-action[bot] 2025-02-04 19:53:55 +01:00
parent 1211aa377f
commit d57eb0346a
928 changed files with 16651 additions and 8438 deletions

4
Cargo.lock generated
View file

@ -2430,6 +2430,7 @@ dependencies = [
"gkrust_utils",
"http_sfv",
"idna_glue",
"ipcclientcerts",
"ipdl_utils",
"jog",
"jsrust_shared",
@ -3236,11 +3237,10 @@ dependencies = [
]
[[package]]
name = "ipcclientcerts-static"
name = "ipcclientcerts"
version = "0.1.0"
dependencies = [
"byteorder",
"mozilla-central-workspace-hack",
"pkcs11-bindings",
"rsclientcerts",
"sha2",

View file

@ -1158,7 +1158,7 @@ static const nsRoleMapEntry sWAIRoleMaps[] = {
},
{ // searchbox
nsGkAtoms::searchbox,
roles::ENTRY,
roles::SEARCHBOX,
kUseMapRole,
eNoValue,
eActivateAction,

View file

@ -4,7 +4,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "CssAltContent.h"
#include "mozilla/a11y/DocAccessible.h"
#include "DocAccessible-inl.h"
#include "mozilla/a11y/DocManager.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/Element.h"

View file

@ -660,7 +660,7 @@ ROLE(PASSWORD_TEXT,
nullptr,
ATK_ROLE_PASSWORD_TEXT,
NSAccessibilityTextFieldRole,
NSAccessibilityUnknownSubrole,
NSAccessibilitySecureTextFieldSubrole,
ROLE_SYSTEM_TEXT,
ROLE_SYSTEM_TEXT,
java::SessionAccessibility::CLASSNAME_EDITTEXT,
@ -1844,4 +1844,17 @@ ROLE(ROWGROUP,
IsAccessibilityElementRule::IfChildlessWithNameAndFocusable,
UIA_GroupControlTypeId,
eNameFromSubtreeIfReqRule)
ROLE(SEARCHBOX,
"searchbox",
nsGkAtoms::searchbox,
ATK_ROLE_ENTRY,
NSAccessibilityTextFieldRole,
@"AXSearchField",
ROLE_SYSTEM_TEXT,
ROLE_SYSTEM_TEXT,
java::SessionAccessibility::CLASSNAME_EDITTEXT,
IsAccessibilityElementRule::Yes,
UIA_EditControlTypeId,
eNameFromValueRule)
// clang-format on

View file

@ -451,16 +451,6 @@ already_AddRefed<nsIURI> Accessible::AnchorURIAt(uint32_t aAnchorIndex) const {
return nullptr;
}
bool Accessible::IsSearchbox() const {
const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
if (roleMapEntry && roleMapEntry->Is(nsGkAtoms::searchbox)) {
return true;
}
RefPtr<nsAtom> inputType = InputType();
return inputType == nsGkAtoms::search;
}
#ifdef A11Y_LOG
void Accessible::DebugDescription(nsCString& aDesc) const {
aDesc.Truncate();
@ -632,9 +622,6 @@ nsStaticAtom* Accessible::ComputedARIARole() const {
// map to a unique Gecko role.
return roleMap->roleAtom;
}
if (IsSearchbox()) {
return nsGkAtoms::searchbox;
}
role geckoRole = Role();
if (geckoRole == roles::LANDMARK) {
// Landmark role from native markup; e.g. <main>, <nav>.

View file

@ -442,11 +442,6 @@ class Accessible {
*/
virtual nsAtom* TagName() const = 0;
/**
* Return input `type` attribute
*/
virtual already_AddRefed<nsAtom> InputType() const = 0;
/**
* Return a landmark role if applied.
*/
@ -646,8 +641,6 @@ class Accessible {
bool IsDateTimeField() const { return mType == eHTMLDateTimeFieldType; }
bool IsSearchbox() const;
virtual bool HasNumericValue() const = 0;
/**

View file

@ -4304,20 +4304,6 @@ nsAtom* LocalAccessible::TagName() const {
: nullptr;
}
already_AddRefed<nsAtom> LocalAccessible::InputType() const {
if (!IsTextField() && !IsDateTimeField()) {
return nullptr;
}
dom::Element* el = mContent->AsElement();
if (const nsAttrValue* attr = el->GetParsedAttr(nsGkAtoms::type)) {
RefPtr<nsAtom> inputType = attr->GetAsAtom();
return inputType.forget();
}
return nullptr;
}
already_AddRefed<nsAtom> LocalAccessible::DisplayStyle() const {
dom::Element* elm = Elm();
if (!elm) {

View file

@ -733,8 +733,6 @@ class LocalAccessible : public nsISupports, public Accessible {
virtual nsAtom* TagName() const override;
virtual already_AddRefed<nsAtom> InputType() const override;
virtual already_AddRefed<nsAtom> DisplayStyle() const override;
virtual float Opacity() const override;

View file

@ -289,9 +289,16 @@ role HTMLTextFieldAccessible::NativeRole() const {
if (mType == eHTMLTextPasswordFieldType) {
return roles::PASSWORD_TEXT;
}
if (mContent->AsElement()->HasAttr(nsGkAtoms::list_)) {
dom::Element* el = mContent->AsElement();
if (el->HasAttr(nsGkAtoms::list_)) {
return roles::EDITCOMBOBOX;
}
if (const nsAttrValue* attr = el->GetParsedAttr(nsGkAtoms::type)) {
RefPtr<nsAtom> inputType = attr->GetAsAtom();
if (inputType == nsGkAtoms::search) {
return roles::SEARCHBOX;
}
}
return roles::ENTRY;
}

View file

@ -813,4 +813,9 @@ interface nsIAccessibleRole : nsISupports
* elements in an HTML table element.
*/
const unsigned long ROLE_ROWGROUP = 139;
/**
* A type of textbox intended for specifying search criteria.
*/
const unsigned long ROLE_SEARCHBOX = 140;
};

View file

@ -374,6 +374,9 @@ static uint64_t GetAccessibilityTraits(Accessible* aAccessible) {
case roles::PASSWORD_TEXT:
traits |= Trait::SecureTextField;
break;
case roles::SEARCHBOX:
traits |= Trait::SearchField;
break;
default:
break;
}
@ -405,10 +408,6 @@ static uint64_t GetAccessibilityTraits(Accessible* aAccessible) {
traits |= Trait::IsEditing | Trait::TextOperationsAvailable;
}
if (aAccessible->IsSearchbox()) {
traits |= Trait::SearchField;
}
if (state & states::MULTI_LINE) {
traits |= Trait::TextArea;
}

View file

@ -878,23 +878,17 @@ ipc::IPCResult DocAccessibleParent::AddChildDoc(DocAccessibleParent* aChildDoc,
if (aChildDoc->IsTopLevelInContentProcess()) {
// aChildDoc is an embedded document in a different content process to
// this document.
auto embeddedBrowser =
static_cast<dom::BrowserParent*>(aChildDoc->Manager());
dom::BrowserBridgeParent* bridge =
embeddedBrowser->GetBrowserBridgeParent();
if (bridge) {
#if defined(XP_WIN)
if (nsWinUtils::IsWindowEmulationStarted()) {
aChildDoc->SetEmulatedWindowHandle(mEmulatedWindowHandle);
}
#endif // defined(XP_WIN)
// We need to fire a reorder event on the outer doc accessible.
// For same-process documents, this is fired by the content process, but
// this isn't possible when the document is in a different process to its
// embedder.
// FireEvent fires both OS and XPCOM events.
FireEvent(outerDoc, nsIAccessibleEvent::EVENT_REORDER);
if (nsWinUtils::IsWindowEmulationStarted()) {
aChildDoc->SetEmulatedWindowHandle(mEmulatedWindowHandle);
}
#endif // defined(XP_WIN)
// We need to fire a reorder event on the outer doc accessible.
// For same-process documents, this is fired by the content process, but
// this isn't possible when the document is in a different process to its
// embedder.
// FireEvent fires both OS and XPCOM events.
FireEvent(outerDoc, nsIAccessibleEvent::EVENT_REORDER);
}
return IPC_OK();

View file

@ -1784,18 +1784,6 @@ nsAtom* RemoteAccessible::TagName() const {
return nullptr;
}
already_AddRefed<nsAtom> RemoteAccessible::InputType() const {
if (mCachedFields) {
if (auto inputType =
mCachedFields->GetAttribute<RefPtr<nsAtom>>(CacheKey::InputType)) {
RefPtr<nsAtom> result = *inputType;
return result.forget();
}
}
return nullptr;
}
already_AddRefed<nsAtom> RemoteAccessible::DisplayStyle() const {
if (RequestDomainsIfInactive(CacheDomain::Style)) {
return nullptr;

View file

@ -205,8 +205,6 @@ class RemoteAccessible : public Accessible, public HyperTextAccessibleBase {
virtual nsAtom* TagName() const override;
virtual already_AddRefed<nsAtom> InputType() const override;
virtual already_AddRefed<nsAtom> DisplayStyle() const override;
virtual float Opacity() const override;

View file

@ -27,9 +27,6 @@
// override
- (NSString*)moxRole;
// override
- (NSString*)moxSubrole;
// override
- (NSNumber*)moxNumberOfCharacters;

View file

@ -113,20 +113,6 @@ inline NSString* ToNSString(id aValue) {
return [super moxRole];
}
- (NSString*)moxSubrole {
MOZ_ASSERT(mGeckoAccessible);
if (mRole == roles::PASSWORD_TEXT) {
return NSAccessibilitySecureTextFieldSubrole;
}
if (mRole == roles::ENTRY && mGeckoAccessible->IsSearchbox()) {
return @"AXSearchField";
}
return nil;
}
- (NSNumber*)moxNumberOfCharacters {
return @([self textLength]);
}

View file

@ -86,6 +86,9 @@ addAccessibleTask(
<!-- True HTML5 search box -->
<input type="search" id="htmlSearch" />
<!-- Password input -->
<input type="password" id="password" />
<!-- A button morphed into a toggle via ARIA -->
<button id="toggle" aria-pressed="false"></button>
@ -208,6 +211,9 @@ addAccessibleTask(
// True HTML5 search field
testRoleAndSubRole(accDoc, "htmlSearch", "AXTextField", "AXSearchField");
// Password input
testRoleAndSubRole(accDoc, "password", "AXTextField", "AXSecureTextField");
// A button morphed into a toggle by ARIA
testRoleAndSubRole(accDoc, "toggle", "AXCheckBox", "AXToggle");

View file

@ -55,8 +55,7 @@ addAccessibleTask(
testComputedARIARole("ariaUnnamedRegion", "navigation");
// The directory ARIA role is an alias of list.
testComputedARIARole("ariaDirectory", "list");
// alertdialog, feed, rowgroup and searchbox map to a Gecko role, but it
// isn't unique.
// alertdialog, feed and rowgroupmap to a Gecko role, but it isn't unique.
testComputedARIARole("ariaAlertdialog", "alertdialog");
testComputedARIARole("ariaFeed", "feed");
testComputedARIARole("ariaRowgroup", "rowgroup");

View file

@ -33,7 +33,7 @@ add_task(async function test_searchbar_a11y_tree() {
// input element
{
role: ROLE_ENTRY,
role: ROLE_SEARCHBOX,
children: [],
},
@ -72,7 +72,7 @@ add_task(async function test_searchbar_a11y_tree() {
// input element
{
role: ROLE_ENTRY,
role: ROLE_SEARCHBOX,
children: [],
},

View file

@ -765,11 +765,24 @@
],
};
testElm("input_email", obj);
testElm("input_search", obj);
testElm("input_tel", obj);
testElm("input_text", obj);
testElm("input_url", obj);
// ////////////////////////////////////////////////////////////////////////
// HTML:input@type="search"
obj = {
role: ROLE_SEARCHBOX,
extraStates: EXT_STATE_EDITABLE | EXT_STATE_SINGLE_LINE,
actions: "activate",
interfaces: [ nsIAccessibleText, nsIAccessibleEditableText ],
children: [
{ role: ROLE_TEXT_LEAF },
],
};
testElm("input_search", obj);
// ////////////////////////////////////////////////////////////////////////
// input @type="text" with placeholder attribute

View file

@ -116,6 +116,7 @@ const ROLE_RICH_OPTION = nsIAccessibleRole.ROLE_RICH_OPTION;
const ROLE_ROW = nsIAccessibleRole.ROLE_ROW;
const ROLE_ROWHEADER = nsIAccessibleRole.ROLE_ROWHEADER;
const ROLE_SCROLLBAR = nsIAccessibleRole.ROLE_SCROLLBAR;
const ROLE_SEARCHBOX = nsIAccessibleRole.ROLE_SEARCHBOX;
const ROLE_SECTION = nsIAccessibleRole.ROLE_SECTION;
const ROLE_SEPARATOR = nsIAccessibleRole.ROLE_SEPARATOR;
const ROLE_SLIDER = nsIAccessibleRole.ROLE_SLIDER;

View file

@ -120,8 +120,8 @@
testRole("aria_region_as_table_with_miscaption_mixed", ROLE_TABLE);
testRole("aria_scrollbar", ROLE_SCROLLBAR);
testRole("aria_scrollbar_mixed", ROLE_SCROLLBAR);
testRole("aria_searchbox", ROLE_ENTRY);
testRole("aria_searchbox_mixed", ROLE_ENTRY);
testRole("aria_searchbox", ROLE_SEARCHBOX);
testRole("aria_searchbox_mixed", ROLE_SEARCHBOX);
testRole("aria_separator", ROLE_SEPARATOR);
testRole("aria_separator_mixed", ROLE_SEPARATOR);
testRole("aria_slider", ROLE_SLIDER);
@ -481,8 +481,8 @@
<table id="aria_region_as_table_with_miscaption_mixed" role="rEGIOn"><caption role="option">hello</caption></table>
<span id="aria_scrollbar" role="scrollbar"></span>
<span id="aria_scrollbar_mixed" role="sCROLLBAr"></span>
<span id="aria_searchbox" role="textbox"></span>
<span id="aria_searchbox_mixed" role="tEXTBOx"></span>
<span id="aria_searchbox" role="searchbox"></span>
<span id="aria_searchbox_mixed" role="sEARCHBOx"></span>
<span id="aria_separator" role="separator"></span>
<span id="aria_separator_mixed" role="sEPARATOr"></span>
<span id="aria_slider" role="slider"></span>

View file

@ -25,6 +25,22 @@ template <typename T>
HRESULT GetAttribute(TEXTATTRIBUTEID aAttributeId, T const& aRangeOrPoint,
VARIANT& aRetVal);
static int CompareVariants(const VARIANT& aFirst, const VARIANT& aSecond) {
// MinGW lacks support for VariantCompare, but does support converting to
// PROPVARIANT and PropVariantCompareEx. Use this as a workaround for MinGW
// builds, but avoid the extra work otherwise. See Bug 1944732.
#if defined(__MINGW32__) || defined(__MINGW64__) || defined(__MINGW__)
PROPVARIANT firstPropVar;
PROPVARIANT secondPropVar;
VariantToPropVariant(&aFirst, &firstPropVar);
VariantToPropVariant(&aSecond, &secondPropVar);
return PropVariantCompareEx(firstPropVar, secondPropVar, PVCU_DEFAULT,
PVCHF_DEFAULT);
#else
return VariantCompare(aFirst, aSecond);
#endif
}
// Used internally to safely get a UiaTextRange from a COM pointer provided
// to us by a client.
// {74B8E664-4578-4B52-9CBC-30A7A8271AE8}
@ -385,9 +401,9 @@ UiaTextRange::FindAttribute(TEXTATTRIBUTEID aAttributeId, VARIANT aVal,
// text attribute runs, we don't need to check the entire range; this
// point's attributes are those of the entire range.
GetAttribute(aAttributeId, startPoint, value);
// VariantCompare is not valid if types are different. Verify the type
// CompareVariants is not valid if types are different. Verify the type
// first so the result is well-defined.
if (aVal.vt == value.vt && VariantCompare(aVal, value) == 0) {
if (aVal.vt == value.vt && CompareVariants(aVal, value) == 0) {
if (!matchingRangeStart) {
matchingRangeStart = Some(startPoint);
}
@ -417,7 +433,7 @@ UiaTextRange::FindAttribute(TEXTATTRIBUTEID aAttributeId, VARIANT aVal,
startPoint = startPoint.FindTextAttrsStart(eDirPrevious);
do {
GetAttribute(aAttributeId, startPoint, value);
if (aVal.vt == value.vt && VariantCompare(aVal, value) == 0) {
if (aVal.vt == value.vt && CompareVariants(aVal, value) == 0) {
if (!matchingRangeEnd) {
matchingRangeEnd = Some(endPoint);
}

View file

@ -564,10 +564,6 @@ pref("browser.urlbar.quicksuggest.showOnboardingDialogAfterNRestarts", 0);
pref("browser.urlbar.quicksuggest.sponsoredIndex", 0);
pref("browser.urlbar.quicksuggest.nonSponsoredIndex", -1);
// Whether quick suggest results can be shown in position specified in the
// suggestions.
pref("browser.urlbar.quicksuggest.allowPositionInSuggestions", true);
// Whether non-sponsored quick suggest results are subject to impression
// frequency caps.
pref("browser.urlbar.quicksuggest.impressionCaps.nonSponsoredEnabled", false);

View file

@ -32,7 +32,7 @@
<toolbarbutton id="appMenu-empty-profiles-button"
class="subviewbutton subviewbutton-nav"
closemenu="none"
data-l10n-id="appmenu-profiles"
data-l10n-id="appmenu-profiles-2"
hidden="true"/>
<toolbarbutton id="appMenu-profiles-button"
class="subviewbutton subviewbutton-nav subviewbutton-iconic"
@ -664,7 +664,7 @@
<toolbarbutton id="PanelUI-fxa-menu-empty-profiles-button"
class="subviewbutton subviewbutton-nav"
closemenu="none"
data-l10n-id="appmenu-profiles"
data-l10n-id="appmenu-profiles-2"
hidden="true"/>
<toolbarbutton id="PanelUI-fxa-menu-profiles-button"
class="subviewbutton subviewbutton-nav subviewbutton-iconic"

View file

@ -272,14 +272,13 @@ document.addEventListener(
case "viewGenaiChatSidebarKb": {
const pref = "browser.ml.chat.enabled";
const enabled = Services.prefs.getBoolPref(pref);
if (!enabled) {
Services.prefs.setBoolPref(pref, true);
}
Glean.genaiChatbot.keyboardShortcut.record({
enabled,
sidebar: SidebarController.currentID,
});
SidebarController.toggle("viewGenaiChatSidebar");
if (enabled) {
SidebarController.toggle("viewGenaiChatSidebar");
}
break;
}
case "toggleSidebarKb":

View file

@ -481,9 +481,9 @@ var gIdentityHandler = {
disableMixedContentProtection() {
// Use telemetry to measure how often unblocking happens
const kMIXED_CONTENT_UNBLOCK_EVENT = 2;
Services.telemetry
.getHistogramById("MIXED_CONTENT_UNBLOCK_COUNTER")
.add(kMIXED_CONTENT_UNBLOCK_EVENT);
Glean.mixedContent.unblockCounter.accumulateSingleSample(
kMIXED_CONTENT_UNBLOCK_EVENT
);
SitePermissions.setForPrincipal(
gBrowser.contentPrincipal,

View file

@ -113,7 +113,7 @@ document.addEventListener(
{
let { tabGroupId } = event.target.parentElement.triggerNode.dataset;
let tabGroup = gBrowser.getTabGroupById(tabGroupId);
gBrowser.replaceGroupWithWindow(tabGroup);
tabGroup.ownerGlobal.gBrowser.replaceGroupWithWindow(tabGroup);
}
break;
case "open-tab-group-context-menu_moveToThisWindow":

View file

@ -17,7 +17,7 @@
align="stretch"
screenX="10" screenY="10"
persist="screenX screenY width height sizemode"
csp="script-src chrome:">
csp="default-src chrome:; img-src *; media-src *; style-src chrome: 'unsafe-inline';">
<linkset>
<html:link

View file

@ -124,8 +124,12 @@ async function testPropertyDeltas(
info(
`${msg} ${resizeCount}. resize event: ${stringifyState(currentSizeState)}`
);
// Linux (only on automation, and only on X11) will sometimes get slightly
// inaccurate configures while moving the window.
const kSlop =
AppConstants.platform == "linux" && !SpecialPowers.isHeadless ? 1 : 0;
let matchingIndex = validResizeStates.findIndex(state =>
sizeProps.every(p => state[p] == currentSizeState[p])
sizeProps.every(p => Math.abs(state[p] - currentSizeState[p]) <= kSlop)
);
if (matchingIndex < 0) {
info(`${msg} Size state should have been one of:`);

View file

@ -5644,7 +5644,7 @@ export var DefaultBrowserCheck = {
* performance optimization. It only works when the "privileged about
* content process" is enabled and when ENABLED_PREF is set to true.
*
* See https://firefox-source-docs.mozilla.org/browser/components/newtab/docs/v2-system-addon/about_home_startup_cache.html
* See https://firefox-source-docs.mozilla.org/browser/extensions/newtab/docs/v2-system-addon/about_home_startup_cache.html
* for further details.
*/
export var AboutHomeStartupCache = {

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 101 KiB

View file

@ -18,6 +18,7 @@ browser.jar:
content/activity-stream/data/content/assets/fox-doodle-tail.png (./assets/fox-doodle-tail.png)
content/activity-stream/data/content/assets/fox-doodle-waving.gif (./assets/fox-doodle-waving.gif)
content/activity-stream/data/content/assets/fox-doodle-waving-static.png (./assets/fox-doodle-waving-static.png)
content/activity-stream/data/content/assets/fox-doodle-waving-laptop.svg (./assets/fox-doodle-waving-laptop.svg)
content/activity-stream/data/content/assets/heart.webp (./assets/heart.webp)
content/activity-stream/data/content/assets/long-zap.svg (./assets/long-zap.svg)
content/activity-stream/data/content/assets/mobile-download-qr-existing-user-cn.svg (./assets/mobile-download-qr-existing-user-cn.svg)

View file

@ -42,6 +42,7 @@ const MR_ABOUT_WELCOME_DEFAULT = {
id: "AW_WELCOME_BACK",
targeting: "isDeviceMigration",
content: {
fullscreen: true,
position: "split",
split_narrow_bkg_position: "-100px",
image_alt_text: {
@ -92,6 +93,7 @@ const MR_ABOUT_WELCOME_DEFAULT = {
targeting:
"doesAppNeedPin && (unhandledCampaignAction != 'SET_DEFAULT_BROWSER') && 'browser.shell.checkDefaultBrowser'|preferenceValue && !isDefaultBrowser",
content: {
fullscreen: true,
position: "split",
split_narrow_bkg_position: "-60px",
image_alt_text: {
@ -226,6 +228,7 @@ const MR_ABOUT_WELCOME_DEFAULT = {
targeting:
"!doesAppNeedPin && (unhandledCampaignAction != 'SET_DEFAULT_BROWSER') && 'browser.shell.checkDefaultBrowser'|preferenceValue && !isDefaultBrowser",
content: {
fullscreen: true,
position: "split",
split_narrow_bkg_position: "-60px",
image_alt_text: {
@ -337,6 +340,7 @@ const MR_ABOUT_WELCOME_DEFAULT = {
targeting:
"doesAppNeedPin && (!'browser.shell.checkDefaultBrowser'|preferenceValue || isDefaultBrowser || (unhandledCampaignAction == 'SET_DEFAULT_BROWSER'))",
content: {
fullscreen: true,
position: "split",
split_narrow_bkg_position: "-60px",
image_alt_text: {
@ -459,6 +463,7 @@ const MR_ABOUT_WELCOME_DEFAULT = {
targeting:
"!doesAppNeedPin && (!'browser.shell.checkDefaultBrowser'|preferenceValue || isDefaultBrowser || (unhandledCampaignAction == 'SET_DEFAULT_BROWSER'))",
content: {
fullscreen: true,
position: "split",
split_narrow_bkg_position: "-60px",
image_alt_text: {
@ -557,6 +562,7 @@ const MR_ABOUT_WELCOME_DEFAULT = {
{
id: "AW_LANGUAGE_MISMATCH",
content: {
fullscreen: true,
position: "split",
background: "var(--mr-screen-background-color)",
progress_bar: true,
@ -597,6 +603,7 @@ const MR_ABOUT_WELCOME_DEFAULT = {
id: "AW_IMPORT_SETTINGS_EMBEDDED",
targeting: `("messaging-system-action.showEmbeddedImport" |preferenceValue == true) && useEmbeddedMigrationWizard`,
content: {
fullscreen: true,
tiles: { type: "migration-wizard" },
position: "split",
split_narrow_bkg_position: "-42px",
@ -632,6 +639,7 @@ const MR_ABOUT_WELCOME_DEFAULT = {
// are either not logged into FxA, or don't have any mobile devices syncing
targeting: "!isFxASignedIn || sync.mobileDevices == 0",
content: {
fullscreen: true,
position: "split",
split_narrow_bkg_position: "-160px",
image_alt_text: {
@ -675,87 +683,39 @@ const MR_ABOUT_WELCOME_DEFAULT = {
},
},
{
id: "AW_ADDONS_PICKER",
// Show to en-* locales only
id: "AW_AMO_INTRODUCE",
targeting: "localeLanguageCode == 'en'",
content: {
position: "center",
position: "split",
fullscreen: true,
split_narrow_bkg_position: "-58px",
background:
"url('chrome://activity-stream/content/data/content/assets/mr-amo-collection.svg') var(--mr-secondary-position) no-repeat var(--mr-screen-background-color)",
progress_bar: true,
logo: {},
tiles: {
type: "addons-picker",
data: [
{
id: "jid1-MnnxcxisBPnSXQ@jetpack",
name: "Privacy Badger",
icon: "https://addons.mozilla.org/user-media/addon_icons/506/506646-64.png?modified=mcrushed",
type: "extension",
description: "Automatically learns to block invisible trackers.",
source_id: "ADD_EXTENSION_BUTTON",
action: {
type: "INSTALL_ADDON_FROM_URL",
data: {
url: "https://addons.mozilla.org/firefox/downloads/file/4129240/privacy_badger17-2023.6.23.xpi",
telemetrySource: "aboutwelcome-addon",
},
},
},
{
id: "@contain-facebook",
name: "Facebook Container",
icon: "https://addons.mozilla.org/user-media/addon_icons/954/954390-64.png?modified=97d4c956",
type: "extension",
description: "Prevent Facebook from tracking you around the web.",
source_id: "ADD_EXTENSION_BUTTON",
action: {
type: "INSTALL_ADDON_FROM_URL",
data: {
url: "https://addons.mozilla.org/firefox/downloads/file/4141092/facebook_container-2.3.11.xpi",
telemetrySource: "aboutwelcome-addon",
},
},
},
{
id: "{74145f27-f039-47ce-a470-a662b129930a}",
name: "ClearURLs",
icon: "https://addons.mozilla.org/user-media/addon_icons/839/839767-64.png?modified=b06fa7ed",
type: "extension",
description: "Removes tracking elements from URLs.",
source_id: "ADD_EXTENSION_BUTTON",
action: {
type: "INSTALL_ADDON_FROM_URL",
data: {
url: "https://addons.mozilla.org/firefox/downloads/file/4064884/clearurls-1.26.1.xpi",
telemetrySource: "aboutwelcome-addon",
},
},
},
],
},
title: {
string_id: "amo-picker-title",
string_id: "amo-screen-title",
},
subtitle: {
string_id: "amo-picker-subtitle",
raw: "Extensions are tiny apps that let you customize Firefox. They can boost your privacy, enhance productivity, improve media, change the way Firefox looks, and so much more.",
},
additional_button: {
primary_button: {
label: {
string_id: "amo-picker-collection-link",
raw: "Explore staff recommended extensions",
},
style: "link",
action: {
type: "OPEN_URL",
data: {
args: "https://addons.mozilla.org",
where: "tab",
args: "https://addons.mozilla.org/en-US/firefox/collections/4757633/b4d5649fb087446aa05add5f0258c3/?page=1&collection_sort=-popularity",
where: "tabshifted",
},
navigate: true,
},
},
secondary_button: {
label: {
string_id: "mr2022-onboarding-secondary-skip-button-label",
},
style: "secondary",
action: {
navigate: true,
},
@ -765,6 +725,7 @@ const MR_ABOUT_WELCOME_DEFAULT = {
{
id: "AW_GRATITUDE",
content: {
fullscreen: true,
position: "split",
split_narrow_bkg_position: "-228px",
image_alt_text: {
@ -789,6 +750,50 @@ const MR_ABOUT_WELCOME_DEFAULT = {
},
},
},
targeting: "isFxASignedIn",
},
{
id: "AW_ACCOUNT_LOGIN",
content: {
fullscreen: true,
position: "split",
split_narrow_bkg_position: "-228px",
image_alt_text: {
string_id: "mr2022-onboarding-gratitude-image-alt",
},
background:
"url('chrome://activity-stream/content/data/content/assets/fox-doodle-waving-laptop.svg') center center / 80% no-repeat var(--mr-screen-background-color)",
progress_bar: true,
logo: {},
title: {
string_id: "onboarding-sign-up-title",
},
subtitle: {
string_id: "onboarding-sign-up-description",
},
secondary_button: {
label: {
string_id: "mr2-onboarding-start-browsing-button-label",
},
style: "secondary",
action: {
navigate: true,
},
},
primary_button: {
label: {
string_id: "onboarding-sign-up-button",
},
action: {
data: {
entrypoint: "newuser-onboarding-desktop",
},
type: "FXA_SIGNIN_FLOW",
navigate: true,
},
},
},
targeting: "!isFxASignedIn",
},
],
};

View file

@ -83,7 +83,7 @@ export class AboutWelcomeTelemetry {
* there is a case where spotlight may use this, too)
* containing a nested structure of data for reporting as
* telemetry, as documented in
* https://firefox-source-docs.mozilla.org/browser/components/newtab/docs/v2-system-addon/data_events.html
* https://firefox-source-docs.mozilla.org/browser/extensions/newtab/docs/v2-system-addon/data_events.html
* Does not have all of its data (`_createPing` will augment
* with ids and attribution if available).
*/

View file

@ -810,3 +810,73 @@ add_task(async function test_AWMultistage_newtab_urlbar_focus() {
BrowserTestUtils.removeTab(tab);
sandbox.restore();
});
add_task(async function test_aboutwelcome_gratitude() {
const TEST_CONTENT = [
{
id: "AW_ACCOUNT_LOGIN",
content: {
fullscreen: true,
position: "split",
split_narrow_bkg_position: "-228px",
background:
"url('chrome://activity-stream/content/data/content/assets/fox-doodle-waving-laptop.svg') center center / 80% no-repeat var(--mr-screen-background-color)",
progress_bar: true,
logo: {},
title: {
string_id: "onboarding-sign-up-title",
},
subtitle: {
string_id: "onboarding-sign-up-description",
},
secondary_button: {
label: {
string_id: "mr2-onboarding-start-browsing-button-label",
},
style: "secondary",
action: {
navigate: true,
},
},
primary_button: {
label: {
string_id: "onboarding-sign-up-button",
},
action: {
navigate: true,
},
},
},
},
];
await setAboutWelcomeMultiStage(JSON.stringify(TEST_CONTENT));
let { cleanup, browser } = await openMRAboutWelcome();
await test_screen_content(
browser,
"renders action buttons",
//Expected selectors
[
"main.AW_ACCOUNT_LOGIN",
"button[value='primary_button']",
"button[value='secondary_button']",
],
//Unexpected selectors:
[]
);
// make sure the secondary button navigates to newtab
await clickVisibleButton(browser, ".action-buttons button.secondary");
await test_screen_content(
browser,
"home",
//Expected selectors
["body.activity-stream"],
//Unexpected selectors:
["main.AW_ACCOUNT_LOGIN"]
);
// cleanup
await SpecialPowers.popPrefEnv();
await cleanup();
});

View file

@ -201,7 +201,7 @@ async function main() {
Examples
$ node bin/import-rollouts.js --collection nimbus-preview
$ ./mach npm run import-rollouts --prefix=browser/components/newtab -- -e
$ ./mach npm run import-rollouts --prefix=browser/components/asrouter -- -e
`,
{
description: false,

View file

@ -1,7 +1,6 @@
# Messaging System & Onboarding Telemetry
This document (combined with the [messaging system ping section of the Glean Dictionary](https://dictionary.telemetry.mozilla.org/apps/firefox_desktop/pings/messaging-system)), is now the place to look first for Messaging System and Onboarding telemetry information. For historical reasons, there is still some related documentation mixed in with the Activity Stream documentation. If you can't find what you need here, check [old metrics we collect](/browser/components/newtab/docs/v2-system-addon/data_events.md) and the
[old data dictionary](/browser/components/newtab/docs/v2-system-addon/data_dictionary.md).
This document (combined with the [messaging system ping section of the Glean Dictionary](https://dictionary.telemetry.mozilla.org/apps/firefox_desktop/pings/messaging-system)), is now the place to look first for Messaging System and Onboarding telemetry information. For historical reasons, there is still some related documentation mixed in with the Activity Stream documentation.
## Collection with Glean
@ -32,7 +31,7 @@ file.
## Adding or changing telemetry
A general process overview can be found in the
[Activity Stream telemetry document](/browser/components/newtab/docs/v2-system-addon/telemetry.md).
[Activity Stream telemetry document](/browser/extensions/newtab/docs/v2-system-addon/telemetry.md).
Note that when you need to add new metrics (i.e. JSON keys),
they MUST to be

View file

@ -1675,6 +1675,148 @@ const MESSAGES = () => {
},
targeting: `'cookiebanners.ui.desktop.enabled'|preferenceValue == true && 'cookiebanners.ui.desktop.showCallout'|preferenceValue == true && 'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features' | preferenceValue != false`,
},
{
// "Callout 6" in the Review Checker Figma spec
// Explains where to find Review Checker after closing the sidebar with the X button
// Horizontal tabs layout
id: "REVIEW_CHECKER_SIDEBAR_CLOSED",
template: "feature_callout",
content: {
id: "REVIEW_CHECKER_SIDEBAR_CLOSED",
template: "multistage",
backdrop: "transparent",
transitions: false,
disableHistoryUpdates: true,
screens: [
{
id: "REVIEW_CHECKER_SIDEBAR_CLOSED_HORIZONTAL",
anchors: [
{
selector:
"#sidebar-main:not([positionend]) > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
panel_position: {
anchor_attachment: "rightcenter",
callout_attachment: "topleft",
},
no_open_on_anchor: true,
},
{
selector:
"#sidebar-main[positionend] > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
panel_position: {
anchor_attachment: "leftcenter",
callout_attachment: "topright",
},
no_open_on_anchor: true,
},
],
content: {
position: "callout",
width: "401px",
title: {
string_id: "shopping-integrated-callout-sidebar-closed-title",
},
subtitle: {
string_id:
"shopping-integrated-callout-sidebar-closed-subtitle",
letterSpacing: "0",
},
logo: {
imageURL:
"chrome://browser/content/shopping/assets/reviewCheckerCalloutPriceTag.svg",
height: "195px",
},
dismiss_button: {
action: { dismiss: true },
size: "small",
marginBlock: "28px 0",
marginInline: "0 28px",
},
},
},
],
},
priority: 1,
// Has not opted out of CFRs; Review Checker integrated sidebar is enabled; sidebar revamp is enabled; user is opted in to review checker; Using horizontal tabs.
targeting: `'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features' | preferenceValue && 'browser.shopping.experience2023.integratedSidebar' | preferenceValue && 'sidebar.revamp' | preferenceValue && 'browser.shopping.experience2023.optedIn' | preferenceValue == 1 && isReviewCheckerInSidebarClosed && !'sidebar.verticalTabs' | preferenceValue`,
trigger: {
id: "showRCSidebarClosedCallout",
},
frequency: { lifetime: 1 },
skip_in_tests:
"not tested in automation and might pop up unexpectedly during review checker tests",
},
{
// "Callout 6" in the Review Checker Figma spec
// Explains where to find Review Checker after closing the sidebar with the X button
// Vertical tabs layout
id: "REVIEW_CHECKER_SIDEBAR_CLOSED",
template: "feature_callout",
content: {
id: "REVIEW_CHECKER_SIDEBAR_CLOSED",
template: "multistage",
backdrop: "transparent",
transitions: false,
disableHistoryUpdates: true,
screens: [
{
id: "REVIEW_CHECKER_SIDEBAR_CLOSED_VERTICAL",
anchors: [
{
selector:
"#sidebar-main:not([positionend]) > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
panel_position: {
anchor_attachment: "rightcenter",
callout_attachment: "bottomleft",
},
no_open_on_anchor: true,
},
{
selector:
"#sidebar-main[positionend] > sidebar-main::%shadow% .tools-and-extensions::%shadow% moz-button[view='viewReviewCheckerSidebar']",
panel_position: {
anchor_attachment: "leftcenter",
callout_attachment: "bottomright",
},
no_open_on_anchor: true,
},
],
content: {
position: "callout",
width: "401px",
title: {
string_id: "shopping-integrated-callout-sidebar-closed-title",
},
subtitle: {
string_id:
"shopping-integrated-callout-sidebar-closed-subtitle",
letterSpacing: "0",
},
logo: {
imageURL:
"chrome://browser/content/shopping/assets/reviewCheckerCalloutPriceTag.svg",
height: "195px",
},
dismiss_button: {
action: { dismiss: true },
size: "small",
marginBlock: "28px 0",
marginInline: "0 28px",
},
},
},
],
},
priority: 1,
// Has not opted out of CFRs; Review Checker integrated sidebar is enabled; sidebar revamp is enabled; user is opted in to review checker; Vertical tabs is enabled.
targeting: `'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features' | preferenceValue && 'browser.shopping.experience2023.integratedSidebar' | preferenceValue && 'sidebar.revamp' | preferenceValue && 'browser.shopping.experience2023.optedIn' | preferenceValue == 1 && isReviewCheckerInSidebarClosed && 'sidebar.verticalTabs' | preferenceValue`,
trigger: {
id: "showRCSidebarClosedCallout",
},
frequency: { lifetime: 1 },
skip_in_tests:
"not tested in automation and might pop up unexpectedly during review checker tests",
},
];
messages = add24HourImpressionJEXLTargeting(
["FIREFOX_VIEW_TAB_PICKUP_REMINDER"],

View file

@ -1,6 +1,6 @@
[DEFAULT]
support-files = [
"../../../newtab/test/browser/blue_page.html",
"../../../../extensions/newtab/test/browser/blue_page.html",
"head.js",
]

View file

@ -585,7 +585,7 @@ add_task(async function test_onLocationChange_cb() {
let count = 0;
const triggerHandler = () => ++count;
const TEST_URL =
"https://example.com/browser/browser/components/newtab/test/browser/blue_page.html";
"https://example.com/browser/browser/extensions/newtab/test/browser/blue_page.html";
const browser = gBrowser.selectedBrowser;
await ASRouterTriggerListeners.get("openURL").init(triggerHandler, [

View file

@ -9,7 +9,7 @@ const { DefaultBrowserCheck } = ChromeUtils.importESModule(
);
const PDF_TEST_URL =
"https://example.com/browser/browser/components/newtab/test/browser/file_pdf.PDF";
"https://example.com/browser/browser/extensions/newtab/test/browser/file_pdf.PDF";
async function openURLInWindow(window, url) {
const { selectedBrowser } = window.gBrowser;

View file

@ -1,5 +1,5 @@
import { ASRouterStorage } from "modules/ASRouterStorage.sys.mjs";
import { GlobalOverrider } from "../../../newtab/test/unit/utils";
import { GlobalOverrider } from "tests/unit/utils";
let overrider = new GlobalOverrider();

View file

@ -670,3 +670,88 @@ add_task(async function testRecentlyClosedTabsFromManyWindows() {
await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE);
});
add_task(async function testTabsFromGroupClosedBeforeGroupDeleted() {
// Ensures that bug1945111 has a proper resolution.
info(
"Tabs that were closed from within a tab group should appear in history menus as standalone tabs, even if the tab group is later deleted"
);
await resetClosedTabsAndWindows();
const ORIG_STATE = SessionStore.getBrowserState();
const GROUP_ID = "1234567890-1";
const _makeTab = function (url, { groupId, closedInTabGroup = false } = {}) {
return {
title: url,
closedInTabGroupId: closedInTabGroup ? groupId : null,
state: {
entries: [
{
url,
triggeringPrincipal_base64,
},
],
groupId,
},
};
};
await SessionStoreTestUtils.promiseBrowserState({
windows: [
{
tabs: [_makeTab("about:blank")],
_closedTabs: [_makeTab("about:mozilla", { groupId: GROUP_ID })],
closedGroups: [
{
id: GROUP_ID,
color: "blue",
name: "tab-group",
tabs: [
_makeTab("about:mozilla", {
groupId: GROUP_ID,
closedInTabGroup: true,
}),
_makeTab("about:mozilla", {
groupId: GROUP_ID,
closedInTabGroup: true,
}),
_makeTab("about:mozilla", {
groupId: GROUP_ID,
closedInTabGroup: true,
}),
],
},
],
},
],
});
Assert.equal(
SessionStore.getClosedTabCount(),
4,
"Sanity check number of closed tabs from closed windows"
);
prepareHistoryPanel();
let closeTabsPanel = await openRecentlyClosedTabsMenu();
const topLevelClosedTabItems = closeTabsPanel
.querySelector(".panel-subview-body")
.querySelectorAll(":scope > toolbarbutton[targetURI]");
Assert.equal(
topLevelClosedTabItems.length,
1,
"We have the expected number of top-level closed tab items"
);
const tabGroupClosedTabItems = closeTabsPanel.querySelectorAll(
`panelview#closed-tabs-tab-group-${GROUP_ID} toolbarbutton[targetURI]`
);
Assert.equal(
tabGroupClosedTabItems.length,
3,
"We have the expected number of closed tab items within the tab group"
);
await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE);
});

View file

@ -68,6 +68,8 @@ this.commands = class extends ExtensionAPIPersistent {
getAll: () => this.extension.shortcuts.allCommands(),
update: args => this.extension.shortcuts.updateCommand(args),
reset: name => this.extension.shortcuts.resetCommand(name),
openShortcutSettings: () =>
this.extension.shortcuts.openShortcutSettings(),
onCommand: new EventManager({
context,
module: "commands",

View file

@ -421,6 +421,19 @@ this.windows = class extends ExtensionAPIPersistent {
// 10px offset is same to Chromium
sanitizePositionParams(createData, baseWindow, 10);
if (createData.width !== null) {
features.push("outerWidth=" + createData.width);
}
if (createData.height !== null) {
features.push("outerHeight=" + createData.height);
}
if (createData.left !== null) {
features.push("left=" + createData.left);
}
if (createData.top !== null) {
features.push("top=" + createData.top);
}
let window = Services.ww.openWindow(
null,
AppConstants.BROWSER_CHROME_URL,
@ -430,7 +443,6 @@ this.windows = class extends ExtensionAPIPersistent {
);
let win = windowManager.getWrapper(window);
win.updateGeometry(createData);
// TODO: focused, type

View file

@ -204,6 +204,13 @@
"description": "Called to return the registered commands."
}
]
},
{
"name": "openShortcutSettings",
"type": "function",
"async": true,
"description": "Open extension shortcuts configuration page.",
"parameters": []
}
]
}

View file

@ -132,6 +132,8 @@ skip-if = [
["browser_ext_commands_onCommand.js"]
["browser_ext_commands_openShortcutSettings.js"]
["browser_ext_commands_update.js"]
["browser_ext_connect_and_move_tabs.js"]

View file

@ -16,6 +16,8 @@ add_task(async function testBrowserActionMenuResizeBottomArrow() {
let win = await BrowserTestUtils.openNewBrowserWindow();
info(`Trying to position in (${left}, ${top})`);
win.resizeTo(WIDTH, HEIGHT);
// Sometimes we run into problems on Linux with resizing being asynchronous
@ -24,6 +26,8 @@ add_task(async function testBrowserActionMenuResizeBottomArrow() {
for (let i = 0; i < 20; i++) {
win.moveTo(left, top);
info(`got (${win.screenX}, ${win.screenY})`);
if (win.screenX == left && win.screenY == top) {
break;
}

View file

@ -0,0 +1,295 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
async function loadExtension(options) {
let extension = ExtensionTestUtils.loadExtension({
useAddonManager: "temporary",
manifest: {
permissions: ["tabs"],
...options.manifest,
},
files: {},
background: options.background,
});
await extension.startup();
return extension;
}
add_task(async function run_test_shortcuts_tab() {
const extensionId = "shortcuts@tests.mozilla.org";
let exampleTab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"https://example.com/"
);
let extension = await loadExtension({
manifest: {
browser_specific_settings: {
gecko: { id: extensionId },
},
commands: {
"toggle-feature": {
suggested_key: { default: "F1" },
description: "Send a 'toggle-feature' event to the extension",
},
},
},
background: async function () {
try {
let [firstTab] = await browser.tabs.query({
currentWindow: true,
active: true,
});
browser.test.log("Open shortcuts page. Expect fresh load.");
await browser.commands.openShortcutSettings();
let [shortcutsTab] = await browser.tabs.query({
currentWindow: true,
active: true,
});
browser.test.assertTrue(
shortcutsTab.id != firstTab.id,
"Tab is a new tab"
);
browser.test.assertEq(
"about:addons",
shortcutsTab.url,
"Tab contains AddonManager"
);
browser.test.log("Switch tabs.");
await browser.tabs.update(firstTab.id, { active: true });
browser.test.log("Re-open shortcuts page. Expect tab re-selected.");
await browser.commands.openShortcutSettings();
let [reusedTab] = await browser.tabs.query({
currentWindow: true,
active: true,
});
browser.test.assertEq(
shortcutsTab.id,
reusedTab.id,
"Tab is the same as the previous shortcuts tab"
);
browser.test.assertEq(
"about:addons",
reusedTab.url,
"Tab contains AddonManager"
);
browser.test.log("Remove shortcuts tab.");
await browser.tabs.remove(reusedTab.id);
browser.test.log("Re-open shortcuts page. Expect fresh load.");
await browser.commands.openShortcutSettings();
let [reopenedTab] = await browser.tabs.query({
currentWindow: true,
active: true,
});
browser.test.assertEq(
"about:addons",
reopenedTab.url,
"Tab contains AddonManager"
);
browser.test.assertTrue(
reopenedTab.id != shortcutsTab.id,
"Tab is a new tab"
);
await browser.tabs.remove(firstTab.id);
await browser.tabs.remove(reopenedTab.id);
} catch (error) {
browser.test.fail(`Error: ${error} :: ${error.stack}`);
}
browser.test.sendMessage("background-done");
},
});
await extension.awaitMessage("background-done");
await extension.unload();
BrowserTestUtils.removeTab(exampleTab);
});
add_task(async function run_test_shortcuts_view() {
const extensionId1 = "extension1@tests.mozilla.org";
const extensionId2 = "extension2@tests.mozilla.org";
let exampleTab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"https://example.com/"
);
let extension1 = await loadExtension({
manifest: {
browser_specific_settings: {
gecko: { id: extensionId1 },
},
commands: {
"toggle-feature": {
suggested_key: { default: "F1" },
description: "Send a 'toggle-feature' event to the extension",
},
},
},
background: function () {
browser.test.sendMessage("background1-done");
},
});
let extension2 = await loadExtension({
manifest: {
browser_specific_settings: {
gecko: { id: extensionId2 },
},
commands: {
"toggle-feature": {
suggested_key: { default: "F2" },
description: "Send a 'toggle-feature' event to the extension",
},
},
},
background: async function () {
try {
await browser.commands.openShortcutSettings();
} catch (error) {
browser.test.fail(`Error: ${error} :: ${error.stack}`);
}
browser.test.sendMessage("background2-done");
},
});
await Promise.all([
extension1.awaitMessage("background1-done"),
extension2.awaitMessage("background2-done"),
]);
const addonState = await SpecialPowers.spawn(
gBrowser.selectedTab.linkedBrowser,
[extensionId1, extensionId2],
(ext1, ext2) => {
const addonPageHeader =
content.document.querySelector("addon-page-header");
const extension1Card = content.document.querySelector(
`.card.shortcut[addon-id="${ext1}"]`
);
const extension2Card = content.document.querySelector(
`.card.shortcut[addon-id="${ext2}"]`
);
return {
view: addonPageHeader?.getAttribute("current-view"),
param: addonPageHeader?.getAttribute("current-param"),
extension1Focused:
extension1Card?.classList.contains("focused-extension"),
extension2Focused:
extension2Card?.classList.contains("focused-extension"),
};
}
);
is(addonState.view, "shortcuts", "AddonManager rendered shortcuts view");
is(addonState.param, extensionId2, "AddonManager rendered shortcuts param");
is(addonState.extension1Focused, false, "Extension 1 not focused");
is(addonState.extension2Focused, true, "Extension 2 is focused");
await Promise.all([extension1.unload(), extension2.unload()]);
BrowserTestUtils.removeTab(gBrowser.selectedTab);
BrowserTestUtils.removeTab(exampleTab);
});
add_task(async function run_test_shortcuts_empty_commands() {
const extensionId = "noshortcuts@tests.mozilla.org";
let exampleTab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"https://example.com/"
);
let extension = await loadExtension({
manifest: {
browser_specific_settings: {
gecko: { id: extensionId },
},
commands: {},
},
background: async function () {
try {
await browser.commands.openShortcutSettings();
} catch (error) {
browser.test.fail(`Error: ${error} :: ${error.stack}`);
}
browser.test.sendMessage("background-done");
},
});
await extension.awaitMessage("background-done");
const addonState = await SpecialPowers.spawn(
gBrowser.selectedTab.linkedBrowser,
[extensionId],
() => {
const addonPageHeader =
content.document.querySelector("addon-page-header");
return {
view: addonPageHeader?.getAttribute("current-view"),
param: addonPageHeader?.getAttribute("current-param"),
};
}
);
is(addonState.view, "shortcuts", "AddonManager rendered shortcuts view");
is(addonState.param, extensionId, "AddonManager rendered shortcuts param");
await extension.unload();
BrowserTestUtils.removeTab(gBrowser.selectedTab);
BrowserTestUtils.removeTab(exampleTab);
});
add_task(async function run_test_shortcuts_no_commands() {
const extensionId = "noshortcuts@tests.mozilla.org";
let exampleTab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"https://example.com/"
);
let extension = await loadExtension({
manifest: {
browser_specific_settings: {
gecko: { id: extensionId },
},
},
background: async function () {
try {
browser.test.assertTrue(
!browser.commands?.openShortcutSettings,
"openShortcutSettings not defined"
);
} catch (error) {
browser.test.fail(`Error: ${error} :: ${error.stack}`);
}
browser.test.sendMessage("background-done");
},
});
await extension.awaitMessage("background-done");
await extension.unload();
BrowserTestUtils.removeTab(exampleTab);
});

View file

@ -215,9 +215,15 @@ add_task(async function testPopupTypeWithDimension() {
"expected popup type windows are opened at given coordinates"
);
const actualSizes = windows
.slice(0, 3)
.map(window => `${window.outerWidth}x${window.outerHeight}`);
const isGtk = Services.appinfo.widgetToolkit == "gtk";
const actualSizes = windows.slice(0, 3).map(window => {
// Gtk can't set outer sizes on window creation because it doesn't know how
// big the decorations of the window will be. This was papered over before
// bug 581863 returning outer == innerWidth (even tho it was a lie).
return isGtk
? `${window.innerWidth}x${window.innerHeight}`
: `${window.outerWidth}x${window.outerHeight}`;
});
const expectedSizes = [`151x152`, `152x153`, `153x154`];
is(
actualSizes.join(" / "),

View file

@ -35,6 +35,25 @@ add_task(async function testWindowCreate() {
let windowId;
async function checkWindow(expected, retries = 5) {
let geom = await getWindowSize();
geom.width = geom.outerWidth;
geom.height = geom.outerHeight;
let usingInnerSizes = false;
if (navigator.platform.startsWith("Linux")) {
// Unfortunately, on GTK, when creating the initial window, we can
// only specify inner sizes (not outer), since we don't yet know the
// bounds of our window decorations. So we need to check inner rather
// than outer sizes there. This used to be wallpapered before
// bug 581863 because the outer sizes were just wrong (matching always
// inner sizes).
if (expected.height == geom.innerHeight) {
usingInnerSizes = true;
geom.height = geom.innerHeight;
}
if (expected.width == geom.innerWidth) {
usingInnerSizes = true;
geom.width = geom.innerWidth;
}
}
if (retries && KEYS.some(key => expected[key] != geom[key])) {
browser.test.log(
@ -51,11 +70,16 @@ add_task(async function testWindowCreate() {
browser.test.log(`Check actual window size`);
checkGeom(expected, geom);
browser.test.log("Check API-reported window size");
geom = await browser.windows.get(windowId);
checkGeom(expected, geom);
if (usingInnerSizes) {
// Uncommon: see comment at usingInnerSizes.
browser.test.log(
"API-reported window size is inaccurate - skipping check"
);
} else {
browser.test.log("Check API-reported window size");
geom = await browser.windows.get(windowId);
checkGeom(expected, geom);
}
}
try {
@ -108,8 +132,10 @@ add_task(async function testWindowCreate() {
extension.sendMessage("checked-window", {
top: latestWindow.screenY,
left: latestWindow.screenX,
width: latestWindow.outerWidth,
height: latestWindow.outerHeight,
innerWidth: latestWindow.innerWidth,
innerHeight: latestWindow.innerHeight,
outerWidth: latestWindow.outerWidth,
outerHeight: latestWindow.outerHeight,
});
});

View file

@ -119,6 +119,8 @@ async function withWindowOverflowed(
await ensureMaximizedWindow(win);
info(`Window maximized`);
// The OverflowableToolbar operates asynchronously at times, so we will
// poll a widget's overflowedItem attribute to detect whether or not the
// widgets have finished being moved. We'll use the first widget that
@ -227,13 +229,16 @@ async function withWindowOverflowed(
};
CustomizableUI.addListener(listener);
// Start all the extensions sequentially.
info(`starting extensions...`);
for (const extension of extensions) {
await extension.startup();
}
info(`waiting for menu-created messages`);
await Promise.all([
extWithMenuBrowserAction.awaitMessage("menu-created"),
extWithSubMenuBrowserAction.awaitMessage("menu-created"),
]);
info(`waiting for browser action widgets`);
await listener.promise;
CustomizableUI.removeListener(listener);
@ -243,6 +248,7 @@ async function withWindowOverflowed(
info("Running beforeOverflowed task");
await beforeOverflowed(extensionIDs);
} finally {
info(`beforeOverflowed finished`);
// The beforeOverflowed task may have moved some items out from the navbar,
// so only listen for overflows for items still in there.
const browserActionIDs = extensionIDs.map(id =>
@ -271,7 +277,10 @@ async function withWindowOverflowed(
};
CustomizableUI.addListener(widgetOverflowListener);
win.resizeTo(OVERFLOW_WINDOW_WIDTH_PX, win.outerHeight);
info(
`Resizing to overflow window width (current width: ${win.innerWidth})`
);
await ensureWindowInnerDimensions(win, OVERFLOW_WINDOW_WIDTH_PX, null);
await widgetOverflowListener.promise;
CustomizableUI.removeListener(widgetOverflowListener);
@ -292,6 +301,7 @@ async function withWindowOverflowed(
info("Running whenOverflowed task");
await whenOverflowed(defaultList, unifiedExtensionList, extensionIDs);
} finally {
info("whenOverflowed finished, maximizing again");
await ensureMaximizedWindow(win);
// Notably, we don't wait for the nav-bar to not have the "overflowing"
@ -1029,21 +1039,10 @@ add_task(async function test_overflow_with_a_second_window() {
);
// Make sure the first window is the active window.
let windowActivePromise = new Promise(resolve => {
if (Services.focus.activeWindow == win) {
resolve();
} else {
win.addEventListener(
"activate",
() => {
resolve();
},
{ once: true }
);
}
});
win.focus();
await windowActivePromise;
info("second window is maximized, trying to make first window active");
await SimpleTest.promiseFocus(win);
info("first window focused");
let extensionWidgetID;
let aNode;

View file

@ -7,6 +7,7 @@
closeExtensionsPanel,
createExtensions,
ensureMaximizedWindow,
ensureWindowInnerDimensions,
getBlockKey
getMessageBars,
getUnifiedExtensionsItem,
@ -130,44 +131,7 @@ const createExtensions = (
);
};
/**
* Given a window, this test helper resizes it so that the window takes most of
* the available screen size (unless the window is already maximized).
*/
const ensureMaximizedWindow = async win => {
info("ensuring maximized window...");
// Make sure we wait for window position to have settled
// to avoid unexpected failures.
let samePositionTimes = 0;
let lastScreenTop = win.screen.top;
let lastScreenLeft = win.screen.left;
win.moveTo(0, 0);
await TestUtils.waitForCondition(() => {
let isSamePosition =
lastScreenTop === win.screen.top && lastScreenLeft === win.screen.left;
if (!isSamePosition) {
lastScreenTop = win.screen.top;
lastScreenLeft = win.screen.left;
}
samePositionTimes = isSamePosition ? samePositionTimes + 1 : 0;
return samePositionTimes === 10;
}, "Wait for the chrome window position to settle");
const widthDiff = Math.max(win.screen.availWidth - win.outerWidth, 0);
const heightDiff = Math.max(win.screen.availHeight - win.outerHeight, 0);
if (widthDiff || heightDiff) {
info(
`resizing window... widthDiff=${widthDiff} - heightDiff=${heightDiff}`
);
win.windowUtils.ensureDirtyRootFrame();
win.resizeBy(widthDiff, heightDiff);
} else {
info(`not resizing window!`);
}
// Make sure we wait for window size to have settled.
const ensureStableDimensions = async win => {
let lastOuterWidth = win.outerWidth;
let lastOuterHeight = win.outerHeight;
let sameSizeTimes = 0;
@ -183,6 +147,52 @@ const ensureMaximizedWindow = async win => {
}, "Wait for the chrome window size to settle");
};
const _ensureSizeMode = (win, expectedMode, change) => {
info(`ensuring sizeMode ${expectedMode}`);
if (win.windowState == expectedMode) {
info(`already the right mode`);
return;
}
return new Promise(resolve => {
win.addEventListener("sizemodechange", function listener() {
info(`sizeMode changed to ${win.windowState}`);
if (win.windowState == expectedMode) {
win.removeEventListener("sizemodechange", listener);
resolve();
}
});
change();
});
};
/**
* Given a window, this test helper resizes it so that the window takes most of
* the available screen size (unless the window is already maximized).
*/
const ensureMaximizedWindow = win => {
return _ensureSizeMode(win, win.STATE_MAXIMIZED, () => win.maximize());
};
const ensureWindowInnerDimensions = async (win, w, h) => {
// restore() first if needed, because some Linux compositors (older Mutter)
// will not honor the size request otherwise.
await _ensureSizeMode(win, win.STATE_NORMAL, () => win.restore());
await ensureStableDimensions(win);
let diffX = w ? w - win.innerWidth : 0;
let diffY = h ? h - win.innerHeight : 0;
info(`Resizing to ${w} (${diffX}), ${h} (${diffY})`);
if (diffX || diffY) {
await new Promise(resolve => {
win.addEventListener("resize", resolve, { once: true });
win.resizeBy(diffX, diffY);
});
info(`Got window resize: ${win.innerWidth}x${win.innerHeight}`);
}
};
const promiseSetToolbarVisibility = (toolbar, visible) => {
const visibilityChanged = BrowserTestUtils.waitForMutationCondition(
toolbar,

View file

@ -235,19 +235,39 @@ add_task(async function test_keyboard_shortcut() {
key.doCommand();
Assert.ok(
!Services.prefs.getBoolPref(enabled),
"Keyboard shortcut doesn't flip the pref"
);
Assert.ok(
!SidebarController.isOpen,
"Keyboard shortcut doesn't Open chatbot if disabled"
);
await SpecialPowers.pushPrefEnv({ set: [[enabled, true]] });
key.doCommand();
Assert.ok(Services.prefs.getBoolPref(enabled), "Enabled with keyboard");
Assert.ok(SidebarController.isOpen, "Opened chatbot with keyboard");
key.doCommand();
Assert.ok(!SidebarController.isOpen, "Closed chatbot with keyboard");
Assert.ok(
Services.prefs.getBoolPref(enabled),
"Keyboard shortcut doesn't flip the pref"
);
const events = Glean.genaiChatbot.keyboardShortcut.testGetValue();
Assert.equal(events.length, 2, "Got 2 keyboard events");
Assert.equal(events.length, 3, "Got 3 keyboard events");
Assert.equal(events[0].extra.enabled, "false", "Initially disabled");
Assert.equal(events[0].extra.sidebar, "", "Initially closed");
Assert.equal(events[1].extra.enabled, "true", "Already enabled");
Assert.equal(events[1].extra.sidebar, "", "Still closed");
Assert.equal(events[2].extra.enabled, "true", "Still enabled");
Assert.equal(
events[1].extra.sidebar,
events[2].extra.sidebar,
"viewGenaiChatSidebar",
"Already opened"
);

View file

@ -65,7 +65,7 @@ const PREF_ACTIVITY_STREAM_DEBUG = "browser.newtabpage.activity-stream.debug";
* process" first launches, so subsequent loads of about:home do not read
* from this cache.
*
* See https://firefox-source-docs.mozilla.org/browser/components/newtab/docs/v2-system-addon/about_home_startup_cache.html
* See https://firefox-source-docs.mozilla.org/browser/extensions/newtab/docs/v2-system-addon/about_home_startup_cache.html
* for further details.
*/
export const AboutHomeStartupCacheChild = {

View file

@ -7,17 +7,6 @@
with Files("**"):
BUG_COMPONENT = ("Firefox", "New Tab Page")
BROWSER_CHROME_MANIFESTS += [
"test/browser/abouthomecache/browser.toml",
"test/browser/browser.toml",
]
SPHINX_TREES["docs"] = "docs"
XPCSHELL_TESTS_MANIFESTS += [
"test/xpcshell/xpcshell.toml",
]
XPIDL_SOURCES += [
"nsIAboutNewTabService.idl",
]
@ -32,4 +21,6 @@ XPCOM_MANIFESTS += [
"components.conf",
]
JAR_MANIFESTS += ["jar.mn"]
XPCSHELL_TESTS_MANIFESTS += [
"test/xpcshell/xpcshell.toml",
]

View file

@ -1,2 +0,0 @@
second
<a href="https://www.example.com/browser/browser/components/newtab/test/browser/annotation_third.html">goto third</a>

View file

@ -1,38 +1,5 @@
[DEFAULT]
firefox-appdir = "browser"
skip-if = ["os == 'android'"] # bug 1730213
prefs = [
"browser.startup.homepage.abouthome_cache.enabled=true",
"browser.startup.homepage.abouthome_cache.testing=true",
]
["test_AboutHomeStartupCacheChild.js"]
["test_AboutHomeStartupCacheWorker.js"]
support-files = ["topstories.json"]
skip-if = ["socketprocess_networking"] # Bug 1759035
["test_AboutNewTab.js"]
["test_AdsFeed.js"]
["test_FaviconFeed.js"]
support-files = ["favicon.png"]
["test_HighlightsFeed.js"]
["test_PlacesFeed.js"]
["test_Store.js"]
["test_TelemetryFeed.js"]
support-files = ["../schemas/*.schema.json"]
["test_TopSitesFeed.js"]
["test_TopSitesFeed_glean.js"]
["test_WallpaperFeed.js"]
tags = "remote-settings"
["test_WeatherFeed.js"]

View file

@ -53,6 +53,7 @@
-moz-context-properties: fill, stroke;
fill: currentColor;
stroke: currentColor;
align-self: center;
}
}

View file

@ -52,8 +52,8 @@ add_task(async function test_window_resize() {
let contentInfo = await helper.getContentDimensions();
ok(contentInfo, "Got dimensions back from the content");
const originalWindowWidth = window.outerWidth;
const originalWindowHeight = window.outerHeight;
const originalWindowWidth = window.innerWidth;
const originalWindowHeight = window.innerHeight;
const BIG_WINDOW_SIZE = 920;
const SMALL_WINDOW_SIZE = 620;

View file

@ -15,7 +15,7 @@ add_task(async function test_window_resize() {
await new Promise(r => window.requestAnimationFrame(r));
let helper = new ScreenshotsHelper(browser);
await helper.resizeContentWindow(windowWidth, window.outerHeight);
await helper.resizeContentWindow(windowWidth, window.innerHeight);
const originalContentDimensions = await helper.getContentDimensions();
info(JSON.stringify(originalContentDimensions, null, 2));
@ -66,7 +66,7 @@ add_task(async function test_window_resize_vertical_writing_mode() {
await new Promise(r => window.requestAnimationFrame(r));
let helper = new ScreenshotsHelper(browser);
await helper.resizeContentWindow(windowWidth, window.outerHeight);
await helper.resizeContentWindow(windowWidth, window.innerHeight);
const originalContentDimensions = await helper.getContentDimensions();
info(JSON.stringify(originalContentDimensions, null, 2));

View file

@ -495,11 +495,18 @@ class ScreenshotsHelper {
}
async resizeContentWindow(width, height) {
this.browser.ownerGlobal.resizeTo(width, height);
await TestUtils.waitForCondition(
() => window.outerHeight === height && window.outerWidth === width,
"Waiting for window to resize"
info(
`Resizing window to ${width}x${height} from ${innerWidth}x${innerHeight} (outer ${outerWidth}x${outerHeight})`
);
let targetW = outerWidth - innerWidth + width;
let targetH = outerHeight - innerHeight + height;
window.resizeTo(targetW, targetH);
await TestUtils.waitForCondition(() => {
info(
`Current: ${innerWidth}x${innerHeight} (outer ${outerWidth}x${outerHeight})`
);
return outerHeight === targetH && outerWidth === targetW;
}, "Waiting for window to resize");
}
waitForContentMousePosition(left, top) {

View file

@ -87,7 +87,7 @@ export var RecentlyClosedTabsAndWindowsMenuUtils = {
closedTabSets.forEach(tabSet => {
tabSet.forEach((tab, index) => {
let { groupId } = tab.state;
let groupId = tab.closedInTabGroupId;
if (groupId && closedTabGroupsById.has(groupId)) {
if (groupId != currentGroupId) {
// This is the first tab in a new group. Push all the tabs into the menu.

View file

@ -4305,6 +4305,7 @@ var SessionStoreInternal = {
userContextId: state.userContextId,
skipLoad: true,
preferredRemoteType,
tabGroup: tabbrowser.tabGroups.find(g => g.id == state.groupId),
}));
// restore tab content

View file

@ -321,6 +321,8 @@ tags = "os_integration"
["browser_tab_groups_restore_simple.js"]
["browser_tab_groups_restore_to_group.js"]
["browser_tab_groups_save_on_removeAllTabsBut.js"]
["browser_tab_groups_save_on_removeTabsToTheEnd.js"]

View file

@ -0,0 +1,62 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const ORIG_STATE = SessionStore.getBrowserState();
registerCleanupFunction(async () => {
await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE);
});
add_task(async function test_restoreTabToGroup() {
let groupedTabs = [
BrowserTestUtils.addTab(gBrowser, "about:mozilla"),
BrowserTestUtils.addTab(gBrowser, "about:robots"),
];
let tabToRestore = groupedTabs[1];
await Promise.all(
groupedTabs.map(g => promiseBrowserLoaded(g.linkedBrowser))
);
let tabGroup = gBrowser.addTabGroup(groupedTabs);
await SessionStoreTestUtils.closeTab(tabToRestore);
Assert.equal(tabGroup.tabs.length, 1, "Tab group only has one tab");
await TabStateFlusher.flushWindow(window);
let tabRestoredEvent = BrowserTestUtils.waitForEvent(
gBrowser.tabContainer,
"SSTabRestored"
);
SessionStore.undoCloseTab(window);
await tabRestoredEvent;
Assert.equal(tabGroup.tabs.length, 2, "Tab group has two tabs");
let restoredTab = tabGroup.tabs.at(-1);
Assert.equal(
restoredTab.linkedBrowser.currentURI.spec,
"about:robots",
"Closed tab exists in group"
);
BrowserTestUtils.removeTab(restoredTab);
// remove the other tab in the group so it gets deleted
await SessionStoreTestUtils.closeTab(tabGroup.tabs[0]);
tabRestoredEvent = BrowserTestUtils.waitForEvent(
gBrowser.tabContainer,
"SSTabRestored"
);
SessionStore.undoCloseTab(window, 0);
await tabRestoredEvent;
restoredTab = gBrowser.tabs.at(-1);
Assert.ok(
!restoredTab.group,
"Tab was not restored to the group because the group was removed"
);
forgetClosedTabs(window);
});

View file

@ -34,6 +34,18 @@ var gSetBackground = {
document
.getElementById("SetDesktopBackgroundDialog")
.getButton("accept").hidden = true;
document
.getElementById("setDesktopBackground")
.addEventListener("command", () => this.setDesktopBackground());
document
.getElementById("showDesktopPreferences")
.addEventListener("command", () => {
this._shell
.QueryInterface(Ci.nsIMacShellService)
.showDesktopPreferences();
});
} else {
let multiMonitors = false;
if (AppConstants.platform == "linux") {
@ -50,6 +62,15 @@ var gSetBackground = {
// Hide span option on single monitor systems.
document.getElementById("spanPosition").hidden = true;
}
document
.getElementById("menuPosition")
.addEventListener("command", () => this.updatePosition());
document
.getElementById("desktopColor")
.addEventListener("change", event =>
this.updateColor(event.currentTarget.value)
);
}
document.addEventListener("dialogaccept", function () {
@ -242,8 +263,6 @@ if (AppConstants.platform != "macosx") {
Services.obs.removeObserver(this, "shell:desktop-background-changed");
}
};
gSetBackground.showDesktopPrefs = function () {
this._shell.QueryInterface(Ci.nsIMacShellService).showDesktopPreferences();
};
}
window.addEventListener("load", () => gSetBackground.load());

View file

@ -7,9 +7,9 @@
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
windowtype="Shell:SetDesktopBackground"
onload="gSetBackground.load();"
data-l10n-id="set-desktop-background-window"
style="min-width: 30em;">
style="min-width: 30em;"
csp="default-src chrome:; style-src chrome: 'unsafe-inline';">
<linkset>
<html:link rel="stylesheet" href="chrome://global/skin/global.css" />
@ -43,8 +43,7 @@
#ifndef XP_MACOSX
<hbox align="center">
<label data-l10n-id="set-background-position"/>
<menulist id="menuPosition"
oncommand="gSetBackground.updatePosition();" native="true">
<menulist id="menuPosition" native="true">
<menupopup>
<menuitem data-l10n-id="set-background-center" value="CENTER"/>
<menuitem data-l10n-id="set-background-tile" value="TILE"/>
@ -56,9 +55,7 @@
</menulist>
<spacer flex="1"/>
<label data-l10n-id="set-background-color"/>
<html:input id="desktopColor"
type="color"
onchange="gSetBackground.updateColor(this.value);"/>
<html:input id="desktopColor" type="color"/>
</hbox>
#endif
@ -78,11 +75,9 @@
<hbox pack="end">
<button id="setDesktopBackground"
data-l10n-id="set-desktop-background-accept"
oncommand="gSetBackground.setDesktopBackground();"/>
data-l10n-id="set-desktop-background-accept"/>
<button id="showDesktopPreferences"
data-l10n-id="open-desktop-prefs"
oncommand="gSetBackground.showDesktopPrefs();"
hidden="true"/>
</hbox>
#endif

View file

@ -214,6 +214,13 @@ export class ReviewCheckerManager {
}
case "CloseReviewCheckerSidebar": {
this.hideSidebar();
lazy.ShoppingUtils.sendTrigger({
browser: this.window.gBrowser,
id: "showRCSidebarClosedCallout",
context: {
isReviewCheckerInSidebarClosed: !this.SidebarController?.isOpen,
},
});
break;
}
}

View file

@ -0,0 +1,77 @@
<!-- 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/. -->
<svg width="352" height="196" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#a)">
<path d="M0 0h352v196H0V0Z" fill="#fff"/>
<path d="M0 0h352v196H0V0Z" fill="#E7DFFF"/>
<g clip-path="url(#b)" fill="#8F8F9D">
<path d="m65.9 130.3.4-1.7c.1-.8 1.3-.8 1.4 0l.4 1.7c0 .3.3.5.6.6l1.7.4c.8.1.8 1.3 0 1.4l-1.7.4c-.3 0-.5.3-.6.6l-.4 1.7c-.1.8-1.3.8-1.4 0l-.4-1.7c0-.3-.3-.5-.6-.6l-1.7-.4c-.8-.1-.8-1.3 0-1.4l1.7-.4c.3 0 .5-.3.6-.6ZM65.9 154.3l.4-1.7c.1-.8 1.3-.8 1.4 0l.4 1.7c0 .3.3.5.6.6l1.7.4c.8.1.8 1.3 0 1.4l-1.7.4c-.3 0-.5.3-.6.6l-.4 1.7c-.1.8-1.3.8-1.4 0l-.4-1.7c0-.3-.3-.5-.6-.6l-1.7-.4c-.8-.1-.8-1.3 0-1.4l1.7-.4c.3 0 .5-.3.6-.6Z"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M83.5 131.7a2.7 2.7 0 0 0-5 0l-2.5 6.4c-.2.4-.5.7-1 .9l-6.3 2.5a2.7 2.7 0 0 0 0 5l6.4 2.5c.4.2.7.5.9 1l2.5 6.3c.9 2.3 4.1 2.3 5 0l2.5-6.4c.2-.4.5-.7 1-.9l6.3-2.5c2.3-.9 2.3-4.1 0-5l-6.4-2.5c-.4-.2-.7-.5-.9-1l-2.5-6.3Zm-4.7 7.5 2.2-5.7 2.2 5.7c.5 1.2 1.4 2 2.6 2.6l5.7 2.2-5.7 2.2a4.6 4.6 0 0 0-2.6 2.6l-2.2 5.7-2.3-5.7c-.4-1.2-1.3-2-2.5-2.6l-5.7-2.2 5.7-2.3a4.6 4.6 0 0 0 2.6-2.5Z"/>
</g>
<g clip-path="url(#c)">
<path d="M298 110.9a3.1 3.1 0 0 1-2.9-2c-.2-.7-.2-1.3 0-2l1.6-6.4-5.2-4.3a3.1 3.1 0 0 1 0-5l1.8-.6 6.7-.4 2.5-6.3c.5-1.2 1.6-2 3-2 1.2 0 2.4.8 2.8 2l2.5 6.3 6.7.4a3.1 3.1 0 0 1 2.8 4c-.1.6-.5 1.2-1 1.6l-5.1 4.3 1.6 6.5a3.1 3.1 0 0 1-1.2 3.3 3.1 3.1 0 0 1-3.5 0l-5.7-3.5-5.7 3.6c-.5.3-1 .5-1.6.5Zm7.4-26.5a.7.7 0 0 0-.7.4l-2.7 7-1 .7-7.5.5a.7.7 0 0 0-.7.5.7.7 0 0 0 .3.8l5.7 4.8.4 1.2-1.8 7.3a.7.7 0 0 0 .2.7c.2.1.5.3.9 0l6.3-4h1.3l6.3 4a.7.7 0 0 0 .8 0 .7.7 0 0 0 .3-.7l-1.9-7.3.4-1.2 5.8-4.8a.7.7 0 0 0 .2-.8.7.7 0 0 0-.6-.5l-7.5-.5-1-.7-2.8-7a.7.7 0 0 0-.7-.4Z" fill="#8F8F9D"/>
</g>
<g clip-path="url(#d)">
<g clip-path="url(#e)" fill="#8F8F9D">
<path d="M66.6 47.4v-2.1L65 43.7h-2.1v-2.6h2c2.4 0 4.3 1.9 4.3 4.2v2.1h-2.6ZM46.6 53.7v2.1l1.6 1.6h2.1V60h-2a4.2 4.2 0 0 1-4.3-4.2v-2.1h2.6ZM65 57.4h-2.1V60h2c2.4 0 4.3-1.9 4.3-4.2v-2.1h-2.6v2.1L65 57.4Z"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M39.8 39c0-2.3 1.9-4.2 4.2-4.2h12.6c2.3 0 4.2 1.9 4.2 4.2v6.3c0 2.3-1.9 4.2-4.2 4.2H44a4.2 4.2 0 0 1-4.2-4.2V39Zm12.6-1.6H44c-.9 0-1.6.7-1.6 1.6v6.3c0 .9.7 1.6 1.6 1.6h8.4v-9.5Zm4.2 1.6c.9 0 1.6.7 1.6 1.6v3.1a1.6 1.6 0 0 1-3.2 0v-3.1c0-.9.7-1.6 1.6-1.6Z"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M44 55.8v-4.2h2.6v4.2c0 .9.7 1.6 1.6 1.6H65c.9 0 1.6-.7 1.6-1.6V45.3c0-.9-.7-1.6-1.6-1.6h-2.1v-2.6h2c2.4 0 4.3 1.9 4.3 4.2v10.5c0 2.3-1.9 4.2-4.2 4.2H68a3.1 3.1 0 0 1 0 6.3h-23a3.1 3.1 0 0 1 0-6.3h3.1a4.2 4.2 0 0 1-4.2-4.2Zm9.5 5.8c0-1 .7-1.6 1.5-1.6h3.2a1.6 1.6 0 0 1 0 3.1H55c-.8 0-1.5-.7-1.5-1.5Z"/>
<path d="M46.1 37.4H44L42.4 39v2.1h-2.6v-2c0-2.4 1.9-4.3 4.2-4.3h2.1v2.6ZM42.4 43.2v2.1L44 47h2.1v2.6h-2a4.2 4.2 0 0 1-4.3-4.2v-2h2.6Z"/>
</g>
</g>
<g clip-path="url(#f)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M247.1 52.3h2a2.7 2.7 0 0 0 2.6-2l.7-2.3 1.3-.7 2.4.6a2.7 2.7 0 0 0 3-1.3l1-1.8c.5-1 .4-2.3-.4-3.1l-1.7-1.9a5.7 5.7 0 0 0 0-1.4l1.7-1.8c.8-.8 1-2.1.3-3.1l-1-1.8a2.7 2.7 0 0 0-3-1.3l-2.3.6-1.3-.7-.7-2.4c-.3-1.1-1.3-1.9-2.5-1.9h-2a2.7 2.7 0 0 0-2.6 2l-.7 2.3-1.3.7-2.4-.6a2.7 2.7 0 0 0-3 1.3l-1 1.8a2.7 2.7 0 0 0 .4 3.1l1.7 1.8a6 6 0 0 0 0 1.4l-1.7 1.9a2.7 2.7 0 0 0-.3 3.1l1 1.8a2.7 2.7 0 0 0 3 1.3l2.3-.6 1.3.7.7 2.4a2.7 2.7 0 0 0 2.5 1.9Zm-3.6-7-1-.1-2.8.7a.6.6 0 0 1-.6-.3l-1-1.8a.6.6 0 0 1 0-.7l2-2.2.3-.8v-1.9l-.2-.9-2-2a.6.6 0 0 1-.2-.8l1-1.8a.6.6 0 0 1 .7-.3l3 .7.8-.2 1.6-1 .6-.6.8-2.8a.6.6 0 0 1 .6-.4h2a.6.6 0 0 1 .7.4l.8 2.8.6.7c.6.2 1.1.5 1.6 1l.9.1 2.9-.7a.6.6 0 0 1 .6.3l1 1.8a.6.6 0 0 1 0 .7l-2 2.1-.3.9V40l.2.8 2 2.2a.6.6 0 0 1 .2.7l-1 1.8a.6.6 0 0 1-.7.3l-2.9-.7-.9.1c-.6.5-1 .8-1.6 1l-.6.7-.8 2.8a.6.6 0 0 1-.6.4h-2a.6.6 0 0 1-.7-.4l-.8-2.8-.6-.7c-.6-.2-1.1-.6-1.6-1Zm7.5-6.2a2.9 2.9 0 1 0-5.7 0 2.9 2.9 0 0 0 5.7 0Zm-6.3-3.4a5 5 0 1 1 7 7 5 5 0 0 1-7-7Z" fill="#8F8F9D"/>
</g>
<g clip-path="url(#g)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M262.6 160.8a11.5 11.5 0 1 0-23 0 11.5 11.5 0 0 0 23 0ZM245.8 148a13.8 13.8 0 1 1 10.5 25.5 13.8 13.8 0 0 1-10.5-25.5Zm10.2 17a1.2 1.2 0 1 0 1.2-2l-5-2.8v-5.7a1.2 1.2 0 0 0-2-.8c-.2.2-.3.5-.3.8v6.4l.6 1 5.5 3.2Z" fill="#8F8F9D"/>
</g>
<g filter="url(#h)">
<rect x="130" y="52" width="92" height="92" rx="22.4" fill="#fff"/>
<g clip-path="url(#i)">
<g clip-path="url(#j)" fill="#5B5B66">
<path d="M182.1 72h-19V77h18l17.4 17.5L202 91l-18.2-18.2c-.4-.5-1-.8-1.7-.8ZM170.6 98.4l3.5-3.5-3.5-3.4-3.4 3.4 3.4 3.5Z"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M157 84.2c0-1.3 1-2.4 2.3-2.4H179c.6 0 1.2.2 1.7.7L197 98.9a6.5 6.5 0 0 1 0 9.2l-13.8 13.8a6.5 6.5 0 0 1-9.2 0l-16.4-16.5c-.4-.4-.7-1-.7-1.7V84.2Zm4.8 2.4v16.1l15.7 15.7c.6.6 1.6.6 2.3 0l13.8-13.8c.6-.6.6-1.6 0-2.3l-15.8-15.7h-16Z"/>
</g>
</g>
</g>
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M0 0h352v196H0z"/>
</clipPath>
<clipPath id="b">
<rect x="63" y="128" width="32" height="32" rx="4" fill="#fff"/>
</clipPath>
<clipPath id="c">
<rect x="290.4" y="81.4" width="30.9" height="30.9" rx="3.9" fill="#fff"/>
</clipPath>
<clipPath id="d">
<rect x="37.7" y="32.7" width="33.6" height="33.6" rx="4.2" fill="#fff"/>
</clipPath>
<clipPath id="e">
<path fill="#fff" transform="translate(37.7 32.7)" d="M0 0h33.6v33.6H0z"/>
</clipPath>
<clipPath id="f">
<rect x="235.1" y="26.1" width="26.3" height="26.3" rx="3.3" fill="#fff"/>
</clipPath>
<clipPath id="g">
<rect x="236.8" y="145.8" width="29.5" height="29.5" rx="3.7" fill="#fff"/>
</clipPath>
<clipPath id="i">
<rect x="150" y="72" width="51.9" height="51.9" rx="6.5" fill="#fff"/>
</clipPath>
<clipPath id="j">
<path fill="#fff" transform="translate(150 72)" d="M0 0h51.9v51.9H0z"/>
</clipPath>
<filter id="h" x="106.6" y="35.9" width="138.9" height="138.9" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="7.3"/>
<feGaussianBlur stdDeviation="8.8"/>
<feColorMatrix values="0 0 0 0 0.227451 0 0 0 0 0.223529 0 0 0 0 0.266667 0 0 0 0.2 0"/>
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_14063_68005"/>
<feBlend in="SourceGraphic" in2="effect1_dropShadow_14063_68005" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB

View file

@ -24,4 +24,9 @@ shopping-empty-state-supported-site = View a product and { -brand-product-name }
# This string will be displayed above the list of sites. The list will be hardcoded and does not require localization.
shopping-empty-state-non-supported-site = Review Checker works when you shop on:
## Callout for where to find Review Checker when the sidebar closes
shopping-integrated-callout-sidebar-closed-title = Get back to Review Checker
shopping-integrated-callout-sidebar-closed-subtitle = Select the price tag in the sidebar to see if you can trust a products reviews.
##

View file

@ -115,9 +115,9 @@ export class SidebarState {
} else {
this.launcherVisible = true;
}
// Ensure that tab container has the updated value of `launcherExpanded`.
const { tabContainer } = this.#controllerGlobal.gBrowser;
tabContainer.toggleAttribute("expanded", this.launcherExpanded);
// Explicitly trigger effects to ensure that the UI is kept up to date.
this.launcherExpanded = this.#props.launcherExpanded;
}
/**

View file

@ -439,17 +439,20 @@ add_task(async function test_vertical_tabs_expanded() {
"Sidebar launcher is hidden."
);
info("Enable revamped sidebar and vertical tabs.");
Services.prefs.setBoolPref("sidebar.revamp", true);
info("Enable vertical tabs.");
Services.prefs.setBoolPref("sidebar.verticalTabs", true);
await TestUtils.waitForCondition(
() => BrowserTestUtils.isVisible(document.getElementById("sidebar-main")),
"Sidebar launcher is shown."
);
ok(
const expandedStateValues = [
SidebarController.getUIState().launcherExpanded,
SidebarController.sidebarMain.expanded,
gBrowser.tabContainer.hasAttribute("expanded"),
"Tab container is expanded."
);
];
for (const val of expandedStateValues) {
is(val, false, "Launcher is collapsed.");
}
await SpecialPowers.popPrefEnv();
});

View file

@ -805,7 +805,6 @@
}
this.showTab(aTab);
this.ungroupTab(aTab);
if (this.tabContainer.verticalMode) {
this._handleTabMove(aTab, () =>
this.verticalPinnedTabsContainer.appendChild(aTab)
@ -3532,19 +3531,14 @@
let tabs = contextTab.multiselected ? this.selectedTabs : [contextTab];
// Walk the array in reverse order so the tabs are kept in order.
for (let i = tabs.length - 1; i >= 0; i--) {
let tab = tabs[i];
if (tab._tPos > 0) {
this.moveTabTo(tab, 0);
}
this.moveTabToStart(tabs[i]);
}
}
moveTabsToEnd(contextTab) {
let tabs = contextTab.multiselected ? this.selectedTabs : [contextTab];
for (let tab of tabs) {
if (tab._tPos < this.tabs.length - 1) {
this.moveTabTo(tab, this.tabs.length - 1);
}
this.moveTabToEnd(tab);
}
}
@ -5664,16 +5658,14 @@
* a tab group, since pinned tabs are presently not allowed in tab groups.
* @returns {void}
*/
moveTabTo(aTab, aIndex, options = { forceStandaloneTab: false }) {
const { forceStandaloneTab } = options;
moveTabTo(aTab, aIndex, { forceStandaloneTab = false } = {}) {
// Don't allow mixing pinned and unpinned tabs.
if (aTab.pinned) {
aIndex = Math.min(aIndex, this.pinnedTabCount - 1);
} else {
aIndex = Math.max(aIndex, this.pinnedTabCount);
}
if (aTab._tPos == aIndex) {
if (aTab._tPos == aIndex && !(aTab.group && forceStandaloneTab)) {
return;
}
@ -5684,7 +5676,7 @@
if (forceStandaloneTab && neighbor.group) {
neighbor = neighbor.group;
}
if (neighbor && aIndex >= aTab._tPos) {
if (neighbor && aIndex > aTab._tPos) {
neighbor.after(aTab);
} else {
this.tabContainer.insertBefore(aTab, neighbor);
@ -5878,12 +5870,12 @@
}
}
moveTabToStart() {
this.moveTabTo(this.selectedTab, 0);
moveTabToStart(aTab = this.selectedTab) {
this.moveTabTo(aTab, 0, { forceStandaloneTab: true });
}
moveTabToEnd() {
this.moveTabTo(this.selectedTab, this.tabs.length - 1);
moveTabToEnd(aTab = this.selectedTab) {
this.moveTabTo(aTab, this.tabs.length - 1, { forceStandaloneTab: true });
}
/**
@ -8420,9 +8412,9 @@ var TabContextMenu = {
}
);
let visibleTabs = gBrowser.visibleTabs;
let lastVisibleTab = visibleTabs[visibleTabs.length - 1];
let lastVisibleTab = visibleTabs.at(-1);
let tabsToMove = contextTabs;
let lastTabToMove = tabsToMove[tabsToMove.length - 1];
let lastTabToMove = tabsToMove.at(-1);
let isLastPinnedTab = false;
if (lastTabToMove.pinned) {
@ -8431,11 +8423,13 @@ var TabContextMenu = {
}
contextMoveTabToEnd.disabled =
(lastTabToMove == lastVisibleTab || isLastPinnedTab) &&
!lastTabToMove.group &&
allSelectedTabsAdjacent;
let contextMoveTabToStart = document.getElementById("context_moveToStart");
let isFirstTab =
tabsToMove[0] == visibleTabs[0] ||
tabsToMove[0] == visibleTabs[gBrowser.pinnedTabCount];
!tabsToMove[0].group &&
(tabsToMove[0] == visibleTabs[0] ||
tabsToMove[0] == visibleTabs[gBrowser.pinnedTabCount]);
contextMoveTabToStart.disabled = isFirstTab && allSelectedTabsAdjacent;
document.getElementById("context_openTabInWindow").disabled =

View file

@ -59,6 +59,31 @@
</panel>
`;
static State = {
// Standard create mode (No AI UI)
CREATE_STANDARD_INITIAL: 0,
// Create mode with AI able to suggest tabs
CREATE_AI_INITIAL: 1,
// No ungrouped tabs to suggest (hide AI interactions)
CREATE_AI_INITIAL_SUGGESTIONS_DISABLED: 2,
// Create mode with suggestions
CREATE_AI_WITH_SUGGESTIONS: 3,
// Create mode with no suggestions
CREATE_AI_WITH_NO_SUGGESTIONS: 4,
// Standard edit mode (No AI UI)
EDIT_STANDARD_INITIAL: 5,
// Edit mode with AI able to suggest tabs
EDIT_AI_INITIAL: 6,
// No ungrouped tabs to suggest
EDIT_AI_INITIAL_SUGGESTIONS_DISABLED: 7,
// Edit mode with suggestions
EDIT_AI_WITH_SUGGESTIONS: 8,
// Edit mode with no suggestions
EDIT_AI_WITH_NO_SUGGESTIONS: 9,
LOADING: 10,
ERROR: 11,
};
#activeGroup;
#cancelButton;
#createButton;
@ -68,6 +93,7 @@
#panel;
#swatches;
#swatchesContainer;
#suggestionState = MozTabbrowserTabGroupMenu.State.CREATE_STANDARD_INITIAL;
constructor() {
super();
@ -248,6 +274,8 @@
openCreateModal(group) {
this.activeGroup = group;
this.createMode = true;
this.suggestionState =
MozTabbrowserTabGroupMenu.State.CREATE_STANDARD_INITIAL;
this.#panel.openPopup(group.firstChild, {
position: this.#panelPosition,
});
@ -256,6 +284,8 @@
openEditModal(group) {
this.activeGroup = group;
this.createMode = false;
this.suggestionState =
MozTabbrowserTabGroupMenu.State.EDIT_STANDARD_INITIAL;
this.#panel.openPopup(group.firstChild, {
position: this.#panelPosition,
});
@ -356,6 +386,93 @@
#handleDelete() {
gBrowser.removeTabGroup(this.activeGroup);
}
/**
* @param {number} newState - See MozTabbrowserTabGroupMenu.State
*/
set suggestionState(newState) {
if (this.#suggestionState === newState) {
return;
}
this.#suggestionState = newState;
this.#renderSuggestionState();
}
#resetCommonUI() {
// TODO - has commun UI reset logic
}
#renderSuggestionState() {
switch (this.#suggestionState) {
// CREATE STANDARD INITIAL
case MozTabbrowserTabGroupMenu.State.CREATE_STANDARD_INITIAL:
this.#resetCommonUI();
//TODO
break;
//CREATE AI INITIAL
case MozTabbrowserTabGroupMenu.State.CREATE_AI_INITIAL:
this.#resetCommonUI();
//TODO
break;
// CREATE AI INITIAL SUGGESTIONS DISABLED
case MozTabbrowserTabGroupMenu.State
.CREATE_AI_INITIAL_SUGGESTIONS_DISABLED:
this.#resetCommonUI();
//TODO
break;
// CREATE AI WITH SUGGESTIONS
case MozTabbrowserTabGroupMenu.State.CREATE_AI_WITH_SUGGESTIONS:
//TODO
break;
// CREATE AI WITH NO SUGGESTIONS
case MozTabbrowserTabGroupMenu.State.CREATE_AI_WITH_NO_SUGGESTIONS:
//TODO
break;
// EDIT STANDARD INITIAL
case MozTabbrowserTabGroupMenu.State.EDIT_STANDARD_INITIAL:
this.#resetCommonUI();
//TODO
break;
// EDIT AI INITIAL
case MozTabbrowserTabGroupMenu.State.EDIT_AI_INITIAL:
this.#resetCommonUI();
//TODO
break;
// EDIT AI INITIAL SUGGESTIONS DISABLED
case MozTabbrowserTabGroupMenu.State
.EDIT_AI_INITIAL_SUGGESTIONS_DISABLED:
this.#resetCommonUI();
//TODO
break;
// EDIT AI WITH SUGGESTIONS
case MozTabbrowserTabGroupMenu.State.EDIT_AI_WITH_SUGGESTIONS:
//TODO
break;
// EDIT AI WITH NO SUGGESTIONS
case MozTabbrowserTabGroupMenu.State.EDIT_AI_WITH_NO_SUGGESTIONS:
//TODO
break;
// LOADING
case MozTabbrowserTabGroupMenu.State.LOADING:
//TODO
break;
// ERROR
case MozTabbrowserTabGroupMenu.State.ERROR:
//TODO
break;
}
}
}
customElements.define("tabgroup-menu", MozTabbrowserTabGroupMenu);

View file

@ -1162,8 +1162,8 @@
}
}
} else if (draggedTab) {
// Move the tabs. To avoid multiple tab-switches in the original window,
// the selected tab should be adopted last.
// Move the tabs into this window. To avoid multiple tab-switches in
// the original window, the selected tab should be adopted last.
const dropIndex = this._getDropIndex(event);
let newIndex = dropIndex;
let selectedTab;
@ -2309,7 +2309,8 @@
movingTabs = [...movingTabs].reverse();
}
let directionForward = screen > dragData.animLastScreenPos;
let directionForward =
screen > dragData.animLastScreenPos != this.#rtlMode;
dragData.animLastScreenPos = screen;
let screenAxis = this.verticalMode ? "screenY" : "screenX";

View file

@ -600,6 +600,75 @@ add_task(async function test_moveTabBetweenGroups() {
await removeTabGroup(group2);
});
add_task(async function test_moveTabToStartOrEnd() {
let tab1 = gBrowser.selectedTab;
let tab2 = BrowserTestUtils.addTab(gBrowser, "about:blank");
gBrowser.addTabGroup([tab1, tab2]);
Assert.equal(tab1._tPos, 0, "tab 1 starts at tab index 0");
Assert.equal(tab2._tPos, 1, "tab 2 starts at tab index 1");
Assert.equal(
tab1.elementIndex,
1,
"tab 1 starts at element index 1, after the group label"
);
Assert.equal(
tab2.elementIndex,
2,
"tab 2 starts at element index 2, after tab 1"
);
gBrowser.moveTabToStart(tab1);
Assert.ok(
!tab1.group,
"first tab is not grouped anymore after moving to start"
);
Assert.ok(
tab2.group,
"last tab is still grouped after moving first tab to start"
);
Assert.equal(
tab1._tPos,
0,
"tab 1 remains at tab index 0 after moving to start"
);
Assert.equal(
tab2._tPos,
1,
"tab 2 remains at tab index 1 after tab 1 moved to start"
);
Assert.equal(
tab1.elementIndex,
0,
"tab 1 moved to element index 0, before the group label"
);
Assert.equal(
tab2.elementIndex,
2,
"tab 2 remains at element index 2, after the group label"
);
gBrowser.moveTabToEnd(tab2);
Assert.ok(!tab2.group, "last tab is not grouped anymore after moving to end");
Assert.equal(
tab1._tPos,
0,
"tab 1 remains at tab index 0 after tab 2 moved to end"
);
Assert.equal(
tab2._tPos,
1,
"tab 2 remains at tab index 1 after moving to end"
);
Assert.equal(tab1.elementIndex, 0, "tab 1 remains at element index 0");
Assert.equal(
tab2.elementIndex,
1,
"tab 2 moved at element index 1 since the group label is gone"
);
BrowserTestUtils.removeTab(tab2);
});
add_task(async function test_tabGroupSelect() {
let tab1 = BrowserTestUtils.addTab(gBrowser, "about:blank");
let tab2 = BrowserTestUtils.addTab(gBrowser, "about:blank");

View file

@ -15,6 +15,22 @@ add_setup(async function () {
window.gTabsPanel.init();
});
/**
* One-liner to create a basic tab group
*
* @param {Object} [options] options for addTabGroup
* @param {Window} [options.targetWin] window to create the group in
* @returns {MozTabbrowserTabGroup}
*/
async function createTestGroup(options = {}) {
let win = options.targetWin ? options.targetWin : window;
let tab = await addTabTo(win.gBrowser, `data:text/plain,tab1`, {
skipAnimation: true,
});
await TabStateFlusher.flush(tab.linkedBrowser);
return win.gBrowser.addTabGroup([tab], options);
}
function forgetSavedTabGroups() {
const tabGroups = SessionStore.getSavedTabGroups();
tabGroups.forEach(tabGroup => SessionStore.forgetSavedTabGroup(tabGroup.id));
@ -144,20 +160,11 @@ async function getContextMenu(triggerNode, contextMenuId) {
*/
add_task(async function test_tabGroupsView() {
const savedGroupId = "test-saved-group";
let tabs = [];
for (let i = 1; i <= 5; i++) {
let tab = await addTab(`data:text/plain,tab${i}`, {
skipAnimation: true,
});
await TabStateFlusher.flush(tab.linkedBrowser);
tabs.push(tab);
}
let group1 = gBrowser.addTabGroup([tabs[0], tabs[1]], {
let group1 = await createTestGroup({
id: savedGroupId,
label: "Test Saved Group",
});
let group2 = gBrowser.addTabGroup([tabs[2], tabs[3]], {
let group2 = await createTestGroup({
label: "Test Open Group",
});
@ -235,179 +242,8 @@ add_task(async function test_tabGroupsView() {
await closeTabsMenu();
newWindow = await BrowserTestUtils.openNewBrowserWindow();
newWindow.gTabsPanel.init();
allTabsMenu = await openTabsMenu(newWindow);
let group1MenuItem = allTabsMenu.querySelector(
`#allTabsMenu-groupsView [data-tab-group-id="${savedGroupId}"]`
);
let menu = await getContextMenu(
group1MenuItem,
"open-tab-group-context-menu"
);
let waitForGroup = BrowserTestUtils.waitForEvent(
newWindow.gBrowser.tabContainer,
"TabGroupCreate"
);
menu.querySelector("#open-tab-group-context-menu_moveToThisWindow").click();
await waitForGroup;
Assert.equal(
window.gBrowser.tabGroups.length,
1,
"tab group should have moved from other window"
);
Assert.equal(
newWindow.gBrowser.tabGroups.length,
1,
"tab group should have moved to new window"
);
Assert.equal(
newWindow.gBrowser.tabGroups[0].id,
savedGroupId,
"tab group in new window should be the one that was moved"
);
await closeTabsMenu(newWindow);
allTabsMenu = await openTabsMenu(window);
group1MenuItem = allTabsMenu.querySelector(
`#allTabsMenu-groupsView [data-tab-group-id="${savedGroupId}"]`
);
menu = await getContextMenu(group1MenuItem, "open-tab-group-context-menu");
waitForGroup = BrowserTestUtils.waitForEvent(
newWindow.gBrowser.tabContainer,
"TabGroupRemoved"
);
menu.querySelector("#open-tab-group-context-menu_moveToNewWindow").click();
await waitForGroup;
await closeTabsMenu(window);
Assert.equal(
newWindow.gBrowser.tabGroups.length,
0,
"tab group should have moved out of the new window to some newer window"
);
allTabsMenu = await openTabsMenu(window);
group1MenuItem = allTabsMenu.querySelector(
`#allTabsMenu-groupsView [data-tab-group-id="${savedGroupId}"]`
);
menu = await getContextMenu(group1MenuItem, "open-tab-group-context-menu");
menu.querySelector("#open-tab-group-context-menu_delete").click();
menu.hidePopup();
await TestUtils.waitForCondition(
() => gBrowser.getAllTabGroups().length == 1,
"wait for tab group to be deleted"
);
await closeTabsMenu(window);
Assert.equal(
gBrowser.getAllTabGroups().length,
1,
"the only tab group left should be the unnamed group in the original window"
);
let moreTabs = [];
for (let i = 1; i <= 2; i++) {
moreTabs.push(
await addTabTo(newWindow.gBrowser, `data:text/plain,tab${i}`, {
skipAnimation: true,
})
);
}
group1 = newWindow.gBrowser.addTabGroup([moreTabs[0], moreTabs[1]], {
id: savedGroupId,
label: "Test Saved Group",
});
group1.save();
await removeTabGroup(group1);
Assert.ok(!gBrowser.getTabGroupById(savedGroupId), "Group 1 removed");
allTabsMenu = await openTabsMenu(newWindow);
savedGroupButton = allTabsMenu.querySelector(
`#allTabsMenu-groupsView [data-tab-group-id="${savedGroupId}"]`
);
Assert.equal(
savedGroupButton.label,
"Test Saved Group",
"Saved group once again appears as saved"
);
menu = await getContextMenu(savedGroupButton, "saved-tab-group-context-menu");
waitForGroup = BrowserTestUtils.waitForEvent(newWindow, "SSWindowStateReady");
menu.querySelector("#saved-tab-group-context-menu_openInThisWindow").click();
menu.hidePopup();
await waitForGroup;
await closeTabsMenu(newWindow);
group1 = gBrowser.getTabGroupById(savedGroupId);
Assert.equal(group1.name, "Test Saved Group", "Saved group was reopened");
info("save the group once again");
group1.save();
await removeTabGroup(group1);
Assert.ok(!gBrowser.getTabGroupById(savedGroupId), "Group 1 removed");
// TODO Bug 1940112: "Open Group in New Window" should directly restore saved tab groups into a new window
// allTabsMenu = await openTabsMenu(newWindow);
// savedGroupButton = allTabsMenu.querySelector(
// `#allTabsMenu-groupsView [data-tab-group-id="${savedGroupId}"]`
// );
// Assert.equal(
// savedGroupButton.label,
// "Test Saved Group",
// "Saved group once again appears as saved"
// );
// menu = await getContextMenu(savedGroupButton, "saved-tab-group-context-menu");
// menu
// .querySelector("#saved-tab-group-context-menu_openInNewWindow")
// .click();
// menu.hidePopup();
// await TestUtils.waitForCondition(
// () => gBrowser.getAllTabGroups().length == 2,
// "wait for saved group to be reopened in a new window"
// );
// await closeTabsMenu(newWindow);
// group1 = gBrowser.getTabGroupById(savedGroupId);
// info("save the group yet again");
// group1.save();
// await removeTabGroup(group1);
allTabsMenu = await openTabsMenu(newWindow);
savedGroupButton = allTabsMenu.querySelector(
`#allTabsMenu-groupsView [data-tab-group-id="${savedGroupId}"]`
);
Assert.ok(savedGroupButton, "saved group should be in the TOM");
menu = await getContextMenu(savedGroupButton, "saved-tab-group-context-menu");
menu.querySelector("#saved-tab-group-context-menu_delete").click();
menu.hidePopup();
await closeTabsMenu(newWindow);
allTabsMenu = await openTabsMenu(newWindow);
savedGroupButton = allTabsMenu.querySelector(
`#allTabsMenu-groupsView [data-tab-group-id="${savedGroupId}"]`
);
Assert.ok(!savedGroupButton, "saved group should have been forgotten");
await closeTabsMenu(newWindow);
await BrowserTestUtils.closeWindow(newWindow, { animate: false });
for (let tab of tabs) {
BrowserTestUtils.removeTab(tab);
}
gBrowser.removeTabGroup(group1);
gBrowser.removeTabGroup(group2);
forgetSavedTabGroups();
});
@ -417,16 +253,9 @@ add_task(async function test_tabGroupsView() {
*/
add_task(async function test_groupsViewShowMore() {
const savedGroupId = "test-saved-group";
let tabs = [];
let groups = [];
for (let i = 1; i <= 7; i++) {
let tab = await addTab(`data:text/plain,tab${i}`, {
skipAnimation: true,
});
await TabStateFlusher.flush(tab.linkedBrowser);
tabs.push(tab);
let group = gBrowser.addTabGroup([tab], {
let group = await createTestGroup({
id: savedGroupId + i,
label: "Test Saved Group " + i,
});
@ -491,3 +320,166 @@ add_task(async function test_groupsViewShowMore() {
forgetSavedTabGroups();
});
/**
* Tests the context menu behaviors for saved groups
*/
add_task(async function test_tabGroupsViewContextMenu_savedGroups() {
let savedGroupId = "test-saved-group";
let group1 = await createTestGroup({
id: savedGroupId,
label: "Test Saved Group",
});
group1.save();
await removeTabGroup(group1);
Assert.ok(!gBrowser.getTabGroupById(savedGroupId), "Group 1 removed");
let newWindow = await BrowserTestUtils.openNewBrowserWindow();
newWindow.gTabsPanel.init();
let allTabsMenu = await openTabsMenu(newWindow);
let savedGroupButton = allTabsMenu.querySelector(
`#allTabsMenu-groupsView [data-tab-group-id="${savedGroupId}"]`
);
Assert.equal(
savedGroupButton.label,
"Test Saved Group",
"Saved group appears as saved"
);
info("open saved group in current window");
let menu = await getContextMenu(
savedGroupButton,
"saved-tab-group-context-menu"
);
let waitForGroup = BrowserTestUtils.waitForEvent(
newWindow,
"SSWindowStateReady"
);
menu.querySelector("#saved-tab-group-context-menu_openInThisWindow").click();
menu.hidePopup();
await waitForGroup;
await closeTabsMenu(newWindow);
group1 = gBrowser.getTabGroupById(savedGroupId);
Assert.equal(group1.name, "Test Saved Group", "Saved group was reopened");
// re-save group
group1.save();
await removeTabGroup(group1);
Assert.ok(!gBrowser.getTabGroupById(savedGroupId), "Group 1 removed");
info("delete saved group");
allTabsMenu = await openTabsMenu(newWindow);
savedGroupButton = allTabsMenu.querySelector(
`#allTabsMenu-groupsView [data-tab-group-id="${savedGroupId}"]`
);
Assert.ok(savedGroupButton, "saved group should be in the TOM");
menu = await getContextMenu(savedGroupButton, "saved-tab-group-context-menu");
menu.querySelector("#saved-tab-group-context-menu_delete").click();
menu.hidePopup();
await closeTabsMenu(newWindow);
allTabsMenu = await openTabsMenu(newWindow);
savedGroupButton = allTabsMenu.querySelector(
`#allTabsMenu-groupsView [data-tab-group-id="${savedGroupId}"]`
);
Assert.ok(!savedGroupButton, "saved group should have been forgotten");
await closeTabsMenu(newWindow);
await BrowserTestUtils.closeWindow(newWindow, { animate: false });
});
/**
* Tests behavior of the context menu for open groups
*/
add_task(async function test_tabGroupsViewContextMenu_openGroups() {
let groupId = "test-group";
let otherWindow = await BrowserTestUtils.openNewBrowserWindow();
await createTestGroup({
id: groupId,
label: "Test Group",
targetWin: otherWindow,
});
otherWindow.gTabsPanel.init();
let allTabsMenu = await openTabsMenu();
info("move group from another window to this window");
let group1MenuItem = allTabsMenu.querySelector(
`#allTabsMenu-groupsView [data-tab-group-id="${groupId}"]`
);
let menu = await getContextMenu(
group1MenuItem,
"open-tab-group-context-menu"
);
let waitForGroup = BrowserTestUtils.waitForEvent(
gBrowser.tabContainer,
"TabGroupCreate"
);
menu.querySelector("#open-tab-group-context-menu_moveToThisWindow").click();
await waitForGroup;
Assert.equal(
otherWindow.gBrowser.tabGroups.length,
0,
"tab group should have moved from the second window"
);
Assert.equal(
window.gBrowser.tabGroups.length,
1,
"tab group should have moved to the starting window"
);
Assert.equal(
gBrowser.tabGroups[0].id,
groupId,
"tab group in window should be the one that was moved"
);
await closeTabsMenu();
info("move group to a new window");
allTabsMenu = await openTabsMenu(otherWindow);
group1MenuItem = allTabsMenu.querySelector(
`#allTabsMenu-groupsView [data-tab-group-id="${groupId}"]`
);
menu = await getContextMenu(group1MenuItem, "open-tab-group-context-menu");
waitForGroup = BrowserTestUtils.waitForEvent(
gBrowser.tabContainer,
"TabGroupRemoved"
);
let waitForWindow = BrowserTestUtils.waitForNewWindow();
menu.querySelector("#open-tab-group-context-menu_moveToNewWindow").click();
let menuHidden = BrowserTestUtils.waitForPopupEvent(menu, "hidden");
menu.hidePopup();
await Promise.allSettled([menuHidden, waitForGroup, waitForWindow]);
await closeTabsMenu(otherWindow);
Assert.equal(
otherWindow.gBrowser.tabGroups.length,
0,
"tab group should have moved out of the second window to a new window"
);
await BrowserTestUtils.closeWindow(otherWindow, { animate: false });
info("delete group");
allTabsMenu = await openTabsMenu(window);
group1MenuItem = allTabsMenu.querySelector(
`#allTabsMenu-groupsView [data-tab-group-id="${groupId}"]`
);
menu = await getContextMenu(group1MenuItem, "open-tab-group-context-menu");
menu.querySelector("#open-tab-group-context-menu_delete").click();
menu.hidePopup();
await TestUtils.waitForCondition(
() => !gBrowser.getAllTabGroups().length,
"wait for tab group to be deleted"
);
await closeTabsMenu(window);
Assert.equal(gBrowser.getAllTabGroups().length, 0, "Group was deleted");
});

View file

@ -230,10 +230,6 @@ const PREF_URLBAR_DEFAULTS = new Map([
// Whether we show the Actions section in about:preferences.
["quickactions.showPrefs", false],
// Whether quick suggest results can be shown in position specified in the
// suggestions.
["quicksuggest.allowPositionInSuggestions", true],
// When non-zero, this is the character-count threshold (inclusive) for
// showing AMP suggestions as top picks. If an AMP suggestion is triggered by
// a keyword at least this many characters long, it will be shown as a top

View file

@ -368,16 +368,10 @@ class ProviderQuickSuggest extends UrlbarProvider {
// Set the appropriate suggested index and related properties unless the
// feature did it already.
if (!result.hasSuggestedIndex) {
if (suggestion.is_top_pick) {
result.isBestMatch = true;
if (result.isBestMatch) {
result.isRichSuggestion = true;
result.richSuggestionIconSize ||= 52;
result.suggestedIndex = 1;
} else if (
!isNaN(suggestion.position) &&
lazy.UrlbarPrefs.get("quickSuggestAllowPositionInSuggestions")
) {
result.suggestedIndex = suggestion.position;
} else {
result.isSuggestedIndexRelativeToGroup = true;
if (!result.payload.isSponsored) {
@ -453,13 +447,18 @@ class ProviderQuickSuggest extends UrlbarProvider {
payload.shouldShowUrl = true;
}
return new lazy.UrlbarResult(
UrlbarUtils.RESULT_TYPE.URL,
UrlbarUtils.RESULT_SOURCE.SEARCH,
...lazy.UrlbarResult.payloadAndSimpleHighlights(
queryContext.tokens,
payload
)
return Object.assign(
new lazy.UrlbarResult(
UrlbarUtils.RESULT_TYPE.URL,
UrlbarUtils.RESULT_SOURCE.SEARCH,
...lazy.UrlbarResult.payloadAndSimpleHighlights(
queryContext.tokens,
payload
)
),
{
isBestMatch: !!suggestion.is_top_pick,
}
);
}

View file

@ -177,7 +177,6 @@ class ProviderTopSites extends UrlbarProvider {
);
sites = sites.slice(0, numTopSites);
let index = 1;
sites = sites.map(link => {
let site = {
type: link.searchTopSite ? "search" : "url",
@ -202,10 +201,8 @@ class ProviderTopSites extends UrlbarProvider {
sponsoredTileId: sponsored_tile_id,
sponsoredImpressionUrl: sponsored_impression_url,
sponsoredClickUrl: sponsored_click_url,
position: index,
};
}
index++;
return site;
});

View file

@ -46,10 +46,6 @@ export class MDNSuggestions extends SuggestProvider {
return null;
}
// Set `is_top_pick` on the suggestion to tell the provider to set
// best-match related properties on the result.
suggestion.is_top_pick = true;
const url = new URL(suggestion.url);
url.searchParams.set("utm_medium", "firefox-desktop");
url.searchParams.set("utm_source", "firefox-suggest");
@ -78,7 +74,10 @@ export class MDNSuggestions extends SuggestProvider {
payload
)
),
{ showFeedbackMenu: true }
{
isBestMatch: true,
showFeedbackMenu: true,
}
);
}

View file

@ -74,9 +74,11 @@ export class PocketSuggestions extends SuggestProvider {
}
}
// Merino will set `is_top_pick` if the suggestion should be a best match.
let isBestMatch = !!suggestion.is_top_pick;
if (suggestion.source == "rust") {
suggestion.is_top_pick = suggestion.isTopPick;
delete suggestion.isTopPick;
isBestMatch = suggestion.isTopPick;
// The Rust component doesn't implement these properties. For now we use
// dummy values. See issue #5878 in application-services.
@ -94,12 +96,13 @@ export class PocketSuggestions extends SuggestProvider {
url.searchParams.set("utm_content", "treatment");
let resultProperties = {
isBestMatch,
isRichSuggestion: true,
richSuggestionIconSize: suggestion.is_top_pick ? 24 : 16,
richSuggestionIconSize: isBestMatch ? 24 : 16,
showFeedbackMenu: true,
};
if (!suggestion.is_top_pick) {
if (!isBestMatch) {
let suggestedIndex = lazy.UrlbarPrefs.get("pocketSuggestIndex");
if (suggestedIndex !== null) {
resultProperties.isSuggestedIndexRelativeToGroup = true;
@ -115,10 +118,10 @@ export class PocketSuggestions extends SuggestProvider {
url: url.href,
originalUrl: suggestion.url,
title: [suggestion.title, lazy.UrlbarUtils.HIGHLIGHT.TYPED],
description: suggestion.is_top_pick ? suggestion.description : "",
description: isBestMatch ? suggestion.description : "",
// Use the favicon for non-best matches so the icon exactly matches
// the Pocket favicon in the user's history and tabs.
icon: suggestion.is_top_pick
icon: isBestMatch
? "chrome://global/skin/icons/pocket.svg"
: "chrome://global/skin/icons/pocket-favicon.ico",
shouldShowUrl: true,

View file

@ -173,9 +173,9 @@ export class YelpSuggestions extends SuggestProvider {
let resultProperties = {
isRichSuggestion: true,
showFeedbackMenu: true,
isBestMatch: lazy.UrlbarPrefs.get("yelpSuggestPriority"),
};
suggestion.is_top_pick = lazy.UrlbarPrefs.get("yelpSuggestPriority");
if (!suggestion.is_top_pick) {
if (!resultProperties.isBestMatch) {
let suggestedIndex = lazy.UrlbarPrefs.get("yelpSuggestNonPriorityIndex");
if (suggestedIndex !== null) {
resultProperties.isSuggestedIndexRelativeToGroup = true;

View file

@ -28,7 +28,6 @@ const TEST_MERINO_SUGGESTIONS = [
guid: "first@addon",
},
},
is_top_pick: true,
},
{
provider: "amo",
@ -43,8 +42,6 @@ const TEST_MERINO_SUGGESTIONS = [
guid: "second@addon",
},
},
is_sponsored: true,
is_top_pick: false,
},
{
provider: "amo",
@ -59,7 +56,6 @@ const TEST_MERINO_SUGGESTIONS = [
guid: "third@addon",
},
},
is_top_pick: false,
},
{
provider: "amo",

View file

@ -1422,17 +1422,6 @@ add_task(async function sponsoredPriority_sponsoredIndex() {
});
});
add_task(async function sponsoredPriority_position() {
await doSponsoredPriorityTest({
nimbusSettings: { quickSuggestAllowPositionInSuggestions: true },
searchWord: SPONSORED_SEARCH_STRING,
remoteSettingsData: [
Object.assign({}, REMOTE_SETTINGS_RESULTS[0], { position: 2 }),
],
expectedMatches: [expectedSponsoredPriorityResult()],
});
});
async function doSponsoredPriorityTest({
remoteSettingsConfig = {},
nimbusSettings = {},
@ -1466,6 +1455,13 @@ async function doSponsoredPriorityTest({
});
await cleanUpNimbusEnable();
await QuickSuggestTestUtils.setRemoteSettingsRecords([
{
type: "data",
attachment: REMOTE_SETTINGS_RESULTS,
},
]);
await QuickSuggestTestUtils.setConfig(QuickSuggestTestUtils.DEFAULT_CONFIG);
}
// When a Suggest best match and a tab-to-search (TTS) are shown in the same
@ -1547,84 +1543,6 @@ add_task(async function tabToSearch() {
Services.prefs.clearUserPref("browser.urlbar.quicksuggest.sponsoredPriority");
});
// `suggestion.position` should be ignored when the suggestion is a best match.
add_task(async function position() {
// We'll use a sponsored priority result as the best match result. Different
// types of Suggest results can appear as best matches, and they all should
// have the same behavior.
UrlbarPrefs.set("suggest.quicksuggest.sponsored", true);
await QuickSuggestTestUtils.forceSync();
Services.prefs.setBoolPref(
"browser.urlbar.quicksuggest.sponsoredPriority",
true
);
// Disable search suggestions so we don't hit the network.
Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
// Set the remote settings data with a suggestion containing a position.
UrlbarPrefs.set("quicksuggest.allowPositionInSuggestions", true);
await QuickSuggestTestUtils.setRemoteSettingsRecords([
{
type: "data",
attachment: [
{
...REMOTE_SETTINGS_RESULTS[0],
position: 9,
},
],
},
]);
let context = createContext(SPONSORED_SEARCH_STRING, {
isPrivate: false,
});
// Add some visits to fill up the view.
let maxResultCount = UrlbarPrefs.get("maxRichResults");
let visitResults = [];
for (let i = 0; i < maxResultCount; i++) {
let url = `http://example.com/${SPONSORED_SEARCH_STRING}-${i}`;
await PlacesTestUtils.addVisits(url);
visitResults.unshift(
makeVisitResult(context, {
uri: url,
title: `test visit for ${url}`,
})
);
}
// Do a search.
await check_results({
context,
matches: [
// search heuristic
makeSearchResult(context, {
engineName: Services.search.defaultEngine.name,
engineIconUri: await Services.search.defaultEngine.getIconURL(),
heuristic: true,
}),
// best match whose backing suggestion has a `position`
expectedSponsoredPriorityResult(),
// visits
...visitResults.slice(0, maxResultCount - 2),
],
});
await cleanupPlaces();
await QuickSuggestTestUtils.setRemoteSettingsRecords([
{
type: "data",
attachment: REMOTE_SETTINGS_RESULTS,
},
]);
UrlbarPrefs.clear("quicksuggest.allowPositionInSuggestions");
Services.prefs.clearUserPref("browser.search.suggest.enabled");
Services.prefs.clearUserPref("browser.urlbar.quicksuggest.sponsoredPriority");
});
// The `Amp` and `Wikipedia` Rust providers should be passed to the Rust
// component when querying depending on whether sponsored and non-sponsored
// suggestions are enabled.

View file

@ -30,7 +30,6 @@ const MERINO_SUGGESTIONS = [
url: "https://example.com/merino-addon",
title: "title",
description: "description",
is_top_pick: true,
custom_details: {
amo: {
rating: "5",

View file

@ -1,487 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests for quick suggest result position specified in suggestions.
*/
ChromeUtils.defineESModuleGetters(this, {
UrlbarProviderHeuristicFallback:
"resource:///modules/UrlbarProviderHeuristicFallback.sys.mjs",
UrlbarProviderPlaces: "resource:///modules/UrlbarProviderPlaces.sys.mjs",
UrlbarProviderTabToSearch:
"resource:///modules/UrlbarProviderTabToSearch.sys.mjs",
});
const SPONSORED_SECOND_POSITION_RESULT = {
id: 1,
url: "http://example.com/?q=sponsored-second",
title: "sponsored second",
keywords: ["s-s"],
click_url: "http://click.reporting.test.com/",
impression_url: "http://impression.reporting.test.com/",
advertiser: "TestAdvertiser",
iab_category: "22 - Shopping",
position: 1,
};
const SPONSORED_NORMAL_POSITION_RESULT = {
id: 2,
url: "http://example.com/?q=sponsored-normal",
title: "sponsored normal",
keywords: ["s-n"],
click_url: "http://click.reporting.test.com/",
impression_url: "http://impression.reporting.test.com/",
advertiser: "TestAdvertiser",
iab_category: "22 - Shopping",
};
const NONSPONSORED_SECOND_POSITION_RESULT = {
id: 3,
url: "http://example.com/?q=nonsponsored-second",
title: "nonsponsored second",
keywords: ["n-s"],
click_url: "http://click.reporting.test.com/nonsponsored",
impression_url: "http://impression.reporting.test.com/nonsponsored",
advertiser: "TestAdvertiserNonSponsored",
iab_category: "5 - Education",
position: 1,
};
const NONSPONSORED_NORMAL_POSITION_RESULT = {
id: 4,
url: "http://example.com/?q=nonsponsored-normal",
title: "nonsponsored normal",
keywords: ["n-n"],
click_url: "http://click.reporting.test.com/nonsponsored",
impression_url: "http://impression.reporting.test.com/nonsponsored",
advertiser: "TestAdvertiserNonSponsored",
iab_category: "5 - Education",
};
const FIRST_POSITION_RESULT = {
id: 5,
url: "http://example.com/?q=first-position",
title: "first position suggest",
keywords: ["first-position"],
click_url: "http://click.reporting.test.com/first-position",
impression_url: "http://impression.reporting.test.com/first-position",
advertiser: "TestAdvertiserFirstPositionQuickSuggest",
iab_category: "22 - Shopping",
position: 0,
};
const SECOND_POSITION_RESULT = {
id: 6,
url: "http://example.com/?q=second-position",
title: "second position suggest",
keywords: ["second-position"],
click_url: "http://click.reporting.test.com/second-position",
impression_url: "http://impression.reporting.test.com/second-position",
advertiser: "TestAdvertiserSecondPositionQuickSuggest",
iab_category: "22 - Shopping",
position: 1,
};
const THIRD_POSITION_RESULT = {
id: 7,
url: "http://example.com/?q=third-position",
title: "third position suggest",
keywords: ["third-position"],
click_url: "http://click.reporting.test.com/third-position",
impression_url: "http://impression.reporting.test.com/third-position",
advertiser: "TestAdvertiserThirdPositionQuickSuggest",
iab_category: "22 - Shopping",
position: 2,
};
const TABTOSEARCH_ENGINE_DOMAIN_FOR_FIRST_POSITION_TEST =
"first-position.example.com";
const TABTOSEARCH_ENGINE_DOMAIN_FOR_SECOND_POSITION_TEST =
"second-position.example.com";
const SECOND_POSITION_INTERVENTION_RESULT = new UrlbarResult(
UrlbarUtils.RESULT_TYPE.URL,
UrlbarUtils.RESULT_SOURCE.HISTORY,
{ url: "http://mozilla.org/a" }
);
SECOND_POSITION_INTERVENTION_RESULT.suggestedIndex = 1;
const SECOND_POSITION_INTERVENTION_RESULT_PROVIDER =
new UrlbarTestUtils.TestProvider({
results: [SECOND_POSITION_INTERVENTION_RESULT],
priority: 0,
name: "second_position_intervention_provider",
});
const EXPECTED_GENERAL_HEURISTIC_RESULT = {
providerName: UrlbarProviderHeuristicFallback.name,
type: UrlbarUtils.RESULT_TYPE.SEARCH,
source: UrlbarUtils.RESULT_SOURCE.SEARCH,
heuristic: true,
};
const EXPECTED_GENERAL_PLACES_RESULT = {
providerName: UrlbarProviderPlaces.name,
type: UrlbarUtils.RESULT_TYPE.URL,
source: UrlbarUtils.RESULT_SOURCE.HISTORY,
heuristic: false,
};
const EXPECTED_GENERAL_TABTOSEARCH_RESULT = {
providerName: UrlbarProviderTabToSearch.name,
type: UrlbarUtils.RESULT_TYPE.DYNAMIC,
source: UrlbarUtils.RESULT_SOURCE.SEARCH,
heuristic: false,
};
const EXPECTED_GENERAL_INTERVENTION_RESULT = {
providerName: SECOND_POSITION_INTERVENTION_RESULT_PROVIDER.name,
type: UrlbarUtils.RESULT_TYPE.URL,
source: UrlbarUtils.RESULT_SOURCE.HISTORY,
heuristic: false,
};
function createExpectedQuickSuggestResult(suggest) {
let isSponsored = suggest.iab_category !== "5 - Education";
return {
providerName: UrlbarProviderQuickSuggest.name,
type: UrlbarUtils.RESULT_TYPE.URL,
source: UrlbarUtils.RESULT_SOURCE.SEARCH,
heuristic: false,
payload: {
telemetryType: isSponsored ? "adm_sponsored" : "adm_nonsponsored",
qsSuggestion: suggest.keywords[0],
title: suggest.title,
url: suggest.url,
originalUrl: suggest.url,
icon: null,
sponsoredImpressionUrl: suggest.impression_url,
sponsoredClickUrl: suggest.click_url,
sponsoredBlockId: suggest.id,
sponsoredAdvertiser: suggest.advertiser,
sponsoredIabCategory: suggest.iab_category,
isSponsored,
descriptionL10n: isSponsored
? { id: "urlbar-result-action-sponsored" }
: undefined,
helpUrl: QuickSuggest.HELP_URL,
helpL10n: {
id: "urlbar-result-menu-learn-more-about-firefox-suggest",
},
isBlockable: true,
blockL10n: {
id: "urlbar-result-menu-dismiss-firefox-suggest",
},
displayUrl: suggest.url,
source: "remote-settings",
provider: "AdmWikipedia",
},
};
}
const TEST_CASES = [
{
description: "Test for second placable sponsored suggest",
input: SPONSORED_SECOND_POSITION_RESULT.keywords[0],
prefs: {
"quicksuggest.allowPositionInSuggestions": true,
},
providers: [
UrlbarProviderHeuristicFallback.name,
UrlbarProviderQuickSuggest.name,
UrlbarProviderPlaces.name,
],
expected: [
EXPECTED_GENERAL_HEURISTIC_RESULT,
createExpectedQuickSuggestResult(SPONSORED_SECOND_POSITION_RESULT),
EXPECTED_GENERAL_PLACES_RESULT,
],
},
{
description: "Test for normal sponsored suggest",
input: SPONSORED_NORMAL_POSITION_RESULT.keywords[0],
prefs: {
"quicksuggest.allowPositionInSuggestions": true,
},
providers: [
UrlbarProviderHeuristicFallback.name,
UrlbarProviderQuickSuggest.name,
UrlbarProviderPlaces.name,
],
expected: [
EXPECTED_GENERAL_HEURISTIC_RESULT,
EXPECTED_GENERAL_PLACES_RESULT,
createExpectedQuickSuggestResult(SPONSORED_NORMAL_POSITION_RESULT),
],
},
{
description: "Test for second placable nonsponsored suggest",
input: NONSPONSORED_SECOND_POSITION_RESULT.keywords[0],
prefs: {
"quicksuggest.allowPositionInSuggestions": true,
},
providers: [
UrlbarProviderHeuristicFallback.name,
UrlbarProviderQuickSuggest.name,
UrlbarProviderPlaces.name,
],
expected: [
EXPECTED_GENERAL_HEURISTIC_RESULT,
createExpectedQuickSuggestResult(NONSPONSORED_SECOND_POSITION_RESULT),
EXPECTED_GENERAL_PLACES_RESULT,
],
},
{
description: "Test for normal nonsponsored suggest",
input: NONSPONSORED_NORMAL_POSITION_RESULT.keywords[0],
prefs: {
"quicksuggest.allowPositionInSuggestions": true,
},
providers: [
UrlbarProviderHeuristicFallback.name,
UrlbarProviderQuickSuggest.name,
UrlbarProviderPlaces.name,
],
expected: [
EXPECTED_GENERAL_HEURISTIC_RESULT,
EXPECTED_GENERAL_PLACES_RESULT,
createExpectedQuickSuggestResult(NONSPONSORED_NORMAL_POSITION_RESULT),
],
},
{
description:
"Test for second placable sponsored suggest but secondPosition pref is disabled",
input: SPONSORED_SECOND_POSITION_RESULT.keywords[0],
prefs: {
"quicksuggest.allowPositionInSuggestions": false,
},
providers: [
UrlbarProviderHeuristicFallback.name,
UrlbarProviderQuickSuggest.name,
UrlbarProviderPlaces.name,
],
expected: [
EXPECTED_GENERAL_HEURISTIC_RESULT,
EXPECTED_GENERAL_PLACES_RESULT,
createExpectedQuickSuggestResult(SPONSORED_SECOND_POSITION_RESULT),
],
},
{
description: "Test the results with multi providers having same index",
input: SECOND_POSITION_RESULT.keywords[0],
prefs: {
"quicksuggest.allowPositionInSuggestions": true,
},
providers: [
UrlbarProviderQuickSuggest.name,
UrlbarProviderTabToSearch.name,
SECOND_POSITION_INTERVENTION_RESULT_PROVIDER.name,
],
expected: [
EXPECTED_GENERAL_TABTOSEARCH_RESULT,
createExpectedQuickSuggestResult(SECOND_POSITION_RESULT),
EXPECTED_GENERAL_INTERVENTION_RESULT,
],
},
{
description: "Test the results with tab-to-search",
input: SECOND_POSITION_RESULT.keywords[0],
prefs: {
"quicksuggest.allowPositionInSuggestions": true,
},
providers: [
UrlbarProviderTabToSearch.name,
UrlbarProviderQuickSuggest.name,
],
expected: [
EXPECTED_GENERAL_TABTOSEARCH_RESULT,
createExpectedQuickSuggestResult(SECOND_POSITION_RESULT),
],
},
{
description: "Test the results with another intervention",
input: SECOND_POSITION_RESULT.keywords[0],
prefs: {
"quicksuggest.allowPositionInSuggestions": true,
},
providers: [
UrlbarProviderQuickSuggest.name,
SECOND_POSITION_INTERVENTION_RESULT_PROVIDER.name,
],
expected: [
createExpectedQuickSuggestResult(SECOND_POSITION_RESULT),
EXPECTED_GENERAL_INTERVENTION_RESULT,
],
},
{
description: "Test the results with heuristic and tab-to-search",
input: SECOND_POSITION_RESULT.keywords[0],
prefs: {
"quicksuggest.allowPositionInSuggestions": true,
},
providers: [
UrlbarProviderHeuristicFallback.name,
UrlbarProviderTabToSearch.name,
UrlbarProviderQuickSuggest.name,
],
expected: [
EXPECTED_GENERAL_HEURISTIC_RESULT,
EXPECTED_GENERAL_TABTOSEARCH_RESULT,
createExpectedQuickSuggestResult(SECOND_POSITION_RESULT),
],
},
{
description: "Test the results with heuristic tab-to-search and places",
input: SECOND_POSITION_RESULT.keywords[0],
prefs: {
"quicksuggest.allowPositionInSuggestions": true,
},
providers: [
UrlbarProviderHeuristicFallback.name,
UrlbarProviderTabToSearch.name,
UrlbarProviderQuickSuggest.name,
UrlbarProviderPlaces.name,
],
expected: [
EXPECTED_GENERAL_HEURISTIC_RESULT,
EXPECTED_GENERAL_TABTOSEARCH_RESULT,
createExpectedQuickSuggestResult(SECOND_POSITION_RESULT),
EXPECTED_GENERAL_PLACES_RESULT,
],
},
{
description: "Test the results with heuristic and another intervention",
input: SECOND_POSITION_RESULT.keywords[0],
prefs: {
"quicksuggest.allowPositionInSuggestions": true,
},
providers: [
UrlbarProviderHeuristicFallback.name,
UrlbarProviderQuickSuggest.name,
SECOND_POSITION_INTERVENTION_RESULT_PROVIDER.name,
],
expected: [
EXPECTED_GENERAL_HEURISTIC_RESULT,
createExpectedQuickSuggestResult(SECOND_POSITION_RESULT),
EXPECTED_GENERAL_INTERVENTION_RESULT,
],
},
{
description:
"Test the results with heuristic, another intervention and places",
input: SECOND_POSITION_RESULT.keywords[0],
prefs: {
"quicksuggest.allowPositionInSuggestions": true,
},
providers: [
UrlbarProviderHeuristicFallback.name,
UrlbarProviderQuickSuggest.name,
SECOND_POSITION_INTERVENTION_RESULT_PROVIDER.name,
UrlbarProviderPlaces.name,
],
expected: [
EXPECTED_GENERAL_HEURISTIC_RESULT,
createExpectedQuickSuggestResult(SECOND_POSITION_RESULT),
EXPECTED_GENERAL_INTERVENTION_RESULT,
EXPECTED_GENERAL_PLACES_RESULT,
],
},
{
description: "Test for 0 indexed quick suggest",
input: FIRST_POSITION_RESULT.keywords[0],
prefs: {
"quicksuggest.allowPositionInSuggestions": true,
},
providers: [
UrlbarProviderTabToSearch.name,
UrlbarProviderQuickSuggest.name,
],
expected: [
createExpectedQuickSuggestResult(FIRST_POSITION_RESULT),
EXPECTED_GENERAL_TABTOSEARCH_RESULT,
],
},
{
description: "Test for 2 indexed quick suggest",
input: THIRD_POSITION_RESULT.keywords[0],
prefs: {
"quicksuggest.allowPositionInSuggestions": true,
},
providers: [
UrlbarProviderQuickSuggest.name,
SECOND_POSITION_INTERVENTION_RESULT_PROVIDER.name,
],
expected: [
EXPECTED_GENERAL_INTERVENTION_RESULT,
createExpectedQuickSuggestResult(THIRD_POSITION_RESULT),
],
},
];
add_setup(async function () {
// Setup for quick suggest result.
await QuickSuggestTestUtils.ensureQuickSuggestInit({
remoteSettingsRecords: [
{
type: "data",
attachment: [
SPONSORED_SECOND_POSITION_RESULT,
SPONSORED_NORMAL_POSITION_RESULT,
NONSPONSORED_SECOND_POSITION_RESULT,
NONSPONSORED_NORMAL_POSITION_RESULT,
FIRST_POSITION_RESULT,
SECOND_POSITION_RESULT,
THIRD_POSITION_RESULT,
],
},
],
prefs: [
["suggest.quicksuggest.sponsored", true],
["suggest.quicksuggest.nonsponsored", true],
],
});
// Setup for places result.
await PlacesUtils.history.clear();
await PlacesTestUtils.addVisits([
"http://example.com/" + SPONSORED_SECOND_POSITION_RESULT.keywords[0],
"http://example.com/" + SPONSORED_NORMAL_POSITION_RESULT.keywords[0],
"http://example.com/" + NONSPONSORED_SECOND_POSITION_RESULT.keywords[0],
"http://example.com/" + NONSPONSORED_NORMAL_POSITION_RESULT.keywords[0],
"http://example.com/" + SECOND_POSITION_RESULT.keywords[0],
]);
// Setup for tab-to-search result.
await SearchTestUtils.installSearchExtension({
name: "first",
search_url: `https://${TABTOSEARCH_ENGINE_DOMAIN_FOR_FIRST_POSITION_TEST}/`,
});
await SearchTestUtils.installSearchExtension({
name: "second",
search_url: `https://${TABTOSEARCH_ENGINE_DOMAIN_FOR_SECOND_POSITION_TEST}/`,
});
/// Setup for another intervention result.
UrlbarProvidersManager.registerProvider(
SECOND_POSITION_INTERVENTION_RESULT_PROVIDER
);
});
add_task(async function basic() {
for (const { description, input, prefs, providers, expected } of TEST_CASES) {
info(description);
for (let name in prefs) {
UrlbarPrefs.set(name, prefs[name]);
}
const context = createContext(input, {
providers,
isPrivate: false,
});
await check_results({
context,
matches: expected,
});
for (let name in prefs) {
UrlbarPrefs.clear(name);
}
}
});

View file

@ -36,9 +36,6 @@ skip-if = ["true"] # Bug 1880214
["test_quicksuggest_pocket.js"]
["test_quicksuggest_positionInSuggestions.js"]
skip-if = ["true"] # Bug 1880214
["test_quicksuggest_relevanceRanking.js"]
["test_quicksuggest_scoreMap.js"]

View file

@ -0,0 +1,70 @@
# Browser Startup
## How do first run/first startup experiments work?
Why does synchronously reading Nimbus feature values work for
customizing display features like `about:welcome` onboarding and the
default browser prompt? The key invariant is that the display
decisions wait for `sessionstore-windows-restored` to show
customizable UI, and therefore we just need Nimbus available to read
at that point. This is arranged either via the `--first-startup`
flag; or, for subsequent startups, the relevant Nimbus features being
marked `isEarlyStartup: true`. When `isEarlyStartup: true`, Nimbus
caches all its feature variables as Gecko preferences, ready to be
read during early startup. (See [the early startup
docs](https://experimenter.info/faq/early-startup/what-do-it-do).)
Customizable display features like `about:welcome` or the default
browser prompt are used in
[`_maybeShowDefaultBrowserPrompt()`](https://searchfox.org/mozilla-central/rev/a965e3c683ecc035dee1de72bd33a8d91b1203ed/browser/components/BrowserGlue.sys.mjs#4685),
which is invoked as part of a startup idle task. Startup idle tasks
are [scheduled in response to
`sessionstore-windows-restored`](https://searchfox.org/mozilla-central/rev/a965e3c683ecc035dee1de72bd33a8d91b1203ed/browser/components/BrowserGlue.sys.mjs#2423).
Now, why is `sessionstore-windows-restored` late enough for a
first startup experiment? The answer is subtle.
During Firefox launch, [`final-ui-startup` is
notified](https://searchfox.org/mozilla-central/rev/a965e3c683ecc035dee1de72bd33a8d91b1203ed/toolkit/xre/nsAppRunner.cpp#5764-5765),
and in response `SessionStore` is initialized. Additionally,
Nimbus/Normandy initialization is [started but not
awaited](https://searchfox.org/mozilla-central/rev/a965e3c683ecc035dee1de72bd33a8d91b1203ed/browser/components/BrowserGlue.sys.mjs#1487).
Then the [command line is
handled](https://searchfox.org/mozilla-central/rev/a965e3c683ecc035dee1de72bd33a8d91b1203ed/toolkit/xre/nsAppRunner.cpp#5775-5778).
When `--first-startup` is passed, we [spin the event loop to allow
Nimbus/Normandy time to complete its initialization and first
fetch](https://searchfox.org/mozilla-central/rev/a965e3c683ecc035dee1de72bd33a8d91b1203ed/browser/components/BrowserContentHandler.sys.mjs#677)
before continuing to process the command line. See [the
`FirstStartup`
module](https://firefox-source-docs.mozilla.org/toolkit/modules/toolkit_modules/FirstStartup.html).
(Important caveat: `--first-startup` is only used on Windows; see [Bug
1872934](https://bugzilla.mozilla.org/show_bug.cgi?id=1872934), for
example.)
This races with `SessionStore`, which itself waits for the first
browser window to be shown -- in particular, the
`sessionstore-windows-restored` notification waits for the [first
browser window's `browser-delayed-startup-finished`
notification](https://searchfox.org/mozilla-central/rev/a965e3c683ecc035dee1de72bd33a8d91b1203ed/browser/components/sessionstore/SessionStore.sys.mjs#2147-2159).
This first `browser-delayed-startup-finished` notification **is not
guaranteed** to be after `--first-startup` has spun the event loop!
But, when launched with only `--first-startup` and flags considered
very early in `nsAppRunner.cpp` -- as [the stub installer
does](https://searchfox.org/mozilla-central/rev/a965e3c683ecc035dee1de72bd33a8d91b1203ed/browser/installer/windows/nsis/stub.nsi#1424-1456)
-- then the first window **is guaranteed** to be after the event loop
has been spun, and therefore `sessionstore-windows-restored` is after
as well. (As a counter-example: try `firefox.exe --browser
--first-startup` and witness the [`--browser`
flag](https://searchfox.org/mozilla-central/rev/a965e3c683ecc035dee1de72bd33a8d91b1203ed/browser/components/BrowserContentHandler.sys.mjs#505-508)
creating a window before spinning the event loop, inadvertently racing
against `sessionstore-windows-restored`.) Making this deterministic
is tracked by [Bug
1944431](https://bugzilla.mozilla.org/show_bug.cgi?id=1944431).
Together, this means that first-startup experiments will be loaded in
time to impact display features such as `about:welcome` and the
default browser prompt, and we should not have a "split brain"
scenario in which the Nimbus feature is intermittently unavailable to
the relevant display features.

View file

@ -11,10 +11,11 @@ This is the nascent documentation of the Firefox front-end code.
BrowserUsageTelemetry
FrontendCodeReviewBestPractices
CommandLineParameters
BrowserStartup
components/customizableui/docs/index
components/enterprisepolicies/docs/index
extensions/formautofill/docs/index
components/newtab/docs/index
extensions/newtab/docs/index
components/aboutwelcome/docs/index
installer/windows/installer/index
components/attribution/docs/index

View file

@ -11,4 +11,5 @@ DIRS += [
"report-site-issue",
"pictureinpicture",
"search-detection",
"newtab",
]

View file

@ -114,6 +114,9 @@ module.exports = {
"accessor-pairs": ["error", { setWithoutGet: true, getWithoutSet: false }],
"array-callback-return": "error",
"block-scoped-var": "error",
// XXX Bug 1326071 - This should be reduced down - probably to 20 or to
// be removed & synced with the mozilla/recommended value.
complexity: ["error", 61],
"consistent-this": ["error", "use-bind"],
eqeqeq: "error",
"func-name-matching": "error",

View file

@ -14,7 +14,7 @@ const DEFAULT_OPTIONS = {
// Starting in newtab/bin/ and we want to write to newtab/prerendered/ so we
// go up one level.
addonPath: "..",
// depends on the registration in browser/components/newtab/jar.mn
// depends on the registration in browser/extensions/newtab/jar.mn
baseUrl: "resource://activity-stream/",
baseVendorUrl: "chrome://global/content/",
};

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