Update On Sun Apr 27 20:21:31 CEST 2025

This commit is contained in:
github-action[bot] 2025-04-27 20:21:32 +02:00
parent b4d5fcf244
commit cba8eeb49a
2509 changed files with 287913 additions and 64393 deletions

65
Cargo.lock generated
View file

@ -916,8 +916,6 @@ version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81"
dependencies = [
"serde",
"termcolor",
"unicode-width 0.2.0",
]
@ -1854,7 +1852,7 @@ dependencies = [
[[package]]
name = "error-support"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
dependencies = [
"error-support-macros",
"lazy_static",
@ -1866,7 +1864,7 @@ dependencies = [
[[package]]
name = "error-support-macros"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
dependencies = [
"proc-macro2",
"quote",
@ -1983,7 +1981,7 @@ dependencies = [
[[package]]
name = "firefox-versioning"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
dependencies = [
"serde_json",
"thiserror 1.999.999",
@ -3320,7 +3318,7 @@ dependencies = [
[[package]]
name = "interrupt-support"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
dependencies = [
"lazy_static",
"parking_lot",
@ -3452,6 +3450,7 @@ dependencies = [
"firefox-on-glean",
"log",
"mozbuild",
"nserror",
"nsstring",
"once_cell",
"serde",
@ -4587,11 +4586,12 @@ checksum = "a2983372caf4480544083767bf2d27defafe32af49ab4df3a0b7fc90793a3664"
[[package]]
name = "naga"
version = "25.0.0"
source = "git+https://github.com/gfx-rs/wgpu?rev=f1c496523ff0aa10c162fd01ad606960e925a5a4#f1c496523ff0aa10c162fd01ad606960e925a5a4"
source = "git+https://github.com/gfx-rs/wgpu?rev=35f131ff10153e48b790e1f156c864734063ce71#35f131ff10153e48b790e1f156c864734063ce71"
dependencies = [
"arrayvec",
"bit-set",
"bitflags 2.9.0",
"cfg-if",
"cfg_aliases",
"codespan-reporting",
"half 2.5.0",
@ -4604,7 +4604,7 @@ dependencies = [
"rustc-hash 1.999.999",
"serde",
"spirv",
"strum 0.26.999",
"strum 0.27.1",
"thiserror 2.0.9",
"unicode-ident",
]
@ -5035,7 +5035,7 @@ checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba"
[[package]]
name = "payload-support"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
dependencies = [
"serde",
"serde_derive",
@ -5474,9 +5474,9 @@ dependencies = [
[[package]]
name = "raw-window-handle"
version = "0.6.0"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42a9830a0e1b9fb145ebb365b8bc4ccd75f290f98c0247deafbbe2c75cefb544"
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
[[package]]
name = "rayon"
@ -5538,7 +5538,7 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "relevancy"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
dependencies = [
"anyhow",
"base64 0.21.999",
@ -5563,7 +5563,7 @@ dependencies = [
[[package]]
name = "remote_settings"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
dependencies = [
"anyhow",
"camino",
@ -5692,13 +5692,6 @@ dependencies = [
"serde",
]
[[package]]
name = "ron"
version = "0.9.999"
dependencies = [
"ron 0.10.1",
]
[[package]]
name = "ron"
version = "0.10.1"
@ -5911,7 +5904,7 @@ dependencies = [
[[package]]
name = "search"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
dependencies = [
"error-support",
"firefox-versioning",
@ -6202,7 +6195,7 @@ dependencies = [
[[package]]
name = "sql-support"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
dependencies = [
"interrupt-support",
"lazy_static",
@ -6408,7 +6401,7 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]]
name = "suggest"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
dependencies = [
"anyhow",
"chrono",
@ -6460,7 +6453,7 @@ dependencies = [
[[package]]
name = "sync-guid"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
dependencies = [
"base64 0.21.999",
"rand",
@ -6471,7 +6464,7 @@ dependencies = [
[[package]]
name = "sync15"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
dependencies = [
"anyhow",
"error-support",
@ -6511,7 +6504,7 @@ dependencies = [
[[package]]
name = "tabs"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
dependencies = [
"anyhow",
"error-support",
@ -6855,7 +6848,7 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "types"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
dependencies = [
"rusqlite 0.33.0",
"serde",
@ -7237,7 +7230,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "viaduct"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
dependencies = [
"ffi-support",
"log",
@ -7407,7 +7400,7 @@ dependencies = [
[[package]]
name = "webext-storage"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
dependencies = [
"anyhow",
"error-support",
@ -7452,7 +7445,7 @@ dependencies = [
"peek-poke",
"plane-split",
"rayon",
"ron 0.10.1",
"ron",
"serde",
"smallvec",
"svg_fmt",
@ -7546,7 +7539,7 @@ dependencies = [
[[package]]
name = "wgpu-core"
version = "25.0.0"
source = "git+https://github.com/gfx-rs/wgpu?rev=f1c496523ff0aa10c162fd01ad606960e925a5a4#f1c496523ff0aa10c162fd01ad606960e925a5a4"
source = "git+https://github.com/gfx-rs/wgpu?rev=35f131ff10153e48b790e1f156c864734063ce71#35f131ff10153e48b790e1f156c864734063ce71"
dependencies = [
"arrayvec",
"bit-set",
@ -7562,7 +7555,7 @@ dependencies = [
"once_cell",
"parking_lot",
"profiling",
"ron 0.9.999",
"ron",
"rustc-hash 1.999.999",
"serde",
"smallvec",
@ -7576,7 +7569,7 @@ dependencies = [
[[package]]
name = "wgpu-core-deps-apple"
version = "25.0.0"
source = "git+https://github.com/gfx-rs/wgpu?rev=f1c496523ff0aa10c162fd01ad606960e925a5a4#f1c496523ff0aa10c162fd01ad606960e925a5a4"
source = "git+https://github.com/gfx-rs/wgpu?rev=35f131ff10153e48b790e1f156c864734063ce71#35f131ff10153e48b790e1f156c864734063ce71"
dependencies = [
"wgpu-hal",
]
@ -7584,7 +7577,7 @@ dependencies = [
[[package]]
name = "wgpu-core-deps-windows-linux-android"
version = "25.0.0"
source = "git+https://github.com/gfx-rs/wgpu?rev=f1c496523ff0aa10c162fd01ad606960e925a5a4#f1c496523ff0aa10c162fd01ad606960e925a5a4"
source = "git+https://github.com/gfx-rs/wgpu?rev=35f131ff10153e48b790e1f156c864734063ce71#35f131ff10153e48b790e1f156c864734063ce71"
dependencies = [
"wgpu-hal",
]
@ -7592,7 +7585,7 @@ dependencies = [
[[package]]
name = "wgpu-hal"
version = "25.0.0"
source = "git+https://github.com/gfx-rs/wgpu?rev=f1c496523ff0aa10c162fd01ad606960e925a5a4#f1c496523ff0aa10c162fd01ad606960e925a5a4"
source = "git+https://github.com/gfx-rs/wgpu?rev=35f131ff10153e48b790e1f156c864734063ce71#35f131ff10153e48b790e1f156c864734063ce71"
dependencies = [
"android_system_properties",
"arrayvec",
@ -7628,7 +7621,7 @@ dependencies = [
[[package]]
name = "wgpu-types"
version = "25.0.0"
source = "git+https://github.com/gfx-rs/wgpu?rev=f1c496523ff0aa10c162fd01ad606960e925a5a4#f1c496523ff0aa10c162fd01ad606960e925a5a4"
source = "git+https://github.com/gfx-rs/wgpu?rev=35f131ff10153e48b790e1f156c864734063ce71#35f131ff10153e48b790e1f156c864734063ce71"
dependencies = [
"bitflags 2.9.0",
"bytemuck",

View file

@ -214,9 +214,6 @@ half = { path = "build/rust/half" }
# Upgrade `rusqlite` 0.31 to 0.33.
rusqlite = { path = "build/rust/rusqlite" }
# Patch `ron` 0.9.* to 0.10.
ron = { path = "build/rust/ron" }
# Patch `strum` 0.26.* to 0.27.
strum = { path = "build/rust/strum" }
@ -263,14 +260,14 @@ malloc_size_of_derive = { path = "xpcom/rust/malloc_size_of_derive" }
objc = { git = "https://github.com/glandium/rust-objc", rev = "4de89f5aa9851ceca4d40e7ac1e2759410c04324" }
# application-services overrides to make updating them all simpler.
interrupt-support = { git = "https://github.com/mozilla/application-services", rev = "41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8" }
relevancy = { git = "https://github.com/mozilla/application-services", rev = "41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8" }
search = { git = "https://github.com/mozilla/application-services", rev = "41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8" }
sql-support = { git = "https://github.com/mozilla/application-services", rev = "41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8" }
suggest = { git = "https://github.com/mozilla/application-services", rev = "41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8" }
sync15 = { git = "https://github.com/mozilla/application-services", rev = "41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8" }
tabs = { git = "https://github.com/mozilla/application-services", rev = "41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8" }
viaduct = { git = "https://github.com/mozilla/application-services", rev = "41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8" }
webext-storage = { git = "https://github.com/mozilla/application-services", rev = "41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8" }
interrupt-support = { git = "https://github.com/mozilla/application-services", rev = "280db3a3a06a8f517151ff0b84b5ce67fcc7afd6" }
relevancy = { git = "https://github.com/mozilla/application-services", rev = "280db3a3a06a8f517151ff0b84b5ce67fcc7afd6" }
search = { git = "https://github.com/mozilla/application-services", rev = "280db3a3a06a8f517151ff0b84b5ce67fcc7afd6" }
sql-support = { git = "https://github.com/mozilla/application-services", rev = "280db3a3a06a8f517151ff0b84b5ce67fcc7afd6" }
suggest = { git = "https://github.com/mozilla/application-services", rev = "280db3a3a06a8f517151ff0b84b5ce67fcc7afd6" }
sync15 = { git = "https://github.com/mozilla/application-services", rev = "280db3a3a06a8f517151ff0b84b5ce67fcc7afd6" }
tabs = { git = "https://github.com/mozilla/application-services", rev = "280db3a3a06a8f517151ff0b84b5ce67fcc7afd6" }
viaduct = { git = "https://github.com/mozilla/application-services", rev = "280db3a3a06a8f517151ff0b84b5ce67fcc7afd6" }
webext-storage = { git = "https://github.com/mozilla/application-services", rev = "280db3a3a06a8f517151ff0b84b5ce67fcc7afd6" }
allocator-api2 = { path = "third_party/rust/allocator-api2" }

View file

@ -63,9 +63,7 @@ class Settings final
SessionAccessibility::SessionAccessibility(
jni::NativeWeakPtr<widget::GeckoViewSupport> aWindow,
java::SessionAccessibility::NativeProvider::Param aSessionAccessibility)
: mWindow(aWindow), mSessionAccessibility(aSessionAccessibility) {
SetAttached(true, nullptr);
}
: mWindow(aWindow), mSessionAccessibility(aSessionAccessibility) {}
void SessionAccessibility::SetAttached(bool aAttached,
already_AddRefed<Runnable> aRunnable) {

View file

@ -1612,6 +1612,22 @@ bool aria::IsValidARIAHidden(nsIContent* aContent) {
!ShouldIgnoreARIAHidden(aContent);
}
bool aria::IsValidARIAHidden(DocAccessible* aDocAcc) {
nsCOMPtr<nsIContent> docContent = aDocAcc->GetContent();
// First, check if our Doc Accessible has aria-hidden set on its content
bool isValid = IsValidARIAHidden(docContent);
// If our Doc Accessible was created using an element other than the
// root element, we need to verify the validity of any aria-hidden on
// the root element as well.
auto* rootElement = aDocAcc->DocumentNode()->GetRootElement();
if (docContent != rootElement) {
isValid |= IsValidARIAHidden(rootElement);
}
return isValid;
}
bool aria::ShouldIgnoreARIAHidden(nsIContent* aContent) {
if (!aContent) {
return false;

View file

@ -10,6 +10,7 @@
#include "ARIAStateMap.h"
#include "mozilla/a11y/AccTypes.h"
#include "mozilla/a11y/DocAccessible.h"
#include "mozilla/a11y/Role.h"
#include "nsAtom.h"
@ -310,6 +311,14 @@ uint8_t AttrCharacteristicsFor(nsAtom* aAtom);
*/
bool IsValidARIAHidden(nsIContent* aContent);
/**
* This function calls into the function above. It verifies the validity
* of any `aria-hidden` specified on the given Doc Accessible's
* mContent, as well as on the root element of mContent's owner
* doc.
*/
bool IsValidARIAHidden(DocAccessible* aDocAcc);
/**
* Return true if the element should render its subtree
* regardless of the presence of aria-hidden.

View file

@ -38,6 +38,26 @@ bool EventQueue::PushEvent(AccEvent* aEvent) {
return true;
}
if (aEvent->mEventRule == AccEvent::eRemoveDupes && !mEvents.IsEmpty()) {
// Check for duplicate events. If aEvent is identical to an older event, do
// not append aEvent. We do this here rather than in CoalesceEvents because
// CoalesceEvents never *removes* events; it only sets them to eDoNotEmit.
// If there are many duplicate events and we appended them, this would
// result in a massive event queue and coalescing would become increasingly
// slow with each event queued. Doing it here, we avoid appending a
// duplicate event in the first place.
uint32_t last = mEvents.Length() - 1;
for (uint32_t index = last; index <= last; --index) {
AccEvent* checkEvent = mEvents[index];
if (checkEvent->mEventType == aEvent->mEventType &&
checkEvent->mEventRule == aEvent->mEventRule &&
checkEvent->mAccessible == aEvent->mAccessible) {
aEvent->mEventRule = AccEvent::eDoNotEmit;
return true;
}
}
}
// XXX(Bug 1631371) Check if this should use a fallible operation as it
// pretended earlier, or change the return type to void.
mEvents.AppendElement(aEvent);
@ -45,10 +65,10 @@ bool EventQueue::PushEvent(AccEvent* aEvent) {
// Filter events.
CoalesceEvents();
if (aEvent->mEventRule != AccEvent::eDoNotEmit &&
(aEvent->mEventType == nsIAccessibleEvent::EVENT_NAME_CHANGE ||
if (aEvent->mEventType == nsIAccessibleEvent::EVENT_NAME_CHANGE ||
aEvent->mEventType == nsIAccessibleEvent::EVENT_TEXT_REMOVED ||
aEvent->mEventType == nsIAccessibleEvent::EVENT_TEXT_INSERTED)) {
aEvent->mEventType == nsIAccessibleEvent::EVENT_TEXT_INSERTED) {
MOZ_ASSERT(aEvent->mEventRule != AccEvent::eDoNotEmit);
PushNameOrDescriptionChange(aEvent);
}
return true;
@ -258,23 +278,9 @@ void EventQueue::CoalesceEvents() {
break; // eCoalesceTextSelChange
}
case AccEvent::eRemoveDupes: {
// Check for repeat events, coalesce newly appended event by more older
// event.
for (uint32_t index = tail - 1; index < tail; index--) {
AccEvent* accEvent = mEvents[index];
if (accEvent->mEventType == tailEvent->mEventType &&
accEvent->mEventRule == tailEvent->mEventRule &&
accEvent->mAccessible == tailEvent->mAccessible) {
tailEvent->mEventRule = AccEvent::eDoNotEmit;
return;
}
}
break; // case eRemoveDupes
}
default:
break; // case eAllowDupes, eDoNotEmit
// eRemoveDupes is handled in PushEvent.
break; // case eRemoveDupes, eAllowDupes, eDoNotEmit
} // switch
}

View file

@ -5,6 +5,7 @@
#include "TreeWalker.h"
#include "ARIAMap.h"
#include "nsAccessibilityService.h"
#include "DocAccessible.h"
@ -323,7 +324,8 @@ LocalAccessible* TreeWalker::AccessibleFor(nsIContent* aNode, uint32_t aFlags,
}
// Create an accessible if allowed.
if (!(aFlags & eWalkCache) && mContext->IsAcceptableChild(aNode)) {
if (!(aFlags & eWalkCache) && mContext->IsAcceptableChild(aNode) &&
!aria::IsValidARIAHidden(mDoc)) {
mDoc->RelocateARIAOwnedIfNeeded(aNode);
return GetAccService()->CreateAccessible(aNode, mContext, aSkipSubtree);
}

View file

@ -49,3 +49,39 @@ addAccessibleTask(
},
{ chrome: true, topLevel: false }
);
/**
* Test that switching from a non-remote document to a remote document fires
* focus correctly.
*/
addAccessibleTask(
``,
async function testLocalThenRemoteWithAutofocus(browser) {
info("Loading example.com with autofocus into same tab");
// The accessibility test harness removes maychangeremoteness when we run a
// chrome test, but we explicitly want to change remoteness now.
browser.setAttribute("maychangeremoteness", "true");
const url =
"https://example.com/document-builder.sjs?html=" +
encodeURIComponent(`<!doctype html><input autofocus>`);
let loaded = BrowserTestUtils.browserLoaded(browser);
browser.loadURI(Services.io.newURI(url), {
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
});
await loaded;
is(Services.focus.focusedElement, browser, "<browser> should be focused");
is(
Services.focus.focusedWindow,
window,
"window should be properly focused"
);
await SpecialPowers.spawn(browser, [], async () => {
is(
content.windowUtils.IMEStatus,
content.windowUtils.IME_STATUS_ENABLED,
"IME should be enabled"
);
});
},
{ chrome: true, topLevel: false }
);

View file

@ -30,6 +30,10 @@ skip-if = [
["browser_test_aria_hidden.js"]
["browser_test_aria_hidden_iframe.js"]
["browser_test_aria_hidden_svg.js"]
["browser_searchbar.js"]
["browser_select.js"]

View file

@ -7,11 +7,37 @@
loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
/**
* Verify loading a root doc with aria-hidden renders the document.
* Non-root doc elements, like embedded iframes, should continue
* Verify loading a tab document with aria-hidden specified on the root element
* correctly renders the root element and its content. This test is meaninfully
* different from testTabDocument which tests aria-hidden specified on the
* body element.
*/
addAccessibleTask(
`<html aria-hidden="true"><u>hello world`,
async function testTabRootDocument(_, accDoc) {
const tree = {
DOCUMENT: [
{
TEXT_LEAF: [],
},
],
};
testAccessibleTree(accDoc, tree);
},
{
chrome: true,
topLevel: true,
iframe: false,
remoteIframe: false,
}
);
/**
* Verify loading a tab doc with aria-hidden on the body renders the document.
* Body elements inside of embedded iframes, should continue
* to respect aria-hidden when present. This test ONLY tests
* tab documents, it should not run in iframes. There is a separate
* test for iframes below.
* test for iframes in browser_test_aria_hidden_iframe.js.
*/
addAccessibleTask(
`
@ -35,7 +61,7 @@ addAccessibleTask(
* Non-root doc elements, like embedded iframes, should continue
* to respect aria-hidden when applied. This test ONLY tests
* tab documents, it should not run in iframes. There is a separate
* test for iframes below.
* test for iframes in browser_test_aria_hidden_iframe.js.
*/
addAccessibleTask(
`
@ -56,274 +82,3 @@ addAccessibleTask(
},
{ chrome: true, topLevel: true, iframe: false, remoteIframe: false }
);
/**
* Verify loading an iframe doc with aria-hidden doesn't render the document.
* This test ONLY tests iframe documents, it should not run in tab docs.
* There is a separate test for tab docs above.
*/
addAccessibleTask(
`
<p id="content">I am some content in a document</p>
`,
async function testIframeDocument(browser, docAcc, topLevel) {
const originalTree = { DOCUMENT: [{ INTERNAL_FRAME: [{ DOCUMENT: [] }] }] };
testAccessibleTree(topLevel, originalTree);
},
{
chrome: false,
topLevel: false,
iframe: true,
remoteIframe: true,
iframeDocBodyAttrs: { "aria-hidden": "true" },
}
);
/**
* Verify adding aria-hidden to iframe doc elements removes
* their subtree. This test ONLY tests iframe documents, it
* should not run in tab documents. There is a separate test for
* tab documents above.
*/
addAccessibleTask(
`
<p id="content">I am some content in a document</p>
`,
async function testIframeDocumentMutation(browser, docAcc, topLevel) {
const originalTree = {
DOCUMENT: [
{
INTERNAL_FRAME: [
{
DOCUMENT: [
{
PARAGRAPH: [
{
TEXT_LEAF: [],
},
],
},
],
},
],
},
],
};
testAccessibleTree(topLevel, originalTree);
info("Adding aria-hidden=true to content doc");
await contentSpawnMutation(
browser,
{ expected: [[EVENT_REORDER, docAcc]] },
function () {
const b = content.document.body;
b.setAttribute("aria-hidden", "true");
}
);
const newTree = {
DOCUMENT: [
{
INTERNAL_FRAME: [
{
DOCUMENT: [],
},
],
},
],
};
testAccessibleTree(topLevel, newTree);
},
{ chrome: false, topLevel: false, iframe: true, remoteIframe: true }
);
// // ///////////////////////////////
// // //////////////////// SVG Tests
// // //////////////////////////////
const SVG_DOCUMENT_ID = "rootSVG";
const HIDDEN_SVG_URI =
"data:image/svg+xml,%3Csvg%20id%3D%22rootSVG%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20aria-hidden%3D%22true%22%3E%3Ctext%20x%3D%2210%22%20y%3D%2250%22%20font-size%3D%2230%22%20id%3D%22textSVG%22%3EMy%20SVG%3C%2Ftext%3E%3C%2Fsvg%3E";
const SVG_URI =
"data:image/svg+xml,%3Csvg%20id%3D%22rootSVG%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctext%20x%3D%2210%22%20y%3D%2250%22%20font-size%3D%2230%22%20id%3D%22textSVG%22%3EMy%20SVG%3C%2Ftext%3E%3C%2Fsvg%3E";
/**
* Verify loading an SVG document with aria-hidden=true renders the
* entire document subtree.
* Non-root svg elements, like those in embedded iframes, should
* continue to respect aria-hidden when applied.
*/
addAccessibleTask(
`hello world`,
async function testSVGDocument(browser) {
let loaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, SVG_DOCUMENT_ID);
info("Loading SVG");
browser.loadURI(Services.io.newURI(HIDDEN_SVG_URI), {
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
});
await loaded;
const tree = {
DOCUMENT: [
{
TEXT_CONTAINER: [
{
TEXT_LEAF: [],
},
],
},
],
};
const root = getRootAccessible(document);
const svgRoot = findAccessibleChildByID(root, SVG_DOCUMENT_ID);
testAccessibleTree(svgRoot, tree);
},
{ chrome: true, topLevel: true, iframe: false, remoteIframe: false }
);
///////////
///// TODO: Bug 1960416
//////////
// /**
// * Verify loading an SVG document with aria-hidden=true
// * in an iframe does not render the document subtree.
// */
// addAccessibleTask(
// `hello world`,
// async function testSVGIframeDocument(browser) {
// info("Loading SVG");
// const loaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, SVG_DOCUMENT_ID);
// await SpecialPowers.spawn(browser, [DEFAULT_IFRAME_ID, HIDDEN_SVG_URI], (_id,_uri) => {
// content.document.getElementById(_id).src = _uri;
// });
// await loaded;
// const tree = {
// DOCUMENT: [],
// };
// const root = getRootAccessible(document);
// const svgRoot = findAccessibleChildByID(root, SVG_DOCUMENT_ID);
// testAccessibleTree(svgRoot, tree);
// },
// { chrome: false, topLevel: false, iframe: true, remoteIframe: true }
// );
/**
* Verify adding aria-hidden to root svg elements has no effect.
* Non-root svg elements, like those in embedded iframes, should
* continue to respect aria-hidden when applied.
*/
addAccessibleTask(
`hello world`,
async function testSVGDocumentMutation(browser) {
let loaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, SVG_DOCUMENT_ID);
info("Loading SVG");
browser.loadURI(Services.io.newURI(SVG_URI), {
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
});
await loaded;
const originalTree = {
DOCUMENT: [
{
TEXT_CONTAINER: [
{
TEXT_LEAF: [],
},
],
},
],
};
const root = getRootAccessible(document);
const svgRoot = findAccessibleChildByID(root, SVG_DOCUMENT_ID);
testAccessibleTree(svgRoot, originalTree);
info("Adding aria-hidden=true to svg");
// XXX Bug 1959547: We incorrectly get a reorder
// here. The tree should be unaffected by this attribute,
// but it seems like it isn't! Below we'll verify that
// the tree isn't removed, despite this reorder.
const unexpectedEvents = { expected: [[EVENT_REORDER, SVG_DOCUMENT_ID]] };
info("Adding aria-hidden");
await contentSpawnMutation(
browser,
unexpectedEvents,
function (_id) {
const d = content.document.getElementById(_id);
d.setAttribute("aria-hidden", "true");
},
[SVG_DOCUMENT_ID]
);
// XXX Bug 1959547: We end up with an extra node in the
// tree after adding aria-hidden. It seems like SVG root
// element is splitting off / no longer behaves as the
// document...?
const newTree = {
DOCUMENT: [
{
DIAGRAM: [
{
TEXT_CONTAINER: [
{
TEXT_LEAF: [],
},
],
},
],
},
],
};
testAccessibleTree(svgRoot, newTree);
},
{ chrome: true, topLevel: true, iframe: false, remoteIframe: false }
);
/**
* Verify adding aria-hidden to root svg elements in iframes removes
* the svg subtree.
*/
addAccessibleTask(
`hello world`,
async function testSVGIframeDocumentMutation(browser) {
info("Loading SVG");
const loaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, SVG_DOCUMENT_ID);
await SpecialPowers.spawn(
browser,
[DEFAULT_IFRAME_ID, SVG_URI],
(contentId, _uri) => {
content.document.getElementById(contentId).src = _uri;
}
);
await loaded;
const originalTree = {
DOCUMENT: [
{
TEXT_CONTAINER: [
{
TEXT_LEAF: [],
},
],
},
],
};
const svgRoot = findAccessibleChildByID(
getRootAccessible(document),
SVG_DOCUMENT_ID
);
testAccessibleTree(svgRoot, originalTree);
info("Adding aria-hidden=true to svg");
const events = { expected: [[EVENT_REORDER, SVG_DOCUMENT_ID]] };
await contentSpawnMutation(
browser,
events,
function (_id) {
const d = content.document.getElementById(_id);
d.setAttribute("aria-hidden", "true");
},
[SVG_DOCUMENT_ID]
);
const newTree = { DOCUMENT: [] };
testAccessibleTree(svgRoot, newTree);
},
{ chrome: false, topLevel: false, iframe: true, remoteIframe: true }
);

View file

@ -0,0 +1,127 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/* import-globals-from ../../mochitest/role.js */
loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
/**
* Verify loading an iframe document with aria-hidden specified on the root element
* correctly hides the root element and its content. This test is meaninfully
* different from testIframeDocument which tests aria-hidden specified on the
* body element.
*/
addAccessibleTask(
`hello world`,
async function testIframeRootDocument(browser) {
info("Loading iframe document");
const HIDDEN_IFRAME_URI =
"data:text/html,<html id='new_html' aria-hidden='true'><body id='iframeBody'><u>hello world</u></body></html>";
const loaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, "iframeBody");
await SpecialPowers.spawn(
browser,
[DEFAULT_IFRAME_ID, HIDDEN_IFRAME_URI],
(_id, _uri) => {
content.document.getElementById(_id).src = _uri;
}
);
await loaded;
const tree = {
INTERNAL_FRAME: [
{
DOCUMENT: [],
},
],
};
const root = getRootAccessible(document);
const iframeDoc = findAccessibleChildByID(root, DEFAULT_IFRAME_ID);
testAccessibleTree(iframeDoc, tree);
},
{
chrome: false,
topLevel: false,
iframe: true,
remoteIframe: true,
}
);
/**
* Verify loading an iframe doc with aria-hidden doesn't render the document.
* This test ONLY tests iframe documents, it should not run in tab docs.
* There is a separate test for tab docs in browser_test_aria_hidden.js.
*/
addAccessibleTask(
`
<p id="content">I am some content in a document</p>
`,
async function testIframeDocument(browser, docAcc, topLevel) {
const originalTree = { DOCUMENT: [{ INTERNAL_FRAME: [{ DOCUMENT: [] }] }] };
testAccessibleTree(topLevel, originalTree);
},
{
chrome: false,
topLevel: false,
iframe: true,
remoteIframe: true,
iframeDocBodyAttrs: { "aria-hidden": "true" },
}
);
/**
* Verify adding aria-hidden to iframe doc elements removes
* their subtree. This test ONLY tests iframe documents, it
* should not run in tab documents. There is a separate test for
* tab documents in browser_test_aria_hidden.js.
*/
addAccessibleTask(
`
<p id="content">I am some content in a document</p>
`,
async function testIframeDocumentMutation(browser, docAcc, topLevel) {
const originalTree = {
DOCUMENT: [
{
INTERNAL_FRAME: [
{
DOCUMENT: [
{
PARAGRAPH: [
{
TEXT_LEAF: [],
},
],
},
],
},
],
},
],
};
testAccessibleTree(topLevel, originalTree);
info("Adding aria-hidden=true to content doc");
await contentSpawnMutation(
browser,
{ expected: [[EVENT_REORDER, docAcc]] },
function () {
const b = content.document.body;
b.setAttribute("aria-hidden", "true");
}
);
const newTree = {
DOCUMENT: [
{
INTERNAL_FRAME: [
{
DOCUMENT: [],
},
],
},
],
};
testAccessibleTree(topLevel, newTree);
},
{ chrome: false, topLevel: false, iframe: true, remoteIframe: true }
);

View file

@ -0,0 +1,197 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/* import-globals-from ../../mochitest/role.js */
loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
const SVG_DOCUMENT_ID = "rootSVG";
const HIDDEN_SVG_URI =
"data:image/svg+xml,%3Csvg%20id%3D%22rootSVG%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20aria-hidden%3D%22true%22%3E%3Ctext%20x%3D%2210%22%20y%3D%2250%22%20font-size%3D%2230%22%20id%3D%22textSVG%22%3EMy%20SVG%3C%2Ftext%3E%3C%2Fsvg%3E";
const SVG_URI =
"data:image/svg+xml,%3Csvg%20id%3D%22rootSVG%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctext%20x%3D%2210%22%20y%3D%2250%22%20font-size%3D%2230%22%20id%3D%22textSVG%22%3EMy%20SVG%3C%2Ftext%3E%3C%2Fsvg%3E";
/**
* Verify loading an SVG document with aria-hidden=true renders the
* entire document subtree.
* Non-root svg elements, like those in embedded iframes, should
* continue to respect aria-hidden when applied.
*/
addAccessibleTask(
`hello world`,
async function testSVGDocument(browser) {
let loaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, SVG_DOCUMENT_ID);
info("Loading SVG");
browser.loadURI(Services.io.newURI(HIDDEN_SVG_URI), {
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
});
await loaded;
const tree = {
DOCUMENT: [
{
TEXT_CONTAINER: [
{
TEXT_LEAF: [],
},
],
},
],
};
const root = getRootAccessible(document);
const svgRoot = findAccessibleChildByID(root, SVG_DOCUMENT_ID);
testAccessibleTree(svgRoot, tree);
},
{ chrome: true, topLevel: true, iframe: false, remoteIframe: false }
);
/**
* Verify loading an SVG document with aria-hidden=true
* in an iframe does not render the document subtree.
*/
addAccessibleTask(
`hello world`,
async function testSVGIframeDocument(browser) {
info("Loading SVG");
const loaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, SVG_DOCUMENT_ID);
await SpecialPowers.spawn(
browser,
[DEFAULT_IFRAME_ID, HIDDEN_SVG_URI],
(_id, _uri) => {
content.document.getElementById(_id).src = _uri;
}
);
await loaded;
const tree = {
DOCUMENT: [],
};
const root = getRootAccessible(document);
const svgRoot = findAccessibleChildByID(root, SVG_DOCUMENT_ID);
testAccessibleTree(svgRoot, tree);
},
{ chrome: false, topLevel: false, iframe: true, remoteIframe: true }
);
/**
* Verify adding aria-hidden to root svg elements has no effect.
* Non-root svg elements, like those in embedded iframes, should
* continue to respect aria-hidden when applied.
*/
addAccessibleTask(
`hello world`,
async function testSVGDocumentMutation(browser) {
let loaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, SVG_DOCUMENT_ID);
info("Loading SVG");
browser.loadURI(Services.io.newURI(SVG_URI), {
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
});
await loaded;
const originalTree = {
DOCUMENT: [
{
TEXT_CONTAINER: [
{
TEXT_LEAF: [],
},
],
},
],
};
const root = getRootAccessible(document);
const svgRoot = findAccessibleChildByID(root, SVG_DOCUMENT_ID);
testAccessibleTree(svgRoot, originalTree);
info("Adding aria-hidden=true to svg");
// XXX Bug 1959547: We incorrectly get a reorder
// here. The tree should be unaffected by this attribute,
// but it seems like it isn't! Below we'll verify that
// the tree isn't removed, despite this reorder.
const unexpectedEvents = { expected: [[EVENT_REORDER, SVG_DOCUMENT_ID]] };
info("Adding aria-hidden");
await contentSpawnMutation(
browser,
unexpectedEvents,
function (_id) {
const d = content.document.getElementById(_id);
d.setAttribute("aria-hidden", "true");
},
[SVG_DOCUMENT_ID]
);
// XXX Bug 1959547: We end up with an extra node in the
// tree after adding aria-hidden. It seems like SVG root
// element is splitting off / no longer behaves as the
// document...?
const newTree = {
DOCUMENT: [
{
DIAGRAM: [
{
TEXT_CONTAINER: [
{
TEXT_LEAF: [],
},
],
},
],
},
],
};
testAccessibleTree(svgRoot, newTree);
},
{ chrome: true, topLevel: true, iframe: false, remoteIframe: false }
);
/**
* Verify adding aria-hidden to root svg elements in iframes removes
* the svg subtree.
*/
addAccessibleTask(
`hello world`,
async function testSVGIframeDocumentMutation(browser) {
info("Loading SVG");
const loaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, SVG_DOCUMENT_ID);
await SpecialPowers.spawn(
browser,
[DEFAULT_IFRAME_ID, SVG_URI],
(contentId, _uri) => {
content.document.getElementById(contentId).src = _uri;
}
);
await loaded;
const originalTree = {
DOCUMENT: [
{
TEXT_CONTAINER: [
{
TEXT_LEAF: [],
},
],
},
],
};
const svgRoot = findAccessibleChildByID(
getRootAccessible(document),
SVG_DOCUMENT_ID
);
testAccessibleTree(svgRoot, originalTree);
info("Adding aria-hidden=true to svg");
const events = { expected: [[EVENT_REORDER, SVG_DOCUMENT_ID]] };
await contentSpawnMutation(
browser,
events,
function (_id) {
const d = content.document.getElementById(_id);
d.setAttribute("aria-hidden", "true");
},
[SVG_DOCUMENT_ID]
);
const newTree = { DOCUMENT: [] };
testAccessibleTree(svgRoot, newTree);
},
{ chrome: false, topLevel: false, iframe: true, remoteIframe: true }
);

14
aclocal.m4 vendored
View file

@ -1,14 +0,0 @@
dnl
dnl Local autoconf macros used with mozilla
dnl The contents of this file are under the Public Domain.
dnl
builtin(include, build/autoconf/hooks.m4)dnl
builtin(include, build/autoconf/config.status.m4)dnl
builtin(include, build/autoconf/altoptions.m4)dnl
# Read the user's .mozconfig script. We can't do this in
# configure.in: autoconf puts the argument parsing code above anything
# expanded from configure.in, and we need to get the configure options
# from .mozconfig in place before that argument parsing code.
MOZ_READ_MOZCONFIG(.)

View file

@ -1650,7 +1650,7 @@ export class SearchSERPTelemetryChild extends JSWindowActorChild {
* @param {object} event The event details.
*/
handleEvent(event) {
if (!this.#urlIsSERP(this.document.documentURI)) {
if (!this.#urlIsSERP()) {
return;
}
switch (event.type) {
@ -1719,7 +1719,7 @@ export class SearchSERPTelemetryChild extends JSWindowActorChild {
documentToSubmitMap.delete(this.document);
}
#urlIsSERP(url) {
#urlIsSERP() {
let provider = this._getProviderInfoForUrl(this.document.documentURI);
if (provider) {
// Some URLs can match provider info but also be the provider's homepage
@ -1727,7 +1727,7 @@ export class SearchSERPTelemetryChild extends JSWindowActorChild {
// e.g. https://example.com/ vs. https://example.com/?foo=bar
// To check this, we look for the presence of the query parameter
// that contains a search term.
let queries = new URLSearchParams(url.split("#")[0].split("?")[1]);
let queries = URL.fromURI(this.document.documentURIObject).searchParams;
for (let queryParamName of provider.queryParamNames) {
if (queries.get(queryParamName)) {
return true;

View file

@ -208,19 +208,6 @@ pref("app.update.langpack.enabled", true);
pref("app.update.noWindowAutoRestart.delayMs", 300000);
#endif
// The Multi Session Install Lockout prevents updates from being installed at
// startup when they normally would be if there are other instances using the
// installation. We only do this for a limited amount of time before we go ahead
// and apply the update anyways.
// Hopefully, at some point, updating Firefox while it is running will not break
// things and this mechanism can be removed.
// Note that these prefs are bit dangerous because having different values in
// different profiles could cause erratic behavior.
// This feature is also affected by
// `app.update.multiSessionInstallLockout.timeoutMs`, which is in the branding
// section.
pref("app.update.multiSessionInstallLockout.enabled", false);
#if defined(MOZ_BACKGROUNDTASKS)
// The amount of time, in seconds, before background tasks time out and exit.
// Tasks can override this default (10 minutes).
@ -388,21 +375,12 @@ pref("browser.overlink-delay", 80);
pref("browser.taskbarTabs.enabled", false);
pref("browser.theme.colorway-closet", true);
#if defined(MOZ_WIDGET_GTK)
pref("browser.theme.native-theme", true);
#else
pref("browser.theme.native-theme", false);
#endif
// Whether expired built-in colorways themes that are active or retained
// should be allowed to check for updates and be updated to an AMO hosted
// theme with the same id (as part of preparing to remove from mozilla-central
// all the expired built-in colorways themes, after existing users have been
// migrated to colorways themes hosted on AMO).
pref("browser.theme.colorway-migration", true);
// Whether using `ctrl` when hitting return/enter in the URL bar
// (or clicking 'go') should prefix 'www.' and suffix
// browser.fixup.alternate.suffix to the URL bar value prior to
@ -1063,6 +1041,9 @@ pref("browser.tabs.groups.smart.enabled", true);
pref("browser.tabs.groups.smart.enabled", false);
#endif
// KMEANS_WITH_ANCHOR or NEAREST_NEIGHBOR
pref("browser.tabs.groups.smart.suggestOtherTabsMethod", "NEAREST_NEIGHBOR");
pref("browser.tabs.groups.smart.optin", false);
pref("browser.tabs.dragDrop.createGroup.delayMS", 240);
@ -1869,8 +1850,7 @@ pref("browser.newtabpage.activity-stream.newNewtabExperience.colors", "#004CA4,#
pref("browser.newtabpage.activity-stream.newtabLayouts.variant-a", false);
pref("browser.newtabpage.activity-stream.newtabLayouts.variant-b", true);
// Shortcuts experiment
pref("browser.newtabpage.activity-stream.newtabShortcuts.refresh", false);
pref("browser.newtabpage.activity-stream.newtabShortcuts.refresh", true);
// Discovery stream ad size experiment
pref("browser.newtabpage.activity-stream.newtabAdSize.variant-a", false);
@ -1991,7 +1971,12 @@ pref("browser.newtabpage.activity-stream.discoverystream.sections.locale-content
pref("browser.newtabpage.activity-stream.discoverystream.sections.region-content-config", "");
pref("browser.newtabpage.activity-stream.discoverystream.sections.cards.enabled", true);
pref("browser.newtabpage.activity-stream.discoverystream.sections.personalization.inferred.enabled", false);
// List of regions that use inferred personalization.
pref("browser.newtabpage.activity-stream.discoverystream.sections.personalization.inferred.region-config", "");
// List of locales that use inferred personalization.
pref("browser.newtabpage.activity-stream.discoverystream.sections.personalization.inferred.locale-config", "en-US,en-GB,en-CA");
pref("browser.newtabpage.activity-stream.discoverystream.sections.personalization.inferred.user.enabled", true);
pref("browser.newtabpage.activity-stream.discoverystream.sections.personalization.inferred.blocked", false);
@ -2152,6 +2137,7 @@ pref("browser.ml.linkPreview.allowedLanguages", "en");
pref("browser.ml.linkPreview.enabled", false);
pref("browser.ml.linkPreview.outputSentences", 3);
pref("browser.ml.linkPreview.blockListEnabled", true);
pref("browser.ml.linkPreview.noKeyPointsRegions", "AD,AT,BE,BG,CH,CY,CZ,DE,DK,EE,ES,FI,FR,GR,HR,HU,IE,IS,IT,LI,LT,LU,LV,MT,NL,NO,PL,PT,RO,SE,SI,SK");
// Block insecure active content on https pages
pref("security.mixed_content.block_active_content", true);

View file

@ -67,6 +67,8 @@ ChromeUtils.defineESModuleGetters(this, {
PopupBlockerObserver: "resource:///modules/PopupBlockerObserver.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
ProcessHangMonitor: "resource:///modules/ProcessHangMonitor.sys.mjs",
ProfilesDatastoreService:
"resource:///modules/profiles/ProfilesDatastoreService.sys.mjs",
PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs",
ReaderMode: "moz-src:///toolkit/components/reader/ReaderMode.sys.mjs",
ResetPBMPanel: "resource:///modules/ResetPBMPanel.sys.mjs",
@ -75,6 +77,8 @@ ChromeUtils.defineESModuleGetters(this, {
SaveToPocket: "chrome://pocket/content/SaveToPocket.sys.mjs",
ScreenshotsUtils: "resource:///modules/ScreenshotsUtils.sys.mjs",
SearchUIUtils: "moz-src:///browser/components/search/SearchUIUtils.sys.mjs",
SelectableProfileService:
"resource:///modules/profiles/SelectableProfileService.sys.mjs",
SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs",
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
SharingUtils: "resource:///modules/SharingUtils.sys.mjs",
@ -111,17 +115,6 @@ ChromeUtils.defineESModuleGetters(this, {
ZoomUI: "resource:///modules/ZoomUI.sys.mjs",
});
// Bug 1894239: We will move this up to ChromeUtils.defineESModuleGetters once
// the MOZ_SELECTABLE_PROFILES flag is removed
ChromeUtils.defineLazyGetter(this, "SelectableProfileService", () => {
if (!AppConstants.MOZ_SELECTABLE_PROFILES) {
return null;
}
return ChromeUtils.importESModule(
"resource:///modules/profiles/SelectableProfileService.sys.mjs"
).SelectableProfileService;
});
ChromeUtils.defineLazyGetter(this, "fxAccounts", () => {
return ChromeUtils.importESModule(
"resource://gre/modules/FxAccounts.sys.mjs"

View file

@ -256,5 +256,6 @@
"gFindBarPromise",
"SelectableProfileService",
"ActionsProviderContextualSearch",
"ToolbarDropHandler"
"ToolbarDropHandler",
"ProfilesDatastoreService"
]

View file

@ -32,6 +32,12 @@
lwtProperty: "ntp_card_background",
},
],
[
"--newtab-background-card",
{
lwtProperty: "ntp_card_background",
},
],
[
"--newtab-text-primary-color",
{

View file

@ -148,7 +148,7 @@ document.addEventListener(
{
let { tabGroupId } = event.target.parentElement.triggerNode.dataset;
SessionStore.openSavedTabGroup(tabGroupId, window, {
source: lazy.TabMetrics.METRIC_SOURCE.RECENT_TABS,
source: lazy.TabMetrics.METRIC_SOURCE.TAB_OVERFLOW_MENU,
});
}
break;
@ -157,7 +157,7 @@ document.addEventListener(
// TODO Bug 1940112: "Open Group in New Window" should directly restore saved tab groups into a new window
let { tabGroupId } = event.target.parentElement.triggerNode.dataset;
let tabGroup = SessionStore.openSavedTabGroup(tabGroupId, window, {
source: lazy.TabMetrics.METRIC_SOURCE.RECENT_TABS,
source: lazy.TabMetrics.METRIC_SOURCE.TAB_OVERFLOW_MENU,
});
gBrowser.replaceGroupWithWindow(tabGroup);
}

View file

@ -13,7 +13,6 @@ prefs = [
# The form autofill framescript is only used in certain locales if this
# pref is set to 'detect', which is the default value on non-Nightly.
"extensions.formautofill.addresses.available='on'",
"browser.urlbar.disableExtendForTests=true",
# For perfomance tests do not enable the remote control cue, which gets set
# when Marionette is enabled, but users normally don't see.
"browser.chrome.disableRemoteControlCueForTests=true",
@ -105,10 +104,8 @@ skip-if = ["os == 'win'"] #Bug 1455054
["browser_toolbariconcolor_restyles.js"]
["browser_urlbar_keyed_search.js"]
skip-if = ["true"] # Bug 1926118: Disabled until the test can be revisited to measure performance under representative conditions.
["browser_urlbar_search.js"]
skip-if = ["true"] # Bug 1926118: Disabled until the test can be revisited to measure performance under representative conditions.
["browser_vsync_accessibility.js"]

View file

@ -108,6 +108,13 @@ const startupPhases = {
},
};
if (AppConstants.platform == "win") {
// On Windows we call checkForLaunchOnLogin early in startup.
startupPhases["before profile selection"].allowlist.modules.add(
"moz-src:///browser/components/shell/StartupOSIntegration.sys.mjs"
);
}
if (
Services.prefs.getBoolPref("browser.startup.blankWindow") &&
Services.prefs.getCharPref(

View file

@ -818,9 +818,18 @@ async function runUrlbarTest(
};
let urlbarRect = URLBar.textbox.getBoundingClientRect();
const SHADOW_SIZE = 17;
// To isolate unexpected repaints, we need to filter out the rectangle of
// pixels changed by showing the urlbar popover
const SHADOW_SIZE = 17; // The blur/spread of the box shadow, plus 1px fudge factor
const INLINE_MARGIN = 5; // Margin applied to the breakout-extend urlbar
const VERTICAL_OFFSET = -4; // The popover positioning requires this offset
let expectedRects = {
filter: rects => {
const referenceRect = {
x1: Math.floor(urlbarRect.left) - INLINE_MARGIN - SHADOW_SIZE,
x2: Math.ceil(urlbarRect.right) + INLINE_MARGIN + SHADOW_SIZE,
y1: Math.floor(urlbarRect.top) + VERTICAL_OFFSET - SHADOW_SIZE,
};
// We put text into the urlbar so expect its textbox to change.
// We expect many changes in the results view.
// So we just allow changes anywhere in the urlbar. We don't check the
@ -831,9 +840,9 @@ async function runUrlbarTest(
return rects.filter(
r =>
!(
r.x1 >= Math.floor(urlbarRect.left) - SHADOW_SIZE &&
r.x2 <= Math.ceil(urlbarRect.right) + SHADOW_SIZE &&
r.y1 >= Math.floor(urlbarRect.top) - SHADOW_SIZE
r.x1 >= referenceRect.x1 &&
r.x2 <= referenceRect.x2 &&
r.y1 >= referenceRect.y1
)
);
},

View file

@ -119,6 +119,10 @@ var gExceptionPaths = [
"resource://builtin-addons/newtab/",
"resource://newtab/",
"chrome://newtab/",
// Bug 1957102 - Temporarily ignore until used by the URLBar provider
"resource://gre/modules/PlacesSemanticHistoryManager.sys.mjs",
"resource://gre/modules/PlacesSemanticHistoryDatabase.sys.mjs",
];
// These are not part of the omni.ja file, so we find them only when running

View file

@ -29,14 +29,6 @@ pref("app.update.checkInstallTime.days", 2);
// Give the user x seconds to reboot before showing a badge on the hamburger
// button. default=4 days
pref("app.update.badgeWaitTime", 345600);
// This represents the duration between an update being ready and it being
// possible to install it while other sessions are running. Note that
// having this pref's duration differ from `app.update.badgeWaitTime` may result
// in undefined behavior such as showing an update prompt that does not result
// in an update when the "Restart to Update" button is clicked. Keep in mind
// that this is in milliseconds and `app.update.badgeWaitTime` is in seconds.
// Note that the effective value of this pref is limited to 1 week, maximum.
pref("app.update.multiSessionInstallLockout.timeoutMs", 345600000);
// Number of usages of the web console.
// If this is less than 5, then pasting code into the web console is disabled

View file

@ -30,14 +30,6 @@ pref("app.update.checkInstallTime.days", 2);
// Give the user x seconds to reboot before showing a badge on the hamburger
// button. default=immediately
pref("app.update.badgeWaitTime", 0);
// This represents the duration between an update being ready and it being
// possible to install it while other sessions are running. Note that
// having this pref's duration differ from `app.update.badgeWaitTime` may result
// in undefined behavior such as showing an update prompt that does not result
// in an update when the "Restart to Update" button is clicked. Keep in mind
// that this is in milliseconds and `app.update.badgeWaitTime` is in seconds.
// Note that the effective value of this pref is limited to 1 week, maximum.
pref("app.update.multiSessionInstallLockout.timeoutMs", 0);
// Number of usages of the web console.
// If this is less than 5, then pasting code into the web console is disabled

View file

@ -42,14 +42,6 @@ pref("app.update.checkInstallTime.days", 63);
// Give the user x seconds to reboot before showing a badge on the hamburger
// button. default=4 days
pref("app.update.badgeWaitTime", 345600);
// This represents the duration between an update being ready and it being
// possible to install it while other sessions are running. Note that
// having this pref's duration differ from `app.update.badgeWaitTime` may result
// in undefined behavior such as showing an update prompt that does not result
// in an update when the "Restart to Update" button is clicked. Keep in mind
// that this is in milliseconds and `app.update.badgeWaitTime` is in seconds.
// Note that the effective value of this pref is limited to 1 week, maximum.
pref("app.update.multiSessionInstallLockout.timeoutMs", 345600000);
// Number of usages of the web console.
// If this is less than 5, then pasting code into the web console is disabled

View file

@ -26,14 +26,6 @@ pref("app.update.checkInstallTime.days", 2);
// Give the user x seconds to reboot before showing a badge on the hamburger
// button. default=immediately
pref("app.update.badgeWaitTime", 0);
// This represents the duration between an update being ready and it being
// possible to install it while other sessions are running. Note that
// having this pref's duration differ from `app.update.badgeWaitTime` may result
// in undefined behavior such as showing an update prompt that does not result
// in an update when the "Restart to Update" button is clicked. Keep in mind
// that this is in milliseconds and `app.update.badgeWaitTime` is in seconds.
// Note that the effective value of this pref is limited to 1 week, maximum.
pref("app.update.multiSessionInstallLockout.timeoutMs", 0);
// Number of usages of the web console.
// If this is less than 5, then pasting code into the web console is disabled

View file

@ -47,12 +47,16 @@ category browser-idle-startup resource:///modules/UrlbarSearchTermsPersistence.s
category browser-idle-startup resource:///modules/ShoppingUtils.sys.mjs ShoppingUtils.init
category browser-idle-startup moz-src:///browser/components/search/SERPCategorization.sys.mjs SERPCategorization.init
category browser-idle-startup resource://gre/modules/ContentRelevancyManager.sys.mjs ContentRelevancyManager.init
category browser-idle-startup resource://gre/modules/ColorwayThemeMigration.sys.mjs ColorwayThemeMigration.maybeWarn
#ifdef MOZ_UPDATER
category browser-idle-startup resource://gre/modules/UpdateListener.sys.mjs UpdateListener.maybeShowUnsupportedNotification
#endif
#ifdef XP_WIN
category browser-idle-startup resource:///modules/WindowsJumpLists.sys.mjs WinTaskbarJumpList.startup
#endif
#if defined(XP_WIN) || defined(XP_MACOSX)
category browser-idle-startup moz-src:///browser/components/shell/StartupOSIntegration.sys.mjs StartupOSIntegration.onStartupIdle
#endif
# Note that these telemetry entries schedule their own idle tasks,
# so they are guaranteed to run after everything else.

View file

@ -42,8 +42,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
DownloadsViewableInternally:
"resource:///modules/DownloadsViewableInternally.sys.mjs",
ExtensionsUI: "resource:///modules/ExtensionsUI.sys.mjs",
FirefoxBridgeExtensionUtils:
"resource:///modules/FirefoxBridgeExtensionUtils.sys.mjs",
// FilePickerCrashed is used by the `listeners` object below.
// eslint-disable-next-line mozilla/valid-lazy
FilePickerCrashed: "resource:///modules/FilePickerCrashed.sys.mjs",
@ -70,6 +68,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
ProcessHangMonitor: "resource:///modules/ProcessHangMonitor.sys.mjs",
ProfileDataUpgrader:
"moz-src:///browser/components/ProfileDataUpgrader.sys.mjs",
ProfilesDatastoreService:
"resource:///modules/profiles/ProfilesDatastoreService.sys.mjs",
RemoteSecuritySettings:
"resource://gre/modules/psm/RemoteSecuritySettings.sys.mjs",
RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
@ -83,10 +83,11 @@ ChromeUtils.defineESModuleGetters(lazy, {
"resource:///modules/profiles/SelectableProfileService.sys.mjs",
SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs",
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
ShellService: "resource:///modules/ShellService.sys.mjs",
ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs",
SpecialMessageActions:
"resource://messaging-system/lib/SpecialMessageActions.sys.mjs",
StartupOSIntegration:
"moz-src:///browser/components/shell/StartupOSIntegration.sys.mjs",
TelemetryReportingPolicy:
"resource://gre/modules/TelemetryReportingPolicy.sys.mjs",
TRRRacer: "resource:///modules/TRRPerformance.sys.mjs",
@ -94,9 +95,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
WebChannel: "resource://gre/modules/WebChannel.sys.mjs",
WebProtocolHandlerRegistrar:
"resource:///modules/WebProtocolHandlerRegistrar.sys.mjs",
WindowsLaunchOnLogin: "resource://gre/modules/WindowsLaunchOnLogin.sys.mjs",
WindowsRegistry: "resource://gre/modules/WindowsRegistry.sys.mjs",
WindowsGPOParser: "resource://gre/modules/policies/WindowsGPOParser.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
});
@ -126,13 +125,6 @@ if (AppConstants.ENABLE_WEBDRIVER) {
const PREF_PDFJS_ISDEFAULT_CACHE_STATE = "pdfjs.enabledCache.state";
const PRIVATE_BROWSING_BINARY = "private_browsing.exe";
// Index of Private Browsing icon in private_browsing.exe
// Must line up with IDI_PBICON_PB_PB_EXE in nsNativeAppSupportWin.h.
const PRIVATE_BROWSING_EXE_ICON_INDEX = 1;
const PREF_PRIVATE_BROWSING_SHORTCUT_CREATED =
"browser.privacySegmentation.createdShortcut";
ChromeUtils.defineLazyGetter(
lazy,
"WeaveService",
@ -157,21 +149,6 @@ ChromeUtils.defineLazyGetter(lazy, "gBrowserBundle", function () {
);
});
ChromeUtils.defineLazyGetter(lazy, "log", () => {
let { ConsoleAPI } = ChromeUtils.importESModule(
"resource://gre/modules/Console.sys.mjs"
);
let consoleOptions = {
// tip: set maxLogLevel to "debug" and use lazy.log.debug() to create
// detailed messages during development. See LOG_LEVELS in Console.sys.mjs
// for details.
maxLogLevel: "error",
maxLogLevelPref: "browser.policies.loglevel",
prefix: "BrowserGlue.sys.mjs",
};
return new ConsoleAPI(consoleOptions);
});
const listeners = {
observers: {
"file-picker-crashed": ["FilePickerCrashed"],
@ -255,77 +232,6 @@ export function BrowserGlue() {
this._init();
}
function WindowsRegPoliciesGetter(wrk, root, regLocation) {
wrk.open(root, regLocation, wrk.ACCESS_READ);
let policies;
if (wrk.hasChild("Mozilla\\" + Services.appinfo.name)) {
policies = lazy.WindowsGPOParser.readPolicies(wrk, policies);
}
wrk.close();
return policies;
}
function isPrivateBrowsingAllowedInRegistry() {
// If there is an attempt to open Private Browsing before
// EnterprisePolicies are initialized the Windows registry
// can be checked to determine if it is enabled
if (Services.policies.status > Ci.nsIEnterprisePolicies.UNINITIALIZED) {
// Yield to policies engine if initialized
let privateAllowed = Services.policies.isAllowed("privatebrowsing");
lazy.log.debug(
`Yield to initialized policies engine: Private Browsing Allowed = ${privateAllowed}`
);
return privateAllowed;
}
if (AppConstants.platform !== "win") {
// Not using Windows so no registry, return true
lazy.log.debug(
"AppConstants.platform is not 'win': Private Browsing allowed"
);
return true;
}
// If all other checks fail only then do we check registry
let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
Ci.nsIWindowsRegKey
);
let regLocation = "SOFTWARE\\Policies";
let userPolicies, machinePolicies;
// Only check HKEY_LOCAL_MACHINE if not in testing
if (!Cu.isInAutomation) {
machinePolicies = WindowsRegPoliciesGetter(
wrk,
wrk.ROOT_KEY_LOCAL_MACHINE,
regLocation
);
}
// Check machine policies before checking user policies
// HKEY_LOCAL_MACHINE supersedes HKEY_CURRENT_USER so only check
// HKEY_CURRENT_USER if the registry key is not present in
// HKEY_LOCAL_MACHINE at all
if (machinePolicies && "DisablePrivateBrowsing" in machinePolicies) {
lazy.log.debug(
`DisablePrivateBrowsing in HKEY_LOCAL_MACHINE is ${machinePolicies.DisablePrivateBrowsing}`
);
return !(machinePolicies.DisablePrivateBrowsing === 1);
}
userPolicies = WindowsRegPoliciesGetter(
wrk,
wrk.ROOT_KEY_CURRENT_USER,
regLocation
);
if (userPolicies && "DisablePrivateBrowsing" in userPolicies) {
lazy.log.debug(
`DisablePrivateBrowsing in HKEY_CURRENT_USER is ${userPolicies.DisablePrivateBrowsing}`
);
return !(userPolicies.DisablePrivateBrowsing === 1);
}
// Private browsing allowed if no registry entry exists
lazy.log.debug(
"No DisablePrivateBrowsing registry entry: Private Browsing allowed"
);
return true;
}
BrowserGlue.prototype = {
_saveSession: false,
_migrationImportsDefaultBookmarks: false,
@ -513,30 +419,8 @@ BrowserGlue.prototype = {
"os-autostart",
false
);
let launchOnLoginPref = "browser.startup.windowsLaunchOnLogin.enabled";
let profileSvc = Cc[
"@mozilla.org/toolkit/profile-service;1"
].getService(Ci.nsIToolkitProfileService);
if (
AppConstants.platform == "win" &&
!profileSvc.startWithLastProfile
) {
// If we don't start with last profile, the user
// likely sees the profile selector on launch.
if (Services.prefs.getBoolPref(launchOnLoginPref)) {
Glean.launchOnLogin.lastProfileDisableStartup.record();
// Disable launch on login messaging if we are disabling the
// feature.
Services.prefs.setBoolPref(
"browser.startup.windowsLaunchOnLogin.disableLaunchOnLoginPrompt",
true
);
}
// To reduce confusion when running multiple Gecko profiles,
// delete launch on login shortcuts and registry keys so that
// users are not presented with the outdated profile selector
// dialog.
lazy.WindowsLaunchOnLogin.removeLaunchOnLogin();
if (AppConstants.platform == "win") {
lazy.StartupOSIntegration.checkForLaunchOnLogin();
}
break;
}
@ -904,7 +788,7 @@ BrowserGlue.prototype = {
let makeWindowPrivate =
cmdLine.findFlag("private-window", false) != -1 &&
isPrivateBrowsingAllowedInRegistry();
lazy.StartupOSIntegration.isPrivateBrowsingAllowedInRegistry();
if (!shouldCreateWindow(makeWindowPrivate)) {
return;
}
@ -1035,9 +919,8 @@ BrowserGlue.prototype = {
lazy.DoHController.init();
if (AppConstants.MOZ_SELECTABLE_PROFILES) {
lazy.ProfilesDatastoreService.init().catch(console.error);
lazy.SelectableProfileService.init().catch(console.error);
}
this._firstWindowTelemetry(aWindow);
this._firstWindowLoaded();
@ -1409,129 +1292,6 @@ BrowserGlue.prototype = {
},
},
{
name: "firefoxBridgeNativeMessaging",
condition:
(AppConstants.platform == "macosx" ||
AppConstants.platform == "win") &&
Services.prefs.getBoolPref("browser.firefoxbridge.enabled", false),
task: async () => {
let profileService = Cc[
"@mozilla.org/toolkit/profile-service;1"
].getService(Ci.nsIToolkitProfileService);
if (
profileService.defaultProfile &&
profileService.currentProfile == profileService.defaultProfile
) {
await lazy.FirefoxBridgeExtensionUtils.ensureRegistered();
} else {
lazy.log.debug(
"FirefoxBridgeExtensionUtils failed to register due to non-default current profile."
);
}
},
},
// Kick off an idle task that will silently pin Firefox to the start menu on
// first run when using MSIX on a new profile.
// If not first run, check if Firefox is no longer pinned to the Start Menu
// when it previously was and send telemetry.
{
name: "maybePinToStartMenuFirstRun",
condition:
AppConstants.platform === "win" &&
Services.sysinfo.getProperty("hasWinPackageId"),
task: async () => {
if (
lazy.BrowserHandler.firstRunProfile &&
(await lazy.ShellService.doesAppNeedStartMenuPin())
) {
await lazy.ShellService.pinToStartMenu();
return;
}
await lazy.ShellService.recordWasPreviouslyPinnedToStartMenu();
},
},
// Ensure a Private Browsing Shortcut exists. This is needed in case
// a user tries to use Windows functionality to pin our Private Browsing
// mode icon to the Taskbar (eg: the "Pin to Taskbar" context menu item).
// This is also created by the installer, but it's possible that a user
// has removed it, or is running out of a zip build. The consequences of not
// having a Shortcut for this are that regular Firefox will be pinned instead
// of the Private Browsing version -- so it's quite important we do our best
// to make sure one is available.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1762994 for additional
// background.
{
name: "ensurePrivateBrowsingShortcutExists",
condition:
AppConstants.platform == "win" &&
Services.prefs.getBoolPref(
"browser.privateWindowSeparation.enabled",
true
) &&
// We don't want a shortcut if it's been disabled, eg: by enterprise policy.
lazy.PrivateBrowsingUtils.enabled &&
// Private Browsing shortcuts for packaged builds come with the package,
// if they exist at all. We shouldn't try to create our own.
!Services.sysinfo.getProperty("hasWinPackageId") &&
// If we've ever done this successfully before, don't try again. The
// user may have deleted the shortcut, and we don't want to force it
// on them.
!Services.prefs.getBoolPref(
PREF_PRIVATE_BROWSING_SHORTCUT_CREATED,
false
),
task: async () => {
let shellService = Cc[
"@mozilla.org/browser/shell-service;1"
].getService(Ci.nsIWindowsShellService);
let winTaskbar = Cc["@mozilla.org/windows-taskbar;1"].getService(
Ci.nsIWinTaskbar
);
if (
!(await shellService.hasPinnableShortcut(
winTaskbar.defaultPrivateGroupId,
true
))
) {
let appdir = Services.dirsvc.get("GreD", Ci.nsIFile);
let exe = appdir.clone();
exe.append(PRIVATE_BROWSING_BINARY);
let strings = new Localization(
["branding/brand.ftl", "browser/browser.ftl"],
true
);
let [desc] = await strings.formatValues([
"private-browsing-shortcut-text-2",
]);
await shellService.createShortcut(
exe,
[],
desc,
exe,
// The code we're calling indexes from 0 instead of 1
PRIVATE_BROWSING_EXE_ICON_INDEX - 1,
winTaskbar.defaultPrivateGroupId,
"Programs",
desc + ".lnk",
appdir
);
}
// We always set this as long as no exception has been thrown. This
// ensure that it is `true` both if we created one because it didn't
// exist, or if it already existed (most likely because it was created
// by the installer). This avoids the need to call `hasPinnableShortcut`
// again, which necessarily does pointless I/O.
Services.prefs.setBoolPref(
PREF_PRIVATE_BROWSING_SHORTCUT_CREATED,
true
);
},
},
{
name: "BrowserGlue._maybeShowDefaultBrowserPrompt",
task: () => {
@ -1740,6 +1500,15 @@ BrowserGlue.prototype = {
},
},
{
name: "Init hasSSD for SystemInfo",
condition: AppConstants.platform == "win",
// Initializes diskInfo to be able to get hasSSD which is part
// of the PageLoad event. Only runs on windows, since diskInfo
// is a no-op on other platforms
task: () => Services.sysinfo.diskInfo,
},
{
name: "browser-startup-idle-tasks-finished",
task: () => {

View file

@ -8,8 +8,6 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
SelectableProfileService:
"resource:///modules/profiles/SelectableProfileService.sys.mjs",
});
/**
@ -597,29 +595,6 @@ let JSWINDOWACTORS = {
},
matches: ["about:editprofile", "about:deleteprofile", "about:newprofile"],
remoteTypes: ["privilegedabout"],
onAddActor(register, unregister) {
let registered = false;
const maybeRegister = () => {
let isEnabled = lazy.SelectableProfileService.isEnabled;
if (isEnabled && !registered) {
register();
} else if (!isEnabled && registered) {
unregister();
}
registered = isEnabled;
};
// Defer all this logic until a little later in startup
Services.obs.addObserver(() => {
// Update when the pref changes
lazy.SelectableProfileService.on("enableChanged", maybeRegister);
maybeRegister();
}, "final-ui-startup");
},
},
Prompt: {

View file

@ -1114,6 +1114,30 @@ html {
width: 30%;
}
}
.message-text.hero-text {
margin-block: auto;
margin-inline: 17% auto;
text-align: start;
width: 100%;
max-width: 450px;
h1 {
font-weight: 510;
font-size: 80px;
margin: 0;
width: 100%;
max-width: 100%;
}
h2 {
font-size: 20px;
margin: 0;
margin-block-start: 24px;
font-weight: 510;
width: 100%;
}
}
}
.addons-picker-container {

View file

@ -432,17 +432,56 @@ export class ProtonScreen extends React.PureComponent {
{content.hero_image ? (
<HeroImage url={content.hero_image.url} />
) : (
this.renderHeroText(content.hero_text)
)}
</div>
);
}
renderHeroText(hero_text) {
if (!hero_text) {
return null;
}
// Check if hero_text is a string or an object with string_id property
// essentially checking if we're using old or new design
const isSimpleText =
typeof hero_text === "string" ||
(typeof hero_text === "object" &&
hero_text !== null &&
"string_id" in hero_text);
const HeroTextWrapper = ({ children, className = "" }) => (
<React.Fragment>
<div className="message-text">
<div className={`message-text ${className}`}>
<div className="spacer-top" />
<Localized text={content.hero_text}>
<h1 />
</Localized>
{children}
<div className="spacer-bottom" />
</div>
</React.Fragment>
);
if (isSimpleText) {
return (
<HeroTextWrapper>
<Localized text={hero_text}>
<h1 />
</Localized>
</HeroTextWrapper>
);
}
return (
<HeroTextWrapper className="hero-text">
<Localized text={hero_text.title}>
<h1 />
</Localized>
{hero_text.subtitle && (
<Localized text={hero_text.subtitle}>
<h2 />
</Localized>
)}
</div>
</HeroTextWrapper>
);
}

View file

@ -1258,15 +1258,38 @@ class ProtonScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
role: "img"
})), content.hero_image ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_HeroImage__WEBPACK_IMPORTED_MODULE_6__.HeroImage, {
url: content.hero_image.url
}) : /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "message-text"
}) : this.renderHeroText(content.hero_text));
}
renderHeroText(hero_text) {
if (!hero_text) {
return null;
}
// Check if hero_text is a string or an object with string_id property
// essentially checking if we're using old or new design
const isSimpleText = typeof hero_text === "string" || typeof hero_text === "object" && hero_text !== null && "string_id" in hero_text;
const HeroTextWrapper = ({
children,
className = ""
}) => /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: `message-text ${className}`
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "spacer-top"
}), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
text: content.hero_text
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("h1", null)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
}), children, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "spacer-bottom"
}))));
})));
if (isSimpleText) {
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(HeroTextWrapper, null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
text: hero_text
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("h1", null)));
}
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(HeroTextWrapper, {
className: "hero-text"
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
text: hero_text.title
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("h1", null)), hero_text.subtitle && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
text: hero_text.subtitle
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("h2", null)));
}
renderOrderedContent(content) {
const elements = [];

View file

@ -2153,6 +2153,27 @@ html {
.onboardingContainer .screen[pos=split][fullscreen] .section-secondary .hero-image img {
width: 30%;
}
.onboardingContainer .screen[pos=split][fullscreen] .section-secondary .message-text.hero-text {
margin-block: auto;
margin-inline: 17% auto;
text-align: start;
width: 100%;
max-width: 450px;
}
.onboardingContainer .screen[pos=split][fullscreen] .section-secondary .message-text.hero-text h1 {
font-weight: 510;
font-size: 80px;
margin: 0;
width: 100%;
max-width: 100%;
}
.onboardingContainer .screen[pos=split][fullscreen] .section-secondary .message-text.hero-text h2 {
font-size: 20px;
margin: 0;
margin-block-start: 24px;
font-weight: 510;
width: 100%;
}
.onboardingContainer .screen[pos=split][fullscreen] .addons-picker-container {
background: none;
}

View file

@ -108,6 +108,149 @@ describe("MultiStageAboutWelcomeProton module", () => {
assert.equal(wrapper.find("main").prop("pos"), "center");
});
it("should render simple hero text if hero_text is a string or object without string_id", () => {
// test simple hero text with hero_text string
const STRING_HERO_TEXT_PROPS = {
content: {
position: "split",
hero_text: "Simple hero text string",
},
};
const wrapper = mount(
<MultiStageProtonScreen {...STRING_HERO_TEXT_PROPS} />
);
assert.ok(wrapper.exists());
assert.equal(
wrapper.find(".section-secondary h1").text(),
"Simple hero text string"
);
// test with simple hero text with hero_text string_id
const STRING_ID_HERO_TEXT_PROPS = {
content: {
position: "split",
hero_text: { string_id: "hero-text-id" },
},
};
const stringIdWrapper = mount(
<MultiStageProtonScreen {...STRING_ID_HERO_TEXT_PROPS} />
);
assert.ok(stringIdWrapper.exists());
assert.equal(
stringIdWrapper.find(".section-secondary h1").prop("data-l10n-id"),
"hero-text-id"
);
// test that we're not using the hero-text class
assert.isFalse(
wrapper.find(".section-secondary .hero-text").exists(),
"Simple hero text should not use hero-text class"
);
});
it("should render complex hero text if hero text is an object with title property", () => {
const COMPLEX_HERO_TEXT_PROPS = {
content: {
position: "split",
hero_text: {
title: "Test title",
},
},
};
const wrapper = mount(
<MultiStageProtonScreen {...COMPLEX_HERO_TEXT_PROPS} />
);
assert.ok(wrapper.exists());
assert.isTrue(
wrapper.find(".section-secondary .hero-text").exists(),
"Text container should use hero-text class"
);
assert.equal(
wrapper.find(".section-secondary .hero-text h1").text(),
"Test title"
);
assert.isFalse(
wrapper.find(".section-secondary .hero-text h2").exists(),
"No subtitle should be rendered"
);
});
it("should render hero text subtitle if both title and subtitle properties are present", () => {
const HERO_TEXT_WITH_SUBTITLE_PROPS = {
content: {
position: "split",
hero_text: {
title: "Title text",
subtitle: "Subtitle text",
},
},
};
const wrapper = mount(
<MultiStageProtonScreen {...HERO_TEXT_WITH_SUBTITLE_PROPS} />
);
assert.ok(wrapper.exists());
assert.isTrue(
wrapper.find(".section-secondary .hero-text").exists(),
"Complex hero text should use hero-text class"
);
assert.equal(
wrapper.find(".section-secondary .hero-text h1").text(),
"Title text"
);
assert.isTrue(
wrapper.find(".section-secondary .hero-text h2").exists(),
"Subtitle should be rendered when provided"
);
assert.equal(
wrapper.find(".section-secondary .hero-text h2").text(),
"Subtitle text"
);
});
it("should render hero text title and subtitle with localization if string ids are present", () => {
const LOCALIZED_HERO_TEXT_PROPS = {
content: {
position: "split",
hero_text: {
title: { string_id: "hero-title-string-id" },
subtitle: { string_id: "hero-subtitle-string-id" },
},
},
};
const wrapper = mount(
<MultiStageProtonScreen {...LOCALIZED_HERO_TEXT_PROPS} />
);
assert.ok(wrapper.exists());
assert.isTrue(
wrapper.find(".section-secondary .hero-text").exists(),
"Text container should use hero-text class"
);
const titleElement = wrapper.find(".section-secondary .hero-text h1");
assert.isTrue(titleElement.exists(), "Title element should exist");
assert.equal(
titleElement.prop("data-l10n-id"),
"hero-title-string-id",
"Title should have correct string ID for localization"
);
const subtitleElement = wrapper.find(".section-secondary .hero-text h2");
assert.isTrue(subtitleElement.exists(), "Subtitle element should exist");
assert.equal(
subtitleElement.prop("data-l10n-id"),
"hero-subtitle-string-id",
"Subtitle should have correct string ID for localization"
);
});
it("should not render multiple action buttons if an additional button does not exist", () => {
const SCREEN_PROPS = {
content: {

View file

@ -548,10 +548,11 @@
"properties": {
"type": {
"type": "string",
"description": "Should the message be global (persisted across tabs) or local (disappear when switching to a different tab).",
"description": "Specifies where the message should appear and persist: 'global' (persists across tabs in the current window), 'tab' (only visible in the current tab), or 'universal' (visible across all tabs and windows, current and future).",
"enum": [
"global",
"tab"
"tab",
"universal"
]
},
"text": {
@ -714,9 +715,29 @@
"type": "string",
"description": "URL for image to use with the content."
},
"imageVerticalOffset": {
"imageWidth": {
"type": "number",
"description": "The margin-block-start value to apply to the image in pixels."
"description": "The image width in pixels. Default is 120px."
},
"imageVerticalTopOffset": {
"type": "number",
"description": "The margin-block-start value to apply to the image in pixels, used in 'column' layouts."
},
"imageVerticalBottomOffset": {
"type": "number",
"description": "The margin-block-end value to apply to the image in pixels, used in 'row' layouts."
},
"containerVerticalBottomOffset": {
"type": "number",
"description": "The container's margin-block-end value in pixels. Used to visually offset the image in 'row' layouts when 'imageVerticalBottomOffset' is applied."
},
"layout": {
"type": "string",
"description": "The layout of the message content and illustration. Row displays the image to the inline to the right of the text, column displays the image above the text.",
"enum": [
"row",
"column"
]
}
}
},

View file

@ -11,8 +11,8 @@
"properties": {
"type": {
"type": "string",
"description": "Should the message be global (persisted across tabs) or local (disappear when switching to a different tab).",
"enum": ["global", "tab"]
"description": "Specifies where the message should appear and persist: 'global' (persists across tabs in the current window), 'tab' (only visible in the current tab), or 'universal' (visible across all tabs and windows, current and future).",
"enum": ["global", "tab", "universal"]
},
"text": {
"$ref": "file:///FxMSCommon.schema.json#/$defs/localizableText",

View file

@ -64,9 +64,26 @@
"type": "string",
"description": "URL for image to use with the content."
},
"imageVerticalOffset": {
"imageWidth": {
"type": "number",
"description": "The margin-block-start value to apply to the image in pixels."
"description": "The image width in pixels. Default is 120px."
},
"imageVerticalTopOffset": {
"type": "number",
"description": "The margin-block-start value to apply to the image in pixels, used in 'column' layouts."
},
"imageVerticalBottomOffset": {
"type": "number",
"description": "The margin-block-end value to apply to the image in pixels, used in 'row' layouts."
},
"containerVerticalBottomOffset": {
"type": "number",
"description": "The container's margin-block-end value in pixels. Used to visually offset the image in 'row' layouts when 'imageVerticalBottomOffset' is applied."
},
"layout": {
"type": "string",
"description": "The layout of the message content and illustration. Row displays the image to the inline to the right of the text, column displays the image above the text.",
"enum": ["row", "column"]
}
}
},

View file

@ -5,7 +5,9 @@
:host {
margin-inline: var(--arrowpanel-menuitem-margin-inline, var(--space-xsmall));
margin-block: var(--space-xsmall);
--illustration-margin-block-offset: 0px;
--illustration-margin-block-start-offset: 0px;
--illustration-margin-block-end-offset: 0px;
--container-margin-block-end-offset: 0px;
}
#container {
@ -20,26 +22,50 @@
color: var(--text-color);
}
#container[layout="row"] {
margin-block-end: var(--container-margin-block-end-offset);
}
#container[layout="row"] #bottom-row {
display: flex;
flex-direction: row;
}
#container[layout="row"] #body-container {
flex: 1 1 0;
min-width: 0;
}
#close-button {
position: absolute;
top: 0;
inset-inline-end: 0;
padding-block: var(--arrowpanel-menuitem-padding-block, var(--space-small));
padding-inline: var(--arrowpanel-menuitem-padding-inline, var(--space-small));
}
#container:not([has-image]) > #illustration-container {
display: none;
}
#container[has-image] > #illustration-container {
#container[has-image][layout="column"] > #illustration-container {
display: flex;
justify-content: center;
margin-block-start: var(--illustration-margin-block-offset);
margin-block-start: var(--illustration-margin-block-start-offset);
margin-block-end: var(--space-xsmall);
pointer-events: none;
}
#container[has-image][layout="row"] #illustration-container {
display: flex;
align-items: flex-end;
justify-content: flex-end;
margin-block-end: var(--illustration-margin-block-end-offset);
pointer-events: none;
}
#container[has-image] #illustration-container img{
width: var(--image-width, 120px);
}
#primary {
font-size: 1.154em;
font-weight: var(--font-weight-bold);

View file

@ -21,6 +21,7 @@ export default class FxAMenuMessage extends MozLitElement {
buttonText: { type: String },
primaryText: { type: String },
secondaryText: { type: String },
layout: { type: String, reflect: true },
};
static queries = {
signUpButton: "#sign-up-button",
@ -29,6 +30,7 @@ export default class FxAMenuMessage extends MozLitElement {
constructor() {
super();
this.layout = "column"; // Default layout
this.addEventListener(
"keydown",
event => {
@ -69,13 +71,55 @@ export default class FxAMenuMessage extends MozLitElement {
});
}
get isRowLayout() {
return this.layout === "row";
}
render() {
return html`
<link
return html`<link
rel="stylesheet"
href="chrome://browser/content/asrouter/components/fxa-menu-message.css"
/>
<div id="container" ?has-image=${this.imageURL}>
<div id="container" layout=${this.layout} ?has-image=${this.imageURL}>
${this.isRowLayout
? html`
<div id="top-row">
<moz-button
id="close-button"
@click=${this.handleClose}
type="ghost"
iconsrc="chrome://global/skin/icons/close-12.svg"
tabindex="2"
data-l10n-id="fxa-menu-message-close-button"
>
</moz-button>
<div id="primary">${this.primaryText}</div>
</div>
<div id="bottom-row">
<div id="body-container">
<div id="secondary">${this.secondaryText}</div>
<moz-button
id="sign-up-button"
@click=${this.handleSignUp}
type="primary"
tabindex="1"
autofocus
title=${this.buttonText}
aria-label=${this.buttonText}
>
${this.buttonText}
</moz-button>
</div>
<div id="illustration-container">
<img
id="illustration"
role="presentation"
src=${this.imageURL}
/>
</div>
</div>
`
: html`
<moz-button
id="close-button"
@click=${this.handleClose}
@ -86,7 +130,11 @@ export default class FxAMenuMessage extends MozLitElement {
>
</moz-button>
<div id="illustration-container">
<img id="illustration" role="presentation" src=${this.imageURL} />
<img
id="illustration"
role="presentation"
src=${this.imageURL}
/>
</div>
<div id="primary">${this.primaryText}</div>
<div id="secondary">${this.secondaryText}</div>
@ -98,10 +146,11 @@ export default class FxAMenuMessage extends MozLitElement {
autofocus
title=${this.buttonText}
aria-label=${this.buttonText}
>${this.buttonText}</moz-button
>
</div>
`;
${this.buttonText}
</moz-button>
`}
</div>`;
}
}

View file

@ -18,7 +18,11 @@ const Template = ({
imageURL,
primaryText,
secondaryText,
imageVerticalOffset,
imageVerticalTopOffset,
imageVerticalBottomOffset,
containerVerticalBottomOffset,
layout,
imageHeight,
}) => html`
<moz-card style="width: 22.5rem;">
<fxa-menu-message
@ -26,7 +30,13 @@ const Template = ({
primaryText=${primaryText}
secondaryText=${secondaryText}
imageURL=${imageURL}
style="--illustration-margin-block-offset: ${imageVerticalOffset}px"
style="
--illustration-margin-block-start-offset: ${imageVerticalTopOffset}px;
--illustration-margin-block-end-offset: ${imageVerticalBottomOffset}px;
--container-margin-block-end-offset: ${containerVerticalBottomOffset}px;
--image-height: ${imageHeight}px;
"
layout=${layout}
>
</fxa-menu-message>
</moz-card>
@ -40,5 +50,9 @@ Default.args = {
primaryText: "Bounce between devices",
secondaryText:
"Sync and encrypt your bookmarks, passwords, and more on all your devices.",
imageVerticalOffset: -20,
imageVerticalTopOffset: -20,
imageVerticalBottomOffset: 0,
containerVerticalBottomOffset: 0,
layout: "column",
imageHeight: 120,
};

View file

@ -177,9 +177,9 @@ XPCOMUtils.defineLazyPreferenceGetter(
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"profileStoreID",
"toolkit.profiles.storeID",
null
"profilesCreated",
"browser.profiles.created",
false
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
@ -616,7 +616,7 @@ const TargetingGetters = {
return lazy.SelectableProfileService?.isEnabled ?? false;
},
get hasSelectableProfiles() {
return !!lazy.profileStoreID;
return lazy.profilesCreated;
},
get profileAgeCreated() {
return lazy.ProfileAge().then(times => times.created);

View file

@ -2,6 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* eslint-disable no-use-before-define */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
@ -11,6 +12,11 @@ ChromeUtils.defineESModuleGetters(lazy, {
"resource://messaging-system/lib/SpecialMessageActions.sys.mjs",
});
const TYPES = {
UNIVERSAL: "universal",
GLOBAL: "global",
};
class InfoBarNotification {
constructor(message, dispatch) {
this._dispatch = dispatch;
@ -31,7 +37,7 @@ class InfoBarNotification {
let { gBrowser } = browser.ownerGlobal;
let doc = gBrowser.ownerDocument;
let notificationContainer;
if (content.type === "global") {
if ([TYPES.GLOBAL, TYPES.UNIVERSAL].includes(content.type)) {
notificationContainer = browser.ownerGlobal.gNotificationBox;
} else {
notificationContainer = gBrowser.getNotificationBox(browser);
@ -51,10 +57,23 @@ class InfoBarNotification {
false,
content.dismissable
);
// If InfoBar is universal, only record an impression for the first
// instance.
if (
content.type !== TYPES.UNIVERSAL ||
!InfoBar._universalInfobars.length
) {
this.addImpression();
}
if (content.type === TYPES.UNIVERSAL) {
InfoBar._universalInfobars.push({
box: browser.ownerGlobal.gNotificationBox,
notification: this.notification,
});
}
}
formatMessageConfig(doc, browser, content) {
const frag = doc.createDocumentFragment();
const parts = Array.isArray(content) ? content : [content];
@ -145,16 +164,36 @@ class InfoBarNotification {
* Called when interacting with the toolbar (but not through the buttons)
*/
infobarCallback(eventType) {
const wasUniversal =
InfoBar._activeInfobar?.message.content.type === TYPES.UNIVERSAL;
if (eventType === "removed") {
this.notification = null;
// eslint-disable-next-line no-use-before-define
InfoBar._activeInfobar = null;
} else if (this.notification) {
this.sendUserEventTelemetry("DISMISSED");
this.notification = null;
// eslint-disable-next-line no-use-before-define
InfoBar._activeInfobar = null;
}
// If one instance of universal infobar is removed, remove all instances and
// the new window observer
if (wasUniversal) {
this.removeUniversalInfobars();
}
}
removeUniversalInfobars() {
try {
Services.obs.removeObserver(InfoBar, "domwindowopened");
} catch (error) {
console.error(
"Error removing domwindowopened observer on InfoBar:",
error
);
}
InfoBar._universalInfobars.forEach(({ box, notification }) => {
box.removeNotification(notification);
});
InfoBar._universalInfobars = [];
}
sendUserEventTelemetry(event) {
@ -171,6 +210,7 @@ class InfoBarNotification {
export const InfoBar = {
_activeInfobar: null,
_universalInfobars: [],
maybeLoadCustomElement(win) {
if (!win.customElements.get("remote-text")) {
@ -188,12 +228,22 @@ export const InfoBar = {
);
},
async showInfoBarMessage(browser, message, dispatch) {
async showNotificationAllWindows(notification) {
for (let win of Services.wm.getEnumerator(null)) {
const browser = win.gBrowser.selectedBrowser;
await notification.showNotification(browser);
}
},
async showInfoBarMessage(browser, message, dispatch, universalInNewWin) {
// Prevent stacking multiple infobars
if (this._activeInfobar) {
if (this._activeInfobar && !universalInNewWin) {
return null;
}
// Check if this is the first instance of a universal infobar
const isFirstUniversal =
!universalInNewWin && message.content.type === TYPES.UNIVERSAL;
const win = browser?.ownerGlobal;
if (!win || lazy.PrivateBrowsingUtils.isWindowPrivate(win)) {
@ -204,9 +254,39 @@ export const InfoBar = {
this.maybeInsertFTL(win);
let notification = new InfoBarNotification(message, dispatch);
if (isFirstUniversal) {
await this.showNotificationAllWindows(notification);
Services.obs.addObserver(this, "domwindowopened");
} else {
await notification.showNotification(browser);
this._activeInfobar = true;
}
if (!universalInNewWin) {
this._activeInfobar = { message, dispatch };
}
return notification;
},
observe(aSubject, aTopic) {
const { message, dispatch } = this._activeInfobar;
if (
aTopic !== "domwindowopened" ||
message?.content.type !== TYPES.UNIVERSAL
) {
return;
}
if (aSubject.document.readyState === "complete") {
let browser = aSubject.gBrowser.selectedBrowser;
this.showInfoBarMessage(browser, message, dispatch, true);
} else {
aSubject.addEventListener(
"load",
() => {
let browser = aSubject.gBrowser.selectedBrowser;
this.showInfoBarMessage(browser, message, dispatch, true);
},
{ once: true }
);
}
},
};

View file

@ -181,6 +181,7 @@ export const MenuMessage = {
win.MozXULElement.insertFTLIfNeeded("browser/newtab/asrouter.ftl");
const msgElement = document.createElement("fxa-menu-message");
msgElement.layout = message.content.layout ?? "column";
msgElement.imageURL = message.content.imageURL;
msgElement.buttonText = await lazy.RemoteL10n.formatLocalizableText(
message.content.primaryActionText
@ -192,9 +193,23 @@ export const MenuMessage = {
message.content.secondaryText
);
msgElement.dataset.navigableWithTabOnly = "true";
if (message.content.imageWidth !== undefined) {
msgElement.style.setProperty(
"--illustration-margin-block-offset",
`${message.content.imageVerticalOffset}px`
"--image-width",
`${message.content.imageWidth}px`
);
}
msgElement.style.setProperty(
"--illustration-margin-block-start-offset",
`${message.content.imageVerticalTopOffset}px`
);
msgElement.style.setProperty(
"--illustration-margin-block-end-offset",
`${message.content.imageVerticalBottomOffset}px`
);
msgElement.style.setProperty(
"--container-margin-block-end-offset",
`${message.content.containerVerticalBottomOffset}px`
);
msgElement.addEventListener("FxAMenuMessage:Close", () => {

View file

@ -1534,7 +1534,7 @@ const MESSAGES = () => [
},
imageURL:
"chrome://browser/content/asrouter/assets/fox-with-box-on-cloud.svg",
imageVerticalOffset: -20,
imageVerticalTopOffset: -20,
},
skip_in_tests: "TODO",
trigger: {

View file

@ -45,6 +45,8 @@ tags = "remote-settings"
tags = "remote-settings"
skip-if = ["a11y_checks"] # Bug 1854515 and 1858041 to investigate intermittent a11y_checks results (fails on Autoland, passes on Try)
["browser_asrouter_universal_infobar.js"]
["browser_bookmarks_bar_button.js"]
["browser_feature_callout.js"]

View file

@ -368,3 +368,28 @@ add_task(async function test_specialMessageAction_onLinkClick() {
handleStub.restore();
});
add_task(async function test_showInfoBarMessage_skipsPrivateWindow() {
const { PrivateBrowsingUtils } = ChromeUtils.importESModule(
"resource://gre/modules/PrivateBrowsingUtils.sys.mjs"
);
sinon.stub(PrivateBrowsingUtils, "isWindowPrivate").returns(true);
let browser = BrowserWindowTracker.getTopWindow().gBrowser.selectedBrowser;
let dispatch = sinon.stub();
let result = await InfoBar.showInfoBarMessage(
browser,
{
id: "Private Win Test",
content: { type: "global", buttons: [], text: "t", dismissable: true },
},
dispatch
);
Assert.equal(result, null);
Assert.equal(dispatch.callCount, 0);
// Cleanup
sinon.restore();
});

View file

@ -122,10 +122,18 @@ async function assertMessageInMenuSource(source, message, win = window) {
"The element should be configured for tab navigation."
);
Assert.equal(
messageEl.layout,
"column",
"The default layout should be 'column'."
);
let messageElStyles = window.getComputedStyle(messageEl);
Assert.equal(
messageElStyles.getPropertyValue("--illustration-margin-block-offset"),
`${message.content.imageVerticalOffset}px`
messageElStyles.getPropertyValue(
"--illustration-margin-block-start-offset"
),
`${message.content.imageVerticalTopOffset}px`
);
if (source === MenuMessage.SOURCES.APP_MENU) {

View file

@ -221,6 +221,12 @@ add_task(async function check_canCreateSelectableProfiles() {
return;
}
// Reset profiles prefs
await pushPrefs(
["browser.profiles.enabled", false],
["browser.profiles.created", false]
);
is(
await ASRouterTargeting.Environment.canCreateSelectableProfiles,
false,
@ -228,8 +234,13 @@ add_task(async function check_canCreateSelectableProfiles() {
);
// We have to fake there being a real profile available and enable the profiles feature
await pushPrefs(["browser.profiles.enabled", "someValue"]);
await SelectableProfileService.resetProfileService({ currentProfile: {} });
await pushPrefs(
["browser.profiles.enabled", true],
["browser.profiles.created", false]
);
await ProfilesDatastoreService.resetProfileService({ currentProfile: {} });
await SelectableProfileService.uninit();
await SelectableProfileService.init();
is(
await ASRouterTargeting.Environment.canCreateSelectableProfiles,
@ -244,7 +255,7 @@ add_task(async function check_canCreateSelectableProfiles() {
"should select correct item by canCreateSelectableProfiles"
);
await SelectableProfileService.resetProfileService(null);
await ProfilesDatastoreService.resetProfileService(null);
});
add_task(async function check_hasSelectableProfiles() {
@ -254,7 +265,7 @@ add_task(async function check_hasSelectableProfiles() {
"should return false before the pref is set"
);
await pushPrefs(["toolkit.profiles.storeID", "someValue"]);
await pushPrefs(["browser.profiles.created", true]);
is(
await ASRouterTargeting.Environment.hasSelectableProfiles,
true,

View file

@ -0,0 +1,263 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { InfoBar } = ChromeUtils.importESModule(
"resource:///modules/asrouter/InfoBar.sys.mjs"
);
const { CFRMessageProvider } = ChromeUtils.importESModule(
"resource:///modules/asrouter/CFRMessageProvider.sys.mjs"
);
const { ASRouter } = ChromeUtils.importESModule(
"resource:///modules/asrouter/ASRouter.sys.mjs"
);
const UNIVERSAL_MESSAGE = {
id: "universal-infobar",
content: {
type: "universal",
text: "t",
buttons: [],
},
};
const cleanupInfobars = () => {
InfoBar._universalInfobars = [];
InfoBar._activeInfobar = null;
};
add_task(async function showNotificationAllWindows() {
let fakeNotification = { showNotification: sinon.stub().resolves() };
let fakeWins = [
{ gBrowser: { selectedBrowser: "win1" } },
{ gBrowser: { selectedBrowser: "win2" } },
{ gBrowser: { selectedBrowser: "win3" } },
];
let origWinManager = Services.wm;
// Using sinon.stub wont work here, because Services.wm is a frozen,
// non-configurable object and its methods cannot be replaced via typical JS
// property assignment.
Object.defineProperty(Services, "wm", {
value: { getEnumerator: () => fakeWins[Symbol.iterator]() },
configurable: true,
writable: true,
});
await InfoBar.showNotificationAllWindows(fakeNotification);
Assert.equal(fakeNotification.showNotification.callCount, 3);
Assert.ok(fakeNotification.showNotification.calledWith("win1"));
Assert.ok(fakeNotification.showNotification.calledWith("win2"));
Assert.ok(fakeNotification.showNotification.calledWith("win3"));
// Cleanup
cleanupInfobars();
sinon.restore();
Object.defineProperty(Services, "wm", {
value: origWinManager,
configurable: true,
writable: true,
});
});
add_task(async function removeUniversalInfobars() {
let browser = BrowserWindowTracker.getTopWindow().gBrowser.selectedBrowser;
let origBox = browser.ownerGlobal.gNotificationBox;
browser.ownerGlobal.gNotificationBox = {
appendNotification: sinon.stub().resolves({}),
removeNotification: sinon.stub(),
};
sinon
.stub(InfoBar, "showNotificationAllWindows")
.callsFake(async notification => {
await notification.showNotification(browser);
});
let notification = await InfoBar.showInfoBarMessage(
browser,
UNIVERSAL_MESSAGE,
sinon.stub()
);
Assert.equal(InfoBar._universalInfobars.length, 1);
notification.removeUniversalInfobars();
Assert.ok(
browser.ownerGlobal.gNotificationBox.removeNotification.calledWith(
notification.notification
)
);
Assert.deepEqual(InfoBar._universalInfobars, []);
// Cleanup
cleanupInfobars();
browser.ownerGlobal.gNotificationBox = origBox;
sinon.restore();
});
add_task(async function initialUniversal_showsAllWindows_andSendsTelemetry() {
let browser = BrowserWindowTracker.getTopWindow().gBrowser.selectedBrowser;
let origBox = browser.ownerGlobal.gNotificationBox;
browser.ownerGlobal.gNotificationBox = {
appendNotification: sinon.stub().resolves({}),
removeNotification: sinon.stub(),
};
let showAll = sinon
.stub(InfoBar, "showNotificationAllWindows")
.callsFake(async notification => {
await notification.showNotification(browser);
});
let dispatch1 = sinon.stub();
let dispatch2 = sinon.stub();
await InfoBar.showInfoBarMessage(browser, UNIVERSAL_MESSAGE, dispatch1);
await InfoBar.showInfoBarMessage(browser, UNIVERSAL_MESSAGE, dispatch2, true);
Assert.ok(showAll.calledOnce);
Assert.equal(InfoBar._universalInfobars.length, 2);
// Dispatch impression (as this is the first universal infobar) and telemetry
// ping
Assert.equal(dispatch1.callCount, 2);
// Do not send telemetry for subsequent appearance of the message
Assert.equal(dispatch2.callCount, 0);
// Cleanup
cleanupInfobars();
browser.ownerGlobal.gNotificationBox = origBox;
sinon.restore();
Services.obs.removeObserver(InfoBar, "domwindowopened");
});
add_task(async function observe_domwindowopened_withLoadEvent() {
let stub = sinon.stub(InfoBar, "showInfoBarMessage").resolves();
InfoBar._activeInfobar = {
message: { content: { type: "universal" } },
dispatch: sinon.stub(),
};
let subject = {
document: { readyState: "loading" },
gBrowser: { selectedBrowser: "b" },
addEventListener(event, cb) {
subject.document.readyState = "complete";
cb();
},
};
InfoBar.observe(subject, "domwindowopened");
Assert.ok(stub.calledOnce);
// Called with universalInNewWin true
Assert.equal(stub.firstCall.args[3], true);
// Cleanup
cleanupInfobars();
sinon.restore();
});
add_task(async function observe_domwindowopened() {
let stub = sinon.stub(InfoBar, "showInfoBarMessage").resolves();
InfoBar._activeInfobar = {
message: { content: { type: "universal" } },
dispatch: sinon.stub(),
};
let win = BrowserWindowTracker.getTopWindow();
InfoBar.observe(win, "domwindowopened");
Assert.ok(stub.calledOnce);
Assert.equal(stub.firstCall.args[3], true);
// Cleanup
cleanupInfobars();
sinon.restore();
});
add_task(async function observe_skips_nonUniversal() {
let stub = sinon.stub(InfoBar, "showInfoBarMessage").resolves();
InfoBar._activeInfobar = {
message: { content: { type: "global" } },
dispatch: sinon.stub(),
};
InfoBar.observe({}, "domwindowopened");
Assert.ok(stub.notCalled);
// Cleanup
cleanupInfobars();
stub.restore();
});
add_task(async function infobarCallback_dismissed_universal() {
const browser = BrowserWindowTracker.getTopWindow().gBrowser.selectedBrowser;
const dispatch = sinon.stub();
sinon
.stub(InfoBar, "showNotificationAllWindows")
.callsFake(async notif => await notif.showNotification(browser));
let infobar = await InfoBar.showInfoBarMessage(
browser,
UNIVERSAL_MESSAGE,
dispatch
);
// Reset the dispatch count to just watch for the DISMISSED ping
dispatch.reset();
infobar.infobarCallback("notremovedevent");
Assert.equal(dispatch.callCount, 1);
Assert.equal(dispatch.firstCall.args[0].data.event, "DISMISSED");
Assert.deepEqual(InfoBar._universalInfobars, []);
// Cleanup
cleanupInfobars();
sinon.restore();
});
add_task(async function removeObserver_on_removeUniversalInfobars() {
const sandbox = sinon.createSandbox();
sandbox.stub(InfoBar, "showNotificationAllWindows").resolves();
let browser = BrowserWindowTracker.getTopWindow().gBrowser.selectedBrowser;
let dispatch = sandbox.stub();
// Show the universal infobar so it registers the observer
let infobar = await InfoBar.showInfoBarMessage(
browser,
UNIVERSAL_MESSAGE,
dispatch
);
Assert.ok(infobar, "Got an InfoBar notification");
// Swap out Services.obs so removeObserver is spyable
let origObs = Services.obs;
let removeSpy = sandbox.spy();
Services.obs = {
addObserver: origObs.addObserver.bind(origObs),
removeObserver: removeSpy,
notifyObservers: origObs.notifyObservers.bind(origObs),
};
infobar.removeUniversalInfobars();
Assert.ok(
removeSpy.calledWith(InfoBar, "domwindowopened"),
"removeObserver was invoked for domwindowopened"
);
// Cleanup
Services.obs = origObs;
sandbox.restore();
cleanupInfobars();
});

View file

@ -19,7 +19,7 @@
* Tests that the exposed properties of the component are reflected
* as expected in the shadow DOM.
*/
add_task(async function test_properties() {
add_task(async function test_exposed_properties() {
let message = document.createElement("fxa-menu-message");
const TEST_IMAGE_URL = "chrome://activity-stream/content/data/content/assets/fox-doodle-waving-static.png";
message.imageURL = TEST_IMAGE_URL;
@ -39,7 +39,7 @@
is(image.src, TEST_IMAGE_URL, "The img element got the expected URL");
let button = shadow.querySelector("#sign-up-button");
is(button.textContent, TEST_BUTTON_TEXT, "The sign-up button got the right text.");
is(button.textContent.trim(), TEST_BUTTON_TEXT, "The sign-up button got the right text.");
let primaryText = shadow.querySelector("#primary");
is(primaryText.textContent, PRIMARY_TEXT, "The primary text was correct.");
@ -47,6 +47,14 @@
let secondaryText = shadow.querySelector("#secondary");
is(secondaryText.textContent, SECONDARY_TEXT, "The secondary text was correct.");
let container = shadow.querySelector("#container");
is(container.getAttribute("layout"), "column", "The layout should default to 'column' when no layout is explicitly set.");
const TEST_ROW_LAYOUT = "row";
message.layout = TEST_ROW_LAYOUT
await message.updateComplete;
is(container.getAttribute("layout"), TEST_ROW_LAYOUT, "The layout attribute should update to 'row' when set.");
message.remove();
});
@ -54,7 +62,7 @@
* Tests that the buttons exposed by the component emit the expected
* events.
*/
add_task(async function test_properties() {
add_task(async function test_button_properties() {
let message = document.createElement("fxa-menu-message");
const TEST_BUTTON_TEXT = "Howdy, partner! Sign up!";
message.buttonText = TEST_BUTTON_TEXT;
@ -84,7 +92,7 @@
* Tests that the sign-up button is focused by default, and that focus
* can be changed via the keyboard.
*/
add_task(async function test_focus() {
add_task(async function test_signup_focus() {
let message = document.createElement("fxa-menu-message");
const TEST_BUTTON_TEXT = "Howdy, partner! Sign up!";
message.buttonText = TEST_BUTTON_TEXT;
@ -125,7 +133,7 @@
* Tests that setting no imageURL makes it so that the image element is
* not visible, and setting one makes it visible.
*/
add_task(async function test_focus() {
add_task(async function test_image_visibility() {
let message = document.createElement("fxa-menu-message");
const TEST_BUTTON_TEXT = "Howdy, partner! Sign up!";
message.buttonText = TEST_BUTTON_TEXT;
@ -144,35 +152,95 @@
message.remove();
});
/**
* Tests that setting the --illustration-margin-block-offset forwards that
* offset to the illustration container.
*/
add_task(async function test_focus() {
async function testIllustrationOffset({
layout,
offsetVar,
cssProperty,
checkContainerOffset = false,
}) {
const TEST_DEFAULT_VALUE = "0px";
const TEST_CONTAINER_OFFSET = "10px";
const TEST_ILLUSTRATION_OFFSET = "123px";
const TEST_IMAGE_URL =
"chrome://activity-stream/content/data/content/assets/fox-doodle-waving-static.png";
let message = document.createElement("fxa-menu-message");
const TEST_IMAGE_URL = "chrome://activity-stream/content/data/content/assets/fox-doodle-waving-static.png";
message.imageURL = TEST_IMAGE_URL;
if (layout) {
message.layout = layout;
}
let content = document.getElementById("content");
content.appendChild(message);
document.getElementById("content").appendChild(message);
await message.updateComplete;
let illustrationContainer = message.shadowRoot.querySelector("#illustration-container");
ok(!isHidden(illustrationContainer), "Illustration container should not be hidden.");
let messageStyle = window.getComputedStyle(illustrationContainer);
is(messageStyle.marginBlockStart, "0px", "Illustration offset should default to 0px.");
let illustrationContainer = message.shadowRoot.querySelector(
"#illustration-container"
);
ok(
!isHidden(illustrationContainer),
"Illustration container should not be hidden."
);
const TEST_OFFSET = "123px";
message.style.setProperty("--illustration-margin-block-offset", TEST_OFFSET);
messageStyle = window.getComputedStyle(illustrationContainer);
let computedStyle = window.getComputedStyle(illustrationContainer);
is(
messageStyle.marginBlockStart,
TEST_OFFSET,
computedStyle[cssProperty],
TEST_DEFAULT_VALUE,
"Illustration offset should default to 0px"
);
message.style.setProperty(offsetVar, TEST_ILLUSTRATION_OFFSET);
computedStyle = window.getComputedStyle(illustrationContainer);
is(
computedStyle[cssProperty],
TEST_ILLUSTRATION_OFFSET,
"Illustration offset should have been forwarded to the container."
);
if (checkContainerOffset) {
const container = message.shadowRoot.querySelector("#container");
let containerStyle = window.getComputedStyle(container);
is(
containerStyle.marginBlockEnd,
TEST_DEFAULT_VALUE,
"Container offset should default to 0px."
);
message.style.setProperty(
"--container-margin-block-end-offset",
TEST_CONTAINER_OFFSET
);
containerStyle = window.getComputedStyle(container);
is(
containerStyle.marginBlockEnd,
TEST_CONTAINER_OFFSET,
"Container offset should have been applied."
);
}
message.remove();
}
/**
* Tests that setting the --illustration-margin-block-start-offset forwards that
* offset to the illustration container for 'column' layout.
*/
add_task(async function test_column_layout_illustration_offset() {
await testIllustrationOffset({
layout: null,
offsetVar: "--illustration-margin-block-start-offset",
cssProperty: "marginBlockStart",
});
});
/**
* Tests that setting the --illustration-margin-block-end-offset forwards that
* offset to the illustration container for 'row' layout.
*/
add_task(async function test_row_layout_illustration_offset() {
await testIllustrationOffset({
layout: "row",
offsetVar: "--illustration-margin-block-end-offset",
cssProperty: "marginBlockEnd",
checkContainerOffset: true,
});
});
</script>
</head>

View file

@ -16,13 +16,7 @@
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
XPCOMUtils.defineLazyServiceGetter(
lazy,
"gContentAnalysis",
"@mozilla.org/contentanalysis;1",
Ci.nsIContentAnalysis
);
let internalContentAnalysisService = undefined;
ChromeUtils.defineESModuleGetters(lazy, {
clearTimeout: "resource://gre/modules/Timer.sys.mjs",
@ -114,6 +108,41 @@ export const ContentAnalysis = {
*/
warnDialogRequestTokens: new Set(),
/**
* The nsIContentAnalysis to use instead of lazy.gContentAnalysis. Should
* only be used for tests.
*
* @type {nsIContentAnalysis?}
*/
mockContentAnalysisForTest: undefined,
/**
* The nsIContentAnalysis to use. Nothing else in this file should
* use lazy.gContentAnalysis.
*
* @returns {nsIContentAnalysis}
*/
get contentAnalysis() {
if (this.mockContentAnalysisForTest) {
return this.mockContentAnalysisForTest;
}
if (!internalContentAnalysisService) {
internalContentAnalysisService = Cc[
"@mozilla.org/contentanalysis;1"
].getService(Ci.nsIContentAnalysis);
}
return internalContentAnalysisService;
},
/**
* Sets the nsIContentAnalysis to use. Should only be used for tests.
*
* @param {nsIContentAnalysis?} contentAnalysis
*/
setMockContentAnalysisForTest(contentAnalysis) {
this.mockContentAnalysisForTest = contentAnalysis;
},
/**
* Registers for various messages/events that will indicate the
* need for communicating something to the user.
@ -121,13 +150,14 @@ export const ContentAnalysis = {
* @param {Window} window - The window to monitor
*/
initialize(window) {
if (!lazy.gContentAnalysis.isActive) {
if (!this.contentAnalysis.isActive) {
this.uninitialize();
return;
}
let doc = window.document;
if (!this.isInitialized) {
this.isInitialized = true;
this.initializeDownloadCA();
this.initializeObservers();
ChromeUtils.defineLazyGetter(this, "l10n", function () {
return new Localization(
@ -153,13 +183,15 @@ export const ContentAnalysis = {
if (this.isInitialized) {
this.isInitialized = false;
this.requestTokenToRequestInfo.clear();
this.userActionToBusyDialogMap.clear();
this.uninitializeObservers();
}
},
/**
* Register UI for file download CA events.
* Register UI for CA events.
*/
async initializeDownloadCA() {
initializeObservers() {
Services.obs.addObserver(this, "dlp-request-made");
Services.obs.addObserver(this, "dlp-response");
Services.obs.addObserver(this, "quit-application");
@ -167,6 +199,17 @@ export const ContentAnalysis = {
Services.obs.addObserver(this, "quit-application-requested");
},
/**
* Unregister UI for CA events.
*/
uninitializeObservers() {
Services.obs.removeObserver(this, "dlp-request-made");
Services.obs.removeObserver(this, "dlp-response");
Services.obs.removeObserver(this, "quit-application");
Services.obs.removeObserver(this, "quit-application-granted");
Services.obs.removeObserver(this, "quit-application-requested");
},
// nsIObserver
async observe(aSubj, aTopic, _aData) {
switch (aTopic) {
@ -216,7 +259,7 @@ export const ContentAnalysis = {
// DLP requests, but the "DLP busy" or "DLP blocked" dialog can block the
// main thread, thus preventing the "quit-application" from being sent,
// which causes a shutdownhang. (bug 1899703)
lazy.gContentAnalysis.cancelAllRequests(true);
this.contentAnalysis.cancelAllRequests(true);
}
break;
}
@ -230,7 +273,7 @@ export const ContentAnalysis = {
// to call respondToWarnDialog() again.
this.warnDialogRequestTokens = new Set();
for (let warnDialogRequestToken of requestTokensToCancel) {
lazy.gContentAnalysis.respondToWarnDialog(
this.contentAnalysis.respondToWarnDialog(
warnDialogRequestToken,
false
);
@ -717,12 +760,15 @@ export const ContentAnalysis = {
// the dialog, no need to log the exception.
})
.finally(() => {
// This is also be called if the tab/window is closed while a request is in progress,
// in which case we need to cancel the request.
// This is also called if the tab/window is closed while a request is
// in progress, in which case we need to cancel all related requests.
if (this.requestTokenToRequestInfo.delete(aRequestToken)) {
// TODO: Is this useful? I think no.
this._removeSlowCAMessage(aUserActionId, aRequestToken);
lazy.gContentAnalysis.cancelRequestsByRequestToken(aRequestToken);
}
this.contentAnalysis.cancelAllRequestsAssociatedWithUserAction(
aUserActionId
);
});
return {
dialogBrowsingContext: aBrowsingContext,
@ -807,7 +853,7 @@ export const ContentAnalysis = {
// to the request already, so don't call respondToWarnDialog()
// if aRequestToken is not in warnDialogRequestTokens.
if (this.warnDialogRequestTokens.delete(aRequestToken)) {
lazy.gContentAnalysis.respondToWarnDialog(aRequestToken, allow);
this.contentAnalysis.respondToWarnDialog(aRequestToken, allow);
}
return null;
}
@ -831,7 +877,7 @@ export const ContentAnalysis = {
case Ci.nsIContentAnalysisRequest.eClipboard: {
// Unlike the cases below, this can be shown when the DLP
// agent is not available. We use a different message for that.
const caInfo = await lazy.gContentAnalysis.getDiagnosticInfo();
const caInfo = await this.contentAnalysis.getDiagnosticInfo();
titleId = "contentanalysis-block-dialog-title-clipboard";
bodyId = caInfo.connectedToAgent
? "contentanalysis-block-dialog-body-clipboard"
@ -980,7 +1026,7 @@ export const ContentAnalysis = {
* @param {ResourceNameOrOperationType} aResourceNameOrOperationType
*/
async _warnDialogText(aResourceNameOrOperationType) {
const caInfo = await lazy.gContentAnalysis.getDiagnosticInfo();
const caInfo = await this.contentAnalysis.getDiagnosticInfo();
if (caInfo.connectedToAgent) {
return await this.l10n.formatValue("contentanalysis-warndialogtext", {
content: this._getResourceNameFromNameOrOperationType(

View file

@ -199,6 +199,21 @@
</popupnotificationcontent>
</popupnotification>
<popupnotification id="appMenu-update-other-instance-notification"
popupid="update-other-instance"
data-lazy-l10n-id="appmenu-update-other-instance"
data-l10n-attrs="buttonlabel, buttonaccesskey, secondarybuttonlabel, secondarybuttonaccesskey"
closebuttonhidden="true"
dropmarkerhidden="true"
checkboxhidden="true"
buttonhighlight="true"
hasicon="true"
hidden="true">
<popupnotificationcontent id="update-other-instance-notification-content" orient="vertical">
<description id="update-other-instance-description" data-lazy-l10n-id="appmenu-update-other-instance-message"></description>
</popupnotificationcontent>
</popupnotification>
<popupnotification id="appMenu-addon-installed-notification"
popupid="addon-installed"
closebuttonhidden="true"

View file

@ -1409,8 +1409,6 @@
},
"SearchEngines": {
"enterprise_only": true,
"type": "object",
"properties": {
"Add": {

View file

@ -159,6 +159,12 @@
"manifest": ["sidebar_action"],
"paths": [["sidebarAction"]]
},
"tabGroups": {
"url": "chrome://browser/content/parent/ext-tabGroups.js",
"schema": "chrome://browser/content/schemas/tabGroups.json",
"scopes": ["addon_parent"],
"paths": [["tabGroups"]]
},
"tabs": {
"url": "chrome://browser/content/parent/ext-tabs.js",
"schema": "chrome://browser/content/schemas/tabs.json",

View file

@ -25,6 +25,7 @@ browser.jar:
content/browser/parent/ext-search.js (parent/ext-search.js)
content/browser/parent/ext-sessions.js (parent/ext-sessions.js)
content/browser/parent/ext-sidebarAction.js (parent/ext-sidebarAction.js)
content/browser/parent/ext-tabGroups.js (parent/ext-tabGroups.js)
content/browser/parent/ext-tabs.js (parent/ext-tabs.js)
content/browser/parent/ext-topSites.js (parent/ext-topSites.js)
content/browser/parent/ext-url-overrides.js (parent/ext-url-overrides.js)

View file

@ -0,0 +1,230 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
ChromeUtils.defineESModuleGetters(this, {
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
});
var { ExtensionError } = ExtensionUtils;
const spellColour = color => (color === "grey" ? "gray" : color);
this.tabGroups = class extends ExtensionAPIPersistent {
queryGroups({ collapsed, color, title, windowId } = {}) {
color = spellColour(color);
let glob = title != null && new MatchGlob(title);
let window =
windowId != null && windowTracker.getWindow(windowId, null, false);
return windowTracker
.browserWindows()
.filter(
win =>
this.extension.canAccessWindow(win) &&
(windowId == null || win === window)
)
.flatMap(win => win.gBrowser.tabGroups)
.filter(
group =>
(collapsed == null || group.collapsed === collapsed) &&
(color == null || group.color === color) &&
(title == null || glob.matches(group.name))
);
}
get(groupId) {
let gid = getInternalTabGroupIdForExtTabGroupId(groupId);
if (!gid) {
throw new ExtensionError(`No group with id: ${groupId}`);
}
for (let group of this.queryGroups()) {
if (group.id === gid) {
return group;
}
}
throw new ExtensionError(`No group with id: ${groupId}`);
}
convert(group) {
return {
collapsed: !!group.collapsed,
/** Internally we use "gray", but Chrome uses "grey" @see spellColour. */
color: group.color === "gray" ? "grey" : group.color,
id: getExtTabGroupIdForInternalTabGroupId(group.id),
title: group.name,
windowId: windowTracker.getId(group.ownerGlobal),
};
}
PERSISTENT_EVENTS = {
onCreated({ fire }) {
let onCreate = event => {
if (event.detail.isAdoptingGroup) {
// Tab group moved from a different window.
return;
}
fire.async(this.convert(event.originalTarget));
};
windowTracker.addListener("TabGroupCreate", onCreate);
return {
unregister() {
windowTracker.removeListener("TabGroupCreate", onCreate);
},
convert(_fire) {
fire = _fire;
},
};
},
onMoved({ fire }) {
let onMove = event => {
fire.async(this.convert(event.originalTarget));
};
let onCreate = event => {
if (event.detail.isAdoptingGroup) {
// Tab group moved from a different window.
fire.async(this.convert(event.originalTarget));
}
};
windowTracker.addListener("TabGroupMoved", onMove);
windowTracker.addListener("TabGroupCreate", onCreate);
return {
unregister() {
windowTracker.removeListener("TabGroupMoved", onMove);
windowTracker.removeListener("TabGroupCreate", onCreate);
},
convert(_fire) {
fire = _fire;
},
};
},
onRemoved({ fire }) {
let onRemove = event => {
if (event.originalTarget.removedByAdoption) {
// Tab group moved to a different window.
return;
}
fire.async(this.convert(event.originalTarget));
};
windowTracker.addListener("TabGroupRemoved", onRemove);
return {
unregister() {
windowTracker.removeListener("TabGroupRemoved", onRemove);
},
convert(_fire) {
fire = _fire;
},
};
},
onUpdated({ fire }) {
let onUpdate = event => {
fire.async(this.convert(event.originalTarget));
};
windowTracker.addListener("TabGroupCollapse", onUpdate);
windowTracker.addListener("TabGroupExpand", onUpdate);
windowTracker.addListener("TabGroupUpdate", onUpdate);
return {
unregister() {
windowTracker.removeListener("TabGroupCollapse", onUpdate);
windowTracker.removeListener("TabGroupExpand", onUpdate);
windowTracker.removeListener("TabGroupUpdate", onUpdate);
},
convert(_fire) {
fire = _fire;
},
};
},
};
getAPI(context) {
const { windowManager } = this.extension;
return {
tabGroups: {
get: groupId => {
return this.convert(this.get(groupId));
},
move: (groupId, { index, windowId }) => {
let group = this.get(groupId);
let win = group.ownerGlobal;
if (windowId != null) {
win = windowTracker.getWindow(windowId, context);
if (
PrivateBrowsingUtils.isWindowPrivate(group.ownerGlobal) !==
PrivateBrowsingUtils.isWindowPrivate(win)
) {
throw new ExtensionError(
"Can't move groups between private and non-private windows"
);
}
if (windowManager.getWrapper(win).type !== "normal") {
throw new ExtensionError(
"Groups can only be moved to normal windows."
);
}
}
if (win !== group.ownerGlobal) {
let last = win.gBrowser.tabContainer.ariaFocusableItems.length + 1;
let elementIndex = index === -1 ? last : Math.min(index, last);
group = win.gBrowser.adoptTabGroup(group, elementIndex);
} else if (index >= 0 && index < win.gBrowser.tabs.length) {
win.gBrowser.moveTabTo(group, { tabIndex: index });
} else if (win.gBrowser.tabs.at(-1) !== group.tabs.at(-1)) {
win.gBrowser.moveTabAfter(group, win.gBrowser.tabs.at(-1));
}
return this.convert(group);
},
query: query => {
return Array.from(this.queryGroups(query ?? {}), group =>
this.convert(group)
);
},
update: (groupId, { collapsed, color, title }) => {
let group = this.get(groupId);
if (collapsed != null) {
group.collapsed = collapsed;
}
if (color != null) {
group.color = spellColour(color);
}
if (title != null) {
group.name = title;
}
return this.convert(group);
},
onCreated: new EventManager({
context,
module: "tabGroups",
event: "onCreated",
extensionApi: this,
}).api(),
onMoved: new EventManager({
context,
module: "tabGroups",
event: "onMoved",
extensionApi: this,
}).api(),
onRemoved: new EventManager({
context,
module: "tabGroups",
event: "onRemoved",
extensionApi: this,
}).api(),
onUpdated: new EventManager({
context,
module: "tabGroups",
event: "onUpdated",
extensionApi: this,
}).api(),
},
};
}
};

View file

@ -267,11 +267,15 @@ this.tabs = class extends ExtensionAPIPersistent {
let moveListener = event => {
let nativeTab = event.originalTarget;
let { previousTabState, currentTabState } = event.detail;
if (tabManager.canAccessTab(nativeTab)) {
let fromIndex = previousTabState.tabIndex;
let toIndex = currentTabState.tabIndex;
// TabMove also fires if its tab group changes; we should only fire
// event if the position actually moved.
if (fromIndex !== toIndex && tabManager.canAccessTab(nativeTab)) {
fire.async(tabTracker.getId(nativeTab), {
windowId: windowTracker.getId(nativeTab.ownerGlobal),
fromIndex: previousTabState.tabIndex,
toIndex: currentTabState.tabIndex,
fromIndex,
toIndex,
});
}
};

View file

@ -20,6 +20,7 @@ browser.jar:
content/browser/schemas/search.json
content/browser/schemas/sessions.json
content/browser/schemas/sidebar_action.json
content/browser/schemas/tabGroups.json
content/browser/schemas/tabs.json
content/browser/schemas/top_sites.json
content/browser/schemas/url_overrides.json

View file

@ -0,0 +1,231 @@
[
{
"namespace": "manifest",
"types": [
{
"$extend": "OptionalPermissionNoPrompt",
"choices": [
{
"type": "string",
"enum": ["tabGroups"]
}
]
}
]
},
{
"namespace": "tabGroups",
"description": "Use the browser.tabGroups API to interact with the browser's tab grouping system. You can use this API to modify, and rearrange tab groups.",
"permissions": ["tabGroups"],
"types": [
{
"id": "Color",
"type": "string",
"description": "The group's color, using 'grey' spelling for compatibility with Chromium.",
"enum": [
"blue",
"cyan",
"grey",
"green",
"orange",
"pink",
"purple",
"red",
"yellow"
]
},
{
"id": "TabGroup",
"type": "object",
"description": "State of a tab group inside of an open window.",
"properties": {
"collapsed": {
"type": "boolean",
"description": "Whether the tab group is collapsed or expanded in the tab strip."
},
"color": {
"$ref": "Color",
"description": "User-selected color name for the tab group's label/icons."
},
"id": {
"type": "integer",
"description": "Unique ID of the tab group."
},
"title": {
"type": "string",
"optional": true,
"description": "User-defined name of the tab group."
},
"windowId": {
"type": "integer",
"description": "Window that the tab group is in."
}
}
}
],
"properties": {
"TAB_GROUP_ID_NONE": {
"value": -1,
"description": "An ID that represents the absence of a group."
}
},
"functions": [
{
"name": "get",
"type": "function",
"description": "Retrieves details about the specified group.",
"async": "callback",
"parameters": [
{
"type": "integer",
"name": "groupId",
"minimum": 0
},
{
"type": "function",
"name": "callback",
"parameters": [{ "name": "group", "$ref": "TabGroup" }]
}
]
},
{
"name": "move",
"type": "function",
"description": "Move a group within, or to another window.",
"async": "callback",
"parameters": [
{
"type": "integer",
"name": "groupId",
"minimum": 0
},
{
"type": "object",
"name": "moveProperties",
"properties": {
"index": {
"type": "integer",
"minimum": -1
},
"windowId": {
"type": "integer",
"optional": true,
"minimum": 0
}
}
},
{
"type": "function",
"name": "callback",
"parameters": [{ "name": "group", "$ref": "TabGroup" }]
}
]
},
{
"name": "query",
"type": "function",
"description": "Return all grups, or find groups with specified properties.",
"async": "callback",
"parameters": [
{
"type": "object",
"name": "queryInfo",
"optional": true,
"properties": {
"collapsed": {
"type": "boolean",
"optional": true
},
"color": {
"$ref": "Color",
"optional": true
},
"title": {
"type": "string",
"optional": true
},
"windowId": {
"type": "integer",
"optional": true,
"minimum": -2
}
}
},
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "groups",
"type": "array",
"items": { "$ref": "TabGroup" }
}
]
}
]
},
{
"name": "update",
"type": "function",
"description": "Modifies state of a specified group.",
"async": "callback",
"parameters": [
{
"type": "integer",
"name": "groupId",
"minimum": 0
},
{
"type": "object",
"name": "updateProperties",
"properties": {
"collapsed": {
"type": "boolean",
"optional": true
},
"color": {
"$ref": "Color",
"optional": true
},
"title": {
"type": "string",
"optional": true
}
}
},
{
"type": "function",
"name": "callback",
"parameters": [{ "name": "group", "$ref": "TabGroup" }]
}
]
}
],
"events": [
{
"name": "onCreated",
"type": "function",
"description": "Fired when a tab group is created.",
"parameters": [{ "$ref": "TabGroup", "name": "group" }]
},
{
"name": "onMoved",
"type": "function",
"description": "Fired when a tab group is moved, within a window or to another window.",
"parameters": [{ "$ref": "TabGroup", "name": "group" }]
},
{
"name": "onRemoved",
"type": "function",
"description": "Fired when a tab group is removed.",
"parameters": [{ "$ref": "TabGroup", "name": "group" }]
},
{
"name": "onUpdated",
"type": "function",
"description": "Fired when a tab group is updated.",
"parameters": [{ "$ref": "TabGroup", "name": "group" }]
}
]
}
]

View file

@ -481,6 +481,14 @@ skip-if = ["true"] # Bug 1575369
["browser_ext_slow_script.js"]
https_first_disabled = true
["browser_ext_tabGroups.js"]
["browser_ext_tabGroups_move_event_order.js"]
["browser_ext_tabGroups_move_onMoved.js"]
["browser_ext_tabGroups_query.js"]
["browser_ext_tab_runtimeConnect.js"]
["browser_ext_tabs_attention.js"]
@ -546,6 +554,8 @@ https_first_disabled = true
["browser_ext_tabs_group_ungroup.js"]
["browser_ext_tabs_group_windowId.js"]
["browser_ext_tabs_hide.js"]
https_first_disabled = true

View file

@ -0,0 +1,211 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const {
Management: {
global: { getExtTabGroupIdForInternalTabGroupId },
},
} = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
add_task(async function tabsGroups_get_private() {
async function loadExt(allowPrivate) {
let ext = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["tabGroups"],
},
incognitoOverride: allowPrivate ? "spanning" : undefined,
background() {
browser.test.onMessage.addListener(async groupId => {
try {
let group = await browser.tabGroups.get(groupId);
let color = browser.tabGroups.Color[group.color.toUpperCase()];
browser.test.assertEq(group.color, color, "A known colour.");
browser.test.sendMessage("group", group);
} catch (e) {
browser.test.sendMessage("error", e.message);
}
});
},
});
await ext.startup();
return ext;
}
let url = "https://example.com/?";
let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
let group1 = gBrowser.addTabGroup([tab1]);
let gid1 = getExtTabGroupIdForInternalTabGroupId(group1.id);
let win2 = await BrowserTestUtils.openNewBrowserWindow({ private: true });
let tab2 = await BrowserTestUtils.openNewForegroundTab(win2.gBrowser, url);
let group2 = win2.gBrowser.addTabGroup([tab2]);
let gid2 = getExtTabGroupIdForInternalTabGroupId(group2.id);
function compare(group, ext, info) {
is(group.collapsed, ext.collapsed, `Collapsed ok - ${info}`);
is(group.name, ext.title, `Title ok - ${info}`);
is(group.color.replace(/^gray$/, "grey"), ext.color, `Color ok - ${info}`);
}
info("Testing extension without private browsing access.");
let ext1 = await loadExt(false);
ext1.sendMessage(gid1);
let g1 = await ext1.awaitMessage("group");
compare(group1, g1, "Ext 1 / group 1.");
ext1.sendMessage(gid2);
let e2 = await ext1.awaitMessage("error");
is(e2, `No group with id: ${gid2}`, "Expected error.");
await ext1.unload();
info("Testing extension with private browsing access.");
let ext2 = await loadExt(true);
ext2.sendMessage(gid1);
g1 = await ext2.awaitMessage("group");
compare(group1, g1, "Ext 2 / group 1.");
ext2.sendMessage(gid1);
let g2 = await ext2.awaitMessage("group");
compare(group1, g2, "Ext 2 / group 2.");
await ext2.unload();
BrowserTestUtils.removeTab(tab1);
await BrowserTestUtils.closeWindow(win2);
});
add_task(async function tabsGroups_update_onUpdated() {
let url = "https://example.com/?";
let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, url + 1);
let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, url + 2);
let group1 = gBrowser.addTabGroup([tab1]);
let group2 = gBrowser.addTabGroup([tab2]);
gBrowser.selectedTab = tab1;
info("Setup initial group colour for consistency.");
group1.color = "red";
group2.color = "blue";
let ext = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["tabGroups"],
},
async background() {
let [tab] = await browser.tabs.query({
lastFocusedWindow: true,
active: true,
});
let group = await browser.tabGroups.get(tab.groupId);
browser.test.assertEq(tab.groupId, group.id, "Group id matches.");
browser.test.assertEq(tab.windowId, group.windowId, "Window id ok.");
browser.tabGroups.onCreated.addListener(group => {
browser.test.sendMessage("created", group);
});
browser.tabGroups.onRemoved.addListener(group => {
browser.test.sendMessage("removed", group);
});
browser.tabGroups.onUpdated.addListener(async updated => {
let group = await browser.tabGroups.get(updated.id);
browser.test.assertDeepEq(updated, group, "It's the same group.");
browser.test.sendMessage("updated", updated);
});
browser.test.assertThrows(
() => browser.tabGroups.update(1e9, { color: "magenta" }),
/Invalid enumeration value "magenta"/,
"Magenta is not a real color."
);
await browser.test.assertRejects(
browser.tabGroups.update(1e9, { title: "blah" }),
"No group with id: 1000000000",
"Invalid group id rejects."
);
browser.test.onMessage.addListener((_msg, update) => {
browser.tabGroups.update(group.id, update);
});
browser.test.sendMessage("group", group);
},
});
await ext.startup();
function compare(first, second, info) {
let { color, title, collapsed } = first;
// XUL element has a .name, and uses "gray".
if (XULElement.isInstance(first)) {
color = first.color.replace(/^gray$/, "grey");
title = first.name;
}
is(collapsed, second.collapsed, `Collapsed ok - ${info}`);
is(color, second.color, `Color ok - ${info}`);
is(title, second.title, `Title ok - ${info}`);
}
let first = await ext.awaitMessage("group");
compare(group1, first, "Initial group.");
info("Updating group1 using extension api.");
ext.sendMessage("update", { collapsed: true });
first.collapsed = true;
let updated = await ext.awaitMessage("updated");
compare(first, updated, "After collapsing 1.");
ext.sendMessage("update", { collapsed: false });
first.collapsed = false;
updated = await ext.awaitMessage("updated");
compare(first, updated, "After expanding 1.");
ext.sendMessage("update", { color: "grey" });
first.color = "grey";
updated = await ext.awaitMessage("updated");
compare(first, updated, "After coloring 1.");
ext.sendMessage("update", { title: "First" });
first.title = "First";
updated = await ext.awaitMessage("updated");
compare(first, updated, "After naming 1.");
info("Creating a new group does not trigger onUpdated event.");
let tab3 = await BrowserTestUtils.openNewForegroundTab(gBrowser, url + 3);
gBrowser.addTabGroup([tab3]);
await ext.awaitMessage("created");
info("Closing tab1, and thus its group, does not trigger onUpdated.");
BrowserTestUtils.removeTab(tab1);
await ext.awaitMessage("removed");
info("Updating group2 directly from outside of the extension.");
group2.collapsed = true;
let second = await ext.awaitMessage("updated");
compare(group2, second, "After collapsing 2.");
group2.color = "pink";
second = await ext.awaitMessage("updated");
compare(group2, second, "After coloring 2.");
group2.name = "Second";
second = await ext.awaitMessage("updated");
compare(group2, second, "After naming 2.");
isnot(first.id, second.id, "Groups have different ids.");
is(first.windowId, second.windowId, "Groups in the same window.");
await ext.unload();
BrowserTestUtils.removeTab(tab3);
BrowserTestUtils.removeTab(tab2);
});

View file

@ -0,0 +1,175 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// TODO bug 1938594: moving a created tab to another window sometimes triggers
// this error. See https://bugzilla.mozilla.org/show_bug.cgi?id=1938594#c1
PromiseTestUtils.allowMatchingRejectionsGlobally(
/Unexpected undefined tabState for onMoveToNewWindow/
);
// There is special logic for adopting tab groups at the end of a tab strip.
// This test tests the behavior at the penultimate tab. For extra coverage,
// this test uses extension APIs only to create windows, tabs, groups.
// It also checks tabs.onMoved, tabs.onAttached and tabs.onDetached.
add_task(async function tabGroups_move_to_other_window() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["tabGroups"],
},
async background() {
// Setup: The tab that we are going to group.
const { id: tabId, windowId: oldWinId } = await browser.tabs.create({});
browser.test.log(`Tab ID: ${tabId}, old win ID: ${oldWinId}`);
const groupId = await browser.tabs.group({ tabIds: tabId });
// Setup: Create window with two tabs.
const { id: windowId } = await browser.windows.create({});
const tab2 = await browser.tabs.create({ windowId });
browser.test.assertEq(1, tab2.index, `Two tabs in window ${windowId}`);
const events = [];
browser.tabGroups.onCreated.addListener(group => {
browser.test.fail(`Unexpected tabGroups.onCreated: ${group.id}`);
});
browser.tabGroups.onRemoved.addListener(group => {
browser.test.fail(`Unexpected tabGroups.onRemoved: ${group.id}`);
});
browser.tabGroups.onMoved.addListener(group => {
browser.test.assertEq(groupId, group.id, "onMoved fired for group");
events.push("tabGroups.onMoved");
});
browser.tabs.onMoved.addListener((movedTabId, moveInfo) => {
// onDetached & onAttached should fire when moving to another window.
browser.test.fail(
`Unexpected tabs.onMoved: ${JSON.stringify(moveInfo)}`
);
});
browser.tabs.onDetached.addListener((movedTabId, detachInfo) => {
browser.test.assertEq(tabId, movedTabId, "Our tab detached");
browser.test.assertEq(oldWinId, detachInfo.oldWindowId, "oldWindowId");
browser.test.assertEq(1, detachInfo.oldPosition, "oldPosition");
events.push("tabs.onDetached");
});
browser.tabs.onAttached.addListener((movedTabId, attachInfo) => {
browser.test.assertEq(tabId, movedTabId, "Our tab attached");
browser.test.assertEq(windowId, attachInfo.newWindowId, "newWindowId");
browser.test.assertEq(1, attachInfo.newPosition, "newPosition");
events.push("tabs.onAttached");
});
browser.test.assertDeepEq([], events, "No tabGroups event yet");
// Move tab group between the (only) two existing tabs in the window.
const moved = await browser.tabGroups.move(groupId, {
windowId,
index: 1,
});
browser.test.assertEq(groupId, moved.id, "Group ID did not change");
browser.test.assertEq(windowId, moved.windowId, "Group moved to window");
browser.test.assertEq(
1,
(await browser.tabs.get(tabId)).index,
"Tab appears at the expected index"
);
await browser.windows.remove(windowId);
browser.test.assertDeepEq(
["tabs.onDetached", "tabs.onAttached", "tabGroups.onMoved"],
events,
"Expected events when moving tab group to a new window"
);
browser.test.sendMessage("done");
},
});
await extension.startup();
await extension.awaitMessage("done");
await extension.unload();
});
add_task(async function tabGroups_move_multiple_tabs_to_other_window() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["tabGroups"],
},
async background() {
// Setup: The tabs that we are going to group.
const tabs = [
await browser.tabs.create({}),
await browser.tabs.create({}),
];
const oldWinId = tabs[0].windowId;
const tabIds = tabs.map(t => t.id);
browser.test.log(`Tab ID: ${tabIds}, old win ID: ${oldWinId}`);
const groupId = await browser.tabs.group({ tabIds });
// Setup: Create window with two tabs.
const { id: windowId } = await browser.windows.create({});
const tab2 = await browser.tabs.create({ windowId });
browser.test.assertEq(1, tab2.index, `Two tabs in window ${windowId}`);
const events = [];
browser.tabGroups.onCreated.addListener(group => {
browser.test.fail(`Unexpected tabGroups.onCreated: ${group.id}`);
});
browser.tabGroups.onRemoved.addListener(group => {
browser.test.fail(`Unexpected tabGroups.onRemoved: ${group.id}`);
});
browser.tabGroups.onMoved.addListener(group => {
browser.test.assertEq(groupId, group.id, "onMoved fired for group");
events.push("tabGroups.onMoved");
});
browser.tabs.onMoved.addListener((movedTabId, moveInfo) => {
// onDetached & onAttached should fire when moving to another window.
browser.test.fail(
`Unexpected tabs.onMoved: ${JSON.stringify(moveInfo)}`
);
});
browser.tabs.onDetached.addListener((movedTabId, detachInfo) => {
browser.test.assertEq(oldWinId, detachInfo.oldWindowId, "oldWindowId");
events.push(`tabs.onDetached:${detachInfo.oldPosition}:${movedTabId}`);
});
browser.tabs.onAttached.addListener((movedTabId, attachInfo) => {
browser.test.assertEq(windowId, attachInfo.newWindowId, "newWindowId");
events.push(`tabs.onAttached:${attachInfo.newPosition}:${movedTabId}`);
});
browser.test.assertDeepEq([], events, "No tabGroups event yet");
// Move tab group to start of tab strip.
const moved = await browser.tabGroups.move(groupId, {
windowId,
index: 0,
});
browser.test.assertEq(groupId, moved.id, "Group ID did not change");
browser.test.assertEq(windowId, moved.windowId, "Group moved to window");
browser.test.assertEq(
0,
(await browser.tabs.get(tabIds[0])).index,
`First tab appears at the expected index`
);
browser.test.assertEq(
1,
(await browser.tabs.get(tabIds[1])).index,
`Second tab appears at the expected index`
);
await browser.windows.remove(windowId);
browser.test.assertDeepEq(
[
`tabs.onDetached:1:${tabIds[0]}`,
`tabs.onAttached:0:${tabIds[0]}`,
`tabs.onDetached:1:${tabIds[1]}`,
`tabs.onAttached:1:${tabIds[1]}`,
"tabGroups.onMoved",
],
events,
"Expected events when moving tab group (2 tabs) to a new window"
);
browser.test.sendMessage("done");
},
});
await extension.startup();
await extension.awaitMessage("done");
await extension.unload();
});

View file

@ -0,0 +1,155 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const {
Management: {
global: { getExtTabGroupIdForInternalTabGroupId },
},
} = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
add_task(async function tabsGroups_move_onMoved() {
async function loadExt() {
let ext = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["tabGroups"],
},
incognitoOverride: "spanning",
async background() {
browser.tabGroups.onCreated.addListener(group => {
browser.test.sendMessage("created", group);
});
browser.tabGroups.onRemoved.addListener(group => {
browser.test.sendMessage("removed", group);
});
browser.tabGroups.onMoved.addListener(moved => {
browser.test.sendMessage("moved", moved);
});
browser.test.onMessage.addListener(async (groupId, moveProps) => {
try {
let group = await browser.tabGroups.move(groupId, moveProps);
browser.test.sendMessage("done", group);
} catch (e) {
browser.test.sendMessage("error", e.message);
}
});
let [tab] = await browser.tabs.query({
lastFocusedWindow: true,
active: true,
});
browser.test.sendMessage("windowId", tab.windowId);
},
});
await ext.startup();
return ext;
}
let tabs = [];
let url = "https://example.com/foo?";
for (let i = 1; i < 10; i++) {
tabs.push(await BrowserTestUtils.openNewForegroundTab(gBrowser, url + i));
}
let group = gBrowser.addTabGroup(gBrowser.tabs.slice(-2));
is(group.tabs[1], gBrowser.tabs.at(-1), "Group's last tab is last.");
let gid = getExtTabGroupIdForInternalTabGroupId(group.id);
let ext = await loadExt();
let windowId = await ext.awaitMessage("windowId");
ext.sendMessage(gid, { index: 6 });
await Promise.all([ext.awaitMessage("done"), ext.awaitMessage("moved")]);
is(group.tabs[0], gBrowser.tabs[6], "Group's first tab moved to index 6.");
ext.sendMessage(gid, { index: 3 });
await Promise.all([ext.awaitMessage("done"), ext.awaitMessage("moved")]);
is(group.tabs[0], gBrowser.tabs[3], "Group's first tab moved to index 3.");
ext.sendMessage(gid, { index: 3 });
await ext.awaitMessage("done");
is(group.tabs[0], gBrowser.tabs[3], "Using same index 3 doesn't move.");
ext.sendMessage(gid, { index: -1 });
await Promise.all([ext.awaitMessage("done"), ext.awaitMessage("moved")]);
is(group.tabs[1], gBrowser.tabs.at(-1), "Group moved to the end.");
ext.sendMessage(gid, { index: -1 });
await ext.awaitMessage("done");
is(group.tabs[1], gBrowser.tabs.at(-1), "Using same index -1 doesn't move.");
ext.sendMessage(gid, { index: 0 });
await Promise.all([ext.awaitMessage("done"), ext.awaitMessage("moved")]);
is(group.tabs[0], gBrowser.tabs[0], "Group moved to the beginning.");
ext.sendMessage(gid, { index: 0 });
await ext.awaitMessage("done");
is(group.tabs[0], gBrowser.tabs[0], "Using same index 0 doesn't move.");
info("Create a large second group, and try to move in the middle of it.");
let group4 = gBrowser.addTabGroup(gBrowser.tabs.slice(5));
let created4 = await ext.awaitMessage("created");
let gid4 = getExtTabGroupIdForInternalTabGroupId(group4.id);
is(created4.id, gid4, "Correct group 4 created event.");
ext.sendMessage(gid, { index: 8 });
await Promise.all([ext.awaitMessage("done"), ext.awaitMessage("moved")]);
is(group.tabs[1], gBrowser.tabs[4], "Moved before the whole group instead.");
for (let tab of tabs) {
BrowserTestUtils.removeTab(tab);
}
let removed1 = await ext.awaitMessage("removed");
let removed4 = await ext.awaitMessage("removed");
is(removed1.id, gid, "Correct group 1 removed event.");
is(removed4.id, gid4, "Correct group 4 removed event.");
info("Test moving a group from a non-private to a private window.");
let win2 = await BrowserTestUtils.openNewBrowserWindow({ private: true });
let tab2 = await BrowserTestUtils.openNewForegroundTab(win2.gBrowser, url);
let group2 = win2.gBrowser.addTabGroup([tab2]);
let gid2 = getExtTabGroupIdForInternalTabGroupId(group2.id);
let created2 = await ext.awaitMessage("created");
is(created2.id, gid2, "Correct group 2 create event.");
ext.sendMessage(gid2, { index: 0, windowId });
let error = await ext.awaitMessage("error");
is(error, "Can't move groups between private and non-private windows");
await BrowserTestUtils.closeWindow(win2);
info("Test moving a group to another window.");
let win3 = await BrowserTestUtils.openNewBrowserWindow();
let tab3 = await BrowserTestUtils.openNewForegroundTab(win3.gBrowser, url);
let group3 = win3.gBrowser.addTabGroup([tab3]);
let gid3 = getExtTabGroupIdForInternalTabGroupId(group3.id);
let created3 = await ext.awaitMessage("created");
is(created3.id, gid3, "Correct group 3 create event.");
ext.sendMessage(gid3, { index: 0, windowId });
await ext.awaitMessage("done");
let moved3 = await ext.awaitMessage("moved");
// Chrome fires onRemoved + onCreated, but we intentionally dispatch onMoved
// because it makes more sense - bug 1962475.
is(moved3.id, gid3, "onMoved fired after moving to another window");
// Add and remove a tab group, so that if onCreated/onRemoved was
// unexpectedly fired by the move above, that we will see (unexpected) gid3
// instead of (expected) gid4.
let tab5 = await BrowserTestUtils.openNewForegroundTab(win3.gBrowser, url);
let group5 = win3.gBrowser.addTabGroup([tab5]);
let created5 = await ext.awaitMessage("created");
let gid5 = getExtTabGroupIdForInternalTabGroupId(group5.id);
is(created5.id, gid5, "Correct group 5 create event.");
win3.gBrowser.removeTabGroup(group5);
let removed5 = await ext.awaitMessage("removed");
is(removed5.id, gid5, "Correct group 5 removed event.");
await BrowserTestUtils.closeWindow(win3);
let group3b = gBrowser.getTabGroupById(group3.id);
BrowserTestUtils.removeTab(group3b.tabs[0]);
let removed3 = await ext.awaitMessage("removed");
is(removed3.id, gid3, "Correct group 3 removed event.");
await ext.unload();
});

View file

@ -0,0 +1,174 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(async function tabsGroups_query() {
async function loadExt(allowPrivate) {
let ext = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["tabGroups"],
},
incognitoOverride: allowPrivate ? "spanning" : undefined,
async background() {
let [tab] = await browser.tabs.query({
lastFocusedWindow: true,
active: true,
});
browser.test.onMessage.addListener(async (_msg, tests) => {
for (let { query, expected } of tests) {
let groups = await browser.tabGroups.query(query);
let titles = groups.map(group => group.title);
browser.test.assertEq(
expected.map(e => String(e)).join(),
titles.sort().join(),
`Expected groups for query - ${JSON.stringify(query)}`
);
}
browser.test.sendMessage("done");
});
browser.test.sendMessage("windowId", tab.windowId);
},
});
await ext.startup();
return ext;
}
let ext = await loadExt();
let windowId = await ext.awaitMessage("windowId");
let url = "https://example.com/foo?";
let groups = [];
let tabs = [];
let colors = ["red", "blue", "red", "pink", "green"];
for (let i = 0; i < 5; i++) {
tabs.push(await BrowserTestUtils.openNewForegroundTab(gBrowser, url + i));
let group = gBrowser.addTabGroup(tabs.slice(-1));
group.color = colors[i];
group.name = String(i);
groups.push(group);
}
groups[2].collapsed = true;
groups[3].collapsed = true;
let win2 = await BrowserTestUtils.openNewBrowserWindow();
let tab5 = await BrowserTestUtils.openNewForegroundTab(win2.gBrowser, url);
let tab6 = await BrowserTestUtils.openNewForegroundTab(win2.gBrowser, url);
let group5 = win2.gBrowser.addTabGroup([tab5]);
let group6 = win2.gBrowser.addTabGroup([tab6]);
group5.name = "5";
group6.name = "6";
group5.color = "red";
group6.color = "gray";
group6.collapsed = true;
ext.sendMessage("runTests", [
{
query: undefined,
expected: [0, 1, 2, 3, 4, 5, 6],
},
{
query: {},
expected: [0, 1, 2, 3, 4, 5, 6],
},
{
query: { collapsed: true },
expected: [2, 3, 6],
},
{
query: { collapsed: false },
expected: [0, 1, 4, 5],
},
{
query: { color: "red" },
expected: [0, 2, 5],
},
{
query: { color: "grey" },
expected: [6],
},
{
query: { title: "4" },
expected: [4],
},
{
query: { title: "*" },
expected: [0, 1, 2, 3, 4, 5, 6],
},
{
query: { windowId },
expected: [0, 1, 2, 3, 4],
},
{
query: { windowId: -2 },
expected: [5, 6],
},
{
query: { windowId: 1e9 },
expected: [],
},
{
query: { collapsed: true, color: "red" },
expected: [2],
},
{
query: { color: "red", windowId },
expected: [0, 2],
},
]);
await ext.awaitMessage("done");
groups[3].name = "Foo 3";
group5.name = "FooBar 5";
let win3 = await BrowserTestUtils.openNewBrowserWindow({ private: true });
let tab7 = await BrowserTestUtils.openNewForegroundTab(win3.gBrowser, url);
let group7 = win3.gBrowser.addTabGroup([tab7]);
group7.name = "FooBaz 7";
let ext2 = await loadExt(true);
windowId = await ext2.awaitMessage("windowId");
info("Extension without private browsing should't see group7 from win3.");
ext.sendMessage("runTests", [
{
query: { title: "Foo*" },
expected: ["Foo 3", "FooBar 5"],
},
{
query: { windowId },
expected: [],
},
{
query: { windowId: -2 },
expected: [],
},
]);
await ext.awaitMessage("done");
info("Extension with private browsing access should see group7 from win3.");
ext2.sendMessage("runTests", [
{
query: { title: "Foo*" },
expected: ["Foo 3", "FooBar 5", "FooBaz 7"],
},
{
query: { windowId },
expected: ["FooBaz 7"],
},
{
query: { windowId: -2 },
expected: ["FooBaz 7"],
},
]);
await ext2.awaitMessage("done");
for (let tab of tabs) {
BrowserTestUtils.removeTab(tab);
}
await BrowserTestUtils.closeWindow(win2);
await BrowserTestUtils.closeWindow(win3);
await ext.unload();
await ext2.unload();
});

View file

@ -2,7 +2,7 @@
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
// TODO bug 1938594: group_across_private_browsing_windows sometimes triggers
// TODO bug 1938594: group_pinned_tab_from_different_window sometimes triggers
// this error. See https://bugzilla.mozilla.org/show_bug.cgi?id=1938594#c1
PromiseTestUtils.allowMatchingRejectionsGlobally(
/Unexpected undefined tabState for onMoveToNewWindow/
@ -10,6 +10,9 @@ PromiseTestUtils.allowMatchingRejectionsGlobally(
add_task(async function group_ungroup_and_index() {
const extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["tabGroups"],
},
files: {
"tab1.htm": "<title>tab1.html</title>",
"tab2.htm": "<title>tab2.html</title>",
@ -17,9 +20,28 @@ add_task(async function group_ungroup_and_index() {
},
async background() {
const { id: tabId1 } = await browser.tabs.create({ url: "tab1.htm" });
const { id: tabId2 } = await browser.tabs.create({ url: "tab2.htm " });
const { id: tabId2 } = await browser.tabs.create({ url: "tab2.htm" });
const { id: tabId3 } = await browser.tabs.create({ url: "tab3.htm" });
let eventIds = [];
let expected = [];
let allEvents = Promise.withResolvers();
browser.tabGroups.onCreated.addListener(group => {
eventIds.push(group.id);
browser.test.log(`Events so far (${eventIds.length}): ${eventIds}`);
});
browser.tabGroups.onRemoved.addListener(group => {
eventIds.push(-group.id);
browser.test.log(`Events so far (${eventIds.length}): ${eventIds}`);
if (eventIds.length === 16) {
allEvents.resolve();
}
if (eventIds.length > 16) {
browser.fail("Extra event received: " + group.id);
}
});
async function assertAllTabExpectations(expectations, desc) {
const tabs = await Promise.all([
browser.tabs.get(tabId1),
@ -52,6 +74,7 @@ add_task(async function group_ungroup_and_index() {
const groupId1 = await browser.tabs.group({ tabIds: tabId1 });
const groupId2 = await browser.tabs.group({ tabIds: [tabId2, tabId3] });
expected.push(groupId1, groupId2);
await assertAllTabExpectations(
{ indexes: [1, 2, 3], groupIds: [groupId1, groupId2, groupId2] },
@ -63,6 +86,7 @@ add_task(async function group_ungroup_and_index() {
// middle of a tab group.
await browser.tabs.ungroup([tabId3, tabId1]);
await browser.tabs.ungroup(tabId2);
expected.push(-groupId1, -groupId2);
await browser.test.assertRejects(
browser.tabs.group({ tabIds: tabId3, groupId: groupId1 }),
@ -81,6 +105,7 @@ add_task(async function group_ungroup_and_index() {
{ indexes: [1, 3, 2], groupIds: [groupId3, groupId3, -1] },
"Tabs in same tab group must be next to each other"
);
expected.push(groupId3);
// Join existing tab group - now we should have three in the tab group.
const groupId4 = await browser.tabs.group({
@ -100,6 +125,7 @@ add_task(async function group_ungroup_and_index() {
);
await browser.tabs.ungroup([tabId1, tabId2, tabId3]);
expected.push(-groupId3);
// Ungrouping of the group should not have changed positions either,
// despite the list of tabIds passed to ungroup() being out of order.
@ -124,6 +150,7 @@ add_task(async function group_ungroup_and_index() {
{ indexes: [1, 2, 3], groupIds: [groupId6, groupId5, groupId5] },
"Leftmost tab should still be ordered before the original tab group"
);
expected.push(groupId5, groupId6);
// Join an existing group (from the left). Position should not change.
await browser.tabs.group({ tabIds: [tabId1], groupId: groupId5 });
@ -136,6 +163,7 @@ add_task(async function group_ungroup_and_index() {
`No group with id: ${groupId6}`,
"Old groupId should be invalid after last tab was moved from group"
);
expected.push(-groupId6);
// Move the middle tab to a new group. That tab should be at the right.
const groupId7 = await browser.tabs.group({ tabIds: [tabId2] });
@ -147,6 +175,7 @@ add_task(async function group_ungroup_and_index() {
// Prepare: tabId1 and tabId2 together at the left, followed by tabId3.
const groupId8 = await browser.tabs.group({ tabIds: [tabId1, tabId2] });
await browser.tabs.ungroup(tabId3);
expected.push(groupId7, groupId8);
// When tabId2 is moved to a new group, it should stay in the middle,
// meaning that the tab was inserted after its original tab group.
@ -156,10 +185,25 @@ add_task(async function group_ungroup_and_index() {
{ indexes: [1, 2, 3], groupIds: [groupId8, groupId9, -1] },
"group() on rightmost tab should appear after original tab group"
);
expected.push(-groupId7, -groupId5, groupId9);
await browser.tabs.remove(tabId1);
await browser.tabs.remove(tabId2);
await browser.tabs.remove(tabId3);
expected.push(-groupId8, -groupId9);
// TODO bug 1962683: Re-enable when events are no longer missing
// await allEvents.promise;
//
// browser.test.assertEq(
// eventIds.join(),
// expected.join(),
// "Received expected onCreated events"
// );
browser.test.log(`Expect: ${eventIds.join()}`);
browser.test.log(`Actual: ${expected.join()}`);
browser.test.sendMessage("done");
},
});
@ -168,221 +212,6 @@ add_task(async function group_ungroup_and_index() {
await extension.unload();
});
add_task(async function group_with_windowId() {
const extension = ExtensionTestUtils.loadExtension({
async background() {
const { id: windowId, tabs: initialTabs } = await browser.windows.create(
{}
);
browser.test.assertEq(1, initialTabs.length, "Got window with 1 tab");
const { id: tabId1 } = await browser.tabs.create({});
const { id: tabId2 } = await browser.tabs.create({});
const groupId1 = await browser.tabs.group({
tabIds: [tabId2, tabId1],
createProperties: { windowId },
});
browser.test.assertDeepEq(
Array.from(await browser.tabs.query({ groupId: groupId1 }), t => t.id),
[tabId2, tabId1],
"Moved tabs to group"
);
browser.test.assertDeepEq(
Array.from(await browser.tabs.query({ windowId }), t => t.id),
[initialTabs[0].id, tabId2, tabId1],
"Moved tabs to group in new window (next to initial tab)"
);
await browser.windows.remove(windowId);
browser.test.sendMessage("done");
},
});
await extension.startup();
await extension.awaitMessage("done");
await extension.unload();
});
add_task(async function group_adopt_from_multiple_windows() {
const extension = ExtensionTestUtils.loadExtension({
incognitoOverride: "spanning",
async background() {
const {
id: windowId1,
tabs: [{ id: tabId1 }],
} = await browser.windows.create({});
const {
id: windowId2,
tabs: [{ id: tabId2 }],
} = await browser.windows.create({});
const {
id: windowId3,
tabs: [{ id: tabId3 }],
} = await browser.windows.create({});
// This confirms that group() can adapt tabs from different windows.
const groupId = await browser.tabs.group({
tabIds: [tabId2, tabId3],
createProperties: { windowId: windowId1 },
});
await browser.test.assertRejects(
browser.windows.get(windowId2),
`Invalid window ID: ${windowId2}`,
"Window closes when group() adopts the last tab of the window"
);
// We just confirmed that window2 is closed, do the same for window3.
await browser.test.assertRejects(
browser.tabs.group({
tabIds: tabId1,
createProperties: { windowId: windowId3 },
}),
`Invalid window ID: ${windowId3}`,
"group() cannot adapt groups from a closed window"
);
browser.test.assertDeepEq(
// Note: tabId1 is missing because the above group() rejected.
[tabId2, tabId3],
Array.from(await browser.tabs.query({ groupId }), tab => tab.id),
"All specified tabIds should now belong to the given group"
);
await browser.windows.remove(windowId1);
browser.test.sendMessage("done");
},
});
await extension.startup();
await extension.awaitMessage("done");
await extension.unload();
});
add_task(async function group_across_private_browsing_windows() {
const extension = ExtensionTestUtils.loadExtension({
incognitoOverride: "spanning",
async background() {
const privateWin = await browser.windows.create({ incognito: true });
const normalWin = await browser.windows.create({ incognito: false });
const otherPrivateWin = await browser.windows.create({ incognito: true });
const otherNormalWin = await browser.windows.create({ incognito: false });
// Mixture of private and non-private tabIDs, so we can easily verify
// whether we inadvertently move some of the tabs.
const privateTab = await browser.tabs.create({ windowId: privateWin.id });
const privateAndNonPrivateTabs = [
privateTab,
await browser.tabs.create({ windowId: normalWin.id }),
await browser.tabs.create({ windowId: privateWin.id }),
await browser.tabs.create({ windowId: normalWin.id }),
];
await browser.test.assertRejects(
browser.tabs.group({
tabIds: privateAndNonPrivateTabs.map(t => t.id),
}),
"Cannot move private tabs to non-private window",
"Should not be able to move private+non-private tabs to current window"
);
await browser.test.assertRejects(
browser.tabs.group({
tabIds: privateAndNonPrivateTabs.map(t => t.id),
createProperties: { windowId: otherPrivateWin.id },
}),
"Cannot move non-private tabs to private window",
"Should not be able to move non-private tab to private window"
);
await browser.test.assertRejects(
browser.tabs.group({
tabIds: privateAndNonPrivateTabs.map(t => t.id),
createProperties: { windowId: otherNormalWin.id },
}),
"Cannot move private tabs to non-private window",
"Should not be able to move private tab to non-private window"
);
const reply = await browser.runtime.sendMessage("@no_private", {
privateWindowId: privateWin.id,
privateTabId: privateTab.id,
});
browser.test.assertEq("no_private:done", reply, "Reply from other ext");
for (const tab of privateAndNonPrivateTabs) {
const actualTab = await browser.tabs.get(tab.id);
browser.test.assertEq(
tab.windowId,
actualTab.windowId,
"Tab should not have moved to a different window"
);
browser.test.assertEq(
tab.windowId,
actualTab.windowId,
"Tab should not have moved within its window"
);
browser.test.assertEq(
tab.groupId,
-1,
"Tab should not have joined a group"
);
}
// Now check that we can actually group tabs in private windows.
const groupId = await browser.tabs.group({
tabIds: privateTab.id,
createProperties: { windowId: otherPrivateWin.id },
});
const updatedPrivateTab = await browser.tabs.get(privateTab.id);
browser.test.assertEq(
groupId,
updatedPrivateTab.groupId,
"group() succeeded with private tab"
);
browser.test.assertEq(
otherPrivateWin.id,
updatedPrivateTab.windowId,
"Private tab is now part of the destination private window"
);
await browser.windows.remove(privateWin.id);
await browser.windows.remove(normalWin.id);
await browser.windows.remove(otherPrivateWin.id);
await browser.windows.remove(otherNormalWin.id);
browser.test.sendMessage("done");
},
});
const extensionWithoutPrivateAccess = ExtensionTestUtils.loadExtension({
manifest: { browser_specific_settings: { gecko: { id: "@no_private" } } },
background() {
browser.runtime.onMessageExternal.addListener(async data => {
const { privateWindowId, privateTabId } = data;
const { id: normalTabId } = await browser.tabs.create({});
await browser.test.assertRejects(
browser.tabs.group({ tabIds: [privateTabId] }),
`Invalid tab ID: ${privateTabId}`,
"@no_private should not be able to group private tabs"
);
await browser.test.assertRejects(
browser.tabs.group({
tabIds: [normalTabId],
createProperties: { windowId: privateWindowId },
}),
`Invalid window ID: ${privateWindowId}`,
"@no_private should not see private windows"
);
await browser.tabs.remove(normalTabId);
return "no_private:done"; // Checked by sender.
});
},
});
await extensionWithoutPrivateAccess.startup();
await extension.startup();
await extension.awaitMessage("done");
await extensionWithoutPrivateAccess.unload();
await extension.unload();
});
add_task(async function group_pinned_tab() {
const extension = ExtensionTestUtils.loadExtension({
async background() {

View file

@ -0,0 +1,224 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
// TODO bug 1938594: group_across_private_browsing_windows sometimes triggers
// this error. See https://bugzilla.mozilla.org/show_bug.cgi?id=1938594#c1
PromiseTestUtils.allowMatchingRejectionsGlobally(
/Unexpected undefined tabState for onMoveToNewWindow/
);
add_task(async function group_with_windowId() {
const extension = ExtensionTestUtils.loadExtension({
async background() {
const { id: windowId, tabs: initialTabs } = await browser.windows.create(
{}
);
browser.test.assertEq(1, initialTabs.length, "Got window with 1 tab");
const { id: tabId1 } = await browser.tabs.create({});
const { id: tabId2 } = await browser.tabs.create({});
const groupId1 = await browser.tabs.group({
tabIds: [tabId2, tabId1],
createProperties: { windowId },
});
browser.test.assertDeepEq(
Array.from(await browser.tabs.query({ groupId: groupId1 }), t => t.id),
[tabId2, tabId1],
"Moved tabs to group"
);
browser.test.assertDeepEq(
Array.from(await browser.tabs.query({ windowId }), t => t.id),
[initialTabs[0].id, tabId2, tabId1],
"Moved tabs to group in new window (next to initial tab)"
);
await browser.windows.remove(windowId);
browser.test.sendMessage("done");
},
});
await extension.startup();
await extension.awaitMessage("done");
await extension.unload();
});
add_task(async function group_adopt_from_multiple_windows() {
const extension = ExtensionTestUtils.loadExtension({
incognitoOverride: "spanning",
async background() {
const {
id: windowId1,
tabs: [{ id: tabId1 }],
} = await browser.windows.create({});
const {
id: windowId2,
tabs: [{ id: tabId2 }],
} = await browser.windows.create({});
const {
id: windowId3,
tabs: [{ id: tabId3 }],
} = await browser.windows.create({});
// This confirms that group() can adapt tabs from different windows.
const groupId = await browser.tabs.group({
tabIds: [tabId2, tabId3],
createProperties: { windowId: windowId1 },
});
await browser.test.assertRejects(
browser.windows.get(windowId2),
`Invalid window ID: ${windowId2}`,
"Window closes when group() adopts the last tab of the window"
);
// We just confirmed that window2 is closed, do the same for window3.
await browser.test.assertRejects(
browser.tabs.group({
tabIds: tabId1,
createProperties: { windowId: windowId3 },
}),
`Invalid window ID: ${windowId3}`,
"group() cannot adapt groups from a closed window"
);
browser.test.assertDeepEq(
// Note: tabId1 is missing because the above group() rejected.
[tabId2, tabId3],
Array.from(await browser.tabs.query({ groupId }), tab => tab.id),
"All specified tabIds should now belong to the given group"
);
await browser.windows.remove(windowId1);
browser.test.sendMessage("done");
},
});
await extension.startup();
await extension.awaitMessage("done");
await extension.unload();
});
add_task(async function group_across_private_browsing_windows() {
const extension = ExtensionTestUtils.loadExtension({
incognitoOverride: "spanning",
async background() {
const privateWin = await browser.windows.create({ incognito: true });
const normalWin = await browser.windows.create({ incognito: false });
const otherPrivateWin = await browser.windows.create({ incognito: true });
const otherNormalWin = await browser.windows.create({ incognito: false });
// Mixture of private and non-private tabIDs, so we can easily verify
// whether we inadvertently move some of the tabs.
const privateTab = await browser.tabs.create({ windowId: privateWin.id });
const privateAndNonPrivateTabs = [
privateTab,
await browser.tabs.create({ windowId: normalWin.id }),
await browser.tabs.create({ windowId: privateWin.id }),
await browser.tabs.create({ windowId: normalWin.id }),
];
await browser.test.assertRejects(
browser.tabs.group({
tabIds: privateAndNonPrivateTabs.map(t => t.id),
}),
"Cannot move private tabs to non-private window",
"Should not be able to move private+non-private tabs to current window"
);
await browser.test.assertRejects(
browser.tabs.group({
tabIds: privateAndNonPrivateTabs.map(t => t.id),
createProperties: { windowId: otherPrivateWin.id },
}),
"Cannot move non-private tabs to private window",
"Should not be able to move non-private tab to private window"
);
await browser.test.assertRejects(
browser.tabs.group({
tabIds: privateAndNonPrivateTabs.map(t => t.id),
createProperties: { windowId: otherNormalWin.id },
}),
"Cannot move private tabs to non-private window",
"Should not be able to move private tab to non-private window"
);
const reply = await browser.runtime.sendMessage("@no_private", {
privateWindowId: privateWin.id,
privateTabId: privateTab.id,
});
browser.test.assertEq("no_private:done", reply, "Reply from other ext");
for (const tab of privateAndNonPrivateTabs) {
const actualTab = await browser.tabs.get(tab.id);
browser.test.assertEq(
tab.windowId,
actualTab.windowId,
"Tab should not have moved to a different window"
);
browser.test.assertEq(
tab.windowId,
actualTab.windowId,
"Tab should not have moved within its window"
);
browser.test.assertEq(
tab.groupId,
-1,
"Tab should not have joined a group"
);
}
// Now check that we can actually group tabs in private windows.
const groupId = await browser.tabs.group({
tabIds: privateTab.id,
createProperties: { windowId: otherPrivateWin.id },
});
const updatedPrivateTab = await browser.tabs.get(privateTab.id);
browser.test.assertEq(
groupId,
updatedPrivateTab.groupId,
"group() succeeded with private tab"
);
browser.test.assertEq(
otherPrivateWin.id,
updatedPrivateTab.windowId,
"Private tab is now part of the destination private window"
);
await browser.windows.remove(privateWin.id);
await browser.windows.remove(normalWin.id);
await browser.windows.remove(otherPrivateWin.id);
await browser.windows.remove(otherNormalWin.id);
browser.test.sendMessage("done");
},
});
const extensionWithoutPrivateAccess = ExtensionTestUtils.loadExtension({
manifest: { browser_specific_settings: { gecko: { id: "@no_private" } } },
background() {
browser.runtime.onMessageExternal.addListener(async data => {
const { privateWindowId, privateTabId } = data;
const { id: normalTabId } = await browser.tabs.create({});
await browser.test.assertRejects(
browser.tabs.group({ tabIds: [privateTabId] }),
`Invalid tab ID: ${privateTabId}`,
"@no_private should not be able to group private tabs"
);
await browser.test.assertRejects(
browser.tabs.group({
tabIds: [normalTabId],
createProperties: { windowId: privateWindowId },
}),
`Invalid window ID: ${privateWindowId}`,
"@no_private should not see private windows"
);
await browser.tabs.remove(normalTabId);
return "no_private:done"; // Checked by sender.
});
},
});
await extensionWithoutPrivateAccess.startup();
await extension.startup();
await extension.awaitMessage("done");
await extensionWithoutPrivateAccess.unload();
await extension.unload();
});

View file

@ -27,6 +27,7 @@
flex-direction: column;
font-size: 0.95rem;
gap: 6px;
padding: 0 8px;
}
.import-history-banner .banner-text span:first-child {
@ -37,7 +38,7 @@
display: grid;
grid-template-columns: 1fr auto;
gap: 16px;
padding: 8px;
padding: 8px 0;
}
.import-history-banner .buttons {

View file

@ -386,62 +386,39 @@ export const GenAI = {
);
return context;
},
/**
* Handle messages from content to show or hide shortcuts.
*
* @param {string} name of message
* @param {{
inputType: string,
selection: string,
delay: number,
x: number,
y: number,
}} data for the message
* @param {MozBrowser} browser that provided the message
*/
handleShortcutsMessage(name, data, browser) {
const isInBrowserStack = browser?.closest(".browserStack");
if (
!isInBrowserStack ||
!browser ||
this.ignoredInputs.has(data.inputType) ||
!lazy.chatShortcuts ||
!this.canShowChatEntrypoint
) {
/**
* Setup helpers and callbacks for ai shortcut button.
*
* @param {MozButton} aiActionButton instance for the browser window
*/
initializeAIShortcut(aiActionButton) {
if (aiActionButton.initialized) {
return;
}
aiActionButton.initialized = true;
const window = browser.ownerGlobal;
const { document, devicePixelRatio } = window;
// Get Panel elements
const document = aiActionButton.ownerDocument;
const buttonActiveState = "icon";
const buttonDefaultState = "icon ghost";
const chatShortcutsOptionsPanel = document.getElementById(
"chat-shortcuts-options-panel"
);
const selectionShortcutActionPanel = document.getElementById(
"selection-shortcut-action-panel"
);
if (!chatShortcutsOptionsPanel || !selectionShortcutActionPanel) {
return;
}
const aiActionButton =
selectionShortcutActionPanel.querySelector("#ai-action-button");
aiActionButton.iconSrc = "chrome://global/skin/icons/highlights.svg";
const buttonActiveState = "icon";
const buttonDefaultState = "icon ghost";
// Hide shortcuts and panel
const hide = () => {
aiActionButton.setAttribute("type", buttonDefaultState);
aiActionButton.removeEventListener("mouseover", aiActionButton.listener);
aiActionButton.listener = null;
aiActionButton.hide = () => {
chatShortcutsOptionsPanel.hidePopup();
selectionShortcutActionPanel.hidePopup();
};
aiActionButton.iconSrc = "chrome://global/skin/icons/highlights.svg";
aiActionButton.setAttribute("type", buttonDefaultState);
chatShortcutsOptionsPanel.addEventListener("popuphidden", () =>
aiActionButton.setAttribute("type", buttonDefaultState)
);
chatShortcutsOptionsPanel.firstChild.id = "ask-chat-shortcuts";
// Helper to show rounded warning numbers
const roundDownToNearestHundred = number => {
return Math.floor(number / 100) * 100;
};
@ -450,9 +427,9 @@ export const GenAI = {
* Create a warning message bar.
*
* @param {{
name: string,
maxLength: number,
}} chatProvider attributes for the warning
* name: string,
* maxLength: number,
* }} chatProvider attributes for the warning
* @returns { mozMessageBarEl } MozMessageBar warning message bar
*/
const createMessageBarWarning = chatProvider => {
@ -481,20 +458,12 @@ export const GenAI = {
return mozMessageBarEl;
};
switch (name) {
case "GenAI:HideShortcuts":
hide();
break;
case "GenAI:ShowShortcuts": {
aiActionButton.setAttribute("type", buttonDefaultState);
// Detect hover to build and open the popup
aiActionButton.listener = async () => {
if (aiActionButton.hasAttribute("active")) {
aiActionButton.addEventListener("mouseover", async () => {
if (chatShortcutsOptionsPanel.state != "closed") {
return;
}
aiActionButton.toggleAttribute("active");
aiActionButton.setAttribute("type", buttonActiveState);
const vbox = chatShortcutsOptionsPanel.querySelector("vbox");
vbox.innerHTML = "";
@ -502,8 +471,7 @@ export const GenAI = {
const chatProvider = this.chatProviders.get(lazy.chatProvider);
const selectionLength = aiActionButton.data.selection.length;
const showWarning =
this.estimateSelectionLimit(chatProvider?.maxLength) <
selectionLength;
this.estimateSelectionLimit(chatProvider?.maxLength) < selectionLength;
// Show warning if selection is too long
if (showWarning) {
@ -519,6 +487,7 @@ export const GenAI = {
return button;
};
const browser = document.ownerGlobal.gBrowser.selectedBrowser;
const context = await this.addAskChatItems(
browser,
aiActionButton.data,
@ -528,14 +497,12 @@ export const GenAI = {
return button;
},
"shortcuts",
hide
aiActionButton.hide
);
// Add custom textarea box if configured
if (lazy.chatShortcutsCustom) {
const textAreaEl = vbox.appendChild(
document.createElement("textarea")
);
const textAreaEl = vbox.appendChild(document.createElement("textarea"));
document.l10n.setAttributes(
textAreaEl,
chatProvider?.name
@ -549,7 +516,7 @@ export const GenAI = {
textAreaEl.addEventListener("keydown", event => {
if (event.key == "Enter" && !event.shiftKey) {
this.handleAskChat({ value: textAreaEl.value }, context);
hide();
aiActionButton.hide();
}
});
@ -569,13 +536,9 @@ export const GenAI = {
};
textAreaEl.addEventListener("input", resetHeight);
chatShortcutsOptionsPanel.addEventListener(
"popupshown",
resetHeight,
{
chatShortcutsOptionsPanel.addEventListener("popupshown", resetHeight, {
once: true,
}
);
});
}
// Allow hiding these shortcuts
@ -595,19 +558,50 @@ export const GenAI = {
0,
10
);
chatShortcutsOptionsPanel.addEventListener(
"popuphidden",
() => aiActionButton.removeAttribute("active"),
{ once: true }
);
Glean.genaiChatbot.shortcutsExpanded.record({
selection: aiActionButton.data.selection.length,
provider: this.getProviderId(),
warning: showWarning,
});
};
aiActionButton.addEventListener("mouseover", aiActionButton.listener);
});
},
/**
* Handle messages from content to show or hide shortcuts.
*
* @param {string} name of message
* @param {{
* inputType: string,
* selection: string,
* delay: number,
* x: number,
* y: number,
* }} data for the message
* @param {MozBrowser} browser that provided the message
*/
handleShortcutsMessage(name, data, browser) {
const isInBrowserStack = browser?.closest(".browserStack");
if (
!isInBrowserStack ||
!browser ||
this.ignoredInputs.has(data.inputType) ||
!lazy.chatShortcuts ||
!this.canShowChatEntrypoint
) {
return;
}
const window = browser.ownerGlobal;
const { document, devicePixelRatio } = window;
const aiActionButton = document.getElementById("ai-action-button");
this.initializeAIShortcut(aiActionButton);
switch (name) {
case "GenAI:HideShortcuts":
aiActionButton.hide();
break;
case "GenAI:ShowShortcuts": {
// Save the latest selection so it can be used by popup
aiActionButton.data = data;
@ -625,7 +619,9 @@ export const GenAI = {
const screenX = data.screenXDevPx / devicePixelRatio;
const screenY = screenYBase + bottomPadding;
selectionShortcutActionPanel.openPopup(
aiActionButton
.closest("panel")
.openPopup(
browser,
"before_start",
screenX - browser.screenX,

View file

@ -9,6 +9,7 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
LinkPreviewModel:
"moz-src:///browser/components/genai/LinkPreviewModel.sys.mjs",
Region: "resource://gre/modules/Region.sys.mjs",
});
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
@ -28,6 +29,11 @@ XPCOMUtils.defineLazyPreferenceGetter(
"browser.ml.linkPreview.prefetchOnEnable",
true
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"noKeyPointsRegions",
"browser.ml.linkPreview.noKeyPointsRegions"
);
export const LinkPreview = {
// Shared downloading state to use across multiple previews
@ -50,7 +56,7 @@ export const LinkPreview = {
}
// Prefetch the model when enabling by simulating a request.
if (enabled && lazy.prefetchOnEnable) {
if (enabled && lazy.prefetchOnEnable && this._isRegionSupported()) {
this.generateKeyPoints();
}
@ -176,6 +182,20 @@ export const LinkPreview = {
}
},
/**
* Checks if the user's region is supported for key points generation.
*
* @returns {boolean} True if the region is supported, false otherwise.
*/
_isRegionSupported() {
const disallowedRegions = lazy.noKeyPointsRegions
.split(",")
.map(region => region.trim().toUpperCase());
const userRegion = lazy.Region.home?.toUpperCase();
return !disallowedRegions.includes(userRegion);
},
/**
* Creates an Open Graph (OG) card using meta information from the page.
*
@ -208,6 +228,7 @@ export const LinkPreview = {
// Generate key points if we have content, language and configured for any
// language or restricted.
if (
this._isRegionSupported() &&
pageData.article.textContent &&
pageData.article.detectedLanguage &&
(!lazy.allowedLanguages ||

View file

@ -16,7 +16,7 @@ const DEFAULT_INPUT_SENTENCES = 6;
const MIN_SENTENCE_LENGTH = 14;
const MIN_WORD_COUNT = 5;
const DEFAULT_INPUT_PROMPT =
"Provide a concise, objective summary of the input text in up to three sentences, focusing on key actions and intentions without using second or third person pronouns.";
"You're an AI assistant for text re-writing and summarization. Rewrite the input text focusing on the main key point in at most three very short sentences.";
// All tokens taken from the model's vocabulary at https://huggingface.co/HuggingFaceTB/SmolLM2-360M-Instruct/raw/main/vocab.json
// Token id for end of text
@ -528,7 +528,10 @@ export class SentencePostProcessor {
// If the sentence contains a block word, abort
if (
this.blockListManager &&
this.blockListManager.matchAtWordBoundary({ text: sentence })
this.blockListManager.matchAtWordBoundary({
// Blocklist is always lowercase
text: sentence.toLowerCase(),
})
) {
sentence = "";
abort = true;

View file

@ -1,2 +1,2 @@
<!-- may be protected as a trademark in some jurisdictions -->
<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" fill="context-fill"><path d="M32.022 16.242a7.574 7.574 0 0 0-.651-6.221 7.66 7.66 0 0 0-8.25-3.675 7.577 7.577 0 0 0-5.714-2.547A7.661 7.661 0 0 0 10.1 9.102a7.578 7.578 0 0 0-5.065 3.674 7.663 7.663 0 0 0 .942 8.983 7.574 7.574 0 0 0 .651 6.221 7.66 7.66 0 0 0 8.25 3.675 7.571 7.571 0 0 0 5.714 2.546 7.662 7.662 0 0 0 7.31-5.307 7.577 7.577 0 0 0 5.065-3.674 7.663 7.663 0 0 0-.944-8.98l-.001.002ZM20.594 32.215a5.68 5.68 0 0 1-3.648-1.32c.047-.024.128-.069.18-.1l6.054-3.497c.31-.176.5-.506.498-.862v-8.535l2.558 1.478a.09.09 0 0 1 .05.07v7.068a5.704 5.704 0 0 1-5.692 5.698ZM8.352 26.986a5.673 5.673 0 0 1-.679-3.817c.045.026.124.075.18.107l6.054 3.496c.307.18.687.18.995 0l7.39-4.267v2.954a.095.095 0 0 1-.036.08l-6.12 3.533a5.705 5.705 0 0 1-7.783-2.086ZM6.76 13.771a5.679 5.679 0 0 1 2.965-2.498l-.002.21v6.993a.985.985 0 0 0 .497.86l7.39 4.268-2.558 1.477a.09.09 0 0 1-.087.008l-6.12-3.537a5.704 5.704 0 0 1-2.086-7.78l.001-.001Zm21.022 4.892-7.39-4.268 2.558-1.476a.09.09 0 0 1 .087-.008l6.12 3.534a5.7 5.7 0 0 1-.88 10.282v-7.203a.983.983 0 0 0-.494-.86Zm2.547-3.833a8.018 8.018 0 0 0-.18-.107l-6.054-3.496a.986.986 0 0 0-.995 0l-7.39 4.268V12.54a.095.095 0 0 1 .035-.08l6.12-3.53a5.697 5.697 0 0 1 8.462 5.9h.002Zm-16.01 5.267-2.56-1.478a.09.09 0 0 1-.05-.07v-7.068a5.699 5.699 0 0 1 9.345-4.376c-.047.025-.127.07-.18.102l-6.054 3.496a.983.983 0 0 0-.498.86l-.004 8.532v.002Zm1.39-2.997L19 15.2l3.292 1.9v3.802l-3.293 1.9-3.292-1.9v-3.8Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" fill="none"><g clip-path="url(#a)"><mask id="b" width="38" height="38" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:luminance"><path fill="#fff" d="M38 0H0v37.66h38V0Z"/></mask><g mask="url(#b)"><path fill="context-fill" d="M14.575 13.709V10.13c0-.302.113-.528.376-.678l7.194-4.143c.979-.565 2.146-.828 3.351-.828 4.52 0 7.382 3.502 7.382 7.23 0 .264 0 .566-.038.867L25.383 8.21c-.451-.263-.904-.263-1.355 0l-9.453 5.498ZM31.37 27.642v-8.55c0-.526-.226-.903-.677-1.167l-9.453-5.498 3.088-1.77c.263-.15.49-.15.753 0l7.193 4.142c2.072 1.206 3.465 3.767 3.465 6.252 0 2.862-1.694 5.498-4.369 6.59v.001Zm-19.018-7.532-3.088-1.808c-.264-.15-.377-.376-.377-.678V9.34c0-4.03 3.088-7.08 7.269-7.08 1.581 0 3.05.527 4.293 1.469l-7.419 4.293c-.452.264-.678.64-.678 1.168V20.11ZM19 23.952l-4.425-2.485v-5.273L19 13.71l4.425 2.485v5.273L19 23.952Zm2.843 11.45c-1.582 0-3.05-.528-4.293-1.47l7.419-4.293c.452-.264.678-.64.678-1.168V17.55l3.126 1.808c.263.15.377.376.377.677v8.286c0 4.03-3.127 7.08-7.307 7.08Zm-8.925-8.399L5.724 22.86C3.653 21.655 2.26 19.094 2.26 16.61c0-2.9 1.733-5.499 4.407-6.59v8.586c0 .527.226.904.678 1.167l9.415 5.46-3.088 1.771c-.264.15-.49.15-.753 0Zm-.414 6.176c-4.256 0-7.382-3.2-7.382-7.155 0-.302.038-.603.075-.904l7.42 4.293c.451.264.903.264 1.355 0l9.453-5.46v3.578c0 .3-.113.527-.377.677l-7.193 4.143c-.98.565-2.147.828-3.352.828Zm9.34 4.482a9.416 9.416 0 0 0 9.226-7.532c4.218-1.093 6.93-5.047 6.93-9.077 0-2.636-1.13-5.197-3.163-7.042.188-.791.3-1.582.3-2.373 0-5.385-4.368-9.415-9.415-9.415-1.016 0-1.995.15-2.975.49C21.052 1.054 18.717 0 16.157 0A9.416 9.416 0 0 0 6.93 7.532C2.712 8.624 0 12.58 0 16.608c0 2.637 1.13 5.197 3.163 7.043-.188.79-.3 1.582-.3 2.372 0 5.386 4.368 9.416 9.415 9.416 1.016 0 1.995-.15 2.975-.49 1.694 1.657 4.03 2.712 6.59 2.712Z"/></g></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h38v38H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Before After
Before After

View file

@ -1,2 +1,2 @@
<!-- may be protected as a trademark in some jurisdictions -->
<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" fill="none"><path fill="#D97757" d="m9.886 23.95 5.903-3.31.098-.29-.098-.161h-.292l-.989-.06-3.373-.09-2.919-.121-2.838-.15-.713-.151L4 18.729l.065-.436.6-.406.86.075 1.897.135 2.854.196 2.06.12 3.064.316h.486l.065-.195-.162-.12-.13-.121-2.95-2.001-3.195-2.106-1.67-1.22-.893-.616-.454-.572-.194-1.264.81-.902 1.103.075.276.075 1.119.858 2.384 1.85 3.113 2.287.454.376.183-.123.028-.087-.21-.346-1.687-3.054-1.8-3.115-.811-1.294-.21-.767a3.51 3.51 0 0 1-.13-.918l.924-1.264.519-.165 1.248.165.52.452.778 1.775 1.248 2.784 1.946 3.79.568 1.13.308 1.037.113.316h.195v-.18l.162-2.137.292-2.617.292-3.37.097-.948.47-1.144.94-.617.73.346.6.858-.08.557-.357 2.317-.697 3.625-.454 2.438h.259l.308-.316 1.233-1.625 2.059-2.588.908-1.023 1.07-1.128.681-.542h1.298l.94 1.414-.421 1.46-1.33 1.685-1.103 1.429-1.58 2.118-.982 1.704.088.14.236-.02 3.568-.767 1.93-.346 2.302-.392 1.038.482.114.496-.406 1.008-2.465.602-2.886.587-4.298 1.012-.048.038.056.083 1.939.176.827.045h2.027l3.778.286.99.647.583.797-.097.617-1.525.767-2.043-.481-4.784-1.144-1.637-.406h-.228v.136l1.363 1.339 2.513 2.256 3.13 2.92.162.721-.405.572-.422-.06L27.27 27.2l-1.07-.933-2.4-2.031h-.162v.21l.551.813 2.935 4.408.146 1.354-.21.436-.763.271-.827-.15-1.735-2.422-1.767-2.709-1.427-2.437-.173.109-.85 9.069-.388.466-.908.346-.763-.572-.405-.933.405-1.85.487-2.407.39-1.911.356-2.377.218-.794-.02-.053-.174.029-1.792 2.458-2.724 3.686-2.157 2.302-.519.21-.892-.466.082-.828.502-.737 2.984-3.791 1.8-2.362 1.16-1.356-.011-.196-.064-.006-7.928 5.169-1.411.18-.616-.572.08-.932.293-.301 2.383-1.64Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" fill="none"><g clip-path="url(#a)"><path fill="#D97757" d="m7.456 25.27 7.477-4.193.124-.367-.124-.204h-.37l-1.253-.076-4.272-.114-3.698-.153-3.595-.19-.903-.191L0 18.657l.082-.552.76-.515 1.09.095 2.403.171 3.615.248 2.609.152 3.881.4h.616l.082-.246-.205-.152-.165-.153-3.737-2.535-4.047-2.668-2.115-1.545-1.131-.78-.575-.725-.246-1.6 1.026-1.143 1.397.095.35.095 1.417 1.086 3.02 2.344 3.943 2.897.575.476.232-.156.035-.11-.266-.438-2.136-3.869-2.28-3.946-1.028-1.639-.266-.971a4.446 4.446 0 0 1-.164-1.163L9.942.21 10.6 0l1.58.209.659.573.985 2.248 1.581 3.526 2.465 4.8.72 1.432.39 1.314.143.4h.247v-.228l.205-2.707.37-3.315.37-4.268.123-1.201.595-1.45 1.19-.78.925.438.76 1.086-.101.706-.452 2.935-.883 4.591-.575 3.089h.328l.39-.4 1.562-2.06 2.608-3.277 1.15-1.296 1.355-1.429.863-.686h1.644l1.19 1.79-.532 1.85-1.685 2.134-1.397 1.81-2.002 2.683-1.243 2.159.111.177.299-.025 4.52-.972 2.444-.438 2.916-.497 1.315.61.144.63-.514 1.276-3.123.762-3.655.744-5.444 1.282-.061.048.07.105 2.457.223 1.047.057h2.568l4.785.362 1.255.82.738 1.01-.123.78-1.931.973-2.588-.61-6.06-1.449-2.074-.514h-.288v.172l1.726 1.696 3.183 2.858 3.965 3.698.205.914-.513.724-.535-.076-3.492-2.63-1.355-1.181-3.04-2.573h-.205v.266l.698 1.03 3.717 5.583.185 1.716-.266.552-.966.343-1.048-.19-2.197-3.068-2.239-3.431-1.807-3.087-.22.138-1.076 11.487-.491.59-1.15.439-.967-.724-.513-1.182.513-2.344.617-3.049.494-2.42.45-3.011.277-1.006-.025-.067-.22.037-2.27 3.113-3.451 4.67-2.732 2.915-.658.266-1.13-.59.104-1.049.636-.933 3.78-4.802 2.28-2.992 1.47-1.718-.015-.248-.08-.008-10.043 6.548-1.787.228-.78-.725.101-1.18.371-.382 3.019-2.077Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h38v38H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before After
Before After

View file

@ -1,2 +1,2 @@
<!-- may be protected as a trademark in some jurisdictions -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" fill="none" class="mb-4 size-12"><path fill="url(#a)" d="M85.855 18.313A11.574 11.574 0 0 0 74.75 10h-3.379a11.574 11.574 0 0 0-11.384 9.485L54.2 51.018l1.436-4.913a11.574 11.574 0 0 1 11.11-8.327H86.38l8.235 3.207 7.937-3.207h-2.316a11.574 11.574 0 0 1-11.105-8.313z"/><path fill="url(#b)" d="M36.326 101.64A11.574 11.574 0 0 0 47.445 110h7.176c6.276 0 11.409-5.002 11.57-11.277l.781-30.405-1.634 5.583a11.574 11.574 0 0 1-11.108 8.321H34.432l-7.058-3.829-7.641 3.83h2.278c5.154 0 9.687 3.408 11.119 8.36z"/><path fill="url(#c)" d="M74.248 10H34.15c-11.457 0-18.33 15.142-22.913 30.283-5.43 17.939-12.534 41.93 8.02 41.93H36.57c5.174 0 9.716-3.421 11.138-8.396 3.01-10.531 8.286-28.903 12.43-42.889 2.105-7.107 3.86-13.211 6.551-17.012C68.2 11.785 70.715 10 74.248 10"/><path fill="url(#d)" d="M74.248 10H34.15c-11.457 0-18.33 15.142-22.913 30.283-5.43 17.939-12.534 41.93 8.02 41.93H36.57c5.174 0 9.716-3.421 11.138-8.396 3.01-10.531 8.286-28.903 12.43-42.889 2.105-7.107 3.86-13.211 6.551-17.012C68.2 11.785 70.715 10 74.248 10"/><path fill="url(#e)" d="M46.744 110h40.099c11.456 0 18.33-15.144 22.913-30.288 5.429-17.942 12.533-41.937-8.02-41.937H84.422a11.576 11.576 0 0 0-11.138 8.396c-3.01 10.533-8.286 28.909-12.43 42.897-2.106 7.109-3.86 13.214-6.552 17.016-1.51 2.131-4.025 3.916-7.558 3.916"/><path fill="url(#f)" d="M46.744 110h40.099c11.456 0 18.33-15.144 22.913-30.288 5.429-17.942 12.533-41.937-8.02-41.937H84.422a11.576 11.576 0 0 0-11.138 8.396c-3.01 10.533-8.286 28.909-12.43 42.897-2.106 7.109-3.86 13.214-6.552 17.016-1.51 2.131-4.025 3.916-7.558 3.916"/><defs><radialGradient id="a" cx="0" cy="0" r="1" gradientTransform="matrix(-27.40125 -33.47302 31.47539 -25.76598 95.512 51.286)" gradientUnits="userSpaceOnUse"><stop offset=".096" stop-color="#00AEFF"/><stop offset=".773" stop-color="#2253CE"/><stop offset="1" stop-color="#0736C4"/></radialGradient><radialGradient id="b" cx="0" cy="0" r="1" gradientTransform="rotate(51.84 -70.254 70.14) scale(39.9779 38.7796)" gradientUnits="userSpaceOnUse"><stop stop-color="#FFB657"/><stop offset=".634" stop-color="#FF5F3D"/><stop offset=".923" stop-color="#C02B3C"/></radialGradient><radialGradient id="e" cx="0" cy="0" r="1" gradientTransform="matrix(-31.67773 90.58917 -108.5232 -37.949 103.796 30.703)" gradientUnits="userSpaceOnUse"><stop offset=".066" stop-color="#8C48FF"/><stop offset=".5" stop-color="#F2598A"/><stop offset=".896" stop-color="#FFB152"/></radialGradient><linearGradient id="c" x1="31.75" x2="37.471" y1="18.75" y2="84.938" gradientUnits="userSpaceOnUse"><stop offset=".156" stop-color="#0D91E1"/><stop offset=".487" stop-color="#52B471"/><stop offset=".652" stop-color="#98BD42"/><stop offset=".937" stop-color="#FFC800"/></linearGradient><linearGradient id="d" x1="36.75" x2="39.874" y1="10" y2="82.213" gradientUnits="userSpaceOnUse"><stop stop-color="#3DCBFF"/><stop offset=".247" stop-color="#0588F7" stop-opacity="0"/></linearGradient><linearGradient id="f" x1="106.964" x2="106.923" y1="33.365" y2="53.037" gradientUnits="userSpaceOnUse"><stop offset=".058" stop-color="#F8ADFA"/><stop offset=".708" stop-color="#A86EDD" stop-opacity="0"/></linearGradient></defs></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" fill="none"><path fill="url(#a)" d="M27.53 4.977a3.893 3.893 0 0 0-3.736-2.796h-1.136a3.893 3.893 0 0 0-3.83 3.19l-1.946 10.607.483-1.652a3.893 3.893 0 0 1 3.737-2.801h6.604l2.77 1.079 2.67-1.08h-.779a3.893 3.893 0 0 1-3.735-2.795L27.53 4.977Z"/><path fill="url(#b)" d="M10.871 33.006a3.894 3.894 0 0 0 3.74 2.812h2.414a3.893 3.893 0 0 0 3.892-3.793l.263-10.227-.55 1.878a3.893 3.893 0 0 1-3.736 2.799h-6.66L7.86 25.185l-2.57 1.289h.766a3.893 3.893 0 0 1 3.74 2.812l1.075 3.72Z"/><path fill="url(#c)" d="M23.625 2.181H10.137c-3.854 0-6.166 5.093-7.707 10.186-1.827 6.035-4.216 14.104 2.697 14.104h5.824c1.74 0 3.268-1.15 3.747-2.824 1.012-3.542 2.787-9.722 4.18-14.426.709-2.39 1.299-4.444 2.204-5.723.508-.716 1.354-1.317 2.543-1.317Z"/><path fill="url(#d)" d="M23.625 2.181H10.137c-3.854 0-6.166 5.093-7.707 10.186-1.827 6.035-4.216 14.104 2.697 14.104h5.824c1.74 0 3.268-1.15 3.747-2.824 1.012-3.542 2.787-9.722 4.18-14.426.709-2.39 1.299-4.444 2.204-5.723.508-.716 1.354-1.317 2.543-1.317Z"/><path fill="url(#e)" d="M14.375 35.818h13.488c3.854 0 6.166-5.094 7.707-10.188 1.827-6.035 4.216-14.106-2.697-14.106h-5.824a3.894 3.894 0 0 0-3.747 2.824 1748.2 1748.2 0 0 1-4.18 14.43c-.71 2.39-1.3 4.444-2.205 5.723-.508.717-1.354 1.317-2.542 1.317Z"/><path fill="url(#f)" d="M14.375 35.818h13.488c3.854 0 6.166-5.094 7.707-10.188 1.827-6.035 4.216-14.106-2.697-14.106h-5.824a3.894 3.894 0 0 0-3.747 2.824 1748.2 1748.2 0 0 1-4.18 14.43c-.71 2.39-1.3 4.444-2.205 5.723-.508.717-1.354 1.317-2.542 1.317Z"/><defs><radialGradient id="a" cx="0" cy="0" r="1" gradientTransform="rotate(-129.304 19.195 .744) scale(14.5508 13.6824)" gradientUnits="userSpaceOnUse"><stop offset=".096" stop-color="#00AEFF"/><stop offset=".773" stop-color="#2253CE"/><stop offset="1" stop-color="#0736C4"/></radialGradient><radialGradient id="b" cx="0" cy="0" r="1" gradientTransform="rotate(51.84 -23.089 21.615) scale(13.4474 13.0443)" gradientUnits="userSpaceOnUse"><stop stop-color="#FFB657"/><stop offset=".634" stop-color="#FF5F3D"/><stop offset=".923" stop-color="#C02B3C"/></radialGradient><radialGradient id="e" cx="0" cy="0" r="1" gradientTransform="matrix(-10.65548 30.47158 -36.504 -12.76492 33.566 9.145)" gradientUnits="userSpaceOnUse"><stop offset=".066" stop-color="#8C48FF"/><stop offset=".5" stop-color="#F2598A"/><stop offset=".896" stop-color="#FFB152"/></radialGradient><linearGradient id="c" x1="9.33" x2="11.254" y1="5.124" y2="27.388" gradientUnits="userSpaceOnUse"><stop offset=".156" stop-color="#0D91E1"/><stop offset=".487" stop-color="#52B471"/><stop offset=".652" stop-color="#98BD42"/><stop offset=".937" stop-color="#FFC800"/></linearGradient><linearGradient id="d" x1="11.012" x2="12.062" y1="2.181" y2="26.471" gradientUnits="userSpaceOnUse"><stop stop-color="#3DCBFF"/><stop offset=".247" stop-color="#0588F7" stop-opacity="0"/></linearGradient><linearGradient id="f" x1="34.631" x2="34.617" y1="10.04" y2="16.658" gradientUnits="userSpaceOnUse"><stop offset=".058" stop-color="#F8ADFA"/><stop offset=".708" stop-color="#A86EDD" stop-opacity="0"/></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before After
Before After

View file

@ -1,2 +1,2 @@
<!-- may be protected as a trademark in some jurisdictions -->
<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" fill="none"><path fill="url(#a)" d="M36.928 18.928c-2.468 0-4.749-.468-6.919-1.388-2.172-.951-4.087-2.25-5.692-3.856-1.606-1.605-2.902-3.522-3.856-5.692-.922-2.17-1.388-4.453-1.388-6.921A.071.071 0 0 0 19 .999a.071.071 0 0 0-.072.072c0 2.468-.482 4.749-1.433 6.921-.923 2.172-2.205 4.087-3.81 5.692-1.606 1.606-3.523 2.903-5.693 3.856-2.17.922-4.453 1.389-6.921 1.389A.072.072 0 0 0 1 19c0 .039.033.072.072.072 2.468 0 4.749.482 6.921 1.433 2.172.923 4.087 2.205 5.692 3.81 1.606 1.607 2.888 3.523 3.81 5.695.952 2.17 1.434 4.45 1.434 6.92 0 .038.033.071.072.071.039 0 .072-.03.072-.072 0-2.468.466-4.749 1.388-6.919.952-2.172 2.248-4.087 3.856-5.694 1.605-1.606 3.52-2.888 5.692-3.81 2.17-.952 4.45-1.434 6.92-1.434.038 0 .071-.031.071-.072a.071.071 0 0 0-.072-.072Z"/><defs><linearGradient id="a" x1="12.431" x2="28.716" y1="24.537" y2="10.806" gradientUnits="userSpaceOnUse"><stop stop-color="#217BFE"/><stop offset=".27" stop-color="#078EFB"/><stop offset=".78" stop-color="#A190FF"/><stop offset="1" stop-color="#BD99FE"/></linearGradient></defs></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="none"><path fill="url(#a)" d="M39.92 19.921c-2.742 0-5.277-.52-7.688-1.542-2.413-1.057-4.54-2.5-6.324-4.285-1.785-1.783-3.225-3.913-4.285-6.324C20.6 5.359 20.081 2.822 20.081.08a.077.077 0 0 0-.023-.057A.08.08 0 0 0 20 0a.079.079 0 0 0-.08.08c0 2.742-.536 5.277-1.592 7.69-1.026 2.413-2.45 4.541-4.234 6.325-1.784 1.784-3.914 3.225-6.325 4.284-2.411 1.024-4.948 1.543-7.69 1.543a.08.08 0 0 0-.079.08c0 .043.037.08.08.08 2.742 0 5.277.535 7.69 1.591 2.413 1.026 4.541 2.45 6.324 4.234 1.785 1.785 3.21 3.914 4.234 6.327 1.058 2.412 1.593 4.945 1.593 7.69a.08.08 0 0 0 .08.078c.043 0 .08-.033.08-.08 0-2.742.518-5.276 1.542-7.688 1.058-2.413 2.498-4.54 4.285-6.326 1.783-1.785 3.91-3.21 6.324-4.234 2.411-1.057 4.945-1.593 7.69-1.593a.08.08 0 0 0 .078-.08.08.08 0 0 0-.05-.074.081.081 0 0 0-.03-.006Z"/><defs><linearGradient id="a" x1="12.701" x2="30.796" y1="26.153" y2="10.897" gradientUnits="userSpaceOnUse"><stop stop-color="#217BFE"/><stop offset=".27" stop-color="#078EFB"/><stop offset=".78" stop-color="#A190FF"/><stop offset="1" stop-color="#BD99FE"/></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before After
Before After

View file

@ -1,2 +1,2 @@
<!-- may be protected as a trademark in some jurisdictions -->
<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" fill="none"><path fill="#FFD21E" d="M3 18.678c0-7.77 6.3-14.07 14.071-14.07h5.478C29.426 4.607 35 10.181 35 17.057c0 6.876-5.574 12.45-12.45 12.45H7.774l-3.848 3.73A.546.546 0 0 1 3 32.845V18.678Z"/><path fill="#32343D" d="M23.836 14.586c.498.177.696 1.205 1.2.936a1.966 1.966 0 0 0 .807-2.653 1.95 1.95 0 0 0-2.643-.812 1.966 1.966 0 0 0-.808 2.654c.24.451.998-.283 1.444-.125ZM14.628 14.586c-.498.177-.696 1.205-1.2.936a1.966 1.966 0 0 1-.807-2.653 1.95 1.95 0 0 1 2.643-.812 1.966 1.966 0 0 1 .808 2.654c-.24.451-.998-.283-1.444-.125ZM19.326 24.003c3.842 0 5.082-3.439 5.082-5.204 0-1.766-2.275.935-5.082.935-2.806 0-5.08-2.701-5.08-.935 0 1.765 1.239 5.204 5.08 5.204Z"/><path fill="#FF323D" d="M22.41 22.999c-.76.601-1.77 1.005-3.086 1.005-1.236 0-2.202-.357-2.945-.898a3.403 3.403 0 0 1 2.074-1.761c.157-.047.317.223.482.5.16.266.322.54.487.54.176 0 .35-.27.518-.532.177-.275.349-.542.515-.489l.145.05c.778.29 1.422.858 1.81 1.585Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" fill="none"><path fill="#FFD21E" d="M0 19.068C0 9.841 7.481 2.36 16.71 2.36h6.504C31.381 2.359 38 8.978 38 17.143s-6.62 14.784-14.784 14.784H5.669l-4.57 4.43A.649.649 0 0 1 0 35.89V19.068Z"/><path fill="#32343D" d="M24.091 13.897c.576.205.805 1.393 1.388 1.082a2.273 2.273 0 0 0-.4-4.172 2.255 2.255 0 0 0-2.824 1.508 2.273 2.273 0 0 0 .167 1.727c.277.521 1.154-.328 1.67-.145Zm-10.646 0c-.576.205-.805 1.393-1.388 1.082a2.273 2.273 0 0 1 .4-4.172 2.255 2.255 0 0 1 2.824 1.508 2.273 2.273 0 0 1-.167 1.727c-.277.521-1.154-.328-1.67-.145Zm5.432 10.889c4.442 0 5.876-3.977 5.876-6.018s-2.63 1.081-5.876 1.081c-3.245 0-5.874-3.122-5.874-1.08 0 2.04 1.433 6.017 5.874 6.017Z"/><path fill="#FF323D" d="M22.443 23.624c-.879.695-2.047 1.163-3.568 1.163-1.43 0-2.546-.413-3.405-1.039a3.936 3.936 0 0 1 2.398-2.036c.181-.054.366.258.557.578.185.308.372.625.563.625.204 0 .405-.313.6-.616.204-.318.403-.626.595-.565l.167.058c.9.335 1.644.992 2.093 1.832Z"/></svg>

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before After
Before After

View file

@ -1,2 +1,2 @@
<!-- may be protected as a trademark in some jurisdictions -->
<svg xmlns="http://www.w3.org/2000/svg" width="1.1em" height="1em" viewBox="0 0 256 233" shape-rendering="crispEdges"><path d="M186.182 0h46.545v46.545h-46.545z"/><path fill="#f7d046" d="M209.455 0H256v46.545h-46.545z"/><path d="M0 0h46.545v46.545H0zm0 46.545h46.545V93.09H0zm0 46.546h46.545v46.545H0zm0 46.545h46.545v46.545H0zm0 46.546h46.545v46.545H0z"/><path fill="#f7d046" d="M23.273 0h46.545v46.545H23.273z"/><path fill="#f2a73b" d="M209.455 46.545H256V93.09h-46.545zm-186.182 0h46.545V93.09H23.273z"/><path d="M139.636 46.545h46.545V93.09h-46.545z"/><path fill="#f2a73b" d="M162.909 46.545h46.545V93.09h-46.545zm-93.091 0h46.545V93.09H69.818z"/><path fill="#ee792f" d="M116.364 93.091h46.545v46.545h-46.545zm46.545 0h46.545v46.545h-46.545zm-93.091 0h46.545v46.545H69.818z"/><path d="M93.091 139.636h46.545v46.545H93.091z"/><path fill="#eb5829" d="M116.364 139.636h46.545v46.545h-46.545z"/><path fill="#ee792f" d="M209.455 93.091H256v46.545h-46.545zm-186.182 0h46.545v46.545H23.273z"/><path d="M186.182 139.636h46.545v46.545h-46.545z"/><path fill="#eb5829" d="M209.455 139.636H256v46.545h-46.545z"/><path d="M186.182 186.182h46.545v46.545h-46.545z"/><path fill="#eb5829" d="M23.273 139.636h46.545v46.545H23.273z"/><path fill="#ea3326" d="M209.455 186.182H256v46.545h-46.545zm-186.182 0h46.545v46.545H23.273z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" fill="none"><path fill="#000" d="M27.18 2.638h6.546v6.545h-6.545V2.638Z"/><path fill="#F7D046" d="M30.454 2.638H37v6.545h-6.546V2.638Z"/><path fill="#000" d="M1 2.638h6.546v6.545H1V2.638Zm0 6.545h6.546v6.544H1V9.183Zm0 6.544h6.546v6.545H1v-6.544Zm0 6.545h6.546v6.545H1v-6.545Zm0 6.545h6.546v6.545H1v-6.545Z"/><path fill="#F7D046" d="M4.271 2.638h6.546v6.545H4.271V2.638Z"/><path fill="#F2A73B" d="M30.454 9.182H37v6.545h-6.546V9.182Zm-26.183 0h6.546v6.545H4.271V9.182Z"/><path fill="#000" d="M20.638 9.182h6.545v6.545h-6.545V9.182Z"/><path fill="#F2A73B" d="M23.909 9.182h6.545v6.545H23.91V9.182Zm-13.092 0h6.546v6.545h-6.546V9.182Z"/><path fill="#EE792F" d="M17.363 15.728h6.546v6.545h-6.546v-6.545Zm6.546 0h6.545v6.545H23.91v-6.545Zm-13.092 0h6.546v6.545h-6.546v-6.545Z"/><path fill="#000" d="M14.089 22.273h6.545v6.544H14.09v-6.544Z"/><path fill="#EB5829" d="M17.362 22.273h6.546v6.544h-6.546v-6.544Z"/><path fill="#EE792F" d="M30.454 15.728H37v6.545h-6.546v-6.545Zm-26.183 0h6.546v6.545H4.271v-6.545Z"/><path fill="#000" d="M27.18 22.273h6.546v6.544h-6.545v-6.544Z"/><path fill="#EB5829" d="M30.454 22.273H37v6.544h-6.546v-6.544Z"/><path fill="#000" d="M27.18 28.817h6.546v6.545h-6.545v-6.545Z"/><path fill="#EB5829" d="M4.271 22.273h6.546v6.544H4.271v-6.544Z"/><path fill="#EA3326" d="M30.454 28.817H37v6.545h-6.546v-6.545Zm-26.183 0h6.546v6.545H4.271v-6.545Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before After
Before After

View file

@ -171,12 +171,15 @@ browser {
}
.icon {
--icon-size: 30px;
background-position: center;
background-repeat: no-repeat;
background-size: contain;
border-radius: 0;
height: var(--icon-size);
margin-inline: var(--space-small);
min-width: 40px;
max-width: var(--icon-size);
min-width: var(--icon-size);
outline: none;
&.claude {

View file

@ -12,6 +12,18 @@ ChromeUtils.defineESModuleGetters(lazy, {
BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
});
ChromeUtils.defineLazyGetter(
lazy,
"numberFormat",
() => new Services.intl.NumberFormat()
);
ChromeUtils.defineLazyGetter(
lazy,
"pluralRules",
() => new Services.intl.PluralRules()
);
const FEEDBACK_LINK =
"https://connect.mozilla.org/t5/discussions/try-out-link-previews-on-firefox-labs/td-p/92012";
@ -107,6 +119,12 @@ class LinkPreviewCard extends MozLitElement {
const readingTimeMinsFast = articleData.readingTimeMinsFast || "";
const readingTimeMinsSlow = articleData.readingTimeMinsSlow || "";
const readingTimeMinsFastStr =
lazy.numberFormat.format(readingTimeMinsFast);
const readingTimeRange = lazy.numberFormat.formatRange(
readingTimeMinsFast,
readingTimeMinsSlow
);
// Check if both metadata and article text content are missing
const isMissingAllContent = !description && !articleData.textContent;
@ -156,11 +174,25 @@ class LinkPreviewCard extends MozLitElement {
? html`<p class="og-card-description">${description}</p>`
: ""}
${readingTimeMinsFast && readingTimeMinsSlow
? html`<div class="og-card-reading-time">
${readingTimeMinsFast === readingTimeMinsSlow
? `${readingTimeMinsFast} min${readingTimeMinsFast > 1 ? "s" : ""} reading time`
: `${readingTimeMinsFast}-${readingTimeMinsSlow} mins reading time`}
</div>`
? html`
<div
class="og-card-reading-time"
data-l10n-id="link-preview-reading-time"
data-l10n-args=${JSON.stringify({
range:
readingTimeMinsFast === readingTimeMinsSlow
? `~${readingTimeMinsFastStr}`
: `${readingTimeRange}`,
rangePlural:
readingTimeMinsFast === readingTimeMinsSlow
? lazy.pluralRules.select(readingTimeMinsFast)
: lazy.pluralRules.selectRange(
readingTimeMinsFast,
readingTimeMinsSlow
),
})}
></div>
`
: ""}
</div>
${this.generating || this.keyPoints.length

View file

@ -141,6 +141,8 @@ add_task(async function test_show_shortcuts_second_tab() {
Assert.equal(stub.callCount, 1, "Shortcuts added on select");
Assert.equal(stub.firstCall.args[0], browser, "Got correct browser");
sandbox.restore();
}
);
});
@ -211,6 +213,23 @@ add_task(async function test_show_warning_label() {
"true",
"Warning lable value is correct"
);
Assert.ok(
!aiActionButton.getAttribute("type").includes("ghost"),
"button is active"
);
const hidden = BrowserTestUtils.waitForEvent(
chatShortcutsOptionsPanel,
"popuphidden"
);
chatShortcutsOptionsPanel.hidePopup();
await hidden;
Assert.ok(
aiActionButton.getAttribute("type").includes("ghost"),
"button is inactive"
);
}
);
});

View file

@ -4,6 +4,9 @@
const { LinkPreview } = ChromeUtils.importESModule(
"moz-src:///browser/components/genai/LinkPreview.sys.mjs"
);
const { Region } = ChromeUtils.importESModule(
"resource://gre/modules/Region.sys.mjs"
);
const { LinkPreviewModel } = ChromeUtils.importESModule(
"moz-src:///browser/components/genai/LinkPreviewModel.sys.mjs"
);
@ -403,3 +406,52 @@ add_task(async function test_skip_keypoints_generation_if_url_not_readable() {
generateStub.restore();
LinkPreview.keyboardComboActive = false;
});
/**
* Test that the Link Preview feature does not generate key points in disallowed regions.
*
* This test performs the following steps:
* 1. Sets the current region as a disallowed region for key point generation via the preference `"browser.ml.linkPreview.noKeyPointsRegions"`.
* 2. Enables the Link Preview feature via the preference `"browser.ml.linkPreview.enabled"`.
* 3. Stubs the `generateTextAI` method to track if it's called.
* 4. Activates the keyboard combination for link preview and sets an over link.
* 5. Verifies that the link preview panel is shown but no AI generation is attempted.
* 6. Ensures that the `generateTextAI` method is not called when the region is disallowed.
* 7. Cleans up by removing the panel, restoring the stub, and clearing the user preference
* `"browser.ml.linkPreview.noKeyPointsRegions"` to avoid affecting other tests.
*/
add_task(async function test_no_key_points_in_disallowed_region() {
const currentRegion = Region.home;
await SpecialPowers.pushPrefEnv({
set: [
["browser.ml.linkPreview.enabled", true],
["browser.ml.linkPreview.noKeyPointsRegions", currentRegion],
],
});
const generateStub = sinon.stub(LinkPreviewModel, "generateTextAI");
LinkPreview.keyboardComboActive = true;
XULBrowserWindow.setOverLink(
"https://example.com/browser/browser/components/genai/tests/browser/data/readableEn.html",
{}
);
let panel = await TestUtils.waitForCondition(() =>
document.getElementById("link-preview-panel")
);
await BrowserTestUtils.waitForEvent(panel, "popupshown");
is(
generateStub.callCount,
0,
"generateTextAI should not be called when region is disallowed"
);
panel.remove();
LinkPreview.keyboardComboActive = false;
generateStub.restore();
Services.prefs.clearUserPref("browser.ml.linkPreview.noKeyPointsRegions");
});

View file

@ -151,7 +151,7 @@ add_task(async function test_generateAI_with_blocklist() {
// Mocked Blocked List Manager
let manager = new BlockListManager({
blockNgrams: [BlockListManager.encodeBase64("Hello")],
blockNgrams: [BlockListManager.encodeBase64("hello")],
language: "en",
});

View file

@ -611,6 +611,7 @@ export class ChromeProfileMigrator extends MigratorBase {
let cards = [];
for (let row of rows) {
try {
cards.push({
"cc-name": row.getResultByName("name_on_card"),
"cc-number": await loginCrypto.decryptData(
@ -621,8 +622,14 @@ export class ChromeProfileMigrator extends MigratorBase {
row.getResultByName("expiration_month"),
10
),
"cc-exp-year": parseInt(row.getResultByName("expiration_year"), 10),
"cc-exp-year": parseInt(
row.getResultByName("expiration_year"),
10
),
});
} catch (e) {
console.error(e);
}
}
await MigrationUtils.insertCreditCardsWrapper(cards);

View file

@ -1002,6 +1002,38 @@ newtab:
send_in_pings:
- newtab
metric_registered:
type: labeled_boolean
description: >
Records technical data about whether the metric registration
at runtime succeeded
bugs:
- https://bugzil.la/1961989
data_reviews:
- https://bugzil.la/1961989
data_sensitivity:
- technical
notification_emails:
- pdahiya@mozilla.com
- mconley@mozilla.com
expires: never
ping_registered:
type: labeled_boolean
description: >
Records technical data about whether the ping registration
at runtime succeeded
bugs:
- https://bugzil.la/1961989
data_reviews:
- https://bugzil.la/1961989
data_sensitivity:
- technical
notification_emails:
- pdahiya@mozilla.com
- mconley@mozilla.com
expires: never
newtab.search:
enabled:
lifetime: application
@ -1503,22 +1535,6 @@ pocket:
description: >
If click belongs in a section, the numeric position of the section
type: string
title: &title
description: >
Title of the article
type: string
url: &url
description: >
Url of the article
type: string
time_sensitive: &time_sensitive
description: >
Indicates whether the article is time sensitive. This is passed down from merino
type: boolean
publisher: &publisher
description: >
derived publisher name of article
type: string
is_section_followed: *is_section_followed
send_in_pings:
- newtab
@ -1586,10 +1602,6 @@ pocket:
If click belongs in a section, the numeric position of the section
type: string
is_section_followed: *is_section_followed
title: *title
url: *url
time_sensitive: *time_sensitive
publisher: *publisher
send_in_pings:
- newtab
@ -1632,10 +1644,6 @@ pocket:
If click belongs in a section, the numeric position of the section
type: string
is_section_followed: *is_section_followed
title: *title
url: *url
time_sensitive: *time_sensitive
publisher: *publisher
send_in_pings:
- newtab
@ -1846,10 +1854,6 @@ pocket:
If event belongs in a section, the numeric position of the section
type: string
is_section_followed: *is_section_followed
title: *title
url: *url
time_sensitive: *time_sensitive
publisher: *publisher
send_in_pings:
- newtab
@ -2073,10 +2077,6 @@ newtab_content:
description: >
If click belongs in a section, if that section is followed
type: boolean
title: *title
url: *url
time_sensitive: *time_sensitive
publisher: *publisher
send_in_pings:
- newtab-content
@ -2134,10 +2134,6 @@ newtab_content:
description: >
If click belongs in a section, if that section is followed
type: boolean
title: *title
url: *url
time_sensitive: *time_sensitive
publisher: *publisher
send_in_pings:
- newtab-content
@ -2179,10 +2175,6 @@ newtab_content:
description: >
If click belongs in a section, if that section is followed
type: boolean
title: *title
url: *url
time_sensitive: *time_sensitive
publisher: *publisher
send_in_pings:
- newtab-content
@ -2237,10 +2229,6 @@ newtab_content:
description: >
If event belongs in a section, if that section is followed
type: boolean
title: *title
url: *url
time_sensitive: *time_sensitive
publisher: *publisher
send_in_pings:
- newtab-content

View file

@ -214,9 +214,7 @@
<html:h2 data-l10n-id="preferences-web-appearance-header"/>
<html:div id="webAppearanceSettings">
<description class="description-deemphasized" data-l10n-id="preferences-web-appearance-description"/>
<html:moz-message-bar id="web-appearance-override-warning" data-l10n-id="preferences-web-appearance-override-warning2">
<button slot="actions" class="accessory-button" data-l10n-id="preferences-colors-manage-button" id="web-appearance-manage-colors-button"/>
</html:moz-message-bar>
<html:moz-message-bar id="web-appearance-override-warning" data-l10n-id="preferences-web-appearance-override-warning3"/>
<form xmlns="http://www.w3.org/1999/xhtml" id="web-appearance-chooser" autocomplete="off">
<label class="web-appearance-choice" data-l10n-id="preferences-web-appearance-choice-tooltip-auto">
<div class="web-appearance-choice-image-container"><img role="presentation" alt="" width="54" height="42" /></div>

View file

@ -4448,11 +4448,6 @@ const AppearanceChooser = {
handleEvent(e) {
if (e.type == "click") {
switch (e.target.id) {
// Forward the click to the "colors" button.
case "web-appearance-manage-colors-button":
document.getElementById("colors").click();
e.preventDefault();
break;
case "web-appearance-manage-themes-link":
window.browsingContext.topChromeWindow.BrowserAddonUI.openAddonsMgr(
"addons://list/theme"

View file

@ -7,6 +7,7 @@
DIRS += ["dialogs"]
BROWSER_CHROME_MANIFESTS += ["tests/browser.toml", "tests/siteData/browser.toml"]
MOCHITEST_CHROME_MANIFESTS += ["tests/chrome/chrome.toml"]
for var in ("MOZ_APP_NAME", "MOZ_MACBUNDLE_NAME"):
DEFINES[var] = CONFIG[var]

View file

@ -439,10 +439,10 @@ var gSyncPane = {
},
_getEntryPoint() {
let params = new URLSearchParams(
document.URL.split("#")[0].split("?")[1] || ""
);
return params.get("entrypoint") || "preferences";
let params = URL.fromURI(document.documentURIObject).searchParams;
let entryPoint = params.get("entrypoint") || "preferences";
entryPoint = entryPoint.replace(/[^-.\w]/g, "");
return entryPoint;
},
openContentInBrowser(url, options) {

View file

@ -76,7 +76,7 @@ add_setup(async function () {
Ci.nsIWebHandlerApp
);
handler2.name = "Handler 2";
handler2.uriTemplate = "http://example.org/second/%s";
handler2.uriTemplate = "https://example.org/second/%s";
gDummyHandlers.push(handler1, handler2);
function substituteWebHandlers(handlerInfo) {

View file

@ -25,10 +25,7 @@ add_task(async function testFilterFeatures() {
{
...DEFAULT_LABS_RECIPES[3],
slug: "test-featureD",
bucketConfig: {
...ExperimentFakes.recipe.bucketConfig,
count: 1000,
},
bucketConfig: NimbusTestUtils.factories.recipe.bucketConfig,
},
];
const cleanup = await setupLabsTest(recipes);

View file

@ -58,6 +58,11 @@ const EXPECTED = {
// run through, so request a longer timeout.
requestLongerTimeout(10);
add_setup(async function () {
// Suggest needs to be initialized in order to dismiss a suggestion.
await QuickSuggestTestUtils.ensureQuickSuggestInit();
});
// The following tasks check the initial visibility of the Firefox Suggest UI
// and the visibility after installing a Nimbus experiment.
@ -199,9 +204,11 @@ add_task(async function initiallyEnabled_settingsUiOfflineOnly() {
// Tests the "Restore" button for dismissed suggestions.
add_task(async function restoreDismissedSuggestions() {
// Start with no dismissed suggestions.
await QuickSuggest.clearDismissedSuggestions();
Assert.ok(
!(await QuickSuggest.canClearDismissedSuggestions()),
"Sanity check: This test expects canClearDismissedSuggestions to return false initially"
"canClearDismissedSuggestions should be false after clearing suggestions"
);
await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
@ -213,27 +220,120 @@ add_task(async function restoreDismissedSuggestions() {
let button = doc.getElementById(BUTTON_RESTORE_DISMISSED_ID);
Assert.ok(button.disabled, "Restore button is disabled initially.");
await QuickSuggest.blockedSuggestions.add("https://example.com/");
await QuickSuggest.dismissResult(QuickSuggestTestUtils.ampResult());
Assert.ok(
await QuickSuggest.canClearDismissedSuggestions(),
"canClearDismissedSuggestions should return true after dismissing a suggestion"
);
Assert.ok(!button.disabled, "Restore button is enabled after blocking URL.");
await TestUtils.waitForCondition(
() => !button.disabled,
"Waiting for Restore button to become enabled after dismissing suggestion"
);
Assert.ok(
!button.disabled,
"Restore button should be enabled after dismissing suggestion"
);
let clearPromise = TestUtils.topicObserved("quicksuggest-dismissals-cleared");
button.click();
await clearPromise;
Assert.ok(
await QuickSuggest.blockedSuggestions.isEmpty(),
"blockedSuggestions.isEmpty() should return true after restoring dismissals"
);
Assert.ok(
!(await QuickSuggest.canClearDismissedSuggestions()),
"canClearDismissedSuggestions should return false after restoring dismissals"
);
Assert.ok(button.disabled, "Restore button is disabled after clicking it.");
await TestUtils.waitForCondition(
() => button.disabled,
"Waiting for Restore button to become disabled after clicking it"
);
Assert.ok(
button.disabled,
"Restore button should be disabled after clearing suggestions"
);
gBrowser.removeCurrentTab();
});
// If the pane is open while Suggest is still initializing and there are
// dismissed suggestions, the "Restore" button should become enabled when init
// finishes.
add_task(async function restoreDismissedSuggestions_init_enabled() {
// Dismiss a suggestion.
await QuickSuggest.dismissResult(QuickSuggestTestUtils.ampResult());
Assert.ok(
await QuickSuggest.canClearDismissedSuggestions(),
"canClearDismissedSuggestions should be true after dismissing suggestion"
);
await doRestoreInitTest(async button => {
// The button should become enabled since we dismissed a suggestion above.
await TestUtils.waitForCondition(
() => !button.disabled,
"Waiting for Restore button to become enabled after re-enabling Rust backend"
);
Assert.ok(
!button.disabled,
"Restore button should be enabled after re-enabling Rust backend"
);
});
});
// If the pane is open while Suggest is still initializing and there are no
// dismissed suggestions, the "Restore" button should remain disabled when init
// finishes.
add_task(async function restoreDismissedSuggestions_init_disabled() {
// Clear dismissed suggestions.
await QuickSuggest.clearDismissedSuggestions();
Assert.ok(
!(await QuickSuggest.canClearDismissedSuggestions()),
"canClearDismissedSuggestions should be false after clearing suggestions"
);
await doRestoreInitTest(async button => {
// The button should remain disabled since there are no dismissed
// suggestions.
await TestUtils.waitForTick();
Assert.ok(
button.disabled,
"Restore button should remain disabled after re-enabling Rust backend"
);
});
});
async function doRestoreInitTest(checkButton) {
// Disable the Suggest Rust backend, which manages individually dismissed
// suggestions. While Rust is disabled, Suggest won't be able to tell whether
// there are any individually dismissed suggestions.
await SpecialPowers.pushPrefEnv({
set: [["browser.urlbar.quicksuggest.rustEnabled", false]],
});
// Open the pane.
await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
let doc = gBrowser.selectedBrowser.contentDocument;
let addressBarSection = doc.getElementById("locationBarGroup");
addressBarSection.scrollIntoView();
let button = doc.getElementById(BUTTON_RESTORE_DISMISSED_ID);
Assert.ok(button.disabled, "Restore button is disabled initially.");
// Re-enable the Rust backend. It will send `quicksuggest-dismissals-changed`
// when it finishes initialization.
let changedPromise = TestUtils.topicObserved(
"quicksuggest-dismissals-changed"
);
await SpecialPowers.popPrefEnv();
info(
"Waiting for quicksuggest-dismissals-changed after re-enabling Rust backend"
);
await changedPromise;
await checkButton(button);
// Clean up.
await QuickSuggest.clearDismissedSuggestions();
gBrowser.removeCurrentTab();
}

View file

@ -0,0 +1,8 @@
[DEFAULT]
support-files = [
"../../../../../toolkit/content/tests/widgets/lit-test-helpers.js",
]
["test_setting_control.html"]
["test_setting_group.html"]

View file

@ -0,0 +1,195 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>setting-control test</title>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link
rel="stylesheet"
href="chrome://mochikit/content/tests/SimpleTest/test.css"
/>
<link rel="stylesheet" href="chrome://global/skin/global.css" />
<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
<script src="../../../../../toolkit/content/tests/widgets/lit-test-helpers.js"></script>
<script
type="module"
src="chrome://browser/content/preferences/widgets/setting-group.mjs"
></script>
<script
type="module"
src="chrome://browser/content/preferences/widgets/setting-control.mjs"
></script>
<script
type="module"
src="chrome://global/content/elements/moz-support-link.mjs"
></script>
<script
type="application/javascript"
src="chrome://global/content/preferencesBindings.js"
></script>
<script>
/* import-globals-from /toolkit/content/preferencesBindings.js */
let html, testHelpers;
const LABEL_L10N_ID = "browsing-use-autoscroll";
const GROUP_L10N_ID = "pane-experimental-reset";
function renderTemplate(itemConfig, setting) {
return testHelpers.renderTemplate(html`
<setting-control
.config=${itemConfig}
.setting=${setting}
></setting-group>
`);
}
function waitForSettingChange(setting) {
return new Promise(resolve => {
setting.on("change", function handler() {
setting.off("change", handler);
resolve();
});
});
}
add_setup(async function setup() {
await SpecialPowers.pushPrefEnv({
set: [["settings.revamp.design", false]],
});
testHelpers = new InputTestHelpers();
({ html } = await testHelpers.setupLit());
testHelpers.setupTests({
templateFn: () => html`<setting-group></setting-group>`,
});
MozXULElement.insertFTLIfNeeded("branding/brand.ftl");
MozXULElement.insertFTLIfNeeded("browser/preferences/preferences.ftl");
});
add_task(async function testSimpleCheckbox() {
const PREF = "test.setting-control.one";
const SETTING = "setting-control-one";
await SpecialPowers.pushPrefEnv({
set: [[PREF, true]],
});
Preferences.addAll([{ id: PREF, type: "bool" }]);
Preferences.addSetting({
id: SETTING,
pref: PREF,
});
let itemConfig = { l10nId: LABEL_L10N_ID, id: SETTING };
let setting = Preferences.getSetting(SETTING);
let result = await renderTemplate(itemConfig, setting);
let control = result.firstElementChild;
is(
control.localName,
"setting-control",
"It rendered the setting-control"
);
is(
control.inputEl.localName,
"moz-checkbox",
"The control rendered a checkbox"
);
is(control.inputEl.dataset.l10nId, LABEL_L10N_ID, "Label is set");
is(control.inputEl.checked, true, "checkbox is checked");
is(Services.prefs.getBoolPref(PREF), true, "pref is true");
let settingChanged = waitForSettingChange(setting);
synthesizeMouseAtCenter(control, {});
await settingChanged;
is(
control.inputEl.checked,
false,
"checkbox becomes unchecked after click"
);
is(Services.prefs.getBoolPref(PREF), false, "pref is false");
settingChanged = waitForSettingChange(setting);
Services.prefs.setBoolPref(PREF, true);
await settingChanged;
is(
control.inputEl.checked,
true,
"checkbox becomes checked after pfef change"
);
is(Services.prefs.getBoolPref(PREF), true, "pref is true");
});
add_task(async function testSupportLinkCheckbox() {
const SETTING = "setting-control-support-link";
Preferences.addSetting({
id: SETTING,
get: () => true,
});
let itemConfig = {
l10nId: LABEL_L10N_ID,
id: SETTING,
supportPage: "foo",
};
let result = await renderTemplate(
itemConfig,
Preferences.getSetting(SETTING)
);
let control = result.firstElementChild;
ok(control, "Got a control");
let checkbox = control.inputEl;
is(checkbox.localName, "moz-checkbox", "moz-checkbox is rendered");
is(
checkbox.supportPage,
"foo",
"The checkbox receives the supportPage"
);
});
add_task(async function testSupportLinkSubcategory() {
const SETTING = "setting-control-subcategory";
Preferences.addSetting({
id: SETTING,
get: () => true,
});
let configOne = {
l10nId: LABEL_L10N_ID,
id: SETTING,
subcategory: "exsubcategory",
};
let result = await renderTemplate(
configOne,
Preferences.getSetting(SETTING)
);
let control = result.firstElementChild;
ok(control, "Got the control");
is(
control.inputEl.dataset.subcategory,
"exsubcategory",
"Subcategory is set"
);
let configTwo = {
l10nId: LABEL_L10N_ID,
id: SETTING,
subcategory: "exsubcategory2",
supportPage: "foo",
};
result = await renderTemplate(
configTwo,
Preferences.getSetting(SETTING)
);
control = result.firstElementChild;
ok(control, "Got the control");
is(
control.inputEl.dataset.subcategory,
"exsubcategory2",
"Subcategory is set"
);
is(control.inputEl.supportPage, "foo", "Input got the supportPage");
});
</script>
</head>
<body>
<p id="display"></p>
<div id="content" style="display: none"></div>
<pre id="test"></pre>
</body>
</html>

View file

@ -0,0 +1,284 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>setting-group Tests</title>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link
rel="stylesheet"
href="chrome://mochikit/content/tests/SimpleTest/test.css"
/>
<link rel="stylesheet" href="chrome://global/skin/global.css" />
<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
<script src="../../../../../toolkit/content/tests/widgets/lit-test-helpers.js"></script>
<script
type="module"
src="chrome://browser/content/preferences/widgets/setting-group.mjs"
></script>
<script
type="module"
src="chrome://browser/content/preferences/widgets/setting-control.mjs"
></script>
<script
type="module"
src="chrome://global/content/elements/moz-support-link.mjs"
></script>
<script
type="application/javascript"
src="chrome://global/content/preferencesBindings.js"
></script>
<script>
/* import-globals-from /toolkit/content/preferencesBindings.js */
let html, testHelpers;
const LABEL_L10N_ID = "browsing-use-autoscroll";
const GROUP_L10N_ID = "pane-experimental-reset";
function renderTemplate(config) {
return testHelpers.renderTemplate(html`
<setting-group
.config=${config}
.getSetting=${(...args) => Preferences.getSetting(...args)}
></setting-group>
`);
}
function waitForSettingChange(setting) {
return new Promise(resolve => {
setting.on("change", function handler() {
setting.off("change", handler);
resolve();
});
});
}
add_setup(async function setup() {
await SpecialPowers.pushPrefEnv({
set: [["settings.revamp.design", false]],
});
testHelpers = new InputTestHelpers();
({ html } = await testHelpers.setupLit());
testHelpers.setupTests({
templateFn: () => html`<setting-group></setting-group>`,
});
MozXULElement.insertFTLIfNeeded("branding/brand.ftl");
MozXULElement.insertFTLIfNeeded("browser/preferences/preferences.ftl");
});
add_task(async function testSimpleXulPrefCheckboxes() {
const PREF_ONE = "test.settings-group.one";
const SETTING_ONE = "setting-one";
const PREF_TWO = "test.settings-group.two";
const SETTING_TWO = "setting-two";
await SpecialPowers.pushPrefEnv({
set: [
[PREF_ONE, true],
[PREF_TWO, false],
],
});
Preferences.addAll([
{ id: PREF_ONE, type: "bool" },
{ id: PREF_TWO, type: "bool" },
]);
Preferences.addSetting({
id: SETTING_ONE,
pref: PREF_ONE,
});
Preferences.addSetting({
id: SETTING_TWO,
pref: PREF_TWO,
});
let config = {
items: [
{ l10nId: LABEL_L10N_ID, id: SETTING_ONE },
{ l10nId: LABEL_L10N_ID, id: SETTING_TWO },
],
};
let result = await renderTemplate(config);
let group = result.querySelector("setting-group");
ok(group, "setting-group is created");
ok(group.config, "it got a config");
let checkboxes = result.querySelectorAll("checkbox");
is(checkboxes.length, 2, "Rendered two checkboxes");
is(checkboxes[0].dataset.l10nId, LABEL_L10N_ID, "Label is set");
is(checkboxes[0].checked, true, "First checkbox is checked");
is(Services.prefs.getBoolPref(PREF_ONE), true, "First pref is true");
is(checkboxes[1].checked, false, "Second checkbox is unchecked");
is(Services.prefs.getBoolPref(PREF_TWO), false, "Second pref is false");
let settingChanged = waitForSettingChange(
Preferences.getSetting(SETTING_ONE)
);
synthesizeMouseAtCenter(
checkboxes[0].querySelector(".checkbox-check"),
{}
);
await settingChanged;
is(checkboxes[0].checked, false, "First checkbox is unchecked");
is(Services.prefs.getBoolPref(PREF_ONE), false, "First pref is false");
settingChanged = waitForSettingChange(
Preferences.getSetting(SETTING_TWO)
);
synthesizeMouseAtCenter(
checkboxes[1].querySelector(".checkbox-check"),
{}
);
await settingChanged;
is(checkboxes[1].checked, true, "Second checkbox is checked");
is(Services.prefs.getBoolPref(PREF_TWO), true, "Second pref is true");
settingChanged = waitForSettingChange(
Preferences.getSetting(SETTING_ONE)
);
Services.prefs.setBoolPref(PREF_ONE, true);
await settingChanged;
is(
checkboxes[0].checked,
true,
"First checkbox becomes checked after pref change"
);
is(Services.prefs.getBoolPref(PREF_ONE), true, "First pref is true");
settingChanged = waitForSettingChange(
Preferences.getSetting(SETTING_TWO)
);
Services.prefs.setBoolPref(PREF_TWO, false);
await settingChanged;
is(
checkboxes[1].checked,
false,
"Second checkbox becomes unchecked after pref change"
);
is(Services.prefs.getBoolPref(PREF_TWO), false, "Second pref is false");
});
add_task(async function testSupportLinkXulCheckbox() {
const SETTING = "setting-support-link";
Preferences.addSetting({
id: SETTING,
get: () => true,
});
let config = {
items: [{ l10nId: LABEL_L10N_ID, id: SETTING, supportPage: "foo" }],
};
let result = await renderTemplate(config);
let checkbox = result.querySelector("checkbox");
ok(checkbox, "Got a checkbox");
ok(
checkbox.classList.contains("tail-with-learn-more"),
"Checkbox gets the correct class"
);
let container = checkbox.parentElement;
is(container.localName, "hbox", "Checkbox is in an hbox");
let supportLink = container.querySelector("a");
ok(supportLink, "The support link was created");
is(
supportLink.constructor,
customElements.get("moz-support-link"),
"It's a support link"
);
is(supportLink.supportPage, "foo", "The support page is set");
});
add_task(async function testSupportLinkXulSubcategory() {
const SETTING = "setting-subcategory";
Preferences.addSetting({
id: SETTING,
get: () => true,
});
let config = {
items: [
{
l10nId: LABEL_L10N_ID,
id: SETTING,
subcategory: "exsubcategory",
},
{
l10nId: LABEL_L10N_ID,
id: SETTING,
subcategory: "exsubcategory2",
supportPage: "foo",
},
],
};
let result = await renderTemplate(config);
let [basic, supportLink] = result.querySelectorAll("checkbox");
ok(basic, "Got the basic checkbox");
is(
basic.dataset.subcategory,
"exsubcategory",
"Subcategory is set for basic"
);
ok(supportLink, "Got the support link checkbox");
let container = supportLink.parentElement;
is(container.localName, "hbox", "Support link is in a container");
is(
container.dataset.subcategory,
"exsubcategory2",
"Support link container has subcategory"
);
});
add_task(async function testSettingGroupRevamp() {
const PREF_ONE = "test.settings-group.itemone";
const SETTING_ONE = "setting-item-one";
const PREF_TWO = "test.settings-group.itemtwo";
const SETTING_TWO = "setting-item-two";
await SpecialPowers.pushPrefEnv({
set: [
[PREF_ONE, true],
[PREF_TWO, false],
["settings.revamp.design", true],
],
});
Preferences.addAll([
{ id: PREF_ONE, type: "bool" },
{ id: PREF_TWO, type: "bool" },
]);
Preferences.addSetting({
id: SETTING_ONE,
pref: PREF_ONE,
});
Preferences.addSetting({
id: SETTING_TWO,
pref: PREF_TWO,
});
let config = {
l10nId: GROUP_L10N_ID,
items: [
{ l10nId: LABEL_L10N_ID, id: SETTING_ONE },
{ l10nId: LABEL_L10N_ID, id: SETTING_TWO },
],
};
let result = await renderTemplate(config);
let group = result.querySelector("setting-group");
ok(group, "setting-group is created");
ok(group.config, "it got a config");
let fieldset = group.children[0];
is(fieldset.localName, "moz-fieldset", "First child is a fieldset");
is(
fieldset.dataset.l10nId,
GROUP_L10N_ID,
"Fieldset has the group label"
);
let [item1, item2] = fieldset.children;
is(item1.localName, "setting-control", "First setting-control");
is(item1.config.id, SETTING_ONE, "First setting-control id is correct");
is(item2.localName, "setting-control", "Second setting-control");
is(
item2.config.id,
SETTING_TWO,
"Second setting-control id is correct"
);
});
</script>
</head>
<body>
<p id="display"></p>
<div id="content" style="display: none"></div>
<pre id="test"></pre>
</body>
</html>

View file

@ -15,7 +15,7 @@ ChromeUtils.defineLazyGetter(this, "QuickSuggestTestUtils", () => {
ChromeUtils.defineESModuleGetters(this, {
ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
ExperimentFakes: "resource://testing-common/NimbusTestUtils.sys.mjs",
NimbusTestUtils: "resource://testing-common/NimbusTestUtils.sys.mjs",
});
const kDefaultWait = 2000;
@ -403,11 +403,7 @@ async function assertSuggestVisibility(expectedByElementId) {
}
const DEFAULT_LABS_RECIPES = [
ExperimentFakes.recipe("nimbus-qa-1", {
bucketConfig: {
...ExperimentFakes.recipe.bucketConfig,
count: 1000,
},
NimbusTestUtils.factories.recipe("nimbus-qa-1", {
targeting: "true",
isRollout: true,
isFirefoxLabsOptIn: true,
@ -432,11 +428,7 @@ const DEFAULT_LABS_RECIPES = [
],
}),
ExperimentFakes.recipe("nimbus-qa-2", {
bucketConfig: {
...ExperimentFakes.recipe.bucketConfig,
count: 1000,
},
NimbusTestUtils.factories.recipe("nimbus-qa-2", {
targeting: "true",
isRollout: true,
isFirefoxLabsOptIn: true,
@ -462,11 +454,7 @@ const DEFAULT_LABS_RECIPES = [
],
}),
ExperimentFakes.recipe("targeting-false", {
bucketConfig: {
...ExperimentFakes.recipe.bucketConfig,
count: 1000,
},
NimbusTestUtils.factories.recipe("targeting-false", {
targeting: "false",
isRollout: true,
isFirefoxLabsOptIn: true,
@ -477,11 +465,12 @@ const DEFAULT_LABS_RECIPES = [
requiresRestart: false,
}),
ExperimentFakes.recipe("bucketing-false", {
NimbusTestUtils.factories.recipe("bucketing-false", {
bucketConfig: {
...ExperimentFakes.recipe.bucketConfig,
...NimbusTestUtils.factories.recipe.bucketConfig,
count: 0,
},
isRollout: true,
targeting: "true",
isFirefoxLabsOptIn: true,
firefoxLabsTitle: "experimental-features-ime-search",

View file

@ -14,6 +14,10 @@ export class SettingControl extends MozLitElement {
value: {},
};
static queries = {
inputEl: "#input",
};
createRenderRoot() {
return this;
}
@ -48,6 +52,7 @@ export class SettingControl extends MozLitElement {
case "checkbox":
default:
return html`<moz-checkbox
id="input"
data-l10n-id=${config.l10nId}
.iconSrc=${config.iconSrc}
.checked=${this.value}

View file

@ -36,6 +36,7 @@ export class SettingGroup extends MozLitElement {
checkbox.addEventListener("command", e =>
setting.userChange(e.target.checked)
);
setting.on("change", () => (checkbox.checked = setting.value));
checkbox.checked = setting.value;
if (item.supportPage) {
let container = document.createXULElement("hbox");

View file

@ -0,0 +1,352 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { DeferredTask } from "resource://gre/modules/DeferredTask.sys.mjs";
const NOTIFY_TIMEOUT = 200;
const STOREID_PREF_NAME = "toolkit.profiles.storeID";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
});
class ProfilesDatastoreServiceClass {
#connection = null;
#asyncShutdownBlocker = null;
#initialized = false;
#storeID = null;
#initPromise = null;
#notifyTask = null;
#profileService = null;
static #dirSvc = null;
/**
* Gets a connection to the database.
*
* Use this connection to query existing tables, but never to create or
* modify schemas. Any schema changes should be added as a new migration,
* see `createTables()`.
*
* Rethrows errors thrown by `Sqlite.openConnection()`; see details in
* `Sqlite.sys.mjs`. TODO: document and handle errors (bug 1960963).
*/
async getConnection() {
await this.init();
return this.#connection;
}
/**
* Create or update tables in this shared cross-profile database.
*
* Includes simple forward-only migration support which applies any new
* migrations based on the schema version.
*
* Notes for migration authors:
*
* Since a mix of Firefox versions may access the database at any time, all
* schema changes must be backwards-compatible.
*
* Please keep your schemas as simple as possible, to reduce the odds of
* corruption affecting all users of the database.
*/
async createTables() {
// TODO: (Bug 1902320) Handle exceptions on connection opening
let currentVersion = await this.#connection.getSchemaVersion();
if (currentVersion == 1) {
return;
}
if (currentVersion < 1) {
// Brand new database or created prior to migration support.
await this.#connection.executeTransaction(async () => {
const createProfilesTable = `
CREATE TABLE IF NOT EXISTS "Profiles" (
id INTEGER NOT NULL,
path TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
avatar TEXT NOT NULL,
themeId TEXT NOT NULL,
themeFg TEXT NOT NULL,
themeBg TEXT NOT NULL,
PRIMARY KEY(id)
);`;
await this.#connection.execute(createProfilesTable);
const createSharedPrefsTable = `
CREATE TABLE IF NOT EXISTS "SharedPrefs" (
id INTEGER NOT NULL,
name TEXT NOT NULL UNIQUE,
value BLOB,
isBoolean INTEGER,
PRIMARY KEY(id)
);`;
await this.#connection.execute(createSharedPrefsTable);
});
}
await this.#connection.setSchemaVersion(1);
}
/**
* Trigger an async cross-instance notification that the data in the
* datastore has been changed.
*
* Two different nsIObserver events may be fired, depending on whether the
* change originated in the current Firefox instance or in another instance.
*
* Changes to the datastore made by the current instance will trigger the
* "pds-datastore-changed" nsIObserver event; see `#datastoreChanged` for
* details. These events are fired whether or not the multiple profiles
* feature is enabled.
*
* If the multiple profiles feature is enabled, then changes to the datastore
* made either by the current instance or another instance in the profile
* group will trigger the "sps-profiles-updated" nsIObserver event. See
* `SelectableProfileService.databaseChanged` for details.
*/
notify() {
this.#notifyTask.arm();
}
/**
* Notify datastore observers that the data has changed by firing
* the "pds-datastore-changed" nsIObserver signal.
*
*@param {"local"|"shutdown"} source The source of the
* notification. Either "local" meaning that the change was made in this
* process and "shutdown" meaning we are closing the connection and
* shutting down.
*/
#datastoreChanged(source) {
Services.obs.notifyObservers(null, "pds-datastore-changed", source);
}
get storeID() {
return new Promise(resolve => {
this.init().then(() => {
resolve(this.#storeID);
});
});
}
get initialized() {
return this.#initialized;
}
static get PROFILE_GROUPS_DIR() {
if (this.#dirSvc && "ProfileGroups" in this.#dirSvc) {
return this.#dirSvc.ProfileGroups;
}
return PathUtils.join(
ProfilesDatastoreServiceClass.getDirectory("UAppData").path,
"Profile Groups"
);
}
overrideDirectoryService(dirSvc) {
if (!Cu.isInAutomation) {
return;
}
ProfilesDatastoreServiceClass.#dirSvc = dirSvc;
}
static getDirectory(id) {
if (this.#dirSvc) {
if (id in this.#dirSvc) {
return this.#dirSvc[id].clone();
}
}
return Services.dirsvc.get(id, Ci.nsIFile);
}
/**
* For use in testing only, override the profile service with a mock version
* and reset state accordingly.
*
* @param {Ci.nsIToolkitProfileService} profileService The mock profile service
*/
async resetProfileService(profileService) {
if (!Cu.isInAutomation) {
return;
}
await this.uninit();
this.#profileService =
profileService ??
Cc["@mozilla.org/toolkit/profile-service;1"].getService(
Ci.nsIToolkitProfileService
);
await this.init();
}
get toolkitProfileService() {
return this.#profileService;
}
constructor() {
this.#asyncShutdownBlocker = () => this.uninit();
this.#profileService = Cc[
"@mozilla.org/toolkit/profile-service;1"
].getService(Ci.nsIToolkitProfileService);
}
/**
* At startup, get the groupDBPath from the prefs, and connect to it.
*
* @returns {Promise}
*/
init() {
if (!this.#initPromise) {
this.#initPromise = this.#init().finally(
() => (this.#initPromise = null)
);
}
return this.#initPromise;
}
async #init() {
if (this.#initialized) {
return;
}
// We read the store ID from prefs but in early startup (for example in the profile selector)
// this is not available so we get it from the current profile.
this.#storeID = Services.startup.startingUp
? this.#profileService.currentProfile?.storeID
: Services.prefs.getStringPref(STOREID_PREF_NAME, "");
// This could fail if we're adding it during shutdown. In this case,
// don't throw but don't continue initialization.
try {
lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
"ProfilesDatastoreService uninit",
this.#asyncShutdownBlocker
);
} catch (ex) {
console.error(ex);
return;
}
this.#notifyTask = new DeferredTask(async () => {
this.#datastoreChanged("local");
}, NOTIFY_TIMEOUT);
try {
await this.#initConnection();
} catch (e) {
console.error(e);
await this.uninit();
return;
}
this.#initialized = true;
}
async uninit() {
lazy.AsyncShutdown.profileChangeTeardown.removeBlocker(
this.#asyncShutdownBlocker
);
// During shutdown we don't need to notify ourselves, just other instances
// so rather than finalizing the task just disarm it and do the notification
// manually.
if (this.#notifyTask.isArmed) {
this.#notifyTask.disarm();
this.#datastoreChanged("shutdown");
}
await this.closeConnection();
this.#storeID = null;
this.#initialized = false;
}
async #initConnection() {
if (this.#connection) {
return;
}
let path = await this.getProfilesStorePath();
// TODO: (Bug 1902320) Handle exceptions on connection opening
// This could fail if the store is corrupted.
this.#connection = await lazy.Sqlite.openConnection({
path,
openNotExclusive: true,
});
await this.#connection.execute("PRAGMA journal_mode = WAL");
await this.#connection.execute("PRAGMA wal_autocheckpoint = 16");
await this.createTables();
}
async closeConnection() {
if (!this.#connection) {
return;
}
// An error could occur while closing the connection. We suppress the
// error since it is not a critical part of the browser.
try {
await this.#connection.close();
} catch (ex) {}
this.#connection = null;
}
async maybeCreateProfilesStorePath() {
if (this.#storeID) {
return;
}
await IOUtils.makeDirectory(
ProfilesDatastoreServiceClass.PROFILE_GROUPS_DIR
);
const storageID = Services.uuid
.generateUUID()
.toString()
.replace("{", "")
.split("-")[0];
this.#storeID = storageID;
Services.prefs.setStringPref(STOREID_PREF_NAME, storageID);
}
async getProfilesStorePath() {
await this.maybeCreateProfilesStorePath();
// If we are not running in a named nsIToolkitProfile, the datastore path
// should be in the profile directory. This is true in a local build or a
// CI test build, for example.
if (
!this.#profileService.currentProfile &&
!this.#profileService.groupProfile
) {
return PathUtils.join(
ProfilesDatastoreServiceClass.getDirectory("ProfD").path,
`${this.#storeID}.sqlite`
);
}
return PathUtils.join(
ProfilesDatastoreServiceClass.PROFILE_GROUPS_DIR,
`${this.#storeID}.sqlite`
);
}
}
const ProfilesDatastoreService = new ProfilesDatastoreServiceClass();
export { ProfilesDatastoreService };

View file

@ -2,6 +2,9 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { ProfilesDatastoreService } from "resource:///modules/profiles/ProfilesDatastoreService.sys.mjs";
import { SelectableProfileService } from "resource:///modules/profiles/SelectableProfileService.sys.mjs";
/**
* The selectable profile
*/
@ -26,9 +29,7 @@ export class SelectableProfile {
#themeFg;
#themeBg;
#selectableProfileService = null;
constructor(row, selectableProfileService) {
constructor(row) {
this.#id = row.getResultByName("id");
this.#path = row.getResultByName("path");
this.#name = row.getResultByName("name");
@ -36,8 +37,6 @@ export class SelectableProfile {
this.#themeId = row.getResultByName("themeId");
this.#themeFg = row.getResultByName("themeFg");
this.#themeBg = row.getResultByName("themeBg");
this.#selectableProfileService = selectableProfileService;
}
/**
@ -81,7 +80,7 @@ export class SelectableProfile {
*/
get path() {
return PathUtils.joinRelative(
this.#selectableProfileService.constructor.getDirectory("UAppData").path,
ProfilesDatastoreService.constructor.getDirectory("UAppData").path,
this.#path
);
}
@ -105,10 +104,10 @@ export class SelectableProfile {
get localDir() {
return this.rootDir.then(root => {
let relative = root.getRelativePath(
this.#selectableProfileService.constructor.getDirectory("DefProfRt")
ProfilesDatastoreService.constructor.getDirectory("DefProfRt")
);
let local =
this.#selectableProfileService.constructor.getDirectory("DefProfLRt");
ProfilesDatastoreService.constructor.getDirectory("DefProfLRt");
local.appendRelativePath(relative);
return local;
});
@ -203,7 +202,7 @@ export class SelectableProfile {
}
saveUpdatesToDB() {
this.#selectableProfileService.updateProfile(this);
SelectableProfileService.updateProfile(this);
}
toObject() {

View file

@ -2,11 +2,12 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { SelectableProfile } from "./SelectableProfile.sys.mjs";
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { DeferredTask } from "resource://gre/modules/DeferredTask.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
import { ProfilesDatastoreService } from "resource:///modules/profiles/ProfilesDatastoreService.sys.mjs";
import { SelectableProfile } from "resource:///modules/profiles/SelectableProfile.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
@ -15,16 +16,15 @@ const TASKBAR_ICON_CONTROLLERS = new WeakMap();
const PROFILES_PREF_NAME = "browser.profiles.enabled";
const GROUPID_PREF_NAME = "toolkit.telemetry.cachedProfileGroupID";
const DEFAULT_THEME_ID = "default-theme@mozilla.org";
const PROFILES_CREATED_PREF_NAME = "browser.profiles.created";
ChromeUtils.defineESModuleGetters(lazy, {
AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
ClientID: "resource://gre/modules/ClientID.sys.mjs",
CryptoUtils: "resource://services-crypto/utils.sys.mjs",
EveryWindow: "resource:///modules/EveryWindow.sys.mjs",
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
TelemetryUtils: "resource://gre/modules/TelemetryUtils.sys.mjs",
});
@ -40,8 +40,14 @@ XPCOMUtils.defineLazyPreferenceGetter(
() => SelectableProfileService.updateEnabledState()
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"PROFILES_CREATED",
PROFILES_CREATED_PREF_NAME,
false
);
const PROFILES_CRYPTO_SALT_LENGTH_BYTES = 16;
const NOTIFY_TIMEOUT = 200;
const COMMAND_LINE_UPDATE = "profiles-updated";
const COMMAND_LINE_ACTIVATE = "profiles-activate";
@ -87,7 +93,6 @@ function loadImage(url) {
class SelectableProfileServiceClass extends EventEmitter {
#profileService = null;
#connection = null;
#asyncShutdownBlocker = null;
#initialized = false;
#groupToolkitProfile = null;
#storeID = null;
@ -102,10 +107,8 @@ class SelectableProfileServiceClass extends EventEmitter {
"star",
];
#initPromise = null;
#notifyTask = null;
#observedPrefs = null;
#badge = null;
static #dirSvc = null;
#windowActivated = null;
#isEnabled = false;
@ -142,11 +145,7 @@ class SelectableProfileServiceClass extends EventEmitter {
this.matchMediaObserver = this.matchMediaObserver.bind(this);
this.prefObserver = (subject, topic, prefName) =>
this.flushSharedPrefToDatabase(prefName);
this.#profileService = Cc[
"@mozilla.org/toolkit/profile-service;1"
].getService(Ci.nsIToolkitProfileService);
this.#asyncShutdownBlocker = () => this.uninit();
this.#observedPrefs = new Set();
this.#isEnabled = this.#getEnabledState();
@ -158,15 +157,26 @@ class SelectableProfileServiceClass extends EventEmitter {
);
}
// Migrate any early users who created profiles before the datastore service
// was split out, and the PROFILES_CREATED pref replaced storeID as our check
// for whether the profiles feature had been used.
migrateToProfilesCreatedPref() {
if (this.groupToolkitProfile?.storeID && !lazy.PROFILES_CREATED) {
Services.prefs.setBoolPref(PROFILES_CREATED_PREF_NAME, true);
}
}
#getEnabledState() {
if (!Services.policies.isAllowed("profileManagement")) {
return false;
}
this.migrateToProfilesCreatedPref();
// If a storeID has been assigned then profiles may have been created so force us on. Also
// covers the case when the selector is shown at startup and we don't have preferences
// available.
if (this.storeID) {
if (this.groupToolkitProfile?.storeID) {
return true;
}
@ -185,44 +195,6 @@ class SelectableProfileServiceClass extends EventEmitter {
return this.#isEnabled;
}
/**
* For use in testing only, override the profile service with a mock version
* and reset state accordingly.
*
* @param {Ci.nsIToolkitProfileService} profileService The mock profile service
*/
async resetProfileService(profileService) {
if (!Cu.isInAutomation) {
return;
}
await this.uninit();
this.#profileService =
profileService ??
Cc["@mozilla.org/toolkit/profile-service;1"].getService(
Ci.nsIToolkitProfileService
);
await this.init();
}
overrideDirectoryService(dirSvc) {
if (!Cu.isInAutomation) {
return;
}
SelectableProfileServiceClass.#dirSvc = dirSvc;
}
static getDirectory(id) {
if (this.#dirSvc) {
if (id in this.#dirSvc) {
return this.#dirSvc[id].clone();
}
}
return Services.dirsvc.get(id, Ci.nsIFile);
}
async #attemptFlushProfileService() {
try {
await this.#profileService.asyncFlush();
@ -253,16 +225,8 @@ class SelectableProfileServiceClass extends EventEmitter {
return this.#initialized;
}
static get PROFILE_GROUPS_DIR() {
if (this.#dirSvc && "ProfileGroups" in this.#dirSvc) {
return this.#dirSvc.ProfileGroups;
}
return PathUtils.join(this.getDirectory("UAppData").path, "Profile Groups");
}
async maybeCreateProfilesStorePath() {
if (this.storeID) {
async initProfilesData() {
if (lazy.PROFILES_CREATED) {
return;
}
@ -270,29 +234,15 @@ class SelectableProfileServiceClass extends EventEmitter {
throw new Error("Cannot create a store without a group profile.");
}
await IOUtils.makeDirectory(
SelectableProfileServiceClass.PROFILE_GROUPS_DIR
);
Services.prefs.setBoolPref(PROFILES_CREATED_PREF_NAME, true);
const storageID = Services.uuid
.generateUUID()
.toString()
.replace("{", "")
.split("-")[0];
this.#groupToolkitProfile.storeID = storageID;
this.#storeID = storageID;
let storeID = await ProfilesDatastoreService.storeID;
this.#groupToolkitProfile.storeID = storeID;
this.#storeID = storeID;
await this.#attemptFlushProfileService();
}
async getProfilesStorePath() {
await this.maybeCreateProfilesStorePath();
return PathUtils.join(
SelectableProfileServiceClass.PROFILE_GROUPS_DIR,
`${this.storeID}.sqlite`
);
}
onNimbusUpdate() {
if (lazy.NimbusFeatures.selectableProfiles.getVariable("enabled")) {
Services.prefs.setBoolPref(PROFILES_PREF_NAME, true);
@ -303,11 +253,13 @@ class SelectableProfileServiceClass extends EventEmitter {
* At startup, store the nsToolkitProfile for the group.
* Get the groupDBPath from the nsToolkitProfile, and connect to it.
*
* @param {boolean} isInitial true if this is an init prior to creating a new profile.
*
* @returns {Promise}
*/
init() {
init(isInitial = false) {
if (!this.#initPromise) {
this.#initPromise = this.#init().finally(
this.#initPromise = this.#init(isInitial).finally(
() => (this.#initPromise = null)
);
}
@ -315,95 +267,76 @@ class SelectableProfileServiceClass extends EventEmitter {
return this.#initPromise;
}
async #init() {
async #init(isInitial = false) {
if (this.#initialized) {
return;
}
lazy.NimbusFeatures.selectableProfiles.onUpdate(this.onNimbusUpdate);
this.#profileService = ProfilesDatastoreService.toolkitProfileService;
this.#groupToolkitProfile =
this.#profileService.currentProfile ?? this.#profileService.groupProfile;
this.#storeID = this.#groupToolkitProfile?.storeID;
if (!this.storeID) {
this.#storeID = Services.prefs.getCharPref(
"toolkit.profiles.storeID",
""
);
}
this.#storeID = await ProfilesDatastoreService.storeID;
this.updateEnabledState();
if (!this.isEnabled) {
return;
}
// If the storeID doesn't exist, we don't want to create the db until we
// need to so we early return.
if (!this.storeID) {
if (!lazy.PROFILES_CREATED) {
return;
}
// This could fail if we're adding it during shutdown. In this case,
// don't throw but don't continue initialization.
try {
lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
"SelectableProfileService uninit",
this.#asyncShutdownBlocker
);
} catch (ex) {
console.error(ex);
this.#connection = await ProfilesDatastoreService.getConnection();
if (!this.#connection) {
return;
}
this.#notifyTask = new DeferredTask(async () => {
// Notify ourselves.
await this.databaseChanged("local");
// Notify other instances.
await this.#notifyRunningInstances();
}, NOTIFY_TIMEOUT);
try {
await this.initConnection();
} catch (e) {
console.error(e);
// If this was an attempt to recover the storeID then reset it.
if (!this.#groupToolkitProfile?.storeID) {
Services.prefs.clearUserPref("toolkit.profiles.storeID");
}
await this.uninit();
return;
}
// This can happen if profiles.ini has been reset by a version of Firefox
// prior to 67 and the current profile is not the current default for the
// group. We can recover by attempting to find the group profile from the
// database.
if (this.#groupToolkitProfile?.storeID != this.storeID) {
await this.#restoreStoreID();
if (!this.#groupToolkitProfile) {
// If we were unable to find a matching toolkit profile then assume the
// store ID is bogus so clear it and uninit.
Services.prefs.clearUserPref("toolkit.profiles.storeID");
await this.uninit();
return;
}
}
// When we launch into the startup window, the `ProfD` is not defined so
// getting the directory will throw. Leaving the `currentProfile` as null
// is fine for the startup window.
// The current profile will be null now that we are eagerly initing the db.
try {
// Get the SelectableProfile by the profile directory
this.#currentProfile = await this.getProfileByPath(
SelectableProfileServiceClass.getDirectory("ProfD")
ProfilesDatastoreService.constructor.getDirectory("ProfD")
);
} catch {}
// If this isn't the first init prior to creating the first new profile and
// the app is started up we should have found a current profile.
if (!isInitial && !Services.startup.startingUp && !this.#currentProfile) {
let count = await this.getProfileCount();
if (count) {
// There are other profiles, re-create the current profile.
this.#currentProfile = await this.#createProfile(
ProfilesDatastoreService.constructor.getDirectory("ProfD")
);
} else {
// No other profiles. Reset our state.
this.#groupToolkitProfile.storeID = null;
this.#attemptFlushProfileService();
Services.prefs.setBoolPref(PROFILES_CREATED_PREF_NAME, false);
this.#connection = null;
this.updateEnabledState();
return;
}
}
// This can happen if profiles.ini has been reset by a version of Firefox
// prior to 67 and the current profile is not the current default for the
// group. We can recover by overwriting this.#groupToolkitProfile.storeID
// with the current storeID.
if (this.#groupToolkitProfile.storeID != this.storeID) {
this.#groupToolkitProfile.storeID = this.storeID;
this.#attemptFlushProfileService();
}
// On macOS when other applications request we open a url the most recent
// window becomes activated first. This would cause the default profile to
// change before we determine which profile to open the url in. By
@ -430,6 +363,8 @@ class SelectableProfileServiceClass extends EventEmitter {
let prefersDarkQuery = window?.matchMedia("(prefers-color-scheme: dark)");
prefersDarkQuery?.addEventListener("change", this.matchMediaObserver);
Services.obs.addObserver(this, "pds-datastore-changed");
this.#initialized = true;
// this.#currentProfile is unset in the case that the database has only just been created. We
@ -445,41 +380,21 @@ class SelectableProfileServiceClass extends EventEmitter {
return;
}
lazy.AsyncShutdown.profileChangeTeardown.removeBlocker(
this.#asyncShutdownBlocker
);
try {
Services.obs.removeObserver(
this.themeObserver,
"lightweight-theme-styling-update"
);
Services.obs.removeObserver(this, "lightweight-theme-styling-update");
} catch (e) {}
for (let prefName of this.#observedPrefs) {
Services.prefs.removeObserver(prefName, this.prefObserver);
}
this.#observedPrefs.clear();
lazy.NimbusFeatures.selectableProfiles.offUpdate(this.onNimbusUpdate);
// During shutdown we don't need to notify ourselves, just other instances
// so rather than finalizing the task just disarm it and do the notification
// manually.
if (this.#notifyTask.isArmed) {
this.#notifyTask.disarm();
await this.#notifyRunningInstances();
}
await this.closeConnection();
this.#currentProfile = null;
this.#groupToolkitProfile = null;
this.#storeID = null;
this.#badge = null;
this.#connection = null;
lazy.EveryWindow.unregisterCallback(this.#everyWindowCallbackId);
Services.obs.removeObserver(this, "pds-datastore-changed");
this.#initialized = false;
}
@ -524,60 +439,6 @@ class SelectableProfileServiceClass extends EventEmitter {
);
}
async initConnection() {
if (this.#connection) {
return;
}
let path = await this.getProfilesStorePath();
// TODO: (Bug 1902320) Handle exceptions on connection opening
// This could fail if the store is corrupted.
this.#connection = await lazy.Sqlite.openConnection({
path,
openNotExclusive: true,
});
await this.#connection.execute("PRAGMA journal_mode = WAL");
await this.#connection.execute("PRAGMA wal_autocheckpoint = 16");
await this.createProfilesDBTables();
}
async closeConnection() {
if (!this.#connection) {
return;
}
// An error could occur while closing the connection. We suppress the
// error since it is not a critical part of the browser.
try {
await this.#connection.close();
} catch (ex) {}
this.#connection = null;
}
async #restoreStoreID() {
try {
// Finds the first nsIToolkitProfile that matches the path of a
// SelectableProfile in the database.
for (let profile of await this.getAllProfiles()) {
let groupProfile = this.#profileService.getProfileByDir(
await profile.rootDir
);
if (groupProfile && !groupProfile.storeID) {
groupProfile.storeID = this.storeID;
await this.#profileService.asyncFlush();
this.#groupToolkitProfile = groupProfile;
return;
}
}
} catch (e) {
console.error(e);
}
}
async handleEvent(event) {
switch (event.type) {
case "activate": {
@ -596,97 +457,32 @@ class SelectableProfileServiceClass extends EventEmitter {
}
}
/**
* Flushes the value of a preference to the database.
*
* @param {string} prefName the name of the preference.
*/
async flushSharedPrefToDatabase(prefName) {
if (!this.#observedPrefs.has(prefName)) {
Services.prefs.addObserver(prefName, this.prefObserver);
this.#observedPrefs.add(prefName);
}
if (
!SelectableProfileServiceClass.permanentSharedPrefs.includes(prefName) &&
!Services.prefs.prefHasUserValue(prefName)
) {
await this.#deleteDBPref(prefName);
return;
}
let value;
switch (Services.prefs.getPrefType(prefName)) {
case Ci.nsIPrefBranch.PREF_BOOL:
value = Services.prefs.getBoolPref(prefName);
break;
case Ci.nsIPrefBranch.PREF_INT:
value = Services.prefs.getIntPref(prefName);
break;
case Ci.nsIPrefBranch.PREF_STRING:
value = Services.prefs.getCharPref(prefName);
observe(subject, topic, data) {
switch (topic) {
case "pds-datastore-changed": {
this.databaseChanged(data);
break;
}
case "lightweight-theme-styling-update": {
this.themeObserver(subject, topic);
break;
}
await this.#setDBPref(prefName, value);
}
/**
* Create tables for Selectable Profiles if they don't already exist
*/
async createProfilesDBTables() {
// TODO: (Bug 1902320) Handle exceptions on connection opening
await this.#connection.executeTransaction(async () => {
const createProfilesTable = `
CREATE TABLE IF NOT EXISTS "Profiles" (
id INTEGER NOT NULL,
path TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
avatar TEXT NOT NULL,
themeId TEXT NOT NULL,
themeFg TEXT NOT NULL,
themeBg TEXT NOT NULL,
PRIMARY KEY(id)
);`;
await this.#connection.execute(createProfilesTable);
const createSharedPrefsTable = `
CREATE TABLE IF NOT EXISTS "SharedPrefs" (
id INTEGER NOT NULL,
name TEXT NOT NULL UNIQUE,
value BLOB,
isBoolean INTEGER,
PRIMARY KEY(id)
);`;
await this.#connection.execute(createSharedPrefsTable);
});
}
/**
* Create the SQLite DB for the profile group.
* Init shared prefs for the group and add to DB.
* Create the Group DB path to aNamedProfile entry in profiles.ini.
* Import aNamedProfile into DB.
*/
createProfileGroup() {}
/**
* When the last selectable profile in a group is deleted,
* also remove the profile group's named profile entry from profiles.ini
* and vacuum the group DB.
* and set the profiles created pref to false.
*/
async deleteProfileGroup() {
if ((await this.getAllProfiles()).length) {
return;
}
Services.prefs.setBoolPref(PROFILES_CREATED_PREF_NAME, false);
this.#groupToolkitProfile.storeID = null;
this.#storeID = null;
await this.#attemptFlushProfileService();
await this.vacuumAndCloseGroupDB();
}
// App session lifecycle methods and multi-process support
@ -696,7 +492,8 @@ class SelectableProfileServiceClass extends EventEmitter {
* unit testing.
*/
execProcess(aArgs) {
let executable = SelectableProfileServiceClass.getDirectory("XREExeF");
let executable =
ProfilesDatastoreService.constructor.getDirectory("XREExeF");
if (AppConstants.platform == "macosx") {
// Use the application bundle if possible.
@ -752,7 +549,7 @@ class SelectableProfileServiceClass extends EventEmitter {
let profiles = await this.getAllProfiles();
for (let profile of profiles) {
// The current profile was notified above.
if (profile.id === this.#currentProfile?.id) {
if (profile.id === this.currentProfile?.id) {
continue;
}
@ -831,13 +628,22 @@ class SelectableProfileServiceClass extends EventEmitter {
* Invoked when changes have been made to the database. Sends the observer
* notification "sps-profiles-updated" indicating that something has changed.
*
* @param {"local"|"remote"|"startup"} source The source of the notification.
* Either "local" meaning that the change was made in this process, "remote"
* meaning the change was made by a different Firefox instance or "startup"
* meaning the application has just launched and we may need to reload
* changes from the database.
* @param {"local"|"remote"|"startup"|"shutdown"} source The source of the
* notification. Either "local" meaning that the change was made in this
* process, "remote" meaning the change was made by a different Firefox
* instance, "startup" meaning the application has just launched and we may
* need to reload changes from the database, or "shutdown" meaning we are
* closing the connection and shutting down.
*/
async databaseChanged(source) {
if (source === "local" || source === "shutdown") {
this.#notifyRunningInstances();
}
if (source === "shutdown") {
return;
}
if (source != "local") {
await this.loadSharedPrefsFromDatabase();
}
@ -950,6 +756,48 @@ class SelectableProfileServiceClass extends EventEmitter {
};
}
async flushAllSharedPrefsToDatabase() {
for (let prefName of SelectableProfileServiceClass.permanentSharedPrefs) {
await this.flushSharedPrefToDatabase(prefName);
}
}
/**
* Flushes the value of a preference to the database.
*
* @param {string} prefName the name of the preference.
*/
async flushSharedPrefToDatabase(prefName) {
if (!this.#observedPrefs.has(prefName)) {
Services.prefs.addObserver(prefName, this.prefObserver);
this.#observedPrefs.add(prefName);
}
if (
!SelectableProfileServiceClass.permanentSharedPrefs.includes(prefName) &&
!Services.prefs.prefHasUserValue(prefName)
) {
await this.#deleteDBPref(prefName);
return;
}
let value;
switch (Services.prefs.getPrefType(prefName)) {
case Ci.nsIPrefBranch.PREF_BOOL:
value = Services.prefs.getBoolPref(prefName);
break;
case Ci.nsIPrefBranch.PREF_INT:
value = Services.prefs.getIntPref(prefName);
break;
case Ci.nsIPrefBranch.PREF_STRING:
value = Services.prefs.getCharPref(prefName);
break;
}
await this.#setDBPref(prefName, value);
}
/**
* Fetch all prefs from the DB and write to the current instance.
*/
@ -1057,7 +905,7 @@ class SelectableProfileServiceClass extends EventEmitter {
await Promise.all([
IOUtils.makeDirectory(
PathUtils.join(
SelectableProfileServiceClass.getDirectory("DefProfRt").path,
ProfilesDatastoreService.constructor.getDirectory("DefProfRt").path,
profileDir
),
{
@ -1066,7 +914,7 @@ class SelectableProfileServiceClass extends EventEmitter {
),
IOUtils.makeDirectory(
PathUtils.join(
SelectableProfileServiceClass.getDirectory("DefProfLRt").path,
ProfilesDatastoreService.constructor.getDirectory("DefProfLRt").path,
profileDir
),
{
@ -1077,7 +925,7 @@ class SelectableProfileServiceClass extends EventEmitter {
return IOUtils.getDirectory(
PathUtils.join(
SelectableProfileServiceClass.getDirectory("DefProfRt").path,
ProfilesDatastoreService.constructor.getDirectory("DefProfRt").path,
profileDir
)
);
@ -1137,6 +985,7 @@ class SelectableProfileServiceClass extends EventEmitter {
// Preferences that must be set in newly created profiles.
prefsJs.push(`user_pref("browser.profiles.enabled", true);`);
prefsJs.push(`user_pref("browser.profiles.created", true);`);
prefsJs.push(`user_pref("toolkit.profiles.storeID", "${this.storeID}");`);
await IOUtils.writeUTF8(prefsJsFilePath, prefsJs.join(LINEBREAK));
@ -1151,7 +1000,7 @@ class SelectableProfileServiceClass extends EventEmitter {
*/
getRelativeProfilePath(aProfilePath) {
let relativePath = aProfilePath.getRelativePath(
SelectableProfileServiceClass.getDirectory("UAppData")
ProfilesDatastoreService.constructor.getDirectory("UAppData")
);
if (AppConstants.platform === "win") {
@ -1207,24 +1056,19 @@ class SelectableProfileServiceClass extends EventEmitter {
}
/**
* If the user has never created a SelectableProfile before, the group
* datastore will be created and the currently running toolkit profile will
* be added to the datastore.
* If the user has never created a SelectableProfile before, the currently
* running toolkit profile will be added to the datastore and will finish
* initing the service for profiles.
*/
async maybeSetupDataStore() {
if (this.#connection) {
return;
}
// Create the profiles db and set the storeID on the toolkit profile if it
// doesn't exist so we can init the service.
await this.maybeCreateProfilesStorePath();
await this.init();
await this.initProfilesData();
await this.init(true);
// Flush our shared prefs into the database.
for (let prefName of SelectableProfileServiceClass.permanentSharedPrefs) {
await this.flushSharedPrefToDatabase(prefName);
}
await this.flushAllSharedPrefsToDatabase();
// If this is the first time the user has created a selectable profile,
// add the current toolkit profile to the datastore.
@ -1260,31 +1104,6 @@ class SelectableProfileServiceClass extends EventEmitter {
}
}
/**
* Create and launch a new SelectableProfile and add it to the group datastore.
* This is an unmanaged profile from the nsToolkitProfile perspective.
*
* If the user has never created a SelectableProfile before, the group
* datastore will be lazily created and the currently running toolkit profile
* will be added to the datastore along with the newly created profile.
*
* Launches the new SelectableProfile in a new instance after creating it.
*
* @param {boolean} [launchProfile=true] Whether or not this should launch
* the newly created profile.
*
* @returns {SelectableProfile} The profile just created.
*/
async createNewProfile(launchProfile = true) {
await this.maybeSetupDataStore();
let profile = await this.#createProfile();
if (launchProfile) {
this.launchInstance(profile, "about:newprofile");
}
return profile;
}
/**
* Add a profile to the profile group datastore.
*
@ -1316,7 +1135,7 @@ class SelectableProfileServiceClass extends EventEmitter {
profileData
);
this.#notifyTask.arm();
ProfilesDatastoreService.notify();
return this.getProfileByName(profileData.name);
}
@ -1342,7 +1161,7 @@ class SelectableProfileServiceClass extends EventEmitter {
id: aProfile.id,
});
this.#notifyTask.arm();
ProfilesDatastoreService.notify();
}
/**
@ -1407,7 +1226,32 @@ class SelectableProfileServiceClass extends EventEmitter {
this.#currentProfile = aSelectableProfile;
}
this.#notifyTask.arm();
ProfilesDatastoreService.notify();
}
/**
* Create and launch a new SelectableProfile and add it to the group datastore.
* This is an unmanaged profile from the nsToolkitProfile perspective.
*
* If the user has never created a SelectableProfile before, the currently
* running toolkit profile will be added to the datastore along with the
* newly created profile.
*
* Launches the new SelectableProfile in a new instance after creating it.
*
* @param {boolean} [launchProfile=true] Whether or not this should launch
* the newly created profile.
*
* @returns {SelectableProfile} The profile just created.
*/
async createNewProfile(launchProfile = true) {
await this.maybeSetupDataStore();
let profile = await this.#createProfile();
if (launchProfile) {
this.launchInstance(profile, "about:newprofile");
}
return profile;
}
/**
@ -1423,7 +1267,7 @@ class SelectableProfileServiceClass extends EventEmitter {
return (await this.#connection.executeCached("SELECT * FROM Profiles;"))
.map(row => {
return new SelectableProfile(row, this);
return new SelectableProfile(row);
})
.sort((p1, p2) => p1.name.localeCompare(p2.name));
}
@ -1467,7 +1311,7 @@ class SelectableProfileServiceClass extends EventEmitter {
)
)[0];
return row ? new SelectableProfile(row, this) : null;
return row ? new SelectableProfile(row) : null;
}
/**
@ -1491,7 +1335,7 @@ class SelectableProfileServiceClass extends EventEmitter {
)
)[0];
return row ? new SelectableProfile(row, this) : null;
return row ? new SelectableProfile(row) : null;
}
/**
@ -1515,7 +1359,7 @@ class SelectableProfileServiceClass extends EventEmitter {
)
)[0];
return row ? new SelectableProfile(row, this) : null;
return row ? new SelectableProfile(row) : null;
}
// Shared Prefs management
@ -1585,7 +1429,7 @@ class SelectableProfileServiceClass extends EventEmitter {
}
);
this.#notifyTask.arm();
ProfilesDatastoreService.notify();
}
// Starts tracking a new shared pref across the profiles.
@ -1608,23 +1452,7 @@ class SelectableProfileServiceClass extends EventEmitter {
}
);
this.#notifyTask.arm();
}
// DB lifecycle
/**
* Create the SQLite DB for the profile group at groupDBPath.
* Init shared prefs for the group and add to DB.
*/
createGroupDB() {}
/**
* Vacuum the SQLite DB.
*/
async vacuumAndCloseGroupDB() {
await this.#connection.execute("VACUUM;");
await this.closeConnection();
ProfilesDatastoreService.notify();
}
}

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