Update On Thu Dec 19 19:53:27 CET 2024

This commit is contained in:
github-action[bot] 2024-12-19 19:53:28 +01:00
parent 8f87d3e70c
commit 158ed24d8e
1108 changed files with 96120 additions and 5853 deletions

215
Cargo.lock generated
View file

@ -1416,6 +1416,15 @@ dependencies = [
"libdbus-sys", "libdbus-sys",
] ]
[[package]]
name = "debug_tree"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d1ec383f2d844902d3c34e4253ba11ae48513cdaddc565cf1a6518db09a8e57"
dependencies = [
"once_cell",
]
[[package]] [[package]]
name = "debugid" name = "debugid"
version = "0.8.0" version = "0.8.0"
@ -2334,8 +2343,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
] ]
[[package]] [[package]]
@ -2440,6 +2451,7 @@ dependencies = [
"mdns_service", "mdns_service",
"midir_impl", "midir_impl",
"mime-guess-ffi", "mime-guess-ffi",
"mls_gk",
"moz_asserts", "moz_asserts",
"mozannotation_client", "mozannotation_client",
"mozannotation_server", "mozannotation_server",
@ -3779,6 +3791,17 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
name = "maybe-async"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "md-5" name = "md-5"
version = "0.10.5" version = "0.10.5"
@ -4032,6 +4055,169 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "mls-platform-api"
version = "0.1.0"
source = "git+https://github.com/beurdouche/mls-platform-api?rev=19c3f18b747d13354370ba84440bb0b963932634#19c3f18b747d13354370ba84440bb0b963932634"
dependencies = [
"bincode",
"hex",
"mls-rs",
"mls-rs-crypto-nss",
"mls-rs-provider-sqlite",
"serde",
"serde_json",
"sha2",
"thiserror",
]
[[package]]
name = "mls-rs"
version = "0.39.1"
source = "git+https://github.com/beurdouche/mls-rs?rev=eedb37e50e3fca51863f460755afd632137da57c#eedb37e50e3fca51863f460755afd632137da57c"
dependencies = [
"async-trait",
"cfg-if",
"debug_tree",
"futures",
"getrandom",
"hex",
"itertools",
"maybe-async",
"mls-rs-codec",
"mls-rs-core",
"mls-rs-identity-x509",
"mls-rs-provider-sqlite",
"rand_core",
"rayon",
"serde",
"thiserror",
"wasm-bindgen",
"zeroize",
]
[[package]]
name = "mls-rs-codec"
version = "0.5.3"
source = "git+https://github.com/beurdouche/mls-rs?rev=eedb37e50e3fca51863f460755afd632137da57c#eedb37e50e3fca51863f460755afd632137da57c"
dependencies = [
"mls-rs-codec-derive",
"thiserror",
"wasm-bindgen",
]
[[package]]
name = "mls-rs-codec-derive"
version = "0.1.1"
source = "git+https://github.com/beurdouche/mls-rs?rev=eedb37e50e3fca51863f460755afd632137da57c#eedb37e50e3fca51863f460755afd632137da57c"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "mls-rs-core"
version = "0.18.0"
source = "git+https://github.com/beurdouche/mls-rs?rev=eedb37e50e3fca51863f460755afd632137da57c#eedb37e50e3fca51863f460755afd632137da57c"
dependencies = [
"async-trait",
"hex",
"maybe-async",
"mls-rs-codec",
"serde",
"serde_bytes",
"thiserror",
"wasm-bindgen",
"zeroize",
]
[[package]]
name = "mls-rs-crypto-hpke"
version = "0.9.0"
source = "git+https://github.com/beurdouche/mls-rs?rev=eedb37e50e3fca51863f460755afd632137da57c#eedb37e50e3fca51863f460755afd632137da57c"
dependencies = [
"async-trait",
"cfg-if",
"maybe-async",
"mls-rs-core",
"mls-rs-crypto-traits",
"thiserror",
"zeroize",
]
[[package]]
name = "mls-rs-crypto-nss"
version = "0.1.0"
source = "git+https://github.com/beurdouche/mls-rs?rev=eedb37e50e3fca51863f460755afd632137da57c#eedb37e50e3fca51863f460755afd632137da57c"
dependencies = [
"getrandom",
"hex",
"maybe-async",
"mls-rs-core",
"mls-rs-crypto-hpke",
"mls-rs-crypto-traits",
"nss-gk-api",
"rand_core",
"serde",
"thiserror",
"zeroize",
]
[[package]]
name = "mls-rs-crypto-traits"
version = "0.10.0"
source = "git+https://github.com/beurdouche/mls-rs?rev=eedb37e50e3fca51863f460755afd632137da57c#eedb37e50e3fca51863f460755afd632137da57c"
dependencies = [
"async-trait",
"maybe-async",
"mls-rs-core",
]
[[package]]
name = "mls-rs-identity-x509"
version = "0.11.0"
source = "git+https://github.com/beurdouche/mls-rs?rev=eedb37e50e3fca51863f460755afd632137da57c#eedb37e50e3fca51863f460755afd632137da57c"
dependencies = [
"async-trait",
"maybe-async",
"mls-rs-core",
"thiserror",
"wasm-bindgen",
]
[[package]]
name = "mls-rs-provider-sqlite"
version = "0.11.0"
source = "git+https://github.com/beurdouche/mls-rs?rev=eedb37e50e3fca51863f460755afd632137da57c#eedb37e50e3fca51863f460755afd632137da57c"
dependencies = [
"async-trait",
"hex",
"maybe-async",
"mls-rs-core",
"rand",
"rusqlite",
"thiserror",
"zeroize",
]
[[package]]
name = "mls_gk"
version = "0.1.0"
dependencies = [
"hex",
"log",
"mls-platform-api",
"nserror",
"nss-gk-api",
"nsstring",
"rusqlite",
"static_prefs",
"thin-vec",
"xpcom",
]
[[package]] [[package]]
name = "moz_asserts" name = "moz_asserts"
version = "0.1.0" version = "0.1.0"
@ -4493,10 +4679,10 @@ dependencies = [
[[package]] [[package]]
name = "nss-gk-api" name = "nss-gk-api"
version = "0.3.0" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/beurdouche/nss-gk-api?rev=e48a946811ffd64abc78de3ee284957d8d1c0d63#e48a946811ffd64abc78de3ee284957d8d1c0d63"
checksum = "4c17aec6d4e1822c023689899f09311592a36cbf6de8f85dfaf5f01976790d8d"
dependencies = [ dependencies = [
"bindgen 0.69.4", "bindgen 0.69.4",
"log",
"mozbuild", "mozbuild",
"once_cell", "once_cell",
"pkcs11-bindings", "pkcs11-bindings",
@ -5075,9 +5261,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]] [[package]]
name = "quinn-udp" name = "quinn-udp"
version = "0.5.8" version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52cd4b1eff68bf27940dd39811292c49e007f4d0b4c357358dc9b0197be6b527" checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904"
dependencies = [ dependencies = [
"cfg_aliases 0.2.1", "cfg_aliases 0.2.1",
"libc", "libc",
@ -7546,6 +7732,27 @@ dependencies = [
"synstructure", "synstructure",
] ]
[[package]]
name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
dependencies = [
"serde",
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "zerovec" name = "zerovec"
version = "0.10.4" version = "0.10.4"

View file

@ -14,6 +14,7 @@ members = [
"security/manager/ssl/tests/unit/test_builtins", "security/manager/ssl/tests/unit/test_builtins",
"security/manager/ssl/ipcclientcerts", "security/manager/ssl/ipcclientcerts",
"security/manager/ssl/osclientcerts", "security/manager/ssl/osclientcerts",
"security/mls/mls_gk",
"testing/geckodriver", "testing/geckodriver",
"toolkit/components/uniffi-bindgen-gecko-js", "toolkit/components/uniffi-bindgen-gecko-js",
"toolkit/crashreporter/client/app", "toolkit/crashreporter/client/app",
@ -199,6 +200,7 @@ plist = { path = "third_party/rust/plist" }
# To-be-published changes. # To-be-published changes.
unicode-bidi = { git = "https://github.com/servo/unicode-bidi", rev = "ca612daf1c08c53abe07327cb3e6ef6e0a760f0c" } unicode-bidi = { git = "https://github.com/servo/unicode-bidi", rev = "ca612daf1c08c53abe07327cb3e6ef6e0a760f0c" }
nss-gk-api = { git = "https://github.com/beurdouche/nss-gk-api", rev = "e48a946811ffd64abc78de3ee284957d8d1c0d63" }
# Other overrides # Other overrides
any_all_workaround = { git = "https://github.com/hsivonen/any_all_workaround", rev = "7fb1b7034c9f172aade21ee1c8554e8d8a48af80" } any_all_workaround = { git = "https://github.com/hsivonen/any_all_workaround", rev = "7fb1b7034c9f172aade21ee1c8554e8d8a48af80" }

View file

@ -459,6 +459,20 @@ void EventQueue::ProcessEventQueue() {
if (!mDocument) { if (!mDocument) {
return; return;
} }
// Some mutation events may be queued incidentally by this function. Send
// them immediately so they stay in order. This can happen due to code in
// DoInitialUpdate and TextUpdater that calls FireDelayedEvent for mutation
// events, rather than QueueMutationEvent. DoInitialUpdate can do this with
// reorder events, and TextUpdater can do this with text inserted/removed
// events. Process these events now to avoid sending them out-of-order.
if (eventType == nsIAccessibleEvent::EVENT_REORDER ||
eventType == nsIAccessibleEvent::EVENT_TEXT_INSERTED ||
eventType == nsIAccessibleEvent::EVENT_TEXT_REMOVED) {
if (auto* ipcDoc = mDocument->IPCDoc()) {
ipcDoc->SendQueuedMutationEvents();
}
}
} }
if (mDocument && IPCAccessibilityActive() && if (mDocument && IPCAccessibilityActive() &&

View file

@ -1007,6 +1007,12 @@ void NotificationController::WillRefresh(mozilla::TimeStamp aTime) {
CoalesceMutationEvents(); CoalesceMutationEvents();
ProcessMutationEvents(); ProcessMutationEvents();
// ProcessMutationEvents for content process documents merely queues mutation
// events. Send those events in a batch now if applicable.
if (mDocument && mDocument->IPCDoc()) {
mDocument->IPCDoc()->SendQueuedMutationEvents();
}
// When firing mutation events, mObservingState is set to // When firing mutation events, mObservingState is set to
// eRefreshProcessing. Any calls to ScheduleProcessing() that // eRefreshProcessing. Any calls to ScheduleProcessing() that
// occur before mObservingState is reset will be dropped because we only // occur before mObservingState is reset will be dropped because we only
@ -1028,6 +1034,13 @@ void NotificationController::WillRefresh(mozilla::TimeStamp aTime) {
ProcessEventQueue(); ProcessEventQueue();
// There should not be any more mutation events in the mutation event queue.
// ProcessEventQueue should have sent all of them.
if (mDocument && mDocument->IPCDoc()) {
MOZ_ASSERT(mDocument->IPCDoc()->MutationEventQueueLength() == 0,
"Mutation event queue is non-empty.");
}
if (IPCAccessibilityActive()) { if (IPCAccessibilityActive()) {
size_t newDocCount = newChildDocs.Length(); size_t newDocCount = newChildDocs.Length();
for (size_t i = 0; i < newDocCount; i++) { for (size_t i = 0; i < newDocCount; i++) {

View file

@ -280,7 +280,13 @@ class NotificationController final : public EventQueue,
void DropMutationEvent(AccTreeMutationEvent* aEvent); void DropMutationEvent(AccTreeMutationEvent* aEvent);
/** /**
* Fire all necessary mutation events. * For content process documents:
* Assess and queue all necessary mutation events. This function queues the
* events on DocAccessibleChild. To fire the queued events, call
* DocAccessibleChild::SendQueuedMutationEvents. This function may fire
* events that must occur before mutation events.
* For parent process documents:
* Fire all necessary mutation events immediately.
*/ */
void ProcessMutationEvents(); void ProcessMutationEvents();

View file

@ -1750,6 +1750,7 @@ void DocAccessible::DoInitialUpdate() {
for (auto idx = 0U; idx < mChildren.Length(); idx++) { for (auto idx = 0U; idx < mChildren.Length(); idx++) {
ipcDoc->InsertIntoIpcTree(mChildren.ElementAt(idx), true); ipcDoc->InsertIntoIpcTree(mChildren.ElementAt(idx), true);
} }
ipcDoc->SendQueuedMutationEvents();
} }
} }
} }

View file

@ -872,13 +872,15 @@ nsresult LocalAccessible::HandleAccEvent(AccEvent* aEvent) {
break; break;
case nsIAccessibleEvent::EVENT_HIDE: case nsIAccessibleEvent::EVENT_HIDE:
ipcDoc->SendHideEvent(id, aEvent->IsFromUserInput()); ipcDoc->AppendMutationEventData(
HideEventData{id, aEvent->IsFromUserInput()});
break; break;
case nsIAccessibleEvent::EVENT_INNER_REORDER: case nsIAccessibleEvent::EVENT_INNER_REORDER:
case nsIAccessibleEvent::EVENT_REORDER: case nsIAccessibleEvent::EVENT_REORDER:
if (IsTable()) { if (IsTable()) {
SendCache(CacheDomain::Table, CacheUpdateType::Update); SendCache(CacheDomain::Table, CacheUpdateType::Update,
/*aAppendEventData*/ true);
} }
#if defined(XP_WIN) #if defined(XP_WIN)
@ -897,7 +899,8 @@ nsresult LocalAccessible::HandleAccEvent(AccEvent* aEvent) {
// reorder events on the application acc aren't necessary to tell the // reorder events on the application acc aren't necessary to tell the
// parent about new top level documents. // parent about new top level documents.
if (!aEvent->GetAccessible()->IsApplication()) { if (!aEvent->GetAccessible()->IsApplication()) {
ipcDoc->SendEvent(id, aEvent->GetEventType()); ipcDoc->AppendMutationEventData(
ReorderEventData{id, aEvent->GetEventType()});
} }
break; break;
case nsIAccessibleEvent::EVENT_STATE_CHANGE: { case nsIAccessibleEvent::EVENT_STATE_CHANGE: {
@ -917,10 +920,10 @@ nsresult LocalAccessible::HandleAccEvent(AccEvent* aEvent) {
case nsIAccessibleEvent::EVENT_TEXT_INSERTED: case nsIAccessibleEvent::EVENT_TEXT_INSERTED:
case nsIAccessibleEvent::EVENT_TEXT_REMOVED: { case nsIAccessibleEvent::EVENT_TEXT_REMOVED: {
AccTextChangeEvent* event = downcast_accEvent(aEvent); AccTextChangeEvent* event = downcast_accEvent(aEvent);
const nsString& text = event->ModifiedText(); ipcDoc->AppendMutationEventData(TextChangeEventData{
ipcDoc->SendTextChangeEvent( id, event->ModifiedText(), event->GetStartOffset(),
id, text, event->GetStartOffset(), event->GetLength(), event->GetLength(), event->IsTextInserted(),
event->IsTextInserted(), event->IsFromUserInput()); event->IsFromUserInput()});
break; break;
} }
case nsIAccessibleEvent::EVENT_SELECTION: case nsIAccessibleEvent::EVENT_SELECTION:
@ -3317,7 +3320,8 @@ AccGroupInfo* LocalAccessible::GetOrCreateGroupInfo() {
} }
void LocalAccessible::SendCache(uint64_t aCacheDomain, void LocalAccessible::SendCache(uint64_t aCacheDomain,
CacheUpdateType aUpdateType) { CacheUpdateType aUpdateType,
bool aAppendEventData) {
if (!IPCAccessibilityActive() || !Document()) { if (!IPCAccessibilityActive() || !Document()) {
return; return;
} }
@ -3347,7 +3351,12 @@ void LocalAccessible::SendCache(uint64_t aCacheDomain,
} }
nsTArray<CacheData> data; nsTArray<CacheData> data;
data.AppendElement(CacheData(ID(), fields)); data.AppendElement(CacheData(ID(), fields));
ipcDoc->SendCache(aUpdateType, data); if (aAppendEventData) {
ipcDoc->AppendMutationEventData(
CacheEventData{std::move(aUpdateType), std::move(data)});
} else {
ipcDoc->SendCache(aUpdateType, data);
}
if (profiler_thread_is_being_profiled_for_markers()) { if (profiler_thread_is_being_profiled_for_markers()) {
nsAutoCString updateTypeStr; nsAutoCString updateTypeStr;
@ -4184,21 +4193,37 @@ void LocalAccessible::MaybeQueueCacheUpdateForStyleChanges() {
if (nsIFrame* frame = GetFrame()) { if (nsIFrame* frame = GetFrame()) {
const ComputedStyle* newStyle = frame->Style(); const ComputedStyle* newStyle = frame->Style();
nsAutoCString oldOverflow, newOverflow; const auto overflowProps =
mOldComputedStyle->GetComputedPropertyValue(eCSSProperty_overflow, nsCSSPropertyIDSet({eCSSProperty_overflow_x, eCSSProperty_overflow_y});
oldOverflow);
newStyle->GetComputedPropertyValue(eCSSProperty_overflow, newOverflow);
if (oldOverflow != newOverflow) { for (nsCSSPropertyID overflowProp : overflowProps) {
if (oldOverflow.Equals("hidden"_ns) || newOverflow.Equals("hidden"_ns)) { nsAutoCString oldOverflow, newOverflow;
mDoc->QueueCacheUpdate(this, CacheDomain::Style); mOldComputedStyle->GetComputedPropertyValue(overflowProp, oldOverflow);
} newStyle->GetComputedPropertyValue(overflowProp, newOverflow);
if (oldOverflow.Equals("auto"_ns) || newOverflow.Equals("auto"_ns) ||
oldOverflow.Equals("scroll"_ns) || newOverflow.Equals("scroll"_ns)) { if (oldOverflow != newOverflow) {
// We cache a (0,0) scroll position for frames that have overflow if (oldOverflow.Equals("hidden"_ns) ||
// styling which means they _could_ become scrollable, even if the newOverflow.Equals("hidden"_ns)) {
// content within them doesn't currently scroll. mDoc->QueueCacheUpdate(this, CacheDomain::Style);
mDoc->QueueCacheUpdate(this, CacheDomain::ScrollPosition); }
if (oldOverflow.Equals("auto"_ns) || newOverflow.Equals("auto"_ns) ||
oldOverflow.Equals("scroll"_ns) ||
newOverflow.Equals("scroll"_ns)) {
// We cache a (0,0) scroll position for frames that have overflow
// styling which means they _could_ become scrollable, even if the
// content within them doesn't currently scroll.
mDoc->QueueCacheUpdate(this, CacheDomain::ScrollPosition);
}
} else {
ScrollContainerFrame* scrollContainerFrame = do_QueryFrame(frame);
if (!scrollContainerFrame && (newOverflow.Equals("auto"_ns) ||
newOverflow.Equals("scroll"_ns))) {
// A document's body element can lose its scroll frame if the root
// element (eg. <html>) is restyled to overflow scroll/auto. In that
// case we will not get any useful notifications for the body element
// except for a reframe to a non-scrolling frame.
mDoc->QueueCacheUpdate(this, CacheDomain::ScrollPosition);
}
} }
} }

View file

@ -73,12 +73,12 @@ void TreeSize(const char* aTitle, const char* aMsgText, LocalAccessible* aRoot);
typedef nsRefPtrHashtable<nsPtrHashKey<const void>, LocalAccessible> typedef nsRefPtrHashtable<nsPtrHashKey<const void>, LocalAccessible>
AccessibleHashtable; AccessibleHashtable;
#define NS_ACCESSIBLE_IMPL_IID \ #define NS_ACCESSIBLE_IMPL_IID \
{ /* 133c8bf4-4913-4355-bd50-426bd1d6e1ad */ \ {/* 133c8bf4-4913-4355-bd50-426bd1d6e1ad */ \
0x133c8bf4, 0x4913, 0x4355, { \ 0x133c8bf4, \
0xbd, 0x50, 0x42, 0x6b, 0xd1, 0xd6, 0xe1, 0xad \ 0x4913, \
} \ 0x4355, \
} {0xbd, 0x50, 0x42, 0x6b, 0xd1, 0xd6, 0xe1, 0xad}}
/** /**
* An accessibility tree node that originated in mDoc's content process. * An accessibility tree node that originated in mDoc's content process.
@ -723,8 +723,11 @@ class LocalAccessible : public nsISupports, public Accessible {
* Push fields to cache. * Push fields to cache.
* aCacheDomain - describes which fields to bundle and ultimately send * aCacheDomain - describes which fields to bundle and ultimately send
* aUpdate - describes whether this is an initial or subsequent update * aUpdate - describes whether this is an initial or subsequent update
* aAppendEventData - don't send the event now; append it to the mutation
* events list on the DocAccessibleChild
*/ */
void SendCache(uint64_t aCacheDomain, CacheUpdateType aUpdate); void SendCache(uint64_t aCacheDomain, CacheUpdateType aUpdate,
bool aAppendEventData = false);
void MaybeQueueCacheUpdateForStyleChanges(); void MaybeQueueCacheUpdateForStyleChanges();

View file

@ -20,6 +20,13 @@
namespace mozilla { namespace mozilla {
namespace a11y { namespace a11y {
// Exceeding the IPDL maximum message size will cause a crash. Try to avoid
// this by only including kMaxAccsPerMessage Accessibles in a single IPDL
// call. If there are Accessibles beyond this, they will be split across
// multiple calls.
static constexpr uint32_t kMaxAccsPerMessage =
IPC::Channel::kMaximumMessageSize / (2 * 1024);
/* static */ /* static */
void DocAccessibleChild::FlattenTree(LocalAccessible* aRoot, void DocAccessibleChild::FlattenTree(LocalAccessible* aRoot,
nsTArray<LocalAccessible*>& aTree) { nsTArray<LocalAccessible*>& aTree) {
@ -80,34 +87,62 @@ void DocAccessibleChild::InsertIntoIpcTree(LocalAccessible* aChild,
nsTArray<LocalAccessible*> shownTree; nsTArray<LocalAccessible*> shownTree;
FlattenTree(aChild, shownTree); FlattenTree(aChild, shownTree);
uint32_t totalAccs = shownTree.Length(); uint32_t totalAccs = shownTree.Length();
// Exceeding the IPDL maximum message size will cause a crash. Try to avoid nsTArray<AccessibleData> data(std::min(
// this by only including kMaxAccsPerMessage Accessibels in a single IPDL kMaxAccsPerMessage - mMutationEventBatcher.GetCurrentBatchAccCount(),
// call. If there are Accessibles beyond this, they will be split across totalAccs));
// multiple calls.
constexpr uint32_t kMaxAccsPerMessage = for (uint32_t accIndex = 0; accIndex < totalAccs; ++accIndex) {
IPC::Channel::kMaximumMessageSize / (2 * 1024); // This batch of mutation events has no more room left without exceeding our
nsTArray<AccessibleData> data(std::min(kMaxAccsPerMessage, totalAccs)); // limit. Write the show event data to the queue.
for (LocalAccessible* child : shownTree) { if (data.Length() + mMutationEventBatcher.GetCurrentBatchAccCount() ==
if (data.Length() == kMaxAccsPerMessage) { kMaxAccsPerMessage) {
if (ipc::ProcessChild::ExpectingShutdown()) { if (ipc::ProcessChild::ExpectingShutdown()) {
return; return;
} }
SendShowEvent(data, aSuppressShowEvent, false, false); // Note: std::move used on aSuppressShowEvent to force selection of the
data.ClearAndRetainStorage(); // ShowEventData constructor that takes all rvalue reference arguments.
const uint32_t accCount = data.Length();
AppendMutationEventData(
ShowEventData{std::move(data), std::move(aSuppressShowEvent), false,
false},
accCount);
// Reset data to avoid relying on state of moved-from object.
// Preallocate an appropriate capacity to avoid resizing.
data = nsTArray<AccessibleData>(
std::min(kMaxAccsPerMessage, totalAccs - accIndex));
} }
LocalAccessible* child = shownTree[accIndex];
data.AppendElement(SerializeAcc(child)); data.AppendElement(SerializeAcc(child));
} }
if (ipc::ProcessChild::ExpectingShutdown()) { if (ipc::ProcessChild::ExpectingShutdown()) {
return; return;
} }
if (!data.IsEmpty()) { if (!data.IsEmpty()) {
SendShowEvent(data, aSuppressShowEvent, true, false); const uint32_t accCount = data.Length();
AppendMutationEventData(
ShowEventData{std::move(data), std::move(aSuppressShowEvent), true,
false},
accCount);
} }
} }
void DocAccessibleChild::ShowEvent(AccShowEvent* aShowEvent) { void DocAccessibleChild::ShowEvent(AccShowEvent* aShowEvent) {
LocalAccessible* child = aShowEvent->GetAccessible(); LocalAccessible* child = aShowEvent->GetAccessible();
InsertIntoIpcTree(child, false); InsertIntoIpcTree(child, /* aSuppressShowEvent */ false);
}
void DocAccessibleChild::AppendMutationEventData(MutationEventData aData,
uint32_t aAccCount) {
mMutationEventBatcher.AppendMutationEventData(std::move(aData), aAccCount);
}
void DocAccessibleChild::SendQueuedMutationEvents() {
mMutationEventBatcher.SendQueuedMutationEvents(*this);
}
size_t DocAccessibleChild::MutationEventQueueLength() const {
return mMutationEventBatcher.EventCount();
} }
mozilla::ipc::IPCResult DocAccessibleChild::RecvTakeFocus(const uint64_t& aID) { mozilla::ipc::IPCResult DocAccessibleChild::RecvTakeFocus(const uint64_t& aID) {
@ -428,5 +463,54 @@ HyperTextAccessible* DocAccessibleChild::IdToHyperTextAccessible(
return acc && acc->IsHyperText() ? acc->AsHyperText() : nullptr; return acc && acc->IsHyperText() ? acc->AsHyperText() : nullptr;
} }
void DocAccessibleChild::MutationEventBatcher::AppendMutationEventData(
MutationEventData aData, uint32_t aAccCount) {
// We want to send the mutation events in batches. The number of events in a
// batch is unscientific. The goal is to avoid sending more data than would
// overwhelm the IPC mechanism (see IPC::Channel::kMaximumMessageSize), but we
// stop short of measuring actual message size here. We also don't want to
// send too many events in one message, since that could choke up the parent
// process as it tries to fire all the events synchronously. To address these
// constraints, we construct batches of mutation event data, limiting our
// events by number of Accessibles touched.
MOZ_ASSERT(aAccCount <= kMaxAccsPerMessage,
"More accessibles given than can fit in a single batch");
// If the latest batch cannot accommodate the number of new Accessibles,
// create a new batch by marking the batch boundary.
if (mCurrentBatchAccCount + aAccCount > kMaxAccsPerMessage) {
mBatchBoundaries.AppendElement(mMutationEventData.Length());
mCurrentBatchAccCount = 0;
}
mMutationEventData.AppendElement(std::move(aData));
mCurrentBatchAccCount += aAccCount;
}
void DocAccessibleChild::MutationEventBatcher::SendQueuedMutationEvents(
DocAccessibleChild& aDocAcc) {
// Set up the final batch boundary at the end of the event data.
mBatchBoundaries.AppendElement(mMutationEventData.Length());
// Loop over all of the batch boundaries and send the data within.
size_t batchStartIndex = 0;
for (size_t batchEndIndex : mBatchBoundaries) {
Span<const MutationEventData> batch{
mMutationEventData.Elements() + batchStartIndex,
mMutationEventData.Elements() + batchEndIndex};
if (ipc::ProcessChild::ExpectingShutdown()) {
break;
}
if (!batch.IsEmpty()) {
aDocAcc.SendMutationEvents(batch);
}
batchStartIndex = batchEndIndex;
}
// Reset the batcher state.
mMutationEventData.Clear();
mBatchBoundaries.Clear();
mCurrentBatchAccCount = 0;
}
} // namespace a11y } // namespace a11y
} // namespace mozilla } // namespace mozilla

View file

@ -48,11 +48,17 @@ class DocAccessibleChild : public PDocAccessibleChild {
} }
/** /**
* Serializes a shown tree and sends it to the chrome process. * Serializes a shown tree and appends the show event data to the mutation
* event queue with AppendMutationEventData. This function may queue multiple
* show events depending on the size of the flattened tree.
*/ */
void InsertIntoIpcTree(LocalAccessible* aChild, bool aSuppressShowEvent); void InsertIntoIpcTree(LocalAccessible* aChild, bool aSuppressShowEvent);
void ShowEvent(AccShowEvent* aShowEvent); void ShowEvent(AccShowEvent* aShowEvent);
void AppendMutationEventData(MutationEventData aData, uint32_t aAccCount = 1);
void SendQueuedMutationEvents();
size_t MutationEventQueueLength() const;
virtual void ActorDestroy(ActorDestroyReason) override { virtual void ActorDestroy(ActorDestroyReason) override {
if (!mDoc) { if (!mDoc) {
return; return;
@ -169,6 +175,30 @@ class DocAccessibleChild : public PDocAccessibleChild {
DocAccessible* mDoc; DocAccessible* mDoc;
// Utility structure that encapsulates mutation event batching.
struct MutationEventBatcher {
void AppendMutationEventData(MutationEventData aData, uint32_t aAccCount);
void SendQueuedMutationEvents(DocAccessibleChild& aDocAcc);
uint32_t GetCurrentBatchAccCount() const { return mCurrentBatchAccCount; }
size_t EventCount() const { return mMutationEventData.Length(); }
private:
// A collection of mutation events to be sent in batches.
nsTArray<MutationEventData> mMutationEventData;
// Indices that demarcate batch endpoint boundaries. All indices are one
// past the end, to make them suitable for working with Spans. The start
// index of the first batch is implicitly 0.
nsTArray<size_t> mBatchBoundaries;
// The number of accessibles in the current (latest) batch. A show event may
// have many accessibles shown, where each accessible in the show event
// counts separately here. Every other mutation event adds one to this
// count.
uint32_t mCurrentBatchAccCount = 0;
};
MutationEventBatcher mMutationEventBatcher;
friend void DocAccessible::DoInitialUpdate(); friend void DocAccessible::DoInitialUpdate();
}; };

View file

@ -78,11 +78,10 @@ void DocAccessibleParent::SetBrowsingContext(
mBrowsingContext = aBrowsingContext; mBrowsingContext = aBrowsingContext;
} }
mozilla::ipc::IPCResult DocAccessibleParent::RecvShowEvent( mozilla::ipc::IPCResult DocAccessibleParent::ProcessShowEvent(
nsTArray<AccessibleData>&& aNewTree, const bool& aEventSuppressed, nsTArray<AccessibleData>&& aNewTree, const bool& aEventSuppressed,
const bool& aComplete, const bool& aFromUser) { const bool& aComplete, const bool& aFromUser) {
ACQUIRE_ANDROID_LOCK ACQUIRE_ANDROID_LOCK
if (mShutdown) return IPC_OK();
MOZ_ASSERT(CheckDocTree()); MOZ_ASSERT(CheckDocTree());
@ -290,10 +289,9 @@ void DocAccessibleParent::ShutdownOrPrepareForMove(RemoteAccessible* aAcc) {
mMovingIDs.EnsureRemoved(id); mMovingIDs.EnsureRemoved(id);
} }
mozilla::ipc::IPCResult DocAccessibleParent::RecvHideEvent( mozilla::ipc::IPCResult DocAccessibleParent::ProcessHideEvent(
const uint64_t& aRootID, const bool& aFromUser) { const uint64_t& aRootID, const bool& aFromUser) {
ACQUIRE_ANDROID_LOCK ACQUIRE_ANDROID_LOCK
if (mShutdown) return IPC_OK();
MOZ_ASSERT(CheckDocTree()); MOZ_ASSERT(CheckDocTree());
@ -486,13 +484,10 @@ mozilla::ipc::IPCResult DocAccessibleParent::RecvCaretMoveEvent(
return IPC_OK(); return IPC_OK();
} }
mozilla::ipc::IPCResult DocAccessibleParent::RecvTextChangeEvent( mozilla::ipc::IPCResult DocAccessibleParent::ProcessTextChangeEvent(
const uint64_t& aID, const nsAString& aStr, const int32_t& aStart, const uint64_t& aID, const nsAString& aStr, const int32_t& aStart,
const uint32_t& aLen, const bool& aIsInsert, const bool& aFromUser) { const uint32_t& aLen, const bool& aIsInsert, const bool& aFromUser) {
ACQUIRE_ANDROID_LOCK ACQUIRE_ANDROID_LOCK
if (mShutdown) {
return IPC_OK();
}
RemoteAccessible* target = GetAccessible(aID); RemoteAccessible* target = GetAccessible(aID);
if (!target) { if (!target) {
@ -518,6 +513,59 @@ mozilla::ipc::IPCResult DocAccessibleParent::RecvTextChangeEvent(
return IPC_OK(); return IPC_OK();
} }
mozilla::ipc::IPCResult DocAccessibleParent::RecvMutationEvents(
nsTArray<MutationEventData>&& aData) {
// We do not use ACQUIRE_ANDROID_LOCK here since we call functions that do
// that for us. The lock is not re-entrant.
mozilla::ipc::IPCResult result = IPC_OK();
if (mShutdown) {
return result;
}
for (MutationEventData& data : aData) {
switch (data.type()) {
case MutationEventData::Type::TCacheEventData: {
CacheEventData& cacheEventData = data;
result = RecvCache(cacheEventData.UpdateType(),
std::move(cacheEventData.aData()));
break;
}
case MutationEventData::Type::TReorderEventData: {
ReorderEventData& reorderEventData = data;
result = RecvEvent(reorderEventData.ID(), reorderEventData.Type());
break;
}
case MutationEventData::Type::THideEventData: {
HideEventData& hideEventData = data;
result = ProcessHideEvent(hideEventData.ID(),
hideEventData.IsFromUserInput());
break;
}
case MutationEventData::Type::TShowEventData: {
ShowEventData& showEventData = data;
result = ProcessShowEvent(
std::move(showEventData.NewTree()), showEventData.EventSuppressed(),
showEventData.Complete(), showEventData.FromUser());
break;
}
case MutationEventData::Type::TTextChangeEventData: {
TextChangeEventData& textChangeEventData = data;
result = ProcessTextChangeEvent(
textChangeEventData.ID(), textChangeEventData.Str(),
textChangeEventData.Start(), textChangeEventData.Len(),
textChangeEventData.IsInsert(), textChangeEventData.FromUser());
break;
}
default:
break;
}
if (!result) {
return result;
}
}
return IPC_OK();
}
mozilla::ipc::IPCResult DocAccessibleParent::RecvSelectionEvent( mozilla::ipc::IPCResult DocAccessibleParent::RecvSelectionEvent(
const uint64_t& aID, const uint64_t& aWidgetID, const uint32_t& aType) { const uint64_t& aID, const uint64_t& aWidgetID, const uint32_t& aType) {
ACQUIRE_ANDROID_LOCK ACQUIRE_ANDROID_LOCK

View file

@ -98,11 +98,6 @@ class DocAccessibleParent : public RemoteAccessible,
virtual mozilla::ipc::IPCResult RecvEvent(const uint64_t& aID, virtual mozilla::ipc::IPCResult RecvEvent(const uint64_t& aID,
const uint32_t& aType) override; const uint32_t& aType) override;
virtual mozilla::ipc::IPCResult RecvShowEvent(
nsTArray<AccessibleData>&& aNewTree, const bool& aEventSuppressed,
const bool& aComplete, const bool& aFromUser) override;
virtual mozilla::ipc::IPCResult RecvHideEvent(const uint64_t& aRootID,
const bool& aFromUser) override;
mozilla::ipc::IPCResult RecvStateChangeEvent(const uint64_t& aID, mozilla::ipc::IPCResult RecvStateChangeEvent(const uint64_t& aID,
const uint64_t& aState, const uint64_t& aState,
const bool& aEnabled) final; const bool& aEnabled) final;
@ -113,10 +108,8 @@ class DocAccessibleParent : public RemoteAccessible,
const bool& aIsAtEndOfLine, const int32_t& aGranularity, const bool& aIsAtEndOfLine, const int32_t& aGranularity,
const bool& aFromUser) final; const bool& aFromUser) final;
virtual mozilla::ipc::IPCResult RecvTextChangeEvent( virtual mozilla::ipc::IPCResult RecvMutationEvents(
const uint64_t& aID, const nsAString& aStr, const int32_t& aStart, nsTArray<MutationEventData>&& aData) override;
const uint32_t& aLen, const bool& aIsInsert,
const bool& aFromUser) override;
virtual mozilla::ipc::IPCResult RecvFocusEvent( virtual mozilla::ipc::IPCResult RecvFocusEvent(
const uint64_t& aID, const LayoutDeviceIntRect& aCaretRect) override; const uint64_t& aID, const LayoutDeviceIntRect& aCaretRect) override;
@ -343,6 +336,16 @@ class DocAccessibleParent : public RemoteAccessible,
*/ */
void ShutdownOrPrepareForMove(RemoteAccessible* aAcc); void ShutdownOrPrepareForMove(RemoteAccessible* aAcc);
mozilla::ipc::IPCResult ProcessShowEvent(nsTArray<AccessibleData>&& aNewTree,
const bool& aEventSuppressed,
const bool& aComplete,
const bool& aFromUser);
mozilla::ipc::IPCResult ProcessHideEvent(const uint64_t& aRootID,
const bool& aFromUser);
mozilla::ipc::IPCResult ProcessTextChangeEvent(
const uint64_t& aID, const nsAString& aStr, const int32_t& aStart,
const uint32_t& aLen, const bool& aIsInsert, const bool& aFromUser);
nsTArray<uint64_t> mChildDocs; nsTArray<uint64_t> mChildDocs;
#if defined(XP_WIN) #if defined(XP_WIN)

View file

@ -35,6 +35,45 @@ struct AccessibleData
nullable AccAttributes CacheFields; nullable AccAttributes CacheFields;
}; };
struct CacheEventData {
CacheUpdateType UpdateType;
CacheData[] aData;
};
struct ShowEventData {
AccessibleData[] NewTree;
bool EventSuppressed;
bool Complete;
bool FromUser;
};
struct HideEventData {
uint64_t ID;
bool IsFromUserInput;
};
struct ReorderEventData {
uint64_t ID;
uint32_t Type;
};
struct TextChangeEventData {
uint64_t ID;
nsString Str;
int32_t Start;
uint32_t Len;
bool IsInsert;
bool FromUser;
};
union MutationEventData {
CacheEventData;
ShowEventData;
HideEventData;
ReorderEventData;
TextChangeEventData;
};
struct TextRangeData struct TextRangeData
{ {
uint64_t StartID; uint64_t StartID;
@ -56,17 +95,13 @@ parent:
* event. * event.
*/ */
async Event(uint64_t aID, uint32_t type); async Event(uint64_t aID, uint32_t type);
async ShowEvent(AccessibleData[] aNewTree, bool aEventSuppressed,
bool aComplete, bool aFromuser);
async HideEvent(uint64_t aRootID, bool aFromUser);
async StateChangeEvent(uint64_t aID, uint64_t aState, bool aEnabled); async StateChangeEvent(uint64_t aID, uint64_t aState, bool aEnabled);
async CaretMoveEvent(uint64_t aID, async CaretMoveEvent(uint64_t aID,
LayoutDeviceIntRect aCaretRect, LayoutDeviceIntRect aCaretRect,
int32_t aOffset, int32_t aOffset,
bool aIsSelectionCollapsed, bool aIsAtEndOfLine, bool aIsSelectionCollapsed, bool aIsAtEndOfLine,
int32_t aGranularity, bool aFromUser); int32_t aGranularity, bool aFromUser);
async TextChangeEvent(uint64_t aID, nsString aStr, int32_t aStart, uint32_t aLen, async MutationEvents(MutationEventData[] aData);
bool aIsInsert, bool aFromUser);
async SelectionEvent(uint64_t aID, uint64_t aWidgetID, uint32_t aType); async SelectionEvent(uint64_t aID, uint64_t aWidgetID, uint32_t aType);
async RoleChangedEvent(role aRole, uint8_t aRoleMapEntryIndex); async RoleChangedEvent(role aRole, uint8_t aRoleMapEntryIndex);
async FocusEvent(uint64_t aID, LayoutDeviceIntRect aCaretRect); async FocusEvent(uint64_t aID, LayoutDeviceIntRect aCaretRect);

View file

@ -103,3 +103,64 @@ addAccessibleTask(
}, },
{ chrome: true, iframe: true, remoteIframe: true } { chrome: true, iframe: true, remoteIframe: true }
); );
addAccessibleTask(
`<style>
html, body {
height: 100%;
margin: 0;
}
html {
overflow: hidden;
}
body {
overflow: hidden auto;
}
button {
display: block;
height: 100vh;
width: 100%;
}
</style>
<button id="btn1">Hello</button>
<button id="btn2">World</button>`,
async function (browser, accDoc) {
const dpr = await getContentDPR(browser);
let btn1 = findAccessibleChildByID(accDoc, "btn1");
let btn2 = findAccessibleChildByID(accDoc, "btn2");
let [, , width, height] = Layout.getBounds(accDoc, dpr);
await testChildAtPoint(
dpr,
width / 2,
height / 2,
accDoc,
accDoc.firstChild,
btn1
);
await invokeContentTask(browser, [], async () => {
content.document.documentElement.style.overflow = "initial";
await new Promise(resolve => {
content.requestAnimationFrame(() => {
content.scrollTo(0, content.scrollMaxY);
resolve();
});
});
});
await testChildAtPoint(
dpr,
width / 2,
height / 2,
accDoc,
accDoc.firstChild,
btn2
);
}
);

View file

@ -159,7 +159,13 @@ addAccessibleTask(
// test dynamic translation // test dynamic translation
addAccessibleTask( addAccessibleTask(
`<div id="container" style="position: absolute; left: -300px; top: 100px;">Hello</div><button id="b" onclick="container.style.transform = 'translateX(400px)'">Move</button>`, `<div id="container" style="position: absolute; left: -300px; top: 100px;">Hello</div>
<button id="b">Move</button>
<script>
document.getElementById("b").onclick = () => {
container.style.transform = 'translateX(400px)'
};
</script>`,
async function (browser, accDoc) { async function (browser, accDoc) {
const container = findAccessibleChildByID(accDoc, "container"); const container = findAccessibleChildByID(accDoc, "container");
await untilCacheOk( await untilCacheOk(

View file

@ -1045,7 +1045,11 @@ pref("browser.tabs.tooltipsShowPidAndActiveness", false);
pref("browser.tabs.hoverPreview.enabled", true); pref("browser.tabs.hoverPreview.enabled", true);
pref("browser.tabs.hoverPreview.showThumbnails", true); pref("browser.tabs.hoverPreview.showThumbnails", true);
#ifdef NIGHTLY_BUILD
pref("browser.tabs.groups.enabled", true);
#else
pref("browser.tabs.groups.enabled", false); pref("browser.tabs.groups.enabled", false);
#endif
pref("browser.tabs.groups.dragOverThresholdPercent", 20); pref("browser.tabs.groups.dragOverThresholdPercent", 20);
pref("browser.tabs.groups.dragOverDelayMS", 30); pref("browser.tabs.groups.dragOverDelayMS", 30);
pref("browser.tabs.dragdrop.moveOverThresholdPercent", 70); pref("browser.tabs.dragdrop.moveOverThresholdPercent", 70);
@ -2752,6 +2756,7 @@ pref("identity.fxaccounts.toolbar.pxiToolbarEnabled.vpnEnabled", true);
// Prefs to control Mozilla account panels that shows an updated flow // Prefs to control Mozilla account panels that shows an updated flow
// for users who don't have sync enabled // for users who don't have sync enabled
pref("identity.fxaccounts.toolbar.syncSetup.enabled", false); pref("identity.fxaccounts.toolbar.syncSetup.enabled", false);
pref("identity.fxaccounts.toolbar.syncSetup.panelAccessed", false);
// Toolbox preferences // Toolbox preferences
pref("devtools.toolbox.footer.height", 250); pref("devtools.toolbox.footer.height", 250);

View file

@ -753,7 +753,11 @@ var gSync = {
document, document,
"PanelUI-fxa-menu-sync-prefs-button" "PanelUI-fxa-menu-sync-prefs-button"
); );
syncPrefsButtonEl.hidden = !UIState.get().syncEnabled; const syncEnabled = UIState.get().syncEnabled;
syncPrefsButtonEl.hidden = !syncEnabled;
if (!syncEnabled) {
this._disableSyncOffIndicator();
}
// We should ensure that we do not show the sign out button // We should ensure that we do not show the sign out button
// if the user is not signed in // if the user is not signed in
@ -1028,10 +1032,7 @@ var gSync = {
} }
}, },
async toggleAccountPanel( async toggleAccountPanel(anchor = null, aEvent) {
anchor = document.getElementById("fxa-toolbar-menu-button"),
aEvent
) {
// Don't show the panel if the window is in customization mode. // Don't show the panel if the window is in customization mode.
if (document.documentElement.hasAttribute("customizing")) { if (document.documentElement.hasAttribute("customizing")) {
return; return;
@ -1046,10 +1047,15 @@ var gSync = {
return; return;
} }
if ( const fxaToolbarMenuBtn = document.getElementById(
anchor == document.getElementById("fxa-toolbar-menu-button") && "fxa-toolbar-menu-button"
anchor.getAttribute("open") != "true" );
) {
if (anchor === null) {
anchor = fxaToolbarMenuBtn;
}
if (anchor == fxaToolbarMenuBtn && anchor.getAttribute("open") != "true") {
if (ASRouter.initialized) { if (ASRouter.initialized) {
await ASRouter.sendTriggerMessage({ await ASRouter.sendTriggerMessage({
browser: gBrowser.selectedBrowser, browser: gBrowser.selectedBrowser,
@ -1080,7 +1086,7 @@ var gSync = {
this.updateFxAPanel(UIState.get()); this.updateFxAPanel(UIState.get());
this.updateCTAPanel(anchor); this.updateCTAPanel(anchor);
PanelUI.showSubView("PanelUI-fxa", anchor, aEvent); PanelUI.showSubView("PanelUI-fxa", anchor, aEvent);
} else if (anchor == document.getElementById("fxa-toolbar-menu-button")) { } else if (anchor == fxaToolbarMenuBtn) {
// The fxa toolbar button doesn't have much context before the user // The fxa toolbar button doesn't have much context before the user
// clicks it so instead of going straight to the login page, // clicks it so instead of going straight to the login page,
// we take them to a page that has more information // we take them to a page that has more information
@ -1117,9 +1123,34 @@ var gSync = {
} }
}, },
_disableSyncOffIndicator() {
const newSyncSetupEnabled =
NimbusFeatures.syncSetupFlow.getVariable("enabled");
const SYNC_PANEL_ACCESSED_PREF =
"identity.fxaccounts.toolbar.syncSetup.panelAccessed";
// If the user was enrolled in the experiment and hasn't previously accessed
// the panel, we disable the sync off indicator
if (
newSyncSetupEnabled &&
!Services.prefs.getBoolPref(SYNC_PANEL_ACCESSED_PREF, false)
) {
// Turn off the indicator so the user doesn't see it in subsequent openings
Services.prefs.setBoolPref(SYNC_PANEL_ACCESSED_PREF, true);
}
},
_shouldShowSyncOffIndicator() {
const newSyncSetupEnabled =
NimbusFeatures.syncSetupFlow.getVariable("enabled");
if (newSyncSetupEnabled) {
NimbusFeatures.syncSetupFlow.recordExposureEvent();
}
return newSyncSetupEnabled;
},
updateFxAPanel(state = {}) { updateFxAPanel(state = {}) {
const isNewSyncSetupFlowEnabled = const isNewSyncSetupFlowEnabled =
NimbusFeatures.syncDecouplingUpdates.getVariable("syncSetup"); NimbusFeatures.syncSetupFlow.getVariable("enabled");
const mainWindowEl = document.documentElement; const mainWindowEl = document.documentElement;
const menuHeaderTitleEl = PanelMultiView.getViewNode( const menuHeaderTitleEl = PanelMultiView.getViewNode(
@ -1222,6 +1253,9 @@ var gSync = {
if (state.syncEnabled) { if (state.syncEnabled) {
syncNowButtonEl.removeAttribute("hidden"); syncNowButtonEl.removeAttribute("hidden");
syncSetupEl.hidden = true; syncSetupEl.hidden = true;
} else if (this._shouldShowSyncOffIndicator()) {
let fxaButton = document.getElementById("fxa-toolbar-menu-button");
fxaButton?.setAttribute("badge-status", "sync-disabled");
} }
break; break;

View file

@ -6,7 +6,7 @@ function makeInputStream(aString) {
let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
Ci.nsIStringInputStream Ci.nsIStringInputStream
); );
stream.data = aString; stream.setByteStringData(aString);
return stream; // XPConnect will QI this to nsIInputStream for us. return stream; // XPConnect will QI this to nsIInputStream for us.
} }

View file

@ -731,9 +731,9 @@ add_task(async function test_new_sync_setup_ui_exp_enabled() {
// Enroll in the experiment with the feature enabled // Enroll in the experiment with the feature enabled
await ExperimentAPI.ready(); await ExperimentAPI.ready();
let doCleanup = await ExperimentFakes.enrollWithFeatureConfig({ let doCleanup = await ExperimentFakes.enrollWithFeatureConfig({
featureId: NimbusFeatures.syncDecouplingUpdates.featureId, featureId: NimbusFeatures.syncSetupFlow.featureId,
value: { value: {
syncSetup: true, enabled: true,
}, },
}); });
@ -784,9 +784,9 @@ add_task(async function test_new_sync_setup_ui_no_exp() {
// Enroll in the experiment with the feature disabled // Enroll in the experiment with the feature disabled
await ExperimentAPI.ready(); await ExperimentAPI.ready();
let doCleanup = await ExperimentFakes.enrollWithFeatureConfig({ let doCleanup = await ExperimentFakes.enrollWithFeatureConfig({
featureId: NimbusFeatures.syncDecouplingUpdates.featureId, featureId: NimbusFeatures.syncSetupFlow.featureId,
value: { value: {
syncSetup: false, enabled: false,
}, },
}); });

View file

@ -817,7 +817,12 @@ nsBrowserContentHandler.prototype = {
case OVERRIDE_NEW_PROFILE: case OVERRIDE_NEW_PROFILE:
// New profile. // New profile.
gFirstRunProfile = true; gFirstRunProfile = true;
if (lazy.NimbusFeatures.aboutwelcome.getVariable("showModal")) { // If we're showing the main onboarding content in a modal, skip
// showing about:welcome as the homepage.
if (
lazy.NimbusFeatures.aboutwelcome.getVariable("showModal") &&
!lazy.NimbusFeatures.aboutwelcome.getVariable("modalScreens")
) {
break; break;
} }
overridePage = Services.urlFormatter.formatURLPref( overridePage = Services.urlFormatter.formatURLPref(

View file

@ -28,6 +28,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs", BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs",
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
BuiltInThemes: "resource:///modules/BuiltInThemes.sys.mjs", BuiltInThemes: "resource:///modules/BuiltInThemes.sys.mjs",
CaptchaDetectionPingUtils:
"resource://gre/modules/CaptchaDetectionPingUtils.sys.mjs",
ClientID: "resource://gre/modules/ClientID.sys.mjs", ClientID: "resource://gre/modules/ClientID.sys.mjs",
CloseRemoteTab: "resource://gre/modules/FxAccountsCommands.sys.mjs", CloseRemoteTab: "resource://gre/modules/FxAccountsCommands.sys.mjs",
CommonDialog: "resource://gre/modules/CommonDialog.sys.mjs", CommonDialog: "resource://gre/modules/CommonDialog.sys.mjs",
@ -813,6 +815,7 @@ let JSWINDOWACTORS = {
AdClicked: { wantUntrusted: true }, AdClicked: { wantUntrusted: true },
AdImpression: { wantUntrusted: true }, AdImpression: { wantUntrusted: true },
DisableShopping: { wantUntrusted: true }, DisableShopping: { wantUntrusted: true },
CloseShoppingSidebar: { wantUntrusted: true },
}, },
}, },
matches: ["about:shoppingsidebar"], matches: ["about:shoppingsidebar"],
@ -2057,6 +2060,8 @@ BrowserGlue.prototype = {
this._setPrefExpectationsAndUpdate this._setPrefExpectationsAndUpdate
); );
lazy.CaptchaDetectionPingUtils.init();
this._verifySandboxUserNamespaces(aWindow); this._verifySandboxUserNamespaces(aWindow);
}, },
@ -4731,8 +4736,10 @@ BrowserGlue.prototype = {
template: "multistage", template: "multistage",
id: data?.id || "ABOUT_WELCOME_MODAL", id: data?.id || "ABOUT_WELCOME_MODAL",
backdrop: data?.backdrop, backdrop: data?.backdrop,
screens: data?.screens, screens: data?.modalScreens || data?.screens,
UTMTerm: data?.UTMTerm, UTMTerm: data?.UTMTerm,
disableEscClose: data?.requireAction,
// displayed as a window modal by default
}, },
}, },
}; };

View file

@ -482,7 +482,7 @@ const StepsIndicator = props => {
let steps = []; let steps = [];
for (let i = 0; i < props.totalNumberOfScreens; i++) { for (let i = 0; i < props.totalNumberOfScreens; i++) {
let className = `${i === props.order ? "current" : ""} ${i < props.order ? "complete" : ""}`; let className = `${i === props.order ? "current" : ""} ${i < props.order ? "complete" : ""}`;
steps.push( /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { steps.push(/*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
key: i, key: i,
className: `indicator ${className}`, className: `indicator ${className}`,
role: "presentation" role: "presentation"
@ -817,7 +817,7 @@ const Localized = ({
// Add zap style and content in a way that allows fluent to insert too. // Add zap style and content in a way that allows fluent to insert too.
if (text.zap) { if (text.zap) {
props.className += " welcomeZap"; props.className += " welcomeZap";
textNodes.push( /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { textNodes.push(/*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {
className: "short zap", className: "short zap",
"data-l10n-name": "zap", "data-l10n-name": "zap",
ref: zapRef ref: zapRef
@ -1243,7 +1243,7 @@ class ProtonScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
for (const item of content) { for (const item of content) {
switch (item.type) { switch (item.type) {
case "text": case "text":
elements.push( /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_LinkParagraph__WEBPACK_IMPORTED_MODULE_14__.LinkParagraph, { elements.push(/*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_LinkParagraph__WEBPACK_IMPORTED_MODULE_14__.LinkParagraph, {
text_content: item, text_content: item,
handleAction: this.props.handleAction handleAction: this.props.handleAction
})); }));
@ -2999,7 +2999,7 @@ ReturnToAMO.defaultProps = _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_
/******/ /******/
/************************************************************************/ /************************************************************************/
var __webpack_exports__ = {}; var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. // This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk.
(() => { (() => {
__webpack_require__.r(__webpack_exports__); __webpack_require__.r(__webpack_exports__);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
@ -3135,7 +3135,7 @@ async function mount() {
messageId, messageId,
UTMTerm UTMTerm
} = await retrieveRenderContent(); } = await retrieveRenderContent();
react_dom__WEBPACK_IMPORTED_MODULE_1___default().render( /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(AboutWelcome, _extends({ react_dom__WEBPACK_IMPORTED_MODULE_1___default().render(/*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(AboutWelcome, _extends({
messageId: messageId, messageId: messageId,
UTMTerm: UTMTerm UTMTerm: UTMTerm
}, aboutWelcomeProps)), document.getElementById("multi-stage-message-root")); }, aboutWelcomeProps)), document.getElementById("multi-stage-message-root"));

File diff suppressed because it is too large Load diff

View file

@ -9,20 +9,20 @@
"react": "16.13.1", "react": "16.13.1",
"react-dom": "16.13.1", "react-dom": "16.13.1",
"react-redux": "7.2.6", "react-redux": "7.2.6",
"react-transition-group": "4.4.2", "react-transition-group": "4.4.5",
"redux": "4.1.2" "redux": "4.1.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/preset-react": "7.23.3", "@babel/preset-react": "7.26.3",
"@jsdevtools/coverage-istanbul-loader": "^3.0.5", "@jsdevtools/coverage-istanbul-loader": "^3.0.5",
"babel-loader": "9.1.3", "babel-loader": "9.2.1",
"chai": "4.3.4", "chai": "4.3.4",
"enzyme": "3.11.0", "enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.8", "enzyme-adapter-react-16": "1.15.8",
"karma": "6.4.2", "karma": "6.4.4",
"karma-chai": "0.1.0", "karma-chai": "0.1.0",
"karma-coverage-istanbul-reporter": "3.0.3", "karma-coverage-istanbul-reporter": "3.0.3",
"karma-firefox-launcher": "2.1.2", "karma-firefox-launcher": "2.1.3",
"karma-json-reporter": "1.2.1", "karma-json-reporter": "1.2.1",
"karma-mocha": "2.0.1", "karma-mocha": "2.0.1",
"karma-mocha-reporter": "2.2.5", "karma-mocha-reporter": "2.2.5",
@ -30,9 +30,9 @@
"karma-sourcemap-loader": "0.4.0", "karma-sourcemap-loader": "0.4.0",
"karma-webpack": "5.0.1", "karma-webpack": "5.0.1",
"npm-run-all": "4.1.5", "npm-run-all": "4.1.5",
"sass": "1.71.1", "sass": "1.72.0",
"sinon": "17.0.1", "sinon": "17.0.1",
"webpack": "5.90.3", "webpack": "5.97.1",
"webpack-cli": "5.1.4", "webpack-cli": "5.1.4",
"yamscripts": "0.1.0" "yamscripts": "0.1.0"
}, },

View file

@ -500,7 +500,7 @@ const ImpressionsItem = ({
/******/ /******/
/************************************************************************/ /************************************************************************/
var __webpack_exports__ = {}; var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. // This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk.
(() => { (() => {
__webpack_require__.r(__webpack_exports__); __webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ __webpack_require__.d(__webpack_exports__, {
@ -1512,7 +1512,7 @@ class ASRouterAdminInner extends (react__WEBPACK_IMPORTED_MODULE_1___default().P
} }
const ASRouterAdmin = props => /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement(_SimpleHashRouter__WEBPACK_IMPORTED_MODULE_3__.SimpleHashRouter, null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement(ASRouterAdminInner, props)); const ASRouterAdmin = props => /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement(_SimpleHashRouter__WEBPACK_IMPORTED_MODULE_3__.SimpleHashRouter, null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement(ASRouterAdminInner, props));
function renderASRouterAdmin() { function renderASRouterAdmin() {
react_dom__WEBPACK_IMPORTED_MODULE_2___default().render( /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement(ASRouterAdmin, null), document.getElementById("root")); react_dom__WEBPACK_IMPORTED_MODULE_2___default().render(/*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement(ASRouterAdmin, null), document.getElementById("root"));
} }
})(); })();

View file

@ -35,6 +35,10 @@ export const MESSAGING_EXPERIMENTS_DEFAULT_FEATURES = [
"fxms-message-9", "fxms-message-9",
"fxms-message-10", "fxms-message-10",
"fxms-message-11", "fxms-message-11",
"fxms-message-12",
"fxms-message-13",
"fxms-message-14",
"fxms-message-15",
"infobar", "infobar",
"moments-page", "moments-page",
"pbNewtab", "pbNewtab",

File diff suppressed because it is too large Load diff

View file

@ -12,29 +12,29 @@
"redux": "4.1.2" "redux": "4.1.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/preset-react": "7.23.3", "@babel/preset-react": "7.26.3",
"@jsdevtools/coverage-istanbul-loader": "^3.0.5", "@jsdevtools/coverage-istanbul-loader": "^3.0.5",
"babel-loader": "9.1.3", "babel-loader": "9.2.1",
"chai": "4.3.4", "chai": "4.3.4",
"chai-json-schema": "1.5.1", "chai-json-schema": "1.5.1",
"enzyme": "3.11.0", "enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.8", "enzyme-adapter-react-16": "1.15.8",
"karma": "6.4.2", "karma": "6.4.4",
"karma-chai": "0.1.0", "karma-chai": "0.1.0",
"karma-coverage-istanbul-reporter": "3.0.3", "karma-coverage-istanbul-reporter": "3.0.3",
"karma-firefox-launcher": "2.1.2", "karma-firefox-launcher": "2.1.3",
"karma-json-reporter": "1.2.1", "karma-json-reporter": "1.2.1",
"karma-mocha": "2.0.1", "karma-mocha": "2.0.1",
"karma-mocha-reporter": "2.2.5", "karma-mocha-reporter": "2.2.5",
"karma-sinon": "1.0.5", "karma-sinon": "1.0.5",
"karma-sourcemap-loader": "0.4.0", "karma-sourcemap-loader": "0.4.0",
"karma-webpack": "5.0.1", "karma-webpack": "5.0.1",
"mocha": "10.3.0", "mocha": "10.8.2",
"npm-run-all": "4.1.5", "npm-run-all": "4.1.5",
"raw-loader": "4.0.2", "raw-loader": "4.0.2",
"sass": "1.71.1", "sass": "1.72.0",
"sinon": "17.0.1", "sinon": "17.0.1",
"webpack": "5.90.3", "webpack": "5.97.1",
"webpack-cli": "5.1.4", "webpack-cli": "5.1.4",
"yamscripts": "0.1.0" "yamscripts": "0.1.0"
}, },

View file

@ -19,6 +19,12 @@ const PanelUI = {
get kEvents() { get kEvents() {
return ["popupshowing", "popupshown", "popuphiding", "popuphidden"]; return ["popupshowing", "popupshown", "popuphiding", "popuphidden"];
}, },
/// Notification events used for overwriting notification actions
get kNotificationEvents() {
return ["buttoncommand", "secondarybuttoncommand", "learnmoreclick"];
},
/** /**
* Used for lazily getting and memoizing elements from the document. Lazy * Used for lazily getting and memoizing elements from the document. Lazy
* getters are set in init, and memoizing happens after the first retrieval. * getters are set in init, and memoizing happens after the first retrieval.
@ -165,6 +171,9 @@ const PanelUI = {
for (let event of this.kEvents) { for (let event of this.kEvents) {
this.notificationPanel.removeEventListener(event, this); this.notificationPanel.removeEventListener(event, this);
} }
for (let event of this.kNotificationEvents) {
this.notificationPanel.removeEventListener(event, this);
}
} }
Services.obs.removeObserver(this, "fullscreen-nav-toolbox"); Services.obs.removeObserver(this, "fullscreen-nav-toolbox");
@ -323,6 +332,16 @@ const PanelUI = {
case "command": case "command":
this.onCommand(aEvent); this.onCommand(aEvent);
break; break;
case "buttoncommand":
this._onNotificationButtonEvent(aEvent, "buttoncommand");
break;
case "secondarybuttoncommand":
this._onNotificationButtonEvent(aEvent, "secondarybuttoncommand");
break;
case "learnmoreclick":
// Don't fall back to PopupNotifications.
aEvent.preventDefault();
break;
} }
}, },
@ -968,6 +987,9 @@ const PanelUI = {
for (let event of this.kEvents) { for (let event of this.kEvents) {
this._notificationPanel.addEventListener(event, this); this._notificationPanel.addEventListener(event, this);
} }
for (let event of this.kNotificationEvents) {
this._notificationPanel.addEventListener(event, this);
}
} }
return this._notificationPanel; return this._notificationPanel;
}, },
@ -1006,14 +1028,6 @@ const PanelUI = {
let popupnotification = document.getElementById(popupnotificationID); let popupnotification = document.getElementById(popupnotificationID);
popupnotification.setAttribute("id", popupnotificationID); popupnotification.setAttribute("id", popupnotificationID);
popupnotification.setAttribute(
"buttoncommand",
"PanelUI._onNotificationButtonEvent(event, 'buttoncommand');"
);
popupnotification.setAttribute(
"secondarybuttoncommand",
"PanelUI._onNotificationButtonEvent(event, 'secondarybuttoncommand');"
);
if (notification.options.message) { if (notification.options.message) {
let desc = this._formatDescriptionMessage(notification); let desc = this._formatDescriptionMessage(notification);
@ -1082,6 +1096,8 @@ const PanelUI = {
}, },
_onNotificationButtonEvent(event, type) { _onNotificationButtonEvent(event, type) {
event.preventDefault();
let notificationEl = getNotificationFromElement(event.originalTarget); let notificationEl = getNotificationFromElement(event.originalTarget);
if (!notificationEl) { if (!notificationEl) {

View file

@ -334,6 +334,8 @@ skip-if = ["(verify && debug && (os == 'linux' || os == 'mac'))"]
["browser_remove_customized_specials.js"] ["browser_remove_customized_specials.js"]
["browser_remove_sidebar_button_and_sidebar.js"]
["browser_reset_builtin_widget_currentArea.js"] ["browser_reset_builtin_widget_currentArea.js"]
["browser_reset_dom_events.js"] ["browser_reset_dom_events.js"]

View file

@ -0,0 +1,54 @@
/* Any copyright is dedicated to the Public Domain.
* https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_setup(async () => {
await SpecialPowers.pushPrefEnv({
set: [["sidebar.revamp", true]],
});
});
registerCleanupFunction(async () => {
await SpecialPowers.popPrefEnv();
gBrowser.removeAllTabsBut(gBrowser.tabs[0]);
});
add_task(async function () {
const { SidebarController } = window;
await SidebarController.show("viewBookmarksSidebar");
let sidebarButton = await BrowserTestUtils.waitForCondition(
() => document.getElementById("sidebar-button"),
"Sidebar button is shown."
);
ok(sidebarButton, "Sidebar button is shown.");
let sidebarMain = document.getElementById("sidebar-main");
ok(sidebarMain, "Sidebar launcher is shown.");
let sidebarBox = await BrowserTestUtils.waitForCondition(
() => document.getElementById("sidebar-box"),
"Sidebar panel is shown."
);
ok(sidebarBox, "Sidebar panel is shown.");
await startCustomizing();
is(gBrowser.tabs.length, 2, "Should have 2 tabs");
let nonCustomizingTab = gBrowser.tabContainer.querySelector(
"tab:not([customizemode=true])"
);
let finishedCustomizing = BrowserTestUtils.waitForEvent(
gNavToolbox,
"aftercustomization"
);
CustomizableUI.removeWidgetFromArea("sidebar-button");
await BrowserTestUtils.switchTab(gBrowser, nonCustomizingTab);
await finishedCustomizing;
await BrowserTestUtils.waitForCondition(() => {
sidebarButton = document.getElementById("sidebar-button");
return !sidebarButton && sidebarMain.hidden && sidebarBox.hidden;
}, "Sidebar button, panel and launcher are not present");
ok(!sidebarButton, "Sidebar button has been removed.");
ok(sidebarMain.hidden, "Sidebar launcher has been hidden.");
ok(sidebarBox.hidden, "Sidebar panel has been hidden.");
});

View file

@ -255,13 +255,27 @@ export class ExtensionControlledPopup {
let addon = await lazy.AddonManager.getAddonByID(extensionId); let addon = await lazy.AddonManager.getAddonByID(extensionId);
this.populateDescription(doc, addon); this.populateDescription(doc, addon);
// Setup the command handler. // Setup the buttoncommand handler.
let handleCommand = async event => { let handleButtonCommand = async event => {
event.preventDefault();
panel.hidePopup(); panel.hidePopup();
if (event.originalTarget == popupnotification.button) {
// Main action is to keep changes. // Main action is to keep changes.
await this.setConfirmation(extensionId); await this.setConfirmation(extensionId);
} else if (this.preferencesLocation) {
// If the page this is appearing on is the New Tab page then the URL bar may
// have been focused when the doorhanger stole focus away from it. Once an
// action is taken the focus state should be restored to what the user was
// expecting.
if (urlBarWasFocused) {
win.gURLBar.focus();
}
};
let handleSecondaryButtonCommand = async event => {
event.preventDefault();
panel.hidePopup();
if (this.preferencesLocation) {
// Secondary action opens Preferences, if a preferencesLocation option is included. // Secondary action opens Preferences, if a preferencesLocation option is included.
let options = this.Entrypoint let options = this.Entrypoint
? { urlParams: { entrypoint: this.Entrypoint } } ? { urlParams: { entrypoint: this.Entrypoint } }
@ -275,20 +289,24 @@ export class ExtensionControlledPopup {
await addon.disable(); await addon.disable();
} }
// If the page this is appearing on is the New Tab page then the URL bar may
// have been focused when the doorhanger stole focus away from it. Once an
// action is taken the focus state should be restored to what the user was
// expecting.
if (urlBarWasFocused) { if (urlBarWasFocused) {
win.gURLBar.focus(); win.gURLBar.focus();
} }
}; };
panel.addEventListener("command", handleCommand); panel.addEventListener("buttoncommand", handleButtonCommand);
panel.addEventListener(
"secondarybuttoncommand",
handleSecondaryButtonCommand
);
panel.addEventListener( panel.addEventListener(
"popuphidden", "popuphidden",
() => { () => {
popupnotification.hidden = true; popupnotification.hidden = true;
panel.removeEventListener("command", handleCommand); panel.removeEventListener("buttoncommand", handleButtonCommand);
panel.removeEventListener(
"secondarybuttoncommand",
handleSecondaryButtonCommand
);
}, },
{ once: true } { once: true }
); );

View file

@ -785,7 +785,9 @@ export const GenAI = {
options.headers = Cc[ options.headers = Cc[
"@mozilla.org/io/string-input-stream;1" "@mozilla.org/io/string-input-stream;1"
].createInstance(Ci.nsIStringInputStream); ].createInstance(Ci.nsIStringInputStream);
options.headers.data = `${header}: ${encodeURIComponent(prompt)}\r\n`; options.headers.setByteStringData(
`${header}: ${encodeURIComponent(prompt)}\r\n`
);
} else { } else {
url.searchParams.set("q", prompt); url.searchParams.set("q", prompt);
} }

View file

@ -97,10 +97,11 @@ function CardSection({
data: { data: {
section: sectionKey, section: sectionKey,
section_position: sectionPosition, section_position: sectionPosition,
is_secton_followed: following,
}, },
}) })
); );
}, [dispatch, sectionKey, sectionPosition]); }, [dispatch, sectionKey, sectionPosition, following]);
// Ref to hold the section element // Ref to hold the section element
const sectionRefs = useIntersectionObserver(handleIntersection); const sectionRefs = useIntersectionObserver(handleIntersection);

View file

@ -1,13 +1,13 @@
@mixin section-card-small { @mixin section-card-small {
grid-row: span 1; grid-row: span 1;
grid-column: span 1; grid-column: span 1;
display: flex;
flex-direction: row;
position: relative;
align-items: center;
gap: var(--space-medium);
padding: var(--space-large); padding: var(--space-large);
&.ds-card.sections-card-ui {
padding: unset;
}
.stp-context-menu { .stp-context-menu {
display: block; display: block;
} }
@ -16,8 +16,18 @@
display: none; display: none;
} }
.ds-card-link {
display: flex;
flex-direction: row;
position: relative;
align-items: center;
gap: var(--space-medium);
padding: var(--space-large);
}
.img-wrapper { .img-wrapper {
width: 120px; width: 125px;
flex-shrink: 0; flex-shrink: 0;
flex-grow: 0; flex-grow: 0;
aspect-ratio: 1/1; aspect-ratio: 1/1;
@ -44,10 +54,6 @@
} }
} }
.ds-card-link {
inset-inline-end: 0;
}
.meta { .meta {
padding: var(--space-xsmall) 0; padding: var(--space-xsmall) 0;
align-self: flex-start; align-self: flex-start;
@ -72,7 +78,7 @@
} }
.card-stp-button-position-wrapper { .card-stp-button-position-wrapper {
inset-inline-end: 28px; inset-inline-end: 10px;
.card-stp-button { .card-stp-button {
display: none; display: none;
@ -80,6 +86,7 @@
} }
} }
@mixin section-card-medium { @mixin section-card-medium {
grid-row: span 2; grid-row: span 2;
grid-column: span 1; grid-column: span 1;
@ -88,6 +95,8 @@
align-items: initial; align-items: initial;
gap: initial; gap: initial;
.stp-context-menu { .stp-context-menu {
display: none; display: none;
} }
@ -104,12 +113,19 @@
display: block; display: block;
} }
.ds-card-link {
display: flex;
flex-direction: column;
gap: 0;
padding: 0;
}
.img-wrapper { .img-wrapper {
width: 100%; width: 100%;
position: relative; position: relative;
// reset values inherited from small card mixin // reset values inherited from small card mixin
flex-grow: initial; flex-grow: 0;
flex-shrink: initial; flex-shrink: 0;
aspect-ratio: initial; aspect-ratio: initial;
} }
@ -137,6 +153,7 @@
} }
} }
// DS Large Card // DS Large Card
@mixin section-card-large { @mixin section-card-large {
grid-row: span 2; grid-row: span 2;
@ -145,15 +162,16 @@
&.ds-card.sections-card-ui { &.ds-card.sections-card-ui {
@media (min-width: $break-point-layout-variant ) and (max-width: $break-point-widest ), @media (min-width: $break-point-layout-variant ) and (max-width: $break-point-widest ),
(min-width: $break-point-sections-variant ) { (min-width: $break-point-sections-variant ) {
display: grid; align-content: flex-start;
grid-template-columns: auto 1fr;
gap: var(--space-xlarge); .ds-card-link {
padding: var(--space-xxlarge); flex-direction: row;
align-items: center; gap: var(--space-xlarge);
padding: var(--space-xxlarge);
}
.img-wrapper { .img-wrapper {
width: 220px; width: 265px;
height: 220px;
} }
.ds-image.img { .ds-image.img {
@ -182,6 +200,10 @@
font-size: var(--font-size-root); font-size: var(--font-size-root);
} }
} }
.card-stp-button-hover-background {
inset-inline-start: 0;
}
} }
} }
} }
@ -190,10 +212,6 @@
// Use (almost all) default wrapper widths with the sections-grid breakpoints // Use (almost all) default wrapper widths with the sections-grid breakpoints
.has-sections-grid .ds-outer-wrapper-breakpoint-override { .has-sections-grid .ds-outer-wrapper-breakpoint-override {
.ds-layout-topsites { .ds-layout-topsites {
width: 100vw;
margin-inline-start: 50%;
transform: translate3d(-50%, 0, 0);
> div { > div {
width: 266px; width: 266px;
margin-inline: auto; margin-inline: auto;

View file

@ -459,7 +459,10 @@ export class _DSCard extends React.PureComponent {
} }
} }
onThumbsUpClick() { onThumbsUpClick(event) {
event.stopPropagation();
event.preventDefault();
// Toggle active state for thumbs up button to show CSS animation // Toggle active state for thumbs up button to show CSS animation
const currentState = this.state.isThumbsUpActive; const currentState = this.state.isThumbsUpActive;
@ -511,7 +514,10 @@ export class _DSCard extends React.PureComponent {
); );
} }
onThumbsDownClick() { onThumbsDownClick(event) {
event.stopPropagation();
event.preventDefault();
// Toggle active state for thumbs down button to show CSS animation // Toggle active state for thumbs down button to show CSS animation
const currentState = this.state.isThumbsDownActive; const currentState = this.state.isThumbsDownActive;
this.setState({ isThumbsDownActive: !currentState }); this.setState({ isThumbsDownActive: !currentState });
@ -801,27 +807,6 @@ export class _DSCard extends React.PureComponent {
data-position-three={this.props["data-position-one"]} data-position-three={this.props["data-position-one"]}
data-position-four={this.props["data-position-one"]} data-position-four={this.props["data-position-one"]}
> >
{this.props.showTopics &&
!this.props.mayHaveSectionsCards &&
this.props.topic &&
!isListCard && (
<span
className="ds-card-topic"
data-l10n-id={`newtab-topic-label-${this.props.topic}`}
/>
)}
<div className="img-wrapper">
<DSImage
extraClassNames="img"
source={this.props.image_src}
rawSource={this.props.raw_image_src}
sizes={sizes}
url={this.props.url}
title={this.props.title}
isRecentSave={isRecentSave}
alt_text={alt_text}
/>
</div>
<SafeAnchor <SafeAnchor
className="ds-card-link" className="ds-card-link"
dispatch={this.props.dispatch} dispatch={this.props.dispatch}
@ -829,6 +814,27 @@ export class _DSCard extends React.PureComponent {
url={this.props.url} url={this.props.url}
title={this.props.title} title={this.props.title}
> >
{this.props.showTopics &&
!this.props.mayHaveSectionsCards &&
this.props.topic &&
!isListCard && (
<span
className="ds-card-topic"
data-l10n-id={`newtab-topic-label-${this.props.topic}`}
/>
)}
<div className="img-wrapper">
<DSImage
extraClassNames="img"
source={this.props.image_src}
rawSource={this.props.raw_image_src}
sizes={sizes}
url={this.props.url}
title={this.props.title}
isRecentSave={isRecentSave}
alt_text={alt_text}
/>
</div>
<ImpressionStats <ImpressionStats
flightId={this.props.flightId} flightId={this.props.flightId}
rows={[ rows={[
@ -863,46 +869,48 @@ export class _DSCard extends React.PureComponent {
source={this.props.type} source={this.props.type}
firstVisibleTimestamp={this.props.firstVisibleTimestamp} firstVisibleTimestamp={this.props.firstVisibleTimestamp}
/> />
</SafeAnchor>
{ctaButtonVariant === "variant-b" && (
<div className="cta-header">Shop Now</div>
)}
{isFakespot ? (
<div className="meta">
<div className="info-wrap">
<h3 className="title clamp">{this.props.title}</h3>
</div>
</div>
) : (
<DefaultMeta
source={source}
title={this.props.title}
excerpt={excerpt}
newSponsoredLabel={newSponsoredLabel}
timeToRead={timeToRead}
context={this.props.context}
context_type={this.props.context_type}
sponsor={this.props.sponsor}
sponsored_by_override={this.props.sponsored_by_override}
saveToPocketCard={saveToPocketCard}
ctaButtonVariant={ctaButtonVariant}
dispatch={this.props.dispatch}
spocMessageVariant={this.props.spocMessageVariant}
mayHaveThumbsUpDown={this.props.mayHaveThumbsUpDown}
mayHaveSectionsCards={this.props.mayHaveSectionsCards}
onThumbsUpClick={this.onThumbsUpClick}
onThumbsDownClick={this.onThumbsDownClick}
state={this.state}
isListCard={isListCard}
showTopics={this.props.showTopics}
isSectionsCard={
this.props.mayHaveSectionsCards && this.props.topic && !isListCard
}
format={format}
topic={this.props.topic}
/>
)}
{ctaButtonVariant === "variant-b" && (
<div className="cta-header">Shop Now</div>
)}
{isFakespot ? (
<div className="meta">
<div className="info-wrap">
<h3 className="title clamp">{this.props.title}</h3>
</div>
</div>
) : (
<DefaultMeta
source={source}
title={this.props.title}
excerpt={excerpt}
newSponsoredLabel={newSponsoredLabel}
timeToRead={timeToRead}
context={this.props.context}
context_type={this.props.context_type}
sponsor={this.props.sponsor}
sponsored_by_override={this.props.sponsored_by_override}
saveToPocketCard={saveToPocketCard}
ctaButtonVariant={ctaButtonVariant}
dispatch={this.props.dispatch}
spocMessageVariant={this.props.spocMessageVariant}
mayHaveThumbsUpDown={this.props.mayHaveThumbsUpDown}
mayHaveSectionsCards={this.props.mayHaveSectionsCards}
onThumbsUpClick={this.onThumbsUpClick}
onThumbsDownClick={this.onThumbsDownClick}
state={this.state}
isListCard={isListCard}
showTopics={this.props.showTopics}
isSectionsCard={
this.props.mayHaveSectionsCards &&
this.props.topic &&
!isListCard
}
format={format}
topic={this.props.topic}
/>
)}
</SafeAnchor>
<div <div
className={`card-stp-button-hover-background ${compactPocketSavedButtonClassName}`} className={`card-stp-button-hover-background ${compactPocketSavedButtonClassName}`}
> >

View file

@ -229,10 +229,13 @@ $ds-card-image-gradient-solid: rgba(0, 0, 0, 100%);
} }
.ds-card-link { .ds-card-link {
position: absolute; display: flex;
height: 100%; flex-direction: column;
width: 100%; align-items: initial;
text-decoration: none; text-decoration: none;
grid-template-columns: auto 1fr;
gap: inherit;
flex-grow: 1;
&:focus { &:focus {
@include ds-focus; @include ds-focus;
@ -241,11 +244,12 @@ $ds-card-image-gradient-solid: rgba(0, 0, 0, 100%);
} }
} }
> .ds-card-topic { .ds-card-topic {
position: absolute; position: absolute;
z-index: 1; z-index: 1;
background: light-dark(#F0F0F4, var(--newtab-background-color-secondary)); background: light-dark(#F0F0F4, var(--newtab-background-color-secondary));
border-radius: var(--border-radius-small); border-radius: var(--border-radius-small);
color: var(--newtab-text-primary-color);
padding: var(--space-small); padding: var(--space-small);
margin: var(--space-small); margin: var(--space-small);
font-size: 14px; font-size: 14px;
@ -537,6 +541,9 @@ $ds-card-image-gradient-solid: rgba(0, 0, 0, 100%);
height: 28px; height: 28px;
font-size: var(--newtab-font-size-xsmall); font-size: var(--newtab-font-size-xsmall);
color: var(--newtab-text-topic-label-color); color: var(--newtab-text-topic-label-color);
margin: initial;
padding: initial;
background-color: initial;
} }
.card-stp-button-hover-background { .card-stp-button-hover-background {

View file

@ -1,6 +1,7 @@
.impression-observer { .impression-observer {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
pointer-events: none; pointer-events: none;

View file

@ -27,11 +27,8 @@ export const selectLayoutRender = ({ state = {}, prefs = {} }) => {
const results = [...data]; const results = [...data];
for (let position of spocsPositions) { for (let position of spocsPositions) {
const spoc = spocsData[spocIndexPlacementMap[placementName]]; const spoc = spocsData[spocIndexPlacementMap[placementName]];
const format = spoc?.format;
// If there are no spocs left, we can stop filling positions. // If there are no spocs left, we can stop filling positions.
// Since banner-type ads are placed by row and don't use the normal spoc-position, if (!spoc) {
// dont combine with content
if (!spoc || format === "billboard" || format === "leaderboard") {
break; break;
} }
@ -139,12 +136,19 @@ export const selectLayoutRender = ({ state = {}, prefs = {} }) => {
const placement = spocsPlacement || {}; const placement = spocsPlacement || {};
const placementName = placement.name || "newtab_spocs"; const placementName = placement.name || "newtab_spocs";
const spocsData = spocs.data[placementName]; const spocsData = spocs.data[placementName];
// We expect a spoc, spocs are loaded, and the server returned spocs. // We expect a spoc, spocs are loaded, and the server returned spocs.
if (spocs.loaded && spocsData?.items?.length) { if (spocs.loaded && spocsData?.items?.length) {
// Since banner-type ads are placed by row and don't use the normal spoc position,
// dont combine with content
const excludedSpocs = ["billboard", "leaderboard"];
const filteredSpocs = spocsData?.items?.filter(
item => !excludedSpocs.includes(item.format)
);
result = fillSpocPositionsForPlacement( result = fillSpocPositionsForPlacement(
result, result,
spocsPositions, spocsPositions,
spocsData.items, filteredSpocs,
placementName placementName
); );
} }

View file

@ -4485,11 +4485,6 @@ main section {
color: var(--newtab-primary-element-active-color); color: var(--newtab-primary-element-active-color);
} }
.has-sections-grid .ds-outer-wrapper-breakpoint-override .ds-layout-topsites {
width: 100vw;
margin-inline-start: 50%;
transform: translate3d(-50%, 0, 0);
}
.has-sections-grid .ds-outer-wrapper-breakpoint-override .ds-layout-topsites > div { .has-sections-grid .ds-outer-wrapper-breakpoint-override .ds-layout-topsites > div {
width: 266px; width: 266px;
margin-inline: auto; margin-inline: auto;
@ -4723,21 +4718,27 @@ main section {
.ds-section-grid.ds-card-grid .col-1-small { .ds-section-grid.ds-card-grid .col-1-small {
grid-row: span 1; grid-row: span 1;
grid-column: span 1; grid-column: span 1;
display: flex;
flex-direction: row;
position: relative;
align-items: center;
gap: var(--space-medium);
padding: var(--space-large); padding: var(--space-large);
} }
.ds-section-grid.ds-card-grid .col-1-small.ds-card.sections-card-ui {
padding: unset;
}
.ds-section-grid.ds-card-grid .col-1-small .stp-context-menu { .ds-section-grid.ds-card-grid .col-1-small .stp-context-menu {
display: block; display: block;
} }
.ds-section-grid.ds-card-grid .col-1-small .card-stp-thumbs-buttons-wrapper { .ds-section-grid.ds-card-grid .col-1-small .card-stp-thumbs-buttons-wrapper {
display: none; display: none;
} }
.ds-section-grid.ds-card-grid .col-1-small .ds-card-link {
display: flex;
flex-direction: row;
position: relative;
align-items: center;
gap: var(--space-medium);
padding: var(--space-large);
}
.ds-section-grid.ds-card-grid .col-1-small .img-wrapper { .ds-section-grid.ds-card-grid .col-1-small .img-wrapper {
width: 120px; width: 125px;
flex-shrink: 0; flex-shrink: 0;
flex-grow: 0; flex-grow: 0;
aspect-ratio: 1/1; aspect-ratio: 1/1;
@ -4758,9 +4759,6 @@ main section {
width: 100%; width: 100%;
border-radius: var(--border-radius-medium) var(--border-radius-medium); border-radius: var(--border-radius-medium) var(--border-radius-medium);
} }
.ds-section-grid.ds-card-grid .col-1-small .ds-card-link {
inset-inline-end: 0;
}
.ds-section-grid.ds-card-grid .col-1-small .meta { .ds-section-grid.ds-card-grid .col-1-small .meta {
padding: var(--space-xsmall) 0; padding: var(--space-xsmall) 0;
align-self: flex-start; align-self: flex-start;
@ -4780,7 +4778,7 @@ main section {
padding-block-start: unset; padding-block-start: unset;
} }
.ds-section-grid.ds-card-grid .col-1-small .card-stp-button-position-wrapper { .ds-section-grid.ds-card-grid .col-1-small .card-stp-button-position-wrapper {
inset-inline-end: 28px; inset-inline-end: 10px;
} }
.ds-section-grid.ds-card-grid .col-1-small .card-stp-button-position-wrapper .card-stp-button { .ds-section-grid.ds-card-grid .col-1-small .card-stp-button-position-wrapper .card-stp-button {
display: none; display: none;
@ -4805,11 +4803,17 @@ main section {
.ds-section-grid.ds-card-grid .col-1-medium .card-stp-thumbs-buttons-wrapper { .ds-section-grid.ds-card-grid .col-1-medium .card-stp-thumbs-buttons-wrapper {
display: block; display: block;
} }
.ds-section-grid.ds-card-grid .col-1-medium .ds-card-link {
display: flex;
flex-direction: column;
gap: 0;
padding: 0;
}
.ds-section-grid.ds-card-grid .col-1-medium .img-wrapper { .ds-section-grid.ds-card-grid .col-1-medium .img-wrapper {
width: 100%; width: 100%;
position: relative; position: relative;
flex-grow: initial; flex-grow: 0;
flex-shrink: initial; flex-shrink: 0;
aspect-ratio: initial; aspect-ratio: initial;
} }
.ds-section-grid.ds-card-grid .col-1-medium:not(.placeholder) .img-wrapper > .ds-image.img > img { .ds-section-grid.ds-card-grid .col-1-medium:not(.placeholder) .img-wrapper > .ds-image.img > img {
@ -4834,15 +4838,15 @@ main section {
} }
@media (min-width: 610px) and (min-width: 724px) and (max-width: 1122px), (min-width: 610px) and (min-width: 1390px) { @media (min-width: 610px) and (min-width: 724px) and (max-width: 1122px), (min-width: 610px) and (min-width: 1390px) {
.ds-section-grid.ds-card-grid .col-1-large.ds-card.sections-card-ui { .ds-section-grid.ds-card-grid .col-1-large.ds-card.sections-card-ui {
display: grid; align-content: flex-start;
grid-template-columns: auto 1fr; }
.ds-section-grid.ds-card-grid .col-1-large.ds-card.sections-card-ui .ds-card-link {
flex-direction: row;
gap: var(--space-xlarge); gap: var(--space-xlarge);
padding: var(--space-xxlarge); padding: var(--space-xxlarge);
align-items: center;
} }
.ds-section-grid.ds-card-grid .col-1-large.ds-card.sections-card-ui .img-wrapper { .ds-section-grid.ds-card-grid .col-1-large.ds-card.sections-card-ui .img-wrapper {
width: 220px; width: 265px;
height: 220px;
} }
.ds-section-grid.ds-card-grid .col-1-large.ds-card.sections-card-ui .ds-image.img { .ds-section-grid.ds-card-grid .col-1-large.ds-card.sections-card-ui .ds-image.img {
aspect-ratio: 1/1; aspect-ratio: 1/1;
@ -4864,6 +4868,9 @@ main section {
-webkit-line-clamp: 4; -webkit-line-clamp: 4;
font-size: var(--font-size-root); font-size: var(--font-size-root);
} }
.ds-section-grid.ds-card-grid .col-1-large.ds-card.sections-card-ui .card-stp-button-hover-background {
inset-inline-start: 0;
}
} }
@media (min-width: 724px) { @media (min-width: 724px) {
.ds-section-grid.ds-card-grid { .ds-section-grid.ds-card-grid {
@ -4893,21 +4900,27 @@ main section {
.ds-section-grid.ds-card-grid .col-2-small { .ds-section-grid.ds-card-grid .col-2-small {
grid-row: span 1; grid-row: span 1;
grid-column: span 1; grid-column: span 1;
display: flex;
flex-direction: row;
position: relative;
align-items: center;
gap: var(--space-medium);
padding: var(--space-large); padding: var(--space-large);
} }
.ds-section-grid.ds-card-grid .col-2-small.ds-card.sections-card-ui {
padding: unset;
}
.ds-section-grid.ds-card-grid .col-2-small .stp-context-menu { .ds-section-grid.ds-card-grid .col-2-small .stp-context-menu {
display: block; display: block;
} }
.ds-section-grid.ds-card-grid .col-2-small .card-stp-thumbs-buttons-wrapper { .ds-section-grid.ds-card-grid .col-2-small .card-stp-thumbs-buttons-wrapper {
display: none; display: none;
} }
.ds-section-grid.ds-card-grid .col-2-small .ds-card-link {
display: flex;
flex-direction: row;
position: relative;
align-items: center;
gap: var(--space-medium);
padding: var(--space-large);
}
.ds-section-grid.ds-card-grid .col-2-small .img-wrapper { .ds-section-grid.ds-card-grid .col-2-small .img-wrapper {
width: 120px; width: 125px;
flex-shrink: 0; flex-shrink: 0;
flex-grow: 0; flex-grow: 0;
aspect-ratio: 1/1; aspect-ratio: 1/1;
@ -4928,9 +4941,6 @@ main section {
width: 100%; width: 100%;
border-radius: var(--border-radius-medium) var(--border-radius-medium); border-radius: var(--border-radius-medium) var(--border-radius-medium);
} }
.ds-section-grid.ds-card-grid .col-2-small .ds-card-link {
inset-inline-end: 0;
}
.ds-section-grid.ds-card-grid .col-2-small .meta { .ds-section-grid.ds-card-grid .col-2-small .meta {
padding: var(--space-xsmall) 0; padding: var(--space-xsmall) 0;
align-self: flex-start; align-self: flex-start;
@ -4950,7 +4960,7 @@ main section {
padding-block-start: unset; padding-block-start: unset;
} }
.ds-section-grid.ds-card-grid .col-2-small .card-stp-button-position-wrapper { .ds-section-grid.ds-card-grid .col-2-small .card-stp-button-position-wrapper {
inset-inline-end: 28px; inset-inline-end: 10px;
} }
.ds-section-grid.ds-card-grid .col-2-small .card-stp-button-position-wrapper .card-stp-button { .ds-section-grid.ds-card-grid .col-2-small .card-stp-button-position-wrapper .card-stp-button {
display: none; display: none;
@ -4975,11 +4985,17 @@ main section {
.ds-section-grid.ds-card-grid .col-2-medium .card-stp-thumbs-buttons-wrapper { .ds-section-grid.ds-card-grid .col-2-medium .card-stp-thumbs-buttons-wrapper {
display: block; display: block;
} }
.ds-section-grid.ds-card-grid .col-2-medium .ds-card-link {
display: flex;
flex-direction: column;
gap: 0;
padding: 0;
}
.ds-section-grid.ds-card-grid .col-2-medium .img-wrapper { .ds-section-grid.ds-card-grid .col-2-medium .img-wrapper {
width: 100%; width: 100%;
position: relative; position: relative;
flex-grow: initial; flex-grow: 0;
flex-shrink: initial; flex-shrink: 0;
aspect-ratio: initial; aspect-ratio: initial;
} }
.ds-section-grid.ds-card-grid .col-2-medium:not(.placeholder) .img-wrapper > .ds-image.img > img { .ds-section-grid.ds-card-grid .col-2-medium:not(.placeholder) .img-wrapper > .ds-image.img > img {
@ -5004,15 +5020,15 @@ main section {
} }
@media (min-width: 724px) and (min-width: 724px) and (max-width: 1122px), (min-width: 724px) and (min-width: 1390px) { @media (min-width: 724px) and (min-width: 724px) and (max-width: 1122px), (min-width: 724px) and (min-width: 1390px) {
.ds-section-grid.ds-card-grid .col-2-large.ds-card.sections-card-ui { .ds-section-grid.ds-card-grid .col-2-large.ds-card.sections-card-ui {
display: grid; align-content: flex-start;
grid-template-columns: auto 1fr; }
.ds-section-grid.ds-card-grid .col-2-large.ds-card.sections-card-ui .ds-card-link {
flex-direction: row;
gap: var(--space-xlarge); gap: var(--space-xlarge);
padding: var(--space-xxlarge); padding: var(--space-xxlarge);
align-items: center;
} }
.ds-section-grid.ds-card-grid .col-2-large.ds-card.sections-card-ui .img-wrapper { .ds-section-grid.ds-card-grid .col-2-large.ds-card.sections-card-ui .img-wrapper {
width: 220px; width: 265px;
height: 220px;
} }
.ds-section-grid.ds-card-grid .col-2-large.ds-card.sections-card-ui .ds-image.img { .ds-section-grid.ds-card-grid .col-2-large.ds-card.sections-card-ui .ds-image.img {
aspect-ratio: 1/1; aspect-ratio: 1/1;
@ -5034,6 +5050,9 @@ main section {
-webkit-line-clamp: 4; -webkit-line-clamp: 4;
font-size: var(--font-size-root); font-size: var(--font-size-root);
} }
.ds-section-grid.ds-card-grid .col-2-large.ds-card.sections-card-ui .card-stp-button-hover-background {
inset-inline-start: 0;
}
} }
@media (min-width: 1122px) { @media (min-width: 1122px) {
.ds-section-grid.ds-card-grid { .ds-section-grid.ds-card-grid {
@ -5064,21 +5083,27 @@ main section {
.ds-section-grid.ds-card-grid .col-3-small { .ds-section-grid.ds-card-grid .col-3-small {
grid-row: span 1; grid-row: span 1;
grid-column: span 1; grid-column: span 1;
display: flex;
flex-direction: row;
position: relative;
align-items: center;
gap: var(--space-medium);
padding: var(--space-large); padding: var(--space-large);
} }
.ds-section-grid.ds-card-grid .col-3-small.ds-card.sections-card-ui {
padding: unset;
}
.ds-section-grid.ds-card-grid .col-3-small .stp-context-menu { .ds-section-grid.ds-card-grid .col-3-small .stp-context-menu {
display: block; display: block;
} }
.ds-section-grid.ds-card-grid .col-3-small .card-stp-thumbs-buttons-wrapper { .ds-section-grid.ds-card-grid .col-3-small .card-stp-thumbs-buttons-wrapper {
display: none; display: none;
} }
.ds-section-grid.ds-card-grid .col-3-small .ds-card-link {
display: flex;
flex-direction: row;
position: relative;
align-items: center;
gap: var(--space-medium);
padding: var(--space-large);
}
.ds-section-grid.ds-card-grid .col-3-small .img-wrapper { .ds-section-grid.ds-card-grid .col-3-small .img-wrapper {
width: 120px; width: 125px;
flex-shrink: 0; flex-shrink: 0;
flex-grow: 0; flex-grow: 0;
aspect-ratio: 1/1; aspect-ratio: 1/1;
@ -5099,9 +5124,6 @@ main section {
width: 100%; width: 100%;
border-radius: var(--border-radius-medium) var(--border-radius-medium); border-radius: var(--border-radius-medium) var(--border-radius-medium);
} }
.ds-section-grid.ds-card-grid .col-3-small .ds-card-link {
inset-inline-end: 0;
}
.ds-section-grid.ds-card-grid .col-3-small .meta { .ds-section-grid.ds-card-grid .col-3-small .meta {
padding: var(--space-xsmall) 0; padding: var(--space-xsmall) 0;
align-self: flex-start; align-self: flex-start;
@ -5121,7 +5143,7 @@ main section {
padding-block-start: unset; padding-block-start: unset;
} }
.ds-section-grid.ds-card-grid .col-3-small .card-stp-button-position-wrapper { .ds-section-grid.ds-card-grid .col-3-small .card-stp-button-position-wrapper {
inset-inline-end: 28px; inset-inline-end: 10px;
} }
.ds-section-grid.ds-card-grid .col-3-small .card-stp-button-position-wrapper .card-stp-button { .ds-section-grid.ds-card-grid .col-3-small .card-stp-button-position-wrapper .card-stp-button {
display: none; display: none;
@ -5146,11 +5168,17 @@ main section {
.ds-section-grid.ds-card-grid .col-3-medium .card-stp-thumbs-buttons-wrapper { .ds-section-grid.ds-card-grid .col-3-medium .card-stp-thumbs-buttons-wrapper {
display: block; display: block;
} }
.ds-section-grid.ds-card-grid .col-3-medium .ds-card-link {
display: flex;
flex-direction: column;
gap: 0;
padding: 0;
}
.ds-section-grid.ds-card-grid .col-3-medium .img-wrapper { .ds-section-grid.ds-card-grid .col-3-medium .img-wrapper {
width: 100%; width: 100%;
position: relative; position: relative;
flex-grow: initial; flex-grow: 0;
flex-shrink: initial; flex-shrink: 0;
aspect-ratio: initial; aspect-ratio: initial;
} }
.ds-section-grid.ds-card-grid .col-3-medium:not(.placeholder) .img-wrapper > .ds-image.img > img { .ds-section-grid.ds-card-grid .col-3-medium:not(.placeholder) .img-wrapper > .ds-image.img > img {
@ -5175,15 +5203,15 @@ main section {
} }
@media (min-width: 1122px) and (min-width: 724px) and (max-width: 1122px), (min-width: 1122px) and (min-width: 1390px) { @media (min-width: 1122px) and (min-width: 724px) and (max-width: 1122px), (min-width: 1122px) and (min-width: 1390px) {
.ds-section-grid.ds-card-grid .col-3-large.ds-card.sections-card-ui { .ds-section-grid.ds-card-grid .col-3-large.ds-card.sections-card-ui {
display: grid; align-content: flex-start;
grid-template-columns: auto 1fr; }
.ds-section-grid.ds-card-grid .col-3-large.ds-card.sections-card-ui .ds-card-link {
flex-direction: row;
gap: var(--space-xlarge); gap: var(--space-xlarge);
padding: var(--space-xxlarge); padding: var(--space-xxlarge);
align-items: center;
} }
.ds-section-grid.ds-card-grid .col-3-large.ds-card.sections-card-ui .img-wrapper { .ds-section-grid.ds-card-grid .col-3-large.ds-card.sections-card-ui .img-wrapper {
width: 220px; width: 265px;
height: 220px;
} }
.ds-section-grid.ds-card-grid .col-3-large.ds-card.sections-card-ui .ds-image.img { .ds-section-grid.ds-card-grid .col-3-large.ds-card.sections-card-ui .ds-image.img {
aspect-ratio: 1/1; aspect-ratio: 1/1;
@ -5205,6 +5233,9 @@ main section {
-webkit-line-clamp: 4; -webkit-line-clamp: 4;
font-size: var(--font-size-root); font-size: var(--font-size-root);
} }
.ds-section-grid.ds-card-grid .col-3-large.ds-card.sections-card-ui .card-stp-button-hover-background {
inset-inline-start: 0;
}
} }
@media (min-width: 1390px) { @media (min-width: 1390px) {
.ds-section-grid.ds-card-grid { .ds-section-grid.ds-card-grid {
@ -5234,21 +5265,27 @@ main section {
.ds-section-grid.ds-card-grid .col-4-small { .ds-section-grid.ds-card-grid .col-4-small {
grid-row: span 1; grid-row: span 1;
grid-column: span 1; grid-column: span 1;
display: flex;
flex-direction: row;
position: relative;
align-items: center;
gap: var(--space-medium);
padding: var(--space-large); padding: var(--space-large);
} }
.ds-section-grid.ds-card-grid .col-4-small.ds-card.sections-card-ui {
padding: unset;
}
.ds-section-grid.ds-card-grid .col-4-small .stp-context-menu { .ds-section-grid.ds-card-grid .col-4-small .stp-context-menu {
display: block; display: block;
} }
.ds-section-grid.ds-card-grid .col-4-small .card-stp-thumbs-buttons-wrapper { .ds-section-grid.ds-card-grid .col-4-small .card-stp-thumbs-buttons-wrapper {
display: none; display: none;
} }
.ds-section-grid.ds-card-grid .col-4-small .ds-card-link {
display: flex;
flex-direction: row;
position: relative;
align-items: center;
gap: var(--space-medium);
padding: var(--space-large);
}
.ds-section-grid.ds-card-grid .col-4-small .img-wrapper { .ds-section-grid.ds-card-grid .col-4-small .img-wrapper {
width: 120px; width: 125px;
flex-shrink: 0; flex-shrink: 0;
flex-grow: 0; flex-grow: 0;
aspect-ratio: 1/1; aspect-ratio: 1/1;
@ -5269,9 +5306,6 @@ main section {
width: 100%; width: 100%;
border-radius: var(--border-radius-medium) var(--border-radius-medium); border-radius: var(--border-radius-medium) var(--border-radius-medium);
} }
.ds-section-grid.ds-card-grid .col-4-small .ds-card-link {
inset-inline-end: 0;
}
.ds-section-grid.ds-card-grid .col-4-small .meta { .ds-section-grid.ds-card-grid .col-4-small .meta {
padding: var(--space-xsmall) 0; padding: var(--space-xsmall) 0;
align-self: flex-start; align-self: flex-start;
@ -5291,7 +5325,7 @@ main section {
padding-block-start: unset; padding-block-start: unset;
} }
.ds-section-grid.ds-card-grid .col-4-small .card-stp-button-position-wrapper { .ds-section-grid.ds-card-grid .col-4-small .card-stp-button-position-wrapper {
inset-inline-end: 28px; inset-inline-end: 10px;
} }
.ds-section-grid.ds-card-grid .col-4-small .card-stp-button-position-wrapper .card-stp-button { .ds-section-grid.ds-card-grid .col-4-small .card-stp-button-position-wrapper .card-stp-button {
display: none; display: none;
@ -5316,11 +5350,17 @@ main section {
.ds-section-grid.ds-card-grid .col-4-medium .card-stp-thumbs-buttons-wrapper { .ds-section-grid.ds-card-grid .col-4-medium .card-stp-thumbs-buttons-wrapper {
display: block; display: block;
} }
.ds-section-grid.ds-card-grid .col-4-medium .ds-card-link {
display: flex;
flex-direction: column;
gap: 0;
padding: 0;
}
.ds-section-grid.ds-card-grid .col-4-medium .img-wrapper { .ds-section-grid.ds-card-grid .col-4-medium .img-wrapper {
width: 100%; width: 100%;
position: relative; position: relative;
flex-grow: initial; flex-grow: 0;
flex-shrink: initial; flex-shrink: 0;
aspect-ratio: initial; aspect-ratio: initial;
} }
.ds-section-grid.ds-card-grid .col-4-medium:not(.placeholder) .img-wrapper > .ds-image.img > img { .ds-section-grid.ds-card-grid .col-4-medium:not(.placeholder) .img-wrapper > .ds-image.img > img {
@ -5345,15 +5385,15 @@ main section {
} }
@media (min-width: 1390px) and (min-width: 724px) and (max-width: 1122px), (min-width: 1390px) and (min-width: 1390px) { @media (min-width: 1390px) and (min-width: 724px) and (max-width: 1122px), (min-width: 1390px) and (min-width: 1390px) {
.ds-section-grid.ds-card-grid .col-4-large.ds-card.sections-card-ui { .ds-section-grid.ds-card-grid .col-4-large.ds-card.sections-card-ui {
display: grid; align-content: flex-start;
grid-template-columns: auto 1fr; }
.ds-section-grid.ds-card-grid .col-4-large.ds-card.sections-card-ui .ds-card-link {
flex-direction: row;
gap: var(--space-xlarge); gap: var(--space-xlarge);
padding: var(--space-xxlarge); padding: var(--space-xxlarge);
align-items: center;
} }
.ds-section-grid.ds-card-grid .col-4-large.ds-card.sections-card-ui .img-wrapper { .ds-section-grid.ds-card-grid .col-4-large.ds-card.sections-card-ui .img-wrapper {
width: 220px; width: 265px;
height: 220px;
} }
.ds-section-grid.ds-card-grid .col-4-large.ds-card.sections-card-ui .ds-image.img { .ds-section-grid.ds-card-grid .col-4-large.ds-card.sections-card-ui .ds-image.img {
aspect-ratio: 1/1; aspect-ratio: 1/1;
@ -5375,6 +5415,9 @@ main section {
-webkit-line-clamp: 4; -webkit-line-clamp: 4;
font-size: var(--font-size-root); font-size: var(--font-size-root);
} }
.ds-section-grid.ds-card-grid .col-4-large.ds-card.sections-card-ui .card-stp-button-hover-background {
inset-inline-start: 0;
}
} }
.ds-card.sections-card-ui .ds-compact-pocket-saved-button { .ds-card.sections-card-ui .ds-compact-pocket-saved-button {
@ -6003,21 +6046,25 @@ main section {
box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.15); box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.15);
} }
.ds-card .ds-card-link { .ds-card .ds-card-link {
position: absolute; display: flex;
height: 100%; flex-direction: column;
width: 100%; align-items: initial;
text-decoration: none; text-decoration: none;
grid-template-columns: auto 1fr;
gap: inherit;
flex-grow: 1;
} }
.ds-card .ds-card-link:focus { .ds-card .ds-card-link:focus {
border: 0; border: 0;
outline: var(--focus-outline); outline: var(--focus-outline);
transition: none; transition: none;
} }
.ds-card > .ds-card-topic { .ds-card .ds-card-topic {
position: absolute; position: absolute;
z-index: 1; z-index: 1;
background: light-dark(#F0F0F4, var(--newtab-background-color-secondary)); background: light-dark(#F0F0F4, var(--newtab-background-color-secondary));
border-radius: var(--border-radius-small); border-radius: var(--border-radius-small);
color: var(--newtab-text-primary-color);
padding: var(--space-small); padding: var(--space-small);
margin: var(--space-small); margin: var(--space-small);
font-size: 14px; font-size: 14px;
@ -6248,6 +6295,9 @@ main section {
height: 28px; height: 28px;
font-size: var(--newtab-font-size-xsmall); font-size: var(--newtab-font-size-xsmall);
color: var(--newtab-text-topic-label-color); color: var(--newtab-text-topic-label-color);
margin: initial;
padding: initial;
background-color: initial;
} }
.ds-card-grid .sections-card-ui .card-stp-button-hover-background { .ds-card-grid .sections-card-ui .card-stp-button-hover-background {
border-radius: var(--border-radius-large) var(--border-radius-large) 0 0; border-radius: var(--border-radius-large) var(--border-radius-large) 0 0;
@ -6451,6 +6501,7 @@ main section {
.impression-observer { .impression-observer {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
pointer-events: none; pointer-events: none;

View file

@ -3406,7 +3406,10 @@ class _DSCard extends (external_React_default()).PureComponent {
})); }));
} }
} }
onThumbsUpClick() { onThumbsUpClick(event) {
event.stopPropagation();
event.preventDefault();
// Toggle active state for thumbs up button to show CSS animation // Toggle active state for thumbs up button to show CSS animation
const currentState = this.state.isThumbsUpActive; const currentState = this.state.isThumbsUpActive;
@ -3449,7 +3452,10 @@ class _DSCard extends (external_React_default()).PureComponent {
} }
}, "ActivityStream:Content")); }, "ActivityStream:Content"));
} }
onThumbsDownClick() { onThumbsDownClick(event) {
event.stopPropagation();
event.preventDefault();
// Toggle active state for thumbs down button to show CSS animation // Toggle active state for thumbs down button to show CSS animation
const currentState = this.state.isThumbsDownActive; const currentState = this.state.isThumbsDownActive;
this.setState({ this.setState({
@ -3682,6 +3688,12 @@ class _DSCard extends (external_React_default()).PureComponent {
"data-position-two": this.props["data-position-one"], "data-position-two": this.props["data-position-one"],
"data-position-three": this.props["data-position-one"], "data-position-three": this.props["data-position-one"],
"data-position-four": this.props["data-position-one"] "data-position-four": this.props["data-position-one"]
}, /*#__PURE__*/external_React_default().createElement(SafeAnchor, {
className: "ds-card-link",
dispatch: this.props.dispatch,
onLinkClick: !this.props.placeholder ? this.onLinkClick : undefined,
url: this.props.url,
title: this.props.title
}, this.props.showTopics && !this.props.mayHaveSectionsCards && this.props.topic && !isListCard && /*#__PURE__*/external_React_default().createElement("span", { }, this.props.showTopics && !this.props.mayHaveSectionsCards && this.props.topic && !isListCard && /*#__PURE__*/external_React_default().createElement("span", {
className: "ds-card-topic", className: "ds-card-topic",
"data-l10n-id": `newtab-topic-label-${this.props.topic}` "data-l10n-id": `newtab-topic-label-${this.props.topic}`
@ -3696,13 +3708,7 @@ class _DSCard extends (external_React_default()).PureComponent {
title: this.props.title, title: this.props.title,
isRecentSave: isRecentSave, isRecentSave: isRecentSave,
alt_text: alt_text alt_text: alt_text
})), /*#__PURE__*/external_React_default().createElement(SafeAnchor, { })), /*#__PURE__*/external_React_default().createElement(ImpressionStats_ImpressionStats, {
className: "ds-card-link",
dispatch: this.props.dispatch,
onLinkClick: !this.props.placeholder ? this.onLinkClick : undefined,
url: this.props.url,
title: this.props.title
}, /*#__PURE__*/external_React_default().createElement(ImpressionStats_ImpressionStats, {
flightId: this.props.flightId, flightId: this.props.flightId,
rows: [{ rows: [{
id: this.props.id, id: this.props.id,
@ -3733,7 +3739,7 @@ class _DSCard extends (external_React_default()).PureComponent {
isFakespot: isFakespot, isFakespot: isFakespot,
source: this.props.type, source: this.props.type,
firstVisibleTimestamp: this.props.firstVisibleTimestamp firstVisibleTimestamp: this.props.firstVisibleTimestamp
})), ctaButtonVariant === "variant-b" && /*#__PURE__*/external_React_default().createElement("div", { }), ctaButtonVariant === "variant-b" && /*#__PURE__*/external_React_default().createElement("div", {
className: "cta-header" className: "cta-header"
}, "Shop Now"), isFakespot ? /*#__PURE__*/external_React_default().createElement("div", { }, "Shop Now"), isFakespot ? /*#__PURE__*/external_React_default().createElement("div", {
className: "meta" className: "meta"
@ -3765,7 +3771,7 @@ class _DSCard extends (external_React_default()).PureComponent {
isSectionsCard: this.props.mayHaveSectionsCards && this.props.topic && !isListCard, isSectionsCard: this.props.mayHaveSectionsCards && this.props.topic && !isListCard,
format: format, format: format,
topic: this.props.topic topic: this.props.topic
}), /*#__PURE__*/external_React_default().createElement("div", { })), /*#__PURE__*/external_React_default().createElement("div", {
className: `card-stp-button-hover-background ${compactPocketSavedButtonClassName}` className: `card-stp-button-hover-background ${compactPocketSavedButtonClassName}`
}, /*#__PURE__*/external_React_default().createElement("div", { }, /*#__PURE__*/external_React_default().createElement("div", {
className: "card-stp-button-position-wrapper" className: "card-stp-button-position-wrapper"
@ -9520,11 +9526,8 @@ const selectLayoutRender = ({ state = {}, prefs = {} }) => {
const results = [...data]; const results = [...data];
for (let position of spocsPositions) { for (let position of spocsPositions) {
const spoc = spocsData[spocIndexPlacementMap[placementName]]; const spoc = spocsData[spocIndexPlacementMap[placementName]];
const format = spoc?.format;
// If there are no spocs left, we can stop filling positions. // If there are no spocs left, we can stop filling positions.
// Since banner-type ads are placed by row and don't use the normal spoc-position, if (!spoc) {
// dont combine with content
if (!spoc || format === "billboard" || format === "leaderboard") {
break; break;
} }
@ -9632,12 +9635,19 @@ const selectLayoutRender = ({ state = {}, prefs = {} }) => {
const placement = spocsPlacement || {}; const placement = spocsPlacement || {};
const placementName = placement.name || "newtab_spocs"; const placementName = placement.name || "newtab_spocs";
const spocsData = spocs.data[placementName]; const spocsData = spocs.data[placementName];
// We expect a spoc, spocs are loaded, and the server returned spocs. // We expect a spoc, spocs are loaded, and the server returned spocs.
if (spocs.loaded && spocsData?.items?.length) { if (spocs.loaded && spocsData?.items?.length) {
// Since banner-type ads are placed by row and don't use the normal spoc position,
// dont combine with content
const excludedSpocs = ["billboard", "leaderboard"];
const filteredSpocs = spocsData?.items?.filter(
item => !excludedSpocs.includes(item.format)
);
result = fillSpocPositionsForPlacement( result = fillSpocPositionsForPlacement(
result, result,
spocsPositions, spocsPositions,
spocsData.items, filteredSpocs,
placementName placementName
); );
} }
@ -10025,10 +10035,11 @@ function CardSection({
type: actionTypes.CARD_SECTION_IMPRESSION, type: actionTypes.CARD_SECTION_IMPRESSION,
data: { data: {
section: sectionKey, section: sectionKey,
section_position: sectionPosition section_position: sectionPosition,
is_secton_followed: following
} }
})); }));
}, [dispatch, sectionKey, sectionPosition]); }, [dispatch, sectionKey, sectionPosition, following]);
// Ref to hold the section element // Ref to hold the section element
const sectionRefs = useIntersectionObserver(handleIntersection); const sectionRefs = useIntersectionObserver(handleIntersection);

View file

@ -1227,7 +1227,8 @@ export class TelemetryFeed {
handleCardSectionUserEvent(action) { handleCardSectionUserEvent(action) {
const session = this.sessions.get(au.getPortIdOfSender(action)); const session = this.sessions.get(au.getPortIdOfSender(action));
if (session) { if (session) {
const { section, section_position, event_source } = action.data; const { section, section_position, event_source, is_secton_followed } =
action.data;
switch (action.type) { switch (action.type) {
case "BLOCK_SECTION": case "BLOCK_SECTION":
Glean.newtab.sectionsBlockSection.record({ Glean.newtab.sectionsBlockSection.record({
@ -1242,6 +1243,7 @@ export class TelemetryFeed {
newtab_visit_id: session.session_id, newtab_visit_id: session.session_id,
section, section,
section_position, section_position,
is_secton_followed,
}); });
break; break;
case "FOLLOW_SECTION": case "FOLLOW_SECTION":

View file

@ -667,12 +667,14 @@ newtab:
Recorded when a section is viewport and triggers an impression event Recorded when a section is viewport and triggers an impression event
bugs: bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1927916 - https://bugzilla.mozilla.org/show_bug.cgi?id=1927916
- https://bugzilla.mozilla.org/show_bug.cgi?id=1938215
data_reviews: data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1927916 - https://bugzilla.mozilla.org/show_bug.cgi?id=1927916
data_sensitivity: data_sensitivity:
- interaction - interaction
notification_emails: notification_emails:
- nbarrett@mozilla.com - nbarrett@mozilla.com
- mcrawford@mozilla.com
expires: never expires: never
extra_keys: extra_keys:
newtab_visit_id: *newtab_visit_id newtab_visit_id: *newtab_visit_id
@ -684,6 +686,10 @@ newtab:
description: > description: >
position of section on newtab position of section on newtab
type: string type: string
is_secton_followed:
description: >
If click belongs in a section, if that section is followed
type: boolean
send_in_pings: send_in_pings:
- newtab - newtab

View file

@ -55,9 +55,9 @@ describe("<DSCard>", () => {
it("should render a SafeAnchor", () => { it("should render a SafeAnchor", () => {
wrapper.setProps({ url: "https://foo.com" }); wrapper.setProps({ url: "https://foo.com" });
assert.equal(wrapper.children().at(1).type(), SafeAnchor); assert.equal(wrapper.children().at(0).type(), SafeAnchor);
assert.propertyVal( assert.propertyVal(
wrapper.children().at(1).props(), wrapper.children().at(0).props(),
"url", "url",
"https://foo.com" "https://foo.com"
); );
@ -65,7 +65,7 @@ describe("<DSCard>", () => {
it("should pass onLinkClick prop", () => { it("should pass onLinkClick prop", () => {
assert.propertyVal( assert.propertyVal(
wrapper.children().at(1).props(), wrapper.children().at(0).props(),
"onLinkClick", "onLinkClick",
wrapper.instance().onLinkClick wrapper.instance().onLinkClick
); );
@ -614,13 +614,19 @@ describe("<DSCard>", () => {
describe("DSCard onThumbsUpClick", () => { describe("DSCard onThumbsUpClick", () => {
it("should update state.onThumbsUpClick for onThumbsUpClick", () => { it("should update state.onThumbsUpClick for onThumbsUpClick", () => {
wrapper.setState({ isThumbsUpActive: false }); wrapper.setState({ isThumbsUpActive: false });
wrapper.instance().onThumbsUpClick(); wrapper.instance().onThumbsUpClick({
stopPropagation: () => {},
preventDefault: () => {},
});
assert.isTrue(wrapper.instance().state.isThumbsUpActive); assert.isTrue(wrapper.instance().state.isThumbsUpActive);
}); });
it("should not fire telemetry for onThumbsUpClick is clicked twice", () => { it("should not fire telemetry for onThumbsUpClick is clicked twice", () => {
wrapper.setState({ isThumbsUpActive: true }); wrapper.setState({ isThumbsUpActive: true });
wrapper.instance().onThumbsUpClick(); wrapper.instance().onThumbsUpClick({
stopPropagation: () => {},
preventDefault: () => {},
});
// state.isThumbsUpActive remains in active state // state.isThumbsUpActive remains in active state
assert.isTrue(wrapper.instance().state.isThumbsUpActive); assert.isTrue(wrapper.instance().state.isThumbsUpActive);
@ -628,7 +634,10 @@ describe("<DSCard>", () => {
}); });
it("should fire telemetry for onThumbsUpClick", () => { it("should fire telemetry for onThumbsUpClick", () => {
wrapper.instance().onThumbsUpClick(); wrapper.instance().onThumbsUpClick({
stopPropagation: () => {},
preventDefault: () => {},
});
assert.calledTwice(dispatch); assert.calledTwice(dispatch);
@ -659,7 +668,10 @@ describe("<DSCard>", () => {
dispatch, dispatch,
}); });
wrapper.instance().onThumbsDownClick(); wrapper.instance().onThumbsDownClick({
stopPropagation: () => {},
preventDefault: () => {},
});
assert.calledThrice(dispatch); assert.calledThrice(dispatch);
@ -685,7 +697,10 @@ describe("<DSCard>", () => {
it("should update state.onThumbsDownClick for onThumbsDownClick", () => { it("should update state.onThumbsDownClick for onThumbsDownClick", () => {
wrapper.setState({ isThumbsDownActive: false }); wrapper.setState({ isThumbsDownActive: false });
wrapper.instance().onThumbsDownClick(); wrapper.instance().onThumbsDownClick({
stopPropagation: () => {},
preventDefault: () => {},
});
assert.isTrue(wrapper.instance().state.isThumbsDownActive); assert.isTrue(wrapper.instance().state.isThumbsDownActive);
}); });
}); });

View file

@ -150,6 +150,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
formAutofillStorage: "resource://autofill/FormAutofillStorage.sys.mjs", formAutofillStorage: "resource://autofill/FormAutofillStorage.sys.mjs",
LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs", LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
PlacesDBUtils: "resource://gre/modules/PlacesDBUtils.sys.mjs", PlacesDBUtils: "resource://gre/modules/PlacesDBUtils.sys.mjs",
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
AddonManager: "resource://gre/modules/AddonManager.sys.mjs", AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
}); });
@ -248,10 +249,20 @@ export class ProfilesParent extends JSWindowActorParent {
let loginCount = (await lazy.LoginHelper.getAllUserFacingLogins()) let loginCount = (await lazy.LoginHelper.getAllUserFacingLogins())
.length; .length;
let db = await lazy.PlacesUtils.promiseDBConnection();
let bookmarksQuery = `SELECT count(*) FROM moz_bookmarks b
JOIN moz_bookmarks t ON t.id = b.parent
AND t.parent <> :tags_folder
WHERE b.type = :type_bookmark`;
let bookmarksQueryParams = {
tags_folder: lazy.PlacesUtils.tagsFolderId,
type_bookmark: lazy.PlacesUtils.bookmarks.TYPE_BOOKMARK,
};
let bookmarkCount = (
await db.executeCached(bookmarksQuery, bookmarksQueryParams)
)[0].getResultByIndex(0);
let stats = await lazy.PlacesDBUtils.getEntitiesStatsAndCounts(); let stats = await lazy.PlacesDBUtils.getEntitiesStatsAndCounts();
let bookmarkCount = stats.find(
item => item.entity == "moz_bookmarks"
).count;
let visitCount = stats.find( let visitCount = stats.find(
item => item.entity == "moz_historyvisits" item => item.entity == "moz_historyvisits"
).count; ).count;

View file

@ -180,7 +180,7 @@ class SelectableProfileServiceClass {
await this.#profileService.asyncFlush(); await this.#profileService.asyncFlush();
} catch (e) { } catch (e) {
try { try {
await this.#profileService.asyncFlushCurrentProfile(); await this.#profileService.asyncFlushGroupProfile();
} catch (ex) { } catch (ex) {
console.error( console.error(
`Failed to flush changes to the profiles database: ${ex}` `Failed to flush changes to the profiles database: ${ex}`
@ -841,6 +841,7 @@ class SelectableProfileServiceClass {
} }
this.#groupToolkitProfile.rootDir = await aProfile.rootDir; this.#groupToolkitProfile.rootDir = await aProfile.rootDir;
await this.#attemptFlushProfileService(); await this.#attemptFlushProfileService();
Glean.profilesDefault.updated.record();
} }
/** /**
@ -1066,6 +1067,9 @@ class SelectableProfileServiceClass {
if (!this.#currentProfile) { if (!this.#currentProfile) {
let path = this.#profileService.currentProfile.rootDir; let path = this.#profileService.currentProfile.rootDir;
this.#currentProfile = await this.#createProfile(path); this.#currentProfile = await this.#createProfile(path);
// And also set the profile selector window to show at startup (bug 1933911).
this.showProfileSelectorWindow(true);
} }
} }

View file

@ -0,0 +1,33 @@
# 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/.
# Adding a new metric? We have docs for that!
# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html
---
$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
$tags:
- 'Toolkit :: Startup and Profile System'
profiles.default:
updated:
type: event
description: >
Recorded when a new profile from a profile group becomes the default
startup profile for the group. This may happen when a profile other
than the current default is launched from the profile selector window,
or when the user has multiple profiles running at the same time and
switches app focus to a profile other than the current default. An
event is not recorded if the current default profile is launched or
gains app focus.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1918813
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1918813
notification_emails:
- jhirsch@mozilla.com
- dtownsend@mozilla.com
- nbaumgardner@mozilla.com
expires: never
telemetry_mirror: Profiles_Default_Updated

View file

@ -3,6 +3,11 @@
"use strict"; "use strict";
let lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
});
add_task(async function test_serviceInitialized() { add_task(async function test_serviceInitialized() {
await initGroupDatabase(); await initGroupDatabase();
await BrowserTestUtils.withNewTab( await BrowserTestUtils.withNewTab(
@ -33,7 +38,7 @@ add_task(async function test_serviceInitialized() {
"The SelectableProfileService is uninitialized" "The SelectableProfileService is uninitialized"
); );
// Simiulate reload by calling init on the delete card // Simulate reload by calling init on the delete card
await SpecialPowers.spawn(browser, [], async () => { await SpecialPowers.spawn(browser, [], async () => {
let deleteProfileCard = content.document.querySelector( let deleteProfileCard = content.document.querySelector(
"delete-profile-card" "delete-profile-card"
@ -60,3 +65,62 @@ add_task(async function test_serviceInitialized() {
} }
); );
}); });
add_task(async function test_bookmark_counts() {
await initGroupDatabase();
await lazy.PlacesUtils.bookmarks.eraseEverything();
await lazy.PlacesUtils.bookmarks.insert({
parentGuid: PlacesUtils.bookmarks.toolbarGuid,
title: "Example",
url: "https://example.com",
});
await lazy.PlacesUtils.bookmarks.insert({
parentGuid: PlacesUtils.bookmarks.toolbarGuid,
title: "Example 2",
url: "https://example.net",
});
await lazy.PlacesUtils.bookmarks.insert({
parentGuid: PlacesUtils.bookmarks.toolbarGuid,
title: "Example 3",
url: "https://example.org",
});
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: "about:deleteprofile",
},
async browser => {
await SpecialPowers.spawn(browser, [], async () => {
let deleteProfileCard = content.document.querySelector(
"delete-profile-card"
).wrappedJSObject;
await ContentTaskUtils.waitForCondition(
() => deleteProfileCard.initialized,
"Waiting for delete-profile-card to be initialized"
);
Assert.ok(
ContentTaskUtils.isVisible(deleteProfileCard),
"The delete-profile-card is visible"
);
let bookmarkCounts =
deleteProfileCard.shadowRoot.querySelector(
"#bookmarks b"
).textContent;
Assert.equal(
3,
bookmarkCounts,
"Should display expected bookmarks count"
);
});
}
);
await lazy.PlacesUtils.bookmarks.eraseEverything();
});

View file

@ -19,4 +19,9 @@ add_task(async function test_dbLazilyCreated() {
SelectableProfileService.initialized, SelectableProfileService.initialized,
`Selectable Profile Service should be initialized because the store id is ${SelectableProfileService.groupToolkitProfile.storeID}` `Selectable Profile Service should be initialized because the store id is ${SelectableProfileService.groupToolkitProfile.storeID}`
); );
ok(
SelectableProfileService.groupToolkitProfile.showProfileSelector,
"Once the user has created a second profile, ShowSelector should be set to true"
);
}); });

View file

@ -22,6 +22,14 @@ add_task(async function test_updateDefaultProfileOnWindowSwitch() {
`The SelectableProfileService rootDir is correct` `The SelectableProfileService rootDir is correct`
); );
Services.telemetry.clearEvents();
Services.fog.testResetFOG();
is(
null,
Glean.profilesDefault.updated.testGetValue(),
"We have not recorded any Glean data yet"
);
// Override // Override
gProfileService.currentProfile.rootDir = "bad"; gProfileService.currentProfile.rootDir = "bad";
@ -40,7 +48,17 @@ add_task(async function test_updateDefaultProfileOnWindowSwitch() {
`The SelectableProfileService rootDir is correct` `The SelectableProfileService rootDir is correct`
); );
await BrowserTestUtils.closeWindow(w); let testEvents = Glean.profilesDefault.updated.testGetValue();
Assert.equal(
1,
testEvents.length,
"Should have recorded the default profile updated event exactly once"
);
TelemetryTestUtils.assertEvents([["profiles", "default", "updated"]], {
category: "profiles",
method: "default",
});
await BrowserTestUtils.closeWindow(w);
await SelectableProfileService.uninit(); await SelectableProfileService.uninit();
}); });

View file

@ -7,6 +7,10 @@ const { Sqlite } = ChromeUtils.importESModule(
"resource://gre/modules/Sqlite.sys.mjs" "resource://gre/modules/Sqlite.sys.mjs"
); );
const { TelemetryTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/TelemetryTestUtils.sys.mjs"
);
/** /**
* A mock toolkit profile. * A mock toolkit profile.
*/ */

View file

@ -425,7 +425,7 @@ export var SessionStore = {
Override the value of the closedTabsFromAllWindows preference. Override the value of the closedTabsFromAllWindows preference.
* @param {boolean} [aOptions.closedTabsFromClosedWindows] * @param {boolean} [aOptions.closedTabsFromClosedWindows]
Override the value of the closedTabsFromClosedWindows preference. Override the value of the closedTabsFromClosedWindows preference.
* @returns {TabGroupStateData[]} * @returns {ClosedTabGroupStateData[]}
*/ */
getClosedTabGroups: function ss_getClosedTabGroups(aOptions) { getClosedTabGroups: function ss_getClosedTabGroups(aOptions) {
return SessionStoreInternal.getClosedTabGroups(aOptions); return SessionStoreInternal.getClosedTabGroups(aOptions);
@ -824,7 +824,7 @@ export var SessionStore = {
* Retrieve the tab group state of a saved tab group by ID. * Retrieve the tab group state of a saved tab group by ID.
* *
* @param {string} tabGroupId * @param {string} tabGroupId
* @returns {TabGroupStateData|undefined} * @returns {SavedTabGroupStateData|undefined}
*/ */
getSavedTabGroup(tabGroupId) { getSavedTabGroup(tabGroupId) {
return SessionStoreInternal.getSavedTabGroup(tabGroupId); return SessionStoreInternal.getSavedTabGroup(tabGroupId);
@ -832,7 +832,7 @@ export var SessionStore = {
/** /**
* Returns all tab groups that were saved in this session. * Returns all tab groups that were saved in this session.
* @returns {TabGroupStateData[]} * @returns {SavedTabGroupStateData[]}
*/ */
getSavedTabGroups() { getSavedTabGroups() {
return SessionStoreInternal.getSavedTabGroups(); return SessionStoreInternal.getSavedTabGroups();
@ -977,7 +977,7 @@ var SessionStoreInternal = {
// states for all recently closed windows // states for all recently closed windows
_closedWindows: [], _closedWindows: [],
/** @type {TabGroupStateData[]} states for all closed tab groups */ /** @type {SavedTabGroupStateData[]} states for all saved+closed tab groups */
_savedGroups: [], _savedGroups: [],
// collection of session states yet to be restored // collection of session states yet to be restored
@ -2431,6 +2431,7 @@ var SessionStoreInternal = {
// Insert winData at the right position. // Insert winData at the right position.
this._closedWindows.splice(index, 0, winData); this._closedWindows.splice(index, 0, winData);
this._capClosedWindows(); this._capClosedWindows();
this._saveOpenTabGroupsOnClose(winData);
this._closedObjectsChanged = true; this._closedObjectsChanged = true;
// The first time we close a window, ensure it can be restored from the // The first time we close a window, ensure it can be restored from the
// hidden window. // hidden window.
@ -2463,6 +2464,72 @@ var SessionStoreInternal = {
} }
}, },
/**
* If there are any open tab groups in this closing window, move those
* tab groups to the list of saved tab groups so that the user doesn't
* lose them.
*
* The normal API for saving a tab group is `this.addSavedTabGroup`.
* `this.addSavedTabGroup` relies on a MozTabbrowserTabGroup DOM element
* and relies on passing the tab group's MozTabbrowserTab DOM elements to
* `this.maybeSaveClosedTab`. Since this method might be dealing with a closed
* window that has no DOM, this method has a separate but similar
* implementation to `this.addSavedTabGroup` and `this.maybeSaveClosedTab`.
*
* @param {WindowStateData} closedWinData
* @returns {void}
*/
_saveOpenTabGroupsOnClose(closedWinData) {
/** @type Map<string, SavedTabGroupStateData> */
let newlySavedTabGroups = new Map();
// Convert any open tab groups into saved tab groups in place
closedWinData.groups = closedWinData.groups.map(tabGroupState =>
lazy.TabGroupState.savedInClosedWindow(
tabGroupState,
closedWinData.closedId
)
);
for (let tabGroupState of closedWinData.groups) {
newlySavedTabGroups.set(tabGroupState.id, tabGroupState);
}
for (let tIndex = 0; tIndex < closedWinData.tabs.length; tIndex++) {
let tabState = closedWinData.tabs[tIndex];
if (!tabState.groupId) {
continue;
}
if (!newlySavedTabGroups.has(tabState.groupId)) {
continue;
}
if (this._shouldSaveTabState(tabState)) {
// Ensure the index is in bounds.
let activeIndex = tabState.index;
activeIndex = Math.min(activeIndex, tabState.entries.length - 1);
activeIndex = Math.max(activeIndex, 0);
if (!(activeIndex in tabState.entries)) {
continue;
}
let title =
tabState.entries[activeIndex].title ||
tabState.entries[activeIndex].url;
let tabData = {
state: tabState,
title,
image: tabState.image,
pos: tIndex,
closedAt: Date.now(),
closedId: this._nextClosedId++,
};
newlySavedTabGroups.get(tabState.groupId).tabs.push(tabData);
}
}
// Add saved tab group references to saved tab group state.
for (let tabGroupToSave of newlySavedTabGroups.values()) {
this._recordSavedTabGroupState(tabGroupToSave);
}
},
/** /**
* On quit application granted * On quit application granted
*/ */
@ -2980,7 +3047,8 @@ var SessionStoreInternal = {
let closedGroups = this._windows[win.__SSi].closedGroups; let closedGroups = this._windows[win.__SSi].closedGroups;
let tabGroupState = this.buildClosedTabGroupState(tabGroup, win); let tabGroupState = lazy.TabGroupState.closed(tabGroup, win.__SSi);
tabGroupState.tabs = this._collectClosedTabsForTabGroup(tabGroup.tabs, win);
// TODO(jswinarton) it's unclear if updating lastClosedTabGroupCount is // TODO(jswinarton) it's unclear if updating lastClosedTabGroupCount is
// necessary when restoring tab groups — it largely depends on how we // necessary when restoring tab groups — it largely depends on how we
@ -2994,35 +3062,29 @@ var SessionStoreInternal = {
}, },
/** /**
* Build `TabGroupStateData` for a tab group that is about to close. * Collect closed tab states for a tab group that is about to be
* saved and/or closed.
* *
* The `TabGroupState` module is generally responsible for collecting * The `TabGroupState` module is generally responsible for collecting
* tab group state data, but the session store has additional requirements * tab group state data, but the session store has additional requirements
* for closed tabs that are currently only implemented in * for closed tabs that are currently only implemented in
* `SessionStoreInternal.maybeSaveClosedTab`. This method uses `TabGroupState` * `SessionStoreInternal.maybeSaveClosedTab`. This method converts the tabs
* to collect information and then enriches it with metadata specific to tab * in a tab group into the closed tab data schema format required for
* groups that are saved or closed. * closed or saved groups.
* *
* @param {MozTabbrowserTabGroup} tabGroup * @param {MozTabbrowserTab[]} tabs
* @param {Window} win * @param {Window} win
* @returns {TabGroupStateData} * @returns {ClosedTabStateData[]}
* Returns tab group state data from `TabGroupState` but enriched with
* metadata specific to saved or closed tab groups.
*/ */
buildClosedTabGroupState(tabGroup, win) { _collectClosedTabsForTabGroup(tabs, win) {
let tabGroupState = lazy.TabGroupState.collect(tabGroup); let closedTabs = [];
tabGroupState.closedAt = Date.now(); tabs.forEach(tab => {
// Only save tab state for tabs that qualify
tabGroupState.tabs = [];
tabGroup.tabs.forEach(tab => {
let tabState = lazy.TabState.collect(tab, TAB_CUSTOM_VALUES.get(tab)); let tabState = lazy.TabState.collect(tab, TAB_CUSTOM_VALUES.get(tab));
this.maybeSaveClosedTab(win, tab, tabState, { this.maybeSaveClosedTab(win, tab, tabState, {
closedTabsArray: tabGroupState.tabs, closedTabsArray: closedTabs,
}); });
}); });
return closedTabs;
return tabGroupState;
}, },
/** /**
@ -3108,6 +3170,7 @@ var SessionStoreInternal = {
* Tab reference * Tab reference
*/ */
resetBrowserToLazyState(aTab) { resetBrowserToLazyState(aTab) {
const gBrowser = aTab.ownerGlobal.gBrowser;
let browser = aTab.linkedBrowser; let browser = aTab.linkedBrowser;
// Browser is already lazy so don't do anything. // Browser is already lazy so don't do anything.
if (!browser.isConnected) { if (!browser.isConnected) {
@ -3121,6 +3184,7 @@ var SessionStoreInternal = {
this._lastKnownFrameLoader.delete(browser.permanentKey); this._lastKnownFrameLoader.delete(browser.permanentKey);
this._crashedBrowsers.delete(browser.permanentKey); this._crashedBrowsers.delete(browser.permanentKey);
aTab.removeAttribute("crashed"); aTab.removeAttribute("crashed");
gBrowser.tabContainer.updateTabIndicatorAttr(aTab);
let { userTypedValue = null, userTypedClear = 0 } = browser; let { userTypedValue = null, userTypedClear = 0 } = browser;
let hasStartedLoad = browser.didStartLoadSinceLastUserTyping(); let hasStartedLoad = browser.didStartLoadSinceLastUserTyping();
@ -3657,6 +3721,10 @@ var SessionStoreInternal = {
return this._LAST_ACTION_CLOSED_TAB; return this._LAST_ACTION_CLOSED_TAB;
}, },
/**
* @param {number} aClosedId
* @returns {WindowStateData|undefined}
*/
getClosedWindowDataByClosedId: function ssi_getClosedWindowDataByClosedId( getClosedWindowDataByClosedId: function ssi_getClosedWindowDataByClosedId(
aClosedId aClosedId
) { ) {
@ -3948,7 +4016,7 @@ var SessionStoreInternal = {
* @param {boolean} [aOptions.private = false] * @param {boolean} [aOptions.private = false]
* @param {boolean} [aOptions.closedTabsFromAllWindows] * @param {boolean} [aOptions.closedTabsFromAllWindows]
* @param {boolean} [aOptions.closedTabsFromClosedWindows] * @param {boolean} [aOptions.closedTabsFromClosedWindows]
* @returns {TabGroupStateData[]} * @returns {ClosedTabGroupStateData[]}
*/ */
getClosedTabGroups: function ssi_getClosedTabGroups(aOptions) { getClosedTabGroups: function ssi_getClosedTabGroups(aOptions) {
const sourceOptions = this._prepareClosedTabOptions(aOptions); const sourceOptions = this._prepareClosedTabOptions(aOptions);
@ -4291,7 +4359,7 @@ var SessionStoreInternal = {
); );
if (savedGroupIndex < 0) { if (savedGroupIndex < 0) {
throw Components.Exception( throw Components.Exception(
"Closed tab group not found", "Saved tab group not found",
Cr.NS_ERROR_INVALID_ARG Cr.NS_ERROR_INVALID_ARG
); );
} }
@ -4301,6 +4369,9 @@ var SessionStoreInternal = {
this.removeClosedTabData({}, savedGroup.tabs, i); this.removeClosedTabData({}, savedGroup.tabs, i);
} }
this._savedGroups.splice(savedGroupIndex, 1); this._savedGroups.splice(savedGroupIndex, 1);
// Notify of changes to closed objects.
this._closedObjectsChanged = true;
this._notifyOfClosedObjectsChange(); this._notifyOfClosedObjectsChange();
}, },
@ -4366,8 +4437,32 @@ var SessionStoreInternal = {
return this._closedWindows.length; return this._closedWindows.length;
}, },
/**
* @returns {WindowStateData[]}
*/
getClosedWindowData: function ssi_getClosedWindowData() { getClosedWindowData: function ssi_getClosedWindowData() {
return Cu.cloneInto(this._closedWindows, {}); let closedWindows = Cu.cloneInto(this._closedWindows, {});
for (let closedWinData of closedWindows) {
this._trimSavedTabGroupMetadataInClosedWindow(closedWinData);
}
return closedWindows;
},
/**
* If a closed window has a saved tab group inside of it, the closed window's
* `groups` array entry will be a reference to a saved tab group entry.
* However, since saved tab groups contain a lot of extra and duplicate
* information, like their `tabs`, we only want to surface some of the
* metadata about the saved tab groups to outside clients.
*
* @param {WindowStateData} closedWinData
* @returns {void} mutates the argument `closedWinData`
*/
_trimSavedTabGroupMetadataInClosedWindow(closedWinData) {
let abbreviatedGroups = closedWinData.groups?.map(tabGroup =>
lazy.TabGroupState.abbreviated(tabGroup)
);
closedWinData.groups = Cu.cloneInto(abbreviatedGroups, {});
}, },
maybeDontRestoreTabs(aWindow) { maybeDontRestoreTabs(aWindow) {
@ -4394,6 +4489,17 @@ var SessionStoreInternal = {
let state = { windows: this._removeClosedWindow(aIndex) }; let state = { windows: this._removeClosedWindow(aIndex) };
delete state.windows[0].closedAt; // Window is now open. delete state.windows[0].closedAt; // Window is now open.
// If any saved tab groups are in the closed window, convert the saved tab
// groups into open tab groups in the closed window and then forget the saved
// tab groups. This should have the effect of "moving" the saved tab groups
// into the window that's about to be restored.
this._trimSavedTabGroupMetadataInClosedWindow(state.windows[0]);
for (let tabGroup of state.windows[0].groups) {
if (this.getSavedTabGroup(tabGroup.id)) {
this.forgetSavedTabGroup(tabGroup.id);
}
}
let window = this._openWindowWithState(state); let window = this._openWindowWithState(state);
this.windowToFocus = window; this.windowToFocus = window;
WINDOW_SHOWING_PROMISES.get(window).promise.then(win => WINDOW_SHOWING_PROMISES.get(window).promise.then(win =>
@ -4818,6 +4924,7 @@ var SessionStoreInternal = {
); );
} }
const gBrowser = aTab.ownerGlobal.gBrowser;
let browser = aTab.linkedBrowser; let browser = aTab.linkedBrowser;
if (!this._crashedBrowsers.has(browser.permanentKey)) { if (!this._crashedBrowsers.has(browser.permanentKey)) {
return; return;
@ -4837,6 +4944,7 @@ var SessionStoreInternal = {
// a flash of the about:tabcrashed page after selecting // a flash of the about:tabcrashed page after selecting
// the revived tab. // the revived tab.
aTab.removeAttribute("crashed"); aTab.removeAttribute("crashed");
gBrowser.tabContainer.updateTabIndicatorAttr(aTab);
browser.loadURI(lazy.blankURI, { browser.loadURI(lazy.blankURI, {
triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({ triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({
@ -7597,19 +7705,31 @@ var SessionStoreInternal = {
* @param {MozTabbrowserTabGroup} tabGroup * @param {MozTabbrowserTabGroup} tabGroup
*/ */
addSavedTabGroup(tabGroup) { addSavedTabGroup(tabGroup) {
if (this.getSavedTabGroup(tabGroup.id)) { let tabGroupState = lazy.TabGroupState.savedInOpenWindow(
return;
}
let tabGroupState = this.buildClosedTabGroupState(
tabGroup, tabGroup,
tabGroup.ownerGlobal.__SSi
);
tabGroupState.tabs = this._collectClosedTabsForTabGroup(
tabGroup.tabs,
tabGroup.ownerGlobal tabGroup.ownerGlobal
); );
this._savedGroups.push(tabGroupState); this._recordSavedTabGroupState(tabGroupState);
},
/**
* @param {SavedTabGroupStateData} savedTabGroupState
* @returns {void}
*/
_recordSavedTabGroupState(savedTabGroupState) {
if (this.getSavedTabGroup(savedTabGroupState.id)) {
return;
}
this._savedGroups.push(savedTabGroupState);
}, },
/** /**
* @param {string} tabGroupId * @param {string} tabGroupId
* @returns {TabGroupStateData|undefined} * @returns {SavedTabGroupStateData|undefined}
*/ */
getSavedTabGroup(tabGroupId) { getSavedTabGroup(tabGroupId) {
return this._savedGroups.find( return this._savedGroups.find(
@ -7619,7 +7739,7 @@ var SessionStoreInternal = {
/** /**
* Returns all tab groups that were saved in this session. * Returns all tab groups that were saved in this session.
* @returns {TabGroupStateData[]} * @returns {SavedTabGroupStateData[]}
*/ */
getSavedTabGroups() { getSavedTabGroups() {
return Cu.cloneInto(this._savedGroups, {}); return Cu.cloneInto(this._savedGroups, {});
@ -7628,7 +7748,7 @@ var SessionStoreInternal = {
/** /**
* @param {Window|{sourceWindowId: string}|{sourceClosedId: number}} source * @param {Window|{sourceWindowId: string}|{sourceClosedId: number}} source
* @param {string} tabGroupId * @param {string} tabGroupId
* @returns {TabGroupStateData|undefined} * @returns {ClosedTabGroupStateData|undefined}
*/ */
getClosedTabGroup(source, tabGroupId) { getClosedTabGroup(source, tabGroupId) {
let winData = this._resolveClosedDataSource(source); let winData = this._resolveClosedDataSource(source);
@ -7710,6 +7830,22 @@ var SessionStoreInternal = {
); );
} }
// If this saved tab group is present in a closed window, then we need to
// remove references to this saved tab group from that closed window. The
// result should be as if the saved tab group "moved" from the closed window
// into the `targetWindow`.
if (tabGroupData.windowClosedId) {
let closedWinData = this.getClosedWindowDataByClosedId(
tabGroupData.windowClosedId
);
if (closedWinData) {
this._removeSavedTabGroupFromClosedWindow(
closedWinData,
tabGroupData.id
);
}
}
let group = this._createTabsForSavedOrClosedTabGroup( let group = this._createTabsForSavedOrClosedTabGroup(
tabGroupData, tabGroupData,
targetWindow targetWindow
@ -7719,6 +7855,11 @@ var SessionStoreInternal = {
return group; return group;
}, },
/**
* @param {ClosedTabGroupStateData|SavedTabGroupStateData} tabGroupData
* @param {Window} targetWindow
* @returns {MozTabbrowserTabGroup}
*/
_createTabsForSavedOrClosedTabGroup(tabGroupData, targetWindow) { _createTabsForSavedOrClosedTabGroup(tabGroupData, targetWindow) {
let tabDataList = tabGroupData.tabs.map(tab => tab.state); let tabDataList = tabGroupData.tabs.map(tab => tab.state);
let tabs = targetWindow.gBrowser.createTabsForSessionRestore( let tabs = targetWindow.gBrowser.createTabsForSessionRestore(
@ -7755,6 +7896,17 @@ var SessionStoreInternal = {
} }
} }
}, },
/**
* @param {WindowStateData} closedWinData
* @param {string} tabGroupId
* @returns {void} modifies the data in argument `closedWinData`
*/
_removeSavedTabGroupFromClosedWindow(closedWinData, tabGroupId) {
removeWhere(closedWinData.groups, tabGroup => tabGroup.id == tabGroupId);
removeWhere(closedWinData.tabs, tab => tab.groupId == tabGroupId);
this._closedObjectsChanged = true;
},
}; };
/** /**
@ -7999,5 +8151,18 @@ var LastSession = {
}, },
}; };
/**
* @template T
* @param {T[]} array
* @param {function(T):boolean} predicate
*/
function removeWhere(array, predicate) {
for (let i = array.length - 1; i >= 0; i--) {
if (predicate(array[i])) {
array.splice(i, 1);
}
}
}
// Exposed for tests // Exposed for tests
export const _LastSession = LastSession; export const _LastSession = LastSession;

View file

@ -4,6 +4,7 @@
/** /**
* @typedef {object} TabGroupStateData * @typedef {object} TabGroupStateData
* State of a tab group inside of an open window.
* @property {string} id * @property {string} id
* Unique ID of the tab group. * Unique ID of the tab group.
* @property {string} name * @property {string} name
@ -14,6 +15,39 @@
* Whether the tab group is collapsed or expanded in the tab strip. * Whether the tab group is collapsed or expanded in the tab strip.
*/ */
/**
* @typedef {TabGroupStateData} ClosedTabGroupStateData
* State of a tab group that was explicitly closed by the user.
* @property {number} closedAt
* Timestamp from `Date.now()`.
* @property {string} sourceWindowId
* Window that the tab group was in before it was closed.
* @property {ClosedTabStateData[]} tabs
* Copy of all tab data for the tabs that were in this tab group
* at the time it was closed.
*/
/**
* @typedef {TabGroupStateData} SavedTabGroupStateData
* State of a tab group that was explicitly saved and closed by the user
* or implicitly saved on behalf of the user when the user explicitly closed
* a window.
* @property {true} saved
* Indicates that the tab group was saved explicitly by the user or
* automatically by the browser.
* @property {number} closedAt
* Timestamp from `Date.now()`.
* @property {string} [sourceWindowId]
* Window that the tab group was in before a user explicitly saved it. Not set
* when the tab group is saved automatically due to a window closing.
* @property {number} [windowClosedId]
* `closedId` of the closed window if this tab group was saved automatically
* due to a window closing. Not set when a user explicitly saves a tab group.
* @property {ClosedTabStateData[]} tabs
* Copy of all tab data for the tabs that were in this tab group
* at the time it was saved.
*/
/** /**
* Module that contains tab group state collection methods. * Module that contains tab group state collection methods.
*/ */
@ -34,6 +68,88 @@ class _TabGroupState {
collapsed: tabGroup.collapsed, collapsed: tabGroup.collapsed,
}; };
} }
/**
* Create initial state for a tab group that is about to close inside of an
* open window.
*
* The caller is responsible for hydrating closed tabs data into `tabs`
* using the `TabState` class.
*
* @param {MozTabbrowserTabGroup} tabGroup
* @param {string} sourceWindowId
* `window.__SSi` window ID of the open window where the tab group is closing.
* @returns {ClosedTabGroupStateData}
*/
closed(tabGroup, sourceWindowId) {
let closedData = this.collect(tabGroup);
closedData.closedAt = Date.now();
closedData.sourceWindowId = sourceWindowId;
closedData.tabs = [];
return closedData;
}
/**
* Create initial state for a tab group that is about to be explicitly saved
* by the user inside of an open window.
*
* The caller is responsible for hydrating closed tabs data into `tabs`
* using the `TabState` class.
*
* @param {MozTabbrowserTabGroup} tabGroup
* @param {string} sourceWindowId
* `window.__SSi` window ID of the open window where the tab group
* is being saved.
* @returns {SavedTabGroupStateData}
*/
savedInOpenWindow(tabGroup, sourceWindowId) {
let savedData = this.closed(tabGroup, sourceWindowId);
savedData.saved = true;
return savedData;
}
/**
* Convert an existing tab group's state to saved tab group state. The input
* tab group state should come from a closing/closed window; if you need the
* state of a tab group that still exists in the browser, use `savedInOpenWindow`
* instead. This should be used when a tab group is being saved automatically
* due to a user closing a window containing some tab groups.
*
* The caller is responsible for hydrating closed tabs data into `tabs`
* using the `TabState` class.
*
* @param {TabGroupStateData} tabGroupState
* @param {number} windowClosedId
* `WindowStateData.closedId` of the closed window from which this tab group
* should be automatically saved.
*/
savedInClosedWindow(tabGroupState, windowClosedId) {
let savedData = tabGroupState;
savedData.saved = true;
savedData.closedAt = Date.now();
savedData.windowClosedId = windowClosedId;
savedData.tabs = [];
return savedData;
}
/**
* In cases where we surface tab group metadata to external callers, we may want
* to provide an abbreviated set of metadata. This can help hide internal details
* from browser code or from extensions. Hiding those details can make the public
* session APIs simpler and more stable.
*
* @param {TabGroupStateData} tabGroupState
* @returns {TabGroupStateData}
*/
abbreviated(tabGroupState) {
let abbreviatedData = {
id: tabGroupState.id,
name: tabGroupState.name,
color: tabGroupState.color,
collapsed: tabGroupState.collapsed,
};
return abbreviatedData;
}
} }
export const TabGroupState = new _TabGroupState(); export const TabGroupState = new _TabGroupState();

View file

@ -296,6 +296,14 @@ tags = "os_integration"
["browser_tab_groups_restore_simple.js"] ["browser_tab_groups_restore_simple.js"]
["browser_tab_groups_save_on_removeAllTabsBut.js"]
["browser_tab_groups_save_on_removeTabsToTheEnd.js"]
["browser_tab_groups_save_on_removeTabsToTheStart.js"]
["browser_tab_groups_save_on_window_close.js"]
["browser_tab_groups_saved.js"] ["browser_tab_groups_saved.js"]
["browser_tab_groups_state.js"] ["browser_tab_groups_state.js"]

View file

@ -62,6 +62,7 @@ add_task(async function test_RestoreSingleGroup() {
"tab group collapsed state should be restored" "tab group collapsed state should be restored"
); );
win.gBrowser.removeTabGroup(tabGroup);
await BrowserTestUtils.closeWindow(win); await BrowserTestUtils.closeWindow(win);
forgetClosedWindows(); forgetClosedWindows();
}); });

View file

@ -0,0 +1,127 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const ORIG_STATE = SessionStore.getBrowserState();
registerCleanupFunction(async () => {
await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE);
});
add_task(async function test_removeAllTabsBut_default_save_tab_groups() {
let win = await promiseNewWindowLoaded();
let tab1 = BrowserTestUtils.addTab(win.gBrowser, "about:robots");
let tab2 = BrowserTestUtils.addTab(win.gBrowser, "about:robots");
let tab3 = BrowserTestUtils.addTab(win.gBrowser, "about:robots");
let tabGroup = win.gBrowser.addTabGroup([tab2, tab3]);
let tabGroupId = tabGroup.id;
await TabStateFlusher.flushWindow(win);
Assert.equal(
SessionStore.getSavedTabGroups().length,
0,
"should not be any saved tab groups to start"
);
Assert.equal(
SessionStore.getClosedTabGroups(win).length,
0,
"should not be any closed tab groups to start"
);
Assert.equal(
SessionStore.getClosedTabDataForWindow(win).length,
0,
"should not be any closed tabs"
);
win.gBrowser.removeAllTabsBut(tab1);
await TestUtils.waitForCondition(
() => win.gBrowser.tabs.length == 1,
"waiting for other tabs to close"
);
await TabStateFlusher.flushWindow(win);
Assert.equal(
SessionStore.getSavedTabGroups().length,
1,
"should have saved a tab group"
);
Assert.ok(
SessionStore.getSavedTabGroup(tabGroupId),
"should have saved the tab group that was closed"
);
Assert.equal(
SessionStore.getClosedTabGroups(win).length,
0,
"should only have saved the tab group, not deleted it"
);
Assert.equal(
SessionStore.getClosedTabDataForWindow(win).length,
0,
"should not be any closed tabs"
);
await BrowserTestUtils.closeWindow(win);
forgetClosedWindows();
forgetSavedTabGroups();
});
add_task(async function test_removeAllTabsBut_suppress_saving_tab_groups() {
let win = await promiseNewWindowLoaded();
let tab1 = BrowserTestUtils.addTab(win.gBrowser, "about:robots");
let tab2 = BrowserTestUtils.addTab(win.gBrowser, "about:robots");
let tab3 = BrowserTestUtils.addTab(win.gBrowser, "about:robots");
win.gBrowser.addTabGroup([tab2, tab3]);
await TabStateFlusher.flushWindow(win);
Assert.equal(
SessionStore.getSavedTabGroups().length,
0,
"should not be any saved tab groups to start"
);
Assert.equal(
SessionStore.getClosedTabGroups(win).length,
0,
"should not be any closed tab groups to start"
);
Assert.equal(
SessionStore.getClosedTabDataForWindow(win).length,
0,
"should not be any closed tabs"
);
win.gBrowser.removeAllTabsBut(tab1, { skipSessionStore: true });
await TestUtils.waitForCondition(
() => win.gBrowser.tabs.length == 1,
"waiting for other tabs to close"
);
await TabStateFlusher.flushWindow(win);
Assert.equal(
SessionStore.getSavedTabGroups().length,
0,
"should not have saved the tab group"
);
Assert.equal(
SessionStore.getClosedTabGroups(win),
0,
"should still not have any deleted tab groups"
);
Assert.equal(
SessionStore.getClosedTabDataForWindow(win).length,
0,
"should be 0 closed tabs"
);
await BrowserTestUtils.closeWindow(win);
forgetClosedWindows();
forgetSavedTabGroups();
});

View file

@ -0,0 +1,112 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const ORIG_STATE = SessionStore.getBrowserState();
registerCleanupFunction(async () => {
await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE);
});
add_task(async function test_removeTabsToTheEnd_saves_tab_groups() {
let win = await promiseNewWindowLoaded();
let tab1 = BrowserTestUtils.addTab(win.gBrowser, "about:robots");
let tab2 = BrowserTestUtils.addTab(win.gBrowser, "about:robots");
let tab3 = BrowserTestUtils.addTab(win.gBrowser, "about:robots");
let tabGroup = win.gBrowser.addTabGroup([tab2, tab3], { insertBefore: tab2 });
let tabGroupId = tabGroup.id;
await TabStateFlusher.flushWindow(win);
Assert.equal(
win.gBrowser.tabs.length,
4,
"should be 4 tabs: 1 tab from new window and 3 tabs just created"
);
Assert.equal(
SessionStore.getSavedTabGroups().length,
0,
"should not be any saved tab groups to start"
);
win.gBrowser.removeTabsToTheEndFrom(tab1);
await TestUtils.waitForCondition(
() => win.gBrowser.tabs.length == 2,
"waiting for tabs to the end to close"
);
await TabStateFlusher.flushWindow(win);
Assert.equal(
SessionStore.getSavedTabGroups().length,
1,
"should have saved a tab group"
);
Assert.ok(
SessionStore.getSavedTabGroup(tabGroupId),
"should have saved the tab group that was closed"
);
await BrowserTestUtils.closeWindow(win);
forgetClosedWindows();
forgetSavedTabGroups();
});
add_task(
async function test_removeTabsToTheEnd_only_saves_when_whole_tab_group_removed() {
let win = await promiseNewWindowLoaded();
let tab1 = BrowserTestUtils.addTab(win.gBrowser, "about:robots");
let tab2 = BrowserTestUtils.addTab(win.gBrowser, "about:robots");
BrowserTestUtils.addTab(win.gBrowser, "about:robots");
let tabGroup = win.gBrowser.addTabGroup([tab1, tab2], {
insertBefore: tab1,
});
await TabStateFlusher.flushWindow(win);
Assert.equal(
win.gBrowser.tabs.length,
4,
"should be 4 tabs: 1 tab from new window and 3 tabs just created"
);
Assert.equal(
SessionStore.getSavedTabGroups().length,
0,
"should not be any saved tab groups to start"
);
win.gBrowser.removeTabsToTheEndFrom(tab1);
await TestUtils.waitForCondition(
() => win.gBrowser.tabs.length == 2,
"waiting for tabs to the end to close"
);
await TabStateFlusher.flushWindow(win);
Assert.equal(
SessionStore.getSavedTabGroups().length,
0,
"should not have saved a tab group because one did not close"
);
Assert.deepEqual(
win.gBrowser.tabGroups,
[tabGroup],
"tab group should still exist in the tab strip"
);
Assert.deepEqual(
tabGroup.tabs,
[tab1],
"tab group should just have one tab left"
);
win.gBrowser.removeTabGroup(tabGroup);
await BrowserTestUtils.closeWindow(win);
forgetClosedWindows();
forgetSavedTabGroups();
}
);

View file

@ -0,0 +1,112 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const ORIG_STATE = SessionStore.getBrowserState();
registerCleanupFunction(async () => {
await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE);
});
add_task(async function test_removeTabsToTheStart_saves_tab_groups() {
let win = await promiseNewWindowLoaded();
let tab1 = BrowserTestUtils.addTab(win.gBrowser, "about:robots");
let tab2 = BrowserTestUtils.addTab(win.gBrowser, "about:robots");
let tab3 = BrowserTestUtils.addTab(win.gBrowser, "about:robots");
let tabGroup = win.gBrowser.addTabGroup([tab1, tab2], { insertBefore: tab1 });
let tabGroupId = tabGroup.id;
await TabStateFlusher.flushWindow(win);
Assert.equal(
win.gBrowser.tabs.length,
4,
"should be 4 tabs: 1 tab from new window and 3 tabs just created"
);
Assert.equal(
SessionStore.getSavedTabGroups().length,
0,
"should not be any saved tab groups to start"
);
win.gBrowser.removeTabsToTheStartFrom(tab3);
await TestUtils.waitForCondition(
() => win.gBrowser.tabs.length == 1,
"waiting for other tabs to close"
);
await TabStateFlusher.flushWindow(win);
Assert.equal(
SessionStore.getSavedTabGroups().length,
1,
"should have saved a tab group"
);
Assert.ok(
SessionStore.getSavedTabGroup(tabGroupId),
"should have saved the tab group that was closed"
);
await BrowserTestUtils.closeWindow(win);
forgetClosedWindows();
forgetSavedTabGroups();
});
add_task(
async function test_removeTabsToTheStart_only_saves_when_whole_tab_group_removed() {
let win = await promiseNewWindowLoaded();
BrowserTestUtils.addTab(win.gBrowser, "about:robots");
let tab2 = BrowserTestUtils.addTab(win.gBrowser, "about:robots");
let tab3 = BrowserTestUtils.addTab(win.gBrowser, "about:robots");
let tabGroup = win.gBrowser.addTabGroup([tab2, tab3], {
insertBefore: tab2,
});
await TabStateFlusher.flushWindow(win);
Assert.equal(
win.gBrowser.tabs.length,
4,
"should be 4 tabs: 1 tab from new window and 3 tabs just created"
);
Assert.equal(
SessionStore.getSavedTabGroups().length,
0,
"should not be any saved tab groups to start"
);
win.gBrowser.removeTabsToTheStartFrom(tab3);
await TestUtils.waitForCondition(
() => win.gBrowser.tabs.length == 1,
"waiting for tabs to the end to close"
);
await TabStateFlusher.flushWindow(win);
Assert.equal(
SessionStore.getSavedTabGroups().length,
0,
"should not have saved a tab group because one did not close"
);
Assert.deepEqual(
win.gBrowser.tabGroups,
[tabGroup],
"tab group should still exist in the tab strip"
);
Assert.deepEqual(
tabGroup.tabs,
[tab3],
"tab group should just have one tab left"
);
win.gBrowser.removeTabGroup(tabGroup);
await BrowserTestUtils.closeWindow(win);
forgetClosedWindows();
forgetSavedTabGroups();
}
);

View file

@ -0,0 +1,217 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const ORIG_STATE = SessionStore.getBrowserState();
registerCleanupFunction(async () => {
await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE);
});
forgetClosedWindows();
forgetSavedTabGroups();
add_task(async function test_saveOpenTabGroupsOnWindowClose() {
Assert.equal(
ss.getSavedTabGroups().length,
0,
"should start with no saved tab groups"
);
Assert.equal(
ss.getClosedWindowData().length,
0,
"should start with no closed windows"
);
info("open a new window with 1 standalone tab and 2 tabs in a tab group");
let win = await promiseNewWindowLoaded();
let aboutMozillaTab = BrowserTestUtils.addTab(win.gBrowser, "about:mozilla");
let aboutRobotsTab = BrowserTestUtils.addTab(win.gBrowser, "about:robots");
let aboutAboutTab = BrowserTestUtils.addTab(win.gBrowser, "about:about");
const tabGroupToClose = win.gBrowser.addTabGroup(
[aboutRobotsTab, aboutAboutTab],
{
color: "blue",
label: "about pages",
}
);
const tabGroupToCloseId = tabGroupToClose.id;
await Promise.all([
BrowserTestUtils.browserLoaded(
aboutMozillaTab.linkedBrowser,
false,
"about:mozilla"
),
BrowserTestUtils.browserLoaded(
aboutRobotsTab.linkedBrowser,
false,
"about:robots"
),
BrowserTestUtils.browserLoaded(
aboutAboutTab.linkedBrowser,
false,
"about:about"
),
]);
await TabStateFlusher.flushWindow(win);
info("close the window to make sure the tab group gets saved automatically");
await BrowserTestUtils.closeWindow(win);
const closedWindowData = SessionStore.getClosedWindowData();
Assert.equal(closedWindowData.length, 1, "`win` should now be closed");
Assert.equal(
closedWindowData[0].groups.length,
1,
"should be one tab group in the closed window"
);
const tabGroupInClosedWindow = closedWindowData[0].groups[0];
Assert.equal(
tabGroupInClosedWindow.id,
tabGroupToCloseId,
"closed window tab group should be the one that was in the window"
);
Assert.equal(ss.getSavedTabGroups().length, 1, "should be 1 saved tab group");
Assert.equal(
ss.getSavedTabGroup(tabGroupToCloseId).id,
tabGroupInClosedWindow.id,
"saved tab group should be the same as the tab group in the closed window"
);
info(
"reopen the closed window to make sure that the tab group reopens in the window and is no longer saved"
);
win = SessionStore.undoCloseWindow(0);
await BrowserTestUtils.waitForEvent(win, "SSWindowStateReady");
await BrowserTestUtils.waitForEvent(
win.gBrowser.tabContainer,
"SSTabRestored"
);
Assert.equal(
win.gBrowser.tabGroups.length,
1,
"tab group should have been restored"
);
Assert.equal(
win.gBrowser.tabGroups[0].id,
tabGroupToCloseId,
"restored tab group should be the one from the closed window"
);
Assert.equal(
ss.getSavedTabGroups().length,
0,
"saved tab group should no longer be saved because it should be restored into the reopened window"
);
Assert.equal(
ss.getClosedWindowData().length,
0,
"the closed window should have been reopened"
);
await TabStateFlusher.flushWindow(win);
info("close the window again");
await BrowserTestUtils.closeWindow(win);
Assert.equal(
ss.getClosedWindowData()[0].tabs.length,
4,
"re-closed window should have 1 new tab from the new window, 1 standalone tab, and 2 tabs in the tab group"
);
Assert.equal(
ss.getClosedWindowData()[0].groups.length,
1,
"tab group should still be in the closed window"
);
info(
"open the saved tab group into a different window, which should remove it from the closed window"
);
let savedGroupReopened = BrowserTestUtils.waitForEvent(
window,
"SSWindowStateReady"
);
await SessionStore.openSavedTabGroup(tabGroupToCloseId, window);
await savedGroupReopened;
await TabStateFlusher.flushWindow(window);
Assert.equal(
ss.getClosedWindowData()[0].tabs.length,
2,
"re-closed window should now just have 1 new tab from the new window and 1 standalone tab"
);
Assert.equal(
ss.getClosedWindowData()[0].groups.length,
0,
"re-closed window should no longer have tab groups because we opened the saved group into a different window"
);
Assert.equal(
ss.getSavedTabGroups().length,
0,
"saved tab group should no longer be saved because it should be restored into the main window"
);
info(
"reopen the closed window, which should no longer have the saved tab group in it since it moved into a different window"
);
win = SessionStore.undoCloseWindow(0);
await BrowserTestUtils.waitForEvent(win, "SSWindowStateReady");
await BrowserTestUtils.waitForEvent(
win.gBrowser.tabContainer,
"SSTabRestored"
);
Assert.equal(
win.gBrowser.openTabs.length,
2,
"the re-restored window should have 1 new tab and 1 standalone tab"
);
Assert.equal(
win.gBrowser.tabGroups.length,
0,
"the re-restored window should have no tab group"
);
info(
"move the tab group back into the window that we've been opening + closing"
);
let savedTabGroup = window.gBrowser.tabGroups[0];
let savedTabRemoval = BrowserTestUtils.waitForEvent(
savedTabGroup,
"TabGroupRemoved"
);
win.gBrowser.adoptTabGroup(window.gBrowser.tabGroups[0], 2);
await savedTabRemoval;
Assert.equal(
win.gBrowser.openTabs.length,
4,
"the re-restored window should have the tab group again after adopting it"
);
await TabStateFlusher.flushWindow(win);
info(
"close the window and forget it to make sure that the tab group will still be saved even if we forget about the closed window"
);
await BrowserTestUtils.closeWindow(win);
ss.forgetClosedWindow(0);
Assert.equal(
ss.getClosedWindowData().length,
0,
"the closed window should have been forgotten"
);
Assert.equal(
ss.getSavedTabGroups().length,
1,
"but the automatically saved tab group that was in the closed window should remain saved"
);
forgetClosedWindows();
forgetSavedTabGroups();
});

View file

@ -4,7 +4,7 @@
const lazy = {}; const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, { ChromeUtils.defineESModuleGetters(lazy, {
isProductURL: "chrome://global/content/shopping/ShoppingProduct.mjs", isSupportedSiteURL: "chrome://global/content/shopping/ShoppingProduct.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
ShoppingUtils: "resource:///modules/ShoppingUtils.sys.mjs", ShoppingUtils: "resource:///modules/ShoppingUtils.sys.mjs",
}); });
@ -35,19 +35,19 @@ export class ReviewCheckerParent extends JSWindowActorParent {
} }
} }
updateProductURL(uri, flags) { updateCurrentURL(uri, flags, isSupportedSite) {
this.sendAsyncMessage("ShoppingSidebar:UpdateProductURL", { this.sendAsyncMessage("ShoppingSidebar:UpdateProductURL", {
url: uri?.spec ?? null, url: uri?.spec ?? null,
isReload: !!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_RELOAD), isReload: !!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_RELOAD),
isSupportedSite,
}); });
} }
currentProductUrl() { getCurrentURL() {
let window = this.browsingContext.topChromeWindow; let window = this.browsingContext.topChromeWindow;
let { selectedBrowser } = window.gBrowser; let { selectedBrowser } = window.gBrowser;
let uri = selectedBrowser.currentURI; let uri = selectedBrowser.currentURI;
let isProduct = lazy.isProductURL(uri); return uri.spec ?? null;
return isProduct ? uri.spec : null;
} }
async receiveMessage(message) { async receiveMessage(message) {
@ -56,13 +56,16 @@ export class ReviewCheckerParent extends JSWindowActorParent {
} }
switch (message.name) { switch (message.name) {
case "GetProductURL": case "GetProductURL":
return this.currentProductUrl(); return this.getCurrentURL();
case "DisableShopping": case "DisableShopping":
Services.prefs.setIntPref( Services.prefs.setIntPref(
ReviewCheckerParent.SHOPPING_OPTED_IN_PREF, ReviewCheckerParent.SHOPPING_OPTED_IN_PREF,
2 2
); );
break; break;
case "CloseShoppingSidebar":
this.closeSidebarPanel();
break;
} }
return null; return null;
} }
@ -87,11 +90,19 @@ export class ReviewCheckerParent extends JSWindowActorParent {
lazy.ShoppingUtils.onLocationChange(aLocationURI, aFlags); lazy.ShoppingUtils.onLocationChange(aLocationURI, aFlags);
let isProduct = lazy.isProductURL(aLocationURI); this.updateCurrentURL(
if (isProduct) { aLocationURI,
this.updateProductURL(aLocationURI, aFlags); aFlags,
} else { lazy.isSupportedSiteURL(aLocationURI)
this.updateProductURL(null); );
}
closeSidebarPanel() {
let window = this.browsingContext.topChromeWindow;
let { SidebarController } = window;
if (SidebarController?.isOpen) {
SidebarController.hide();
} }
} }
} }

View file

@ -5,7 +5,11 @@
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { RemotePageChild } from "resource://gre/actors/RemotePageChild.sys.mjs"; import { RemotePageChild } from "resource://gre/actors/RemotePageChild.sys.mjs";
import { ShoppingProduct } from "chrome://global/content/shopping/ShoppingProduct.mjs"; import {
ShoppingProduct,
isProductURL,
isSupportedSiteURL,
} from "chrome://global/content/shopping/ShoppingProduct.mjs";
let lazy = {}; let lazy = {};
@ -56,6 +60,12 @@ XPCOMUtils.defineLazyPreferenceGetter(
} }
} }
); );
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"isIntegratedSidebar",
"browser.shopping.experience2023.integratedSidebar",
false
);
export class ShoppingSidebarChild extends RemotePageChild { export class ShoppingSidebarChild extends RemotePageChild {
constructor() { constructor() {
@ -84,21 +94,7 @@ export class ShoppingSidebarChild extends RemotePageChild {
} }
switch (message.name) { switch (message.name) {
case "ShoppingSidebar:UpdateProductURL": case "ShoppingSidebar:UpdateProductURL":
let { url, isReload } = message.data; this.handleURLUpdate(message.data);
let uri = url ? Services.io.newURI(url) : null;
// If we're going from null to null, bail out:
if (!this.#productURI && !uri) {
return null;
}
// If we haven't reloaded, check if the URIs represent the same product
// as sites might change the URI after they have loaded (Bug 1852099).
if (!isReload && this.isSameProduct(uri, this.#productURI)) {
return null;
}
this.#productURI = uri;
this.updateContent({ haveUpdatedURI: true });
break; break;
case "ShoppingSidebar:ShowKeepClosedMessage": case "ShoppingSidebar:ShowKeepClosedMessage":
this.sendToContent("ShowKeepClosedMessage"); this.sendToContent("ShowKeepClosedMessage");
@ -113,6 +109,32 @@ export class ShoppingSidebarChild extends RemotePageChild {
return null; return null;
} }
handleURLUpdate(data) {
let { url, isReload, isSupportedSite } = data;
let uri = url ? Services.io.newURI(url) : null;
// If we're going from null to null, bail out:
if (!this.#productURI && !uri) {
return;
}
// If we haven't reloaded, check if the URIs represent the same product
// as sites might change the URI after they have loaded (Bug 1852099).
if (!isReload && this.isSameProduct(uri, this.#productURI)) {
return;
}
if (!uri || isProductURL(uri) || !lazy.isIntegratedSidebar) {
this.#productURI = uri;
this.updateContent({ haveUpdatedURI: true });
} else {
this.#productURI = null;
// If the URI is not a product page, we should display an empty state.
// That empty state could be for either a support or unsupported site.
this.updateContent({ isProductPage: false, isSupportedSite });
}
}
isSameProduct(newURI, currentURI) { isSameProduct(newURI, currentURI) {
if (!newURI || !currentURI) { if (!newURI || !currentURI) {
return false; return false;
@ -167,6 +189,9 @@ export class ShoppingSidebarChild extends RemotePageChild {
case "DisableShopping": case "DisableShopping":
this.sendAsyncMessage("DisableShopping"); this.sendAsyncMessage("DisableShopping");
break; break;
case "CloseShoppingSidebar":
this.sendAsyncMessage("CloseShoppingSidebar");
break;
} }
} }
@ -249,11 +274,16 @@ export class ShoppingSidebarChild extends RemotePageChild {
* fetching the URI from the parent, and assume `this.#productURI` * fetching the URI from the parent, and assume `this.#productURI`
* is current. Defaults to false. * is current. Defaults to false.
* @param {bool} options.isPolledRequest = false * @param {bool} options.isPolledRequest = false
* @param {bool} options.focusCloseButton = false
* @param {bool} options.isProductPage = true
* @param {bool} options.isSupportedSite = false
*/ */
async updateContent({ async updateContent({
haveUpdatedURI = false, haveUpdatedURI = false,
isPolledRequest = false, isPolledRequest = false,
focusCloseButton = false, focusCloseButton = false,
isProductPage = true,
isSupportedSite = false,
} = {}) { } = {}) {
// updateContent is an async function, and when we're off making requests or doing // updateContent is an async function, and when we're off making requests or doing
// other things asynchronously, the actor can be destroyed, the user // other things asynchronously, the actor can be destroyed, the user
@ -287,8 +317,17 @@ export class ShoppingSidebarChild extends RemotePageChild {
data: null, data: null,
recommendationData: null, recommendationData: null,
focusCloseButton, focusCloseButton,
isProductPage,
isSupportedSite,
}); });
} }
// If this is not a product page then there
// is nothing else we need to do.
if (!isProductPage) {
return;
}
if (this.canFetchAndShowData) { if (this.canFetchAndShowData) {
if (!this.#productURI) { if (!this.#productURI) {
// If we already have a URI and it's just null, bail immediately. // If we already have a URI and it's just null, bail immediately.
@ -296,16 +335,23 @@ export class ShoppingSidebarChild extends RemotePageChild {
return; return;
} }
let url = await this.sendQuery("GetProductURL"); let url = await this.sendQuery("GetProductURL");
if (!url) {
return;
}
// Bail out if we opted out in the meantime, or don't have a URI. // Bail out if we opted out in the meantime, or don't have a URI.
if (!canContinue(null, false)) { if (!canContinue(null, false)) {
return; return;
} }
this.#productURI = Services.io.newURI(url); let uri = url ? Services.io.newURI(url) : null;
if (!uri || isProductURL(uri) || !lazy.isIntegratedSidebar) {
this.#productURI = uri;
} else {
this.#productURI = null;
this.sendToContent("Update", {
isProductPage: false,
isSupportedSite: isSupportedSiteURL(uri),
});
return;
}
} }
let uri = this.#productURI; let uri = this.#productURI;
@ -385,14 +431,12 @@ export class ShoppingSidebarChild extends RemotePageChild {
} }
this.sendToContent("Update", { this.sendToContent("Update", {
adsEnabled: this.adsEnabled,
adsEnabledByUser: this.adsEnabledByUser,
autoOpenEnabled: this.autoOpenEnabled,
autoOpenEnabledByUser: this.autoOpenEnabledByUser,
showOnboarding: false, showOnboarding: false,
data, data,
productUrl: this.#productURI.spec, productUrl: this.#productURI.spec,
isAnalysisInProgress, isAnalysisInProgress,
isProductPage,
isSupportedSite,
}); });
if (!data || data.error) { if (!data || data.error) {
@ -410,9 +454,6 @@ export class ShoppingSidebarChild extends RemotePageChild {
return; return;
} }
let url = await this.sendQuery("GetProductURL"); let url = await this.sendQuery("GetProductURL");
if (!url) {
return;
}
// Similar to canContinue() above, check to see if things // Similar to canContinue() above, check to see if things
// have changed while we were waiting. Bail out if the user // have changed while we were waiting. Bail out if the user
@ -421,12 +462,21 @@ export class ShoppingSidebarChild extends RemotePageChild {
return; return;
} }
this.#productURI = Services.io.newURI(url); let uri = url ? Services.io.newURI(url) : null;
let isProduct = isProductURL(uri);
if (!uri || isProduct || !lazy.isIntegratedSidebar) {
this.#productURI = uri;
} else {
this.#productURI = null;
}
// Send the productURI to content for Onboarding's dynamic text // Send the productURI to content for Onboarding's dynamic text
this.sendToContent("Update", { this.sendToContent("Update", {
showOnboarding: true, showOnboarding: true,
data: null, data: null,
productUrl: this.#productURI.spec, productUrl: this.#productURI?.spec,
isProductPage: isProduct,
isSupportedSite: !isProduct && isSupportedSiteURL(uri),
}); });
} }
} }

View file

@ -37,7 +37,7 @@ export class ShoppingSidebarParent extends JSWindowActorParent {
static INTEGRATED_SIDEBAR_PANEL_PREF = static INTEGRATED_SIDEBAR_PANEL_PREF =
"browser.shopping.experience2023.integratedSidebar"; "browser.shopping.experience2023.integratedSidebar";
updateProductURL(uri, flags) { updateCurrentURL(uri, flags) {
this.sendAsyncMessage("ShoppingSidebar:UpdateProductURL", { this.sendAsyncMessage("ShoppingSidebar:UpdateProductURL", {
url: uri?.spec ?? null, url: uri?.spec ?? null,
isReload: !!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_RELOAD), isReload: !!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_RELOAD),
@ -50,12 +50,7 @@ export class ShoppingSidebarParent extends JSWindowActorParent {
} }
switch (message.name) { switch (message.name) {
case "GetProductURL": case "GetProductURL":
let sidebarBrowser = this.browsingContext.top.embedderElement; return this.getCurrentURL();
let panel = sidebarBrowser.closest(".browserSidebarContainer");
let associatedTabbedBrowser = panel.querySelector(
"browser[messagemanagergroup=browsers]"
);
return associatedTabbedBrowser.currentURI?.spec ?? null;
case "DisableShopping": case "DisableShopping":
Services.prefs.setBoolPref( Services.prefs.setBoolPref(
ShoppingSidebarParent.SHOPPING_ACTIVE_PREF, ShoppingSidebarParent.SHOPPING_ACTIVE_PREF,
@ -70,6 +65,18 @@ export class ShoppingSidebarParent extends JSWindowActorParent {
return null; return null;
} }
/**
* Gets the URL of the current tab.
*/
getCurrentURL() {
let sidebarBrowser = this.browsingContext.top.embedderElement;
let panel = sidebarBrowser.closest(".browserSidebarContainer");
let associatedTabbedBrowser = panel.querySelector(
"browser[messagemanagergroup=browsers]"
);
return associatedTabbedBrowser.currentURI?.spec ?? null;
}
/** /**
* Called when the user clicks the URL bar button. * Called when the user clicks the URL bar button.
*/ */
@ -368,13 +375,13 @@ class ShoppingSidebarManagerClass {
browserPanel.appendChild(splitter); browserPanel.appendChild(splitter);
browserPanel.appendChild(sidebar); browserPanel.appendChild(sidebar);
} else { } else {
actor?.updateProductURL(aLocationURI, aFlags); actor?.updateCurrentURL(aLocationURI, aFlags);
sidebar.hidden = false; sidebar.hidden = false;
let splitter = browserPanel.querySelector(".shopping-sidebar-splitter"); let splitter = browserPanel.querySelector(".shopping-sidebar-splitter");
splitter.hidden = false; splitter.hidden = false;
} }
} else if (sidebar && !sidebar.hidden) { } else if (sidebar && !sidebar.hidden) {
actor?.updateProductURL(null); actor?.updateCurrentURL(null);
sidebar.hidden = true; sidebar.hidden = true;
let splitter = browserPanel.querySelector(".shopping-sidebar-splitter"); let splitter = browserPanel.querySelector(".shopping-sidebar-splitter");
splitter.hidden = true; splitter.hidden = true;

View file

@ -51,6 +51,8 @@ export class ShoppingContainer extends MozLitElement {
autoOpenEnabled: { type: Boolean }, autoOpenEnabled: { type: Boolean },
autoOpenEnabledByUser: { type: Boolean }, autoOpenEnabledByUser: { type: Boolean },
showingKeepClosedMessage: { type: Boolean }, showingKeepClosedMessage: { type: Boolean },
isProductPage: { type: Boolean },
isSupportedSite: { type: Boolean },
}; };
static get queries() { static get queries() {
@ -114,22 +116,27 @@ export class ShoppingContainer extends MozLitElement {
focusCloseButton, focusCloseButton,
autoOpenEnabled, autoOpenEnabled,
autoOpenEnabledByUser, autoOpenEnabledByUser,
isProductPage,
isSupportedSite,
}) { }) {
// If we're not opted in or there's no shopping URL in the main browser, // If we're not opted in or there's no shopping URL in the main browser,
// the actor will pass `null`, which means this will clear out any existing // the actor will pass `null`, which means this will clear out any existing
// content in the sidebar. // content in the sidebar.
this.data = data; this.data = data;
this.showOnboarding = showOnboarding; this.showOnboarding = showOnboarding ?? this.showOnboarding;
this.productUrl = productUrl; this.productUrl = productUrl;
this.recommendationData = recommendationData; this.recommendationData = recommendationData;
this.isOffline = !navigator.onLine; this.isOffline = !navigator.onLine;
this.isAnalysisInProgress = isAnalysisInProgress; this.isAnalysisInProgress = isAnalysisInProgress;
this.adsEnabled = adsEnabled; this.adsEnabled = adsEnabled ?? this.adsEnabled;
this.adsEnabledByUser = adsEnabledByUser; this.adsEnabledByUser = adsEnabledByUser ?? this.adsEnabledByUser;
this.analysisProgress = analysisProgress; this.analysisProgress = analysisProgress;
this.focusCloseButton = focusCloseButton; this.focusCloseButton = focusCloseButton;
this.autoOpenEnabled = autoOpenEnabled; this.autoOpenEnabled = autoOpenEnabled ?? this.autoOpenEnabled;
this.autoOpenEnabledByUser = autoOpenEnabledByUser; this.autoOpenEnabledByUser =
autoOpenEnabledByUser ?? this.autoOpenEnabledByUser;
this.isProductPage = isProductPage ?? true;
this.isSupportedSite = isSupportedSite;
} }
_updateRecommendations({ recommendationData }) { _updateRecommendations({ recommendationData }) {
@ -200,7 +207,7 @@ export class ShoppingContainer extends MozLitElement {
hostname = new URL(this.productUrl)?.hostname; hostname = new URL(this.productUrl)?.hostname;
return hostname; return hostname;
} catch (e) { } catch (e) {
console.error(`Unknown product url ${this.productUrl}.`); console.warn(`Unknown product url ${this.productUrl}.`);
return null; return null;
} }
} }
@ -464,6 +471,9 @@ export class ShoppingContainer extends MozLitElement {
} }
RPMSetPref(SHOPPING_SIDEBAR_ACTIVE_PREF, false); RPMSetPref(SHOPPING_SIDEBAR_ACTIVE_PREF, false);
window.dispatchEvent(
new CustomEvent("CloseShoppingSidebar", { bubbles: true, composed: true })
);
Glean.shopping.surfaceClosed.record({ source: "closeButton" }); Glean.shopping.surfaceClosed.record({ source: "closeButton" });
} }
} }

View file

@ -193,3 +193,122 @@ add_task(async function test_integrated_sidebar_updates_on_tab_switch() {
await BrowserTestUtils.removeTab(newProductTab); await BrowserTestUtils.removeTab(newProductTab);
}); });
}); });
add_task(async function test_integrated_sidebar_close() {
await BrowserTestUtils.withNewTab(CONTENT_PAGE, async function () {
let sandbox = sinon.createSandbox();
// Stub SidebarController.hide as actually closing the sidebar
// will not allow withReviewCheckerSidebar to finish.
let hideStub = sandbox.stub(SidebarController, "hide");
await SidebarController.show("viewReviewCheckerSidebar");
ok(SidebarController.isOpen, "Sidebar is open");
await withReviewCheckerSidebar(async () => {
let shoppingContainer = await ContentTaskUtils.waitForCondition(
() =>
content.document.querySelector("shopping-container")?.wrappedJSObject,
"Review Checker is loaded."
);
let closeButtonEl = await ContentTaskUtils.waitForCondition(
() => shoppingContainer.closeButtonEl,
"close button is present."
);
closeButtonEl.click();
});
Assert.ok(
hideStub.calledOnce,
"SidebarController.hide() is called to close the sidebar."
);
sandbox.restore();
});
});
add_task(async function test_integrated_sidebar_empty_states() {
await BrowserTestUtils.withNewTab(CONTENT_PAGE, async function (browser) {
await SidebarController.show("viewReviewCheckerSidebar");
await withReviewCheckerSidebar(async () => {
let shoppingContainer = await ContentTaskUtils.waitForCondition(
() =>
content.document.querySelector("shopping-container")?.wrappedJSObject,
"Review Checker is loaded."
);
await shoppingContainer.updateComplete;
await ContentTaskUtils.waitForCondition(
() => typeof shoppingContainer.isProductPage !== "undefined",
"isProductPage is set."
);
await ContentTaskUtils.waitForCondition(
() => typeof shoppingContainer.isSupportedSite !== "undefined",
"isSupportedSite is set."
);
Assert.ok(
!shoppingContainer.isProductPage,
"Current page is not a product page"
);
Assert.ok(
shoppingContainer.isSupportedSite,
"Current page is a supported site"
);
});
BrowserTestUtils.startLoadingURIString(browser, PRODUCT_TEST_URL);
await BrowserTestUtils.browserLoaded(browser);
await withReviewCheckerSidebar(async () => {
let shoppingContainer = await ContentTaskUtils.waitForCondition(
() =>
content.document.querySelector("shopping-container")?.wrappedJSObject,
"Review Checker is loaded."
);
await shoppingContainer.updateComplete;
await ContentTaskUtils.waitForCondition(
() => typeof shoppingContainer.isProductPage !== "undefined",
"isProductPage is set."
);
await ContentTaskUtils.waitForCondition(
() => typeof shoppingContainer.isSupportedSite !== "undefined",
"isSupportedSite is set."
);
Assert.ok(
shoppingContainer.isProductPage,
"Current page is a product page"
);
Assert.ok(
!shoppingContainer.isSupportedSite,
"Current page is not a supported site"
);
});
BrowserTestUtils.startLoadingURIString(browser, "about:newtab");
await BrowserTestUtils.browserLoaded(browser);
await withReviewCheckerSidebar(async () => {
let shoppingContainer = await ContentTaskUtils.waitForCondition(
() =>
content.document.querySelector("shopping-container")?.wrappedJSObject,
"Review Checker is loaded."
);
await shoppingContainer.updateComplete;
await ContentTaskUtils.waitForCondition(
() => typeof shoppingContainer.isProductPage !== "undefined",
"isProductPage is set."
);
await ContentTaskUtils.waitForCondition(
() => typeof shoppingContainer.isSupportedSite !== "undefined",
"isSupportedSite is set."
);
Assert.ok(
!shoppingContainer.isProductPage,
"Current page is not a product page"
);
Assert.ok(
!shoppingContainer.isSupportedSite,
"Current page is not a supported site"
);
});
});
});

View file

@ -11,6 +11,12 @@ const { SpecialMessageActions } = ChromeUtils.importESModule(
"resource://messaging-system/lib/SpecialMessageActions.sys.mjs" "resource://messaging-system/lib/SpecialMessageActions.sys.mjs"
); );
const PRODUCT_URI = Services.io.newURI(
"https://example.com/product/B09TJGHL5F"
);
const SHOPPING_SIDEBAR_ACTOR = "ShoppingSidebar";
const REVIEW_CHECKER_ACTOR = "ReviewChecker";
/** /**
* Toggle prefs involved in automatically activating the sidebar on PDPs if the * Toggle prefs involved in automatically activating the sidebar on PDPs if the
* user has not opted in. Onboarding should only try to auto-activate the * user has not opted in. Onboarding should only try to auto-activate the
@ -97,7 +103,10 @@ add_task(async function test_showOnboarding_notOptedIn() {
await Services.fog.testFlushAllChildren(); await Services.fog.testFlushAllChildren();
await SpecialPowers.pushPrefEnv({ await SpecialPowers.pushPrefEnv({
set: [["browser.shopping.experience2023.integratedSidebar", false]], set: [
["browser.shopping.experience2023.integratedSidebar", false],
["browser.shopping.experience2023.shoppingSidebar", true],
],
}); });
await BrowserTestUtils.withNewTab( await BrowserTestUtils.withNewTab(
@ -109,9 +118,9 @@ add_task(async function test_showOnboarding_notOptedIn() {
// Get the actor to update the product URL, since no content will render without one // Get the actor to update the product URL, since no content will render without one
let actor = let actor =
gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getExistingActor( gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getExistingActor(
"ShoppingSidebar" SHOPPING_SIDEBAR_ACTOR
); );
actor.updateProductURL("https://example.com/product/B09TJGHL5F"); actor.updateCurrentURL(PRODUCT_URI);
await SpecialPowers.spawn(browser, [], async () => { await SpecialPowers.spawn(browser, [], async () => {
let shoppingContainer = await ContentTaskUtils.waitForCondition( let shoppingContainer = await ContentTaskUtils.waitForCondition(
@ -157,6 +166,7 @@ add_task(async function test_showOnboarding_notOptedIn() {
info("Failed to get Glean value due to unknown bug. See bug 1862389."); info("Failed to get Glean value due to unknown bug. See bug 1862389.");
} }
} }
await SpecialPowers.popPrefEnv();
}); });
/** /**
@ -172,7 +182,10 @@ add_task(async function test_showOnboarding_notOptedIn_integrated_sidebar() {
await Services.fog.testFlushAllChildren(); await Services.fog.testFlushAllChildren();
await SpecialPowers.pushPrefEnv({ await SpecialPowers.pushPrefEnv({
set: [["browser.shopping.experience2023.integratedSidebar", true]], set: [
["browser.shopping.experience2023.integratedSidebar", true],
["browser.shopping.experience2023.shoppingSidebar", false],
],
}); });
await BrowserTestUtils.withNewTab( await BrowserTestUtils.withNewTab(
@ -184,9 +197,9 @@ add_task(async function test_showOnboarding_notOptedIn_integrated_sidebar() {
// Get the actor to update the product URL, since no content will render without one // Get the actor to update the product URL, since no content will render without one
let actor = let actor =
gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getExistingActor( gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getExistingActor(
"ShoppingSidebar" REVIEW_CHECKER_ACTOR
); );
actor.updateProductURL("https://example.com/product/B09TJGHL5F"); actor.updateCurrentURL(PRODUCT_URI);
await SpecialPowers.spawn(browser, [], async () => { await SpecialPowers.spawn(browser, [], async () => {
let shoppingContainer = await ContentTaskUtils.waitForCondition( let shoppingContainer = await ContentTaskUtils.waitForCondition(
@ -232,6 +245,7 @@ add_task(async function test_showOnboarding_notOptedIn_integrated_sidebar() {
info("Failed to get Glean value due to unknown bug. See bug 1862389."); info("Failed to get Glean value due to unknown bug. See bug 1862389.");
} }
} }
await SpecialPowers.popPrefEnv();
}); });
/** /**
@ -249,9 +263,9 @@ add_task(async function test_hideOnboarding_optedIn() {
// Get the actor to update the product URL, since no content will render without one // Get the actor to update the product URL, since no content will render without one
let actor = let actor =
gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getExistingActor( gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getExistingActor(
"ShoppingSidebar" SHOPPING_SIDEBAR_ACTOR
); );
actor.updateProductURL("https://example.com/product/B09TJGHL5F"); actor.updateCurrentURL(PRODUCT_URI);
await SpecialPowers.spawn(browser, [], async () => { await SpecialPowers.spawn(browser, [], async () => {
await ContentTaskUtils.waitForCondition( await ContentTaskUtils.waitForCondition(
@ -281,7 +295,10 @@ add_task(async function test_hideOnboarding_onClose() {
setOnboardingPrefs({ active: false, optedIn: 0, telemetryEnabled: true }); setOnboardingPrefs({ active: false, optedIn: 0, telemetryEnabled: true });
await SpecialPowers.pushPrefEnv({ await SpecialPowers.pushPrefEnv({
set: [["browser.shopping.experience2023.integratedSidebar", false]], set: [
["browser.shopping.experience2023.integratedSidebar", false],
["browser.shopping.experience2023.shoppingSidebar", true],
],
}); });
await BrowserTestUtils.withNewTab( await BrowserTestUtils.withNewTab(
@ -293,9 +310,9 @@ add_task(async function test_hideOnboarding_onClose() {
// Get the actor to update the product URL, since no content will render without one // Get the actor to update the product URL, since no content will render without one
let actor = let actor =
gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getExistingActor( gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getExistingActor(
"ShoppingSidebar" SHOPPING_SIDEBAR_ACTOR
); );
actor.updateProductURL("https://example.com/product/B09TJGHL5F"); actor.updateCurrentURL(PRODUCT_URI);
await SpecialPowers.spawn(browser, [], async () => { await SpecialPowers.spawn(browser, [], async () => {
let shoppingContainer = await ContentTaskUtils.waitForCondition( let shoppingContainer = await ContentTaskUtils.waitForCondition(
@ -329,6 +346,7 @@ add_task(async function test_hideOnboarding_onClose() {
Assert.greater(events.length, 0); Assert.greater(events.length, 0);
Assert.equal(events[0].category, "shopping"); Assert.equal(events[0].category, "shopping");
Assert.equal(events[0].name, "surface_not_now_clicked"); Assert.equal(events[0].name, "surface_not_now_clicked");
await SpecialPowers.popPrefEnv();
}); });
/** /**
@ -686,9 +704,9 @@ add_task(async function test_hideOnboarding_OptIn_AfterSurveySeen() {
async browser => { async browser => {
let actor = let actor =
gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getExistingActor( gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getExistingActor(
"ShoppingSidebar" SHOPPING_SIDEBAR_ACTOR
); );
actor.updateProductURL("https://example.com/product/B09TJGHL5F"); actor.updateCurrentURL(PRODUCT_URI);
await SpecialPowers.spawn(browser, [], async () => { await SpecialPowers.spawn(browser, [], async () => {
let shoppingContainer = await ContentTaskUtils.waitForCondition( let shoppingContainer = await ContentTaskUtils.waitForCondition(

View file

@ -205,9 +205,24 @@ export class SidebarState {
return this.#props.launcherVisible; return this.#props.launcherVisible;
} }
updateVisibility(visible, openedByToolbarButton = false) { updateVisibility(
visible,
openedByToolbarButton = false,
onToolbarButtonRemoval = false
) {
switch (this.revampVisibility) { switch (this.revampVisibility) {
case "hide-sidebar": case "hide-sidebar":
if (onToolbarButtonRemoval) {
// If we are hiding the sidebar because we removed the toolbar button, close everything
this.#previousLauncherVisible = false;
this.launcherVisible = false;
this.launcherExpanded = true;
if (this.panelOpen) {
this.#controller.hide();
}
return;
}
if (!openedByToolbarButton && !visible && this.panelOpen) { if (!openedByToolbarButton && !visible && this.panelOpen) {
// no-op to handle the case when a user changes the visibility setting via the // no-op to handle the case when a user changes the visibility setting via the
// customize panel, we don't want to close anything on them. // customize panel, we don't want to close anything on them.

View file

@ -349,6 +349,8 @@ var SidebarController = {
this._handleLauncherResize(entry) this._handleLauncherResize(entry)
); );
CustomizableUI.addListener(this);
if (this.sidebarRevampEnabled) { if (this.sidebarRevampEnabled) {
if (!customElements.get("sidebar-main")) { if (!customElements.get("sidebar-main")) {
ChromeUtils.importESModule( ChromeUtils.importESModule(
@ -455,6 +457,8 @@ var SidebarController = {
Services.obs.removeObserver(this, "tabstrip-orientation-change"); Services.obs.removeObserver(this, "tabstrip-orientation-change");
delete this._tabstripOrientationObserverAdded; delete this._tabstripOrientationObserverAdded;
CustomizableUI.removeListener(this);
if (this._observer) { if (this._observer) {
this._observer.disconnect(); this._observer.disconnect();
this._observer = null; this._observer = null;
@ -1684,6 +1688,13 @@ var SidebarController = {
} }
}, },
onWidgetRemoved(aWidgetId) {
if (aWidgetId == "sidebar-button") {
Services.prefs.setStringPref("sidebar.visibility", "hide-sidebar");
this._state.updateVisibility(false, false, true);
}
},
toggleTabstrip() { toggleTabstrip() {
let toVerticalTabs = CustomizableUI.verticalTabsEnabled; let toVerticalTabs = CustomizableUI.verticalTabsEnabled;
let tabStrip = gBrowser.tabContainer; let tabStrip = gBrowser.tabContainer;

View file

@ -14,7 +14,10 @@ const SIDEBAR_VISIBILITY_PREF = "sidebar.visibility";
add_setup(async () => { add_setup(async () => {
await SpecialPowers.pushPrefEnv({ await SpecialPowers.pushPrefEnv({
set: [[SIDEBAR_BUTTON_INTRODUCED_PREF, false]], set: [
[SIDEBAR_BUTTON_INTRODUCED_PREF, false],
[SIDEBAR_VISIBILITY_PREF, "always-show"],
],
}); });
let navbarDefaults = gAreas.get("nav-bar").get("defaultPlacements"); let navbarDefaults = gAreas.get("nav-bar").get("defaultPlacements");
let hadSavedState = !!CustomizableUI.getTestOnlyInternalProp("gSavedState"); let hadSavedState = !!CustomizableUI.getTestOnlyInternalProp("gSavedState");
@ -78,14 +81,9 @@ add_task(async function test_expanded_state_for_always_show() {
info( info(
`Current window's sidebarMain.expanded: ${window.SidebarController.sidebarMain?.expanded}` `Current window's sidebarMain.expanded: ${window.SidebarController.sidebarMain?.expanded}`
); );
await SpecialPowers.pushPrefEnv({
set: [[SIDEBAR_VISIBILITY_PREF, "always-show"]],
});
const win = await BrowserTestUtils.openNewBrowserWindow(); const win = await BrowserTestUtils.openNewBrowserWindow();
const { SidebarController, document } = win; const { SidebarController, document } = win;
const { sidebarMain, toolbarButton } = SidebarController; const { sidebarMain, toolbarButton } = SidebarController;
await SidebarController.promiseInitialized; await SidebarController.promiseInitialized;
info(`New window's sidebarMain.expanded: ${sidebarMain?.expanded}`); info(`New window's sidebarMain.expanded: ${sidebarMain?.expanded}`);
@ -294,6 +292,15 @@ add_task(async function test_sidebar_button_runtime_pref_enabled() {
CustomizableUI.AREA_NAVBAR, CustomizableUI.AREA_NAVBAR,
"The sidebar button is in the nav-bar" "The sidebar button is in the nav-bar"
); );
// When the button was removed, "hide-sidebar" was set automatically. Revert for the next test.
// Expanded is the default when "hide-sidebar" is set - click the button to revert to collapsed for the next test.
await SpecialPowers.pushPrefEnv({
set: [[SIDEBAR_VISIBILITY_PREF, "always-show"]],
});
const sidebar = document.querySelector("sidebar-main");
button.click();
Assert.ok(!sidebar.expanded, "Sidebar collapsed by click");
}); });
/** /**
@ -329,4 +336,6 @@ add_task(async function test_keyboard_shortcut() {
"false", "false",
"Glean event recorded that sidebar was collapsed/hidden with keyboard shortcut" "Glean event recorded that sidebar was collapsed/hidden with keyboard shortcut"
); );
Services.fog.testResetFOG();
}); });

View file

@ -5,6 +5,7 @@ add_setup(async () => {
await SpecialPowers.pushPrefEnv({ await SpecialPowers.pushPrefEnv({
set: [ set: [
["sidebar.verticalTabs", true], ["sidebar.verticalTabs", true],
["sidebar.visibility", "always-show"],
["browser.ml.chat.enabled", true], ["browser.ml.chat.enabled", true],
["browser.shopping.experience2023.integratedSidebar", true], ["browser.shopping.experience2023.integratedSidebar", true],
["sidebar.main.tools", "aichat,reviewchecker,syncedtabs,history"], ["sidebar.main.tools", "aichat,reviewchecker,syncedtabs,history"],

View file

@ -15,7 +15,10 @@ const { NonPrivateTabs } = ChromeUtils.importESModule(
add_setup(async () => { add_setup(async () => {
await SpecialPowers.pushPrefEnv({ await SpecialPowers.pushPrefEnv({
set: [["sidebar.verticalTabs", false]], set: [
["sidebar.verticalTabs", false],
["sidebar.visibility", "always-show"],
],
}); });
Services.telemetry.clearScalars(); Services.telemetry.clearScalars();
SessionStoreTestUtils.init(this, window); SessionStoreTestUtils.init(this, window);

View file

@ -7,7 +7,12 @@
* Check that when enabling vertical tabs, we can still receive a click on the urlbar results view * Check that when enabling vertical tabs, we can still receive a click on the urlbar results view
*/ */
add_task(async function test_click_urlbar_results() { add_task(async function test_click_urlbar_results() {
await SpecialPowers.pushPrefEnv({ set: [["sidebar.verticalTabs", true]] }); await SpecialPowers.pushPrefEnv({
set: [
["sidebar.verticalTabs", true],
["sidebar.visibility", "always-show"],
],
});
await TestUtils.waitForCondition(() => { await TestUtils.waitForCondition(() => {
return BrowserTestUtils.isVisible(document.querySelector("sidebar-main")); return BrowserTestUtils.isVisible(document.querySelector("sidebar-main"));

View file

@ -0,0 +1,132 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
export class GroupsPanel {
constructor({ view, containerNode }) {
this.view = view;
this.containerNode = containerNode;
this.win = containerNode.ownerGlobal;
this.doc = containerNode.ownerDocument;
this.panelMultiView = null;
this.view.addEventListener("ViewShowing", this);
}
handleEvent(event) {
switch (event.type) {
case "ViewShowing":
if (event.target == this.view) {
this.panelMultiView = this.view.panelMultiView;
this.#populate(event);
}
break;
case "PanelMultiViewHidden":
if ((this.panelMultiView = event.target)) {
this.#cleanup();
this.panelMultiView = null;
}
break;
case "command":
this.#handleCommand(event);
break;
}
}
#handleCommand(event) {
let groupId = event.target.closest("toolbaritem").groupId;
switch (event.target.dataset.command) {
case "allTabsGroupView_selectGroup":
let group = this.win.gBrowser.getTabGroupById(groupId);
group.select();
group.ownerGlobal.focus();
break;
case "allTabsGroupView_restoreGroup":
this.win.SessionStore.openSavedTabGroup(groupId, this.win);
break;
}
}
#setupListeners() {
this.view.addEventListener("command", this);
this.view.panelMultiView.addEventListener("PanelMultiViewHidden", this);
}
#cleanup() {
this.containerNode.innerHTML = "";
this.view.removeEventListener("command", this);
}
#populate() {
let fragment = this.doc.createDocumentFragment();
let otherWindowGroups = this.win.gBrowser
.getAllTabGroups()
.filter(group => {
return group.ownerGlobal !== this.win;
});
let savedGroups = this.win.SessionStore.savedGroups;
if (savedGroups.length + otherWindowGroups.length > 0) {
let header = this.doc.createElement("h2");
header.setAttribute("class", "subview-subheader");
this.doc.l10n.setAttributes(header, "tab-group-menu-header");
fragment.appendChild(header);
}
for (let groupData of otherWindowGroups) {
fragment.appendChild(this.#createRow(groupData));
}
for (let groupData of savedGroups) {
fragment.appendChild(this.#createRow(groupData, "closed"));
}
this.containerNode.appendChild(fragment);
this.#setupListeners();
}
#createRow(group, kind = "open") {
let { doc } = this;
let row = doc.createXULElement("toolbaritem");
row.setAttribute("class", "all-tabs-item all-tabs-group-item");
row.setAttribute("context", "tabContextMenu");
row.style.setProperty(
"--tab-group-color",
`var(--tab-group-color-${group.color})`
);
row.style.setProperty(
"--tab-group-color-invert",
`var(--tab-group-color-${group.color}-invert)`
);
row.style.setProperty(
"--tab-group-color-pale",
`var(--tab-group-color-${group.color}-pale)`
);
row.groupId = group.id;
let button = doc.createXULElement("toolbarbutton");
button.setAttribute(
"class",
"all-tabs-button subviewbutton subviewbutton-iconic all-tabs-group-action-button"
);
if (kind != "open") {
button.classList.add("all-tabs-group-saved-group");
button.dataset.command = "allTabsGroupView_restoreGroup";
} else {
button.dataset.command = "allTabsGroupView_selectGroup";
}
button.setAttribute("flex", "1");
button.setAttribute("crop", "end");
if (group.name) {
button.setAttribute("label", group.name);
} else {
doc.l10n
.formatValues([{ id: "tab-group-name-default" }])
.then(([msg]) => {
button.setAttribute("label", msg);
});
}
row.appendChild(button);
return row;
}
}

View file

@ -102,9 +102,14 @@ class TabsListBase {
*/ */
_populate() { _populate() {
let fragment = this.doc.createDocumentFragment(); let fragment = this.doc.createDocumentFragment();
let currentGroupId;
for (let tab of this.gBrowser.tabs) { for (let tab of this.gBrowser.tabs) {
if (this.filterFn(tab)) { if (this.filterFn(tab)) {
if (tab.group && tab.group.id != currentGroupId) {
fragment.appendChild(this._createGroupRow(tab.group));
currentGroupId = tab.group.id;
}
fragment.appendChild(this._createRow(tab)); fragment.appendChild(this._createRow(tab));
} }
} }
@ -121,6 +126,10 @@ class TabsListBase {
* Remove the menuitems from the DOM, cleanup internal state and listeners. * Remove the menuitems from the DOM, cleanup internal state and listeners.
*/ */
_cleanup() { _cleanup() {
this.doc
.querySelectorAll(".all-tabs-group-button")
.forEach(node => node.remove());
for (let item of this.rows) { for (let item of this.rows) {
item.remove(); item.remove();
} }
@ -262,6 +271,9 @@ export class TabsPanel extends TabsListBase {
this.gBrowser.removeTab(event.target.tab); this.gBrowser.removeTab(event.target.tab);
break; break;
} }
if (event.target.classList.contains("all-tabs-group-button")) {
this.gBrowser.getTabGroupById(event.target.groupId).select();
}
// fall through // fall through
default: default:
super.handleEvent(event); super.handleEvent(event);
@ -275,7 +287,10 @@ export class TabsPanel extends TabsListBase {
// The loading throbber can't be set until the toolbarbutton is rendered, // The loading throbber can't be set until the toolbarbutton is rendered,
// so set the image attributes again now that the elements are in the DOM. // so set the image attributes again now that the elements are in the DOM.
for (let row of this.rows) { for (let row of this.rows) {
this._setImageAttributes(row, row.tab); // Ensure this isn't a group label
if (row.tab) {
this._setImageAttributes(row, row.tab);
}
} }
} }
@ -324,6 +339,10 @@ export class TabsPanel extends TabsListBase {
}); });
} }
if (tab.group) {
row.classList.add("grouped");
}
row.appendChild(button); row.appendChild(button);
let muteButton = doc.createXULElement("toolbarbutton"); let muteButton = doc.createXULElement("toolbarbutton");
@ -352,6 +371,49 @@ export class TabsPanel extends TabsListBase {
return row; return row;
} }
_createGroupRow(group) {
let { doc } = this;
let row = doc.createXULElement("toolbaritem");
row.setAttribute("class", "all-tabs-item all-tabs-group-item");
row.setAttribute("context", "none");
row.style.setProperty(
"--tab-group-color",
`var(--tab-group-color-${group.color})`
);
row.style.setProperty(
"--tab-group-color-invert",
`var(--tab-group-color-${group.color}-invert)`
);
row.style.setProperty(
"--tab-group-color-pale",
`var(--tab-group-color-${group.color}-pale)`
);
row.addEventListener("command", this);
let button = doc.createXULElement("toolbarbutton");
button.setAttribute(
"class",
"all-tabs-button all-tabs-group-button subviewbutton subviewbutton-iconic"
);
button.setAttribute("flex", "1");
button.setAttribute("crop", "end");
button.group = group;
button.groupId = group.id;
if (group.label) {
button.label = group.label;
} else {
doc.l10n
.formatValues([{ id: "tab-group-name-default" }])
.then(([msg]) => {
button.label = msg;
});
}
button.image = "chrome://browser/skin/tabbrowser/tab-group-chicklet.svg";
row.appendChild(button);
return row;
}
_setRowAttributes(row, tab) { _setRowAttributes(row, tab) {
setAttributes(row, { selected: tab.selected }); setAttributes(row, { selected: tab.selected });

View file

@ -23,6 +23,9 @@
class="subviewbutton subviewbutton-nav" class="subviewbutton subviewbutton-nav"
closemenu="none" closemenu="none"
data-l10n-id="all-tabs-menu-hidden-tabs"/> data-l10n-id="all-tabs-menu-hidden-tabs"/>
<toolbarseparator id="allTabsMenu-groupsSeparator"/>
<vbox id="allTabsMenu-groupsView" class="panel-subview-body">
</vbox>
<toolbarseparator id="allTabsMenu-tabsSeparator"/> <toolbarseparator id="allTabsMenu-tabsSeparator"/>
<vbox id="allTabsMenu-dropIndicatorHolder"> <vbox id="allTabsMenu-dropIndicatorHolder">
<vbox id="allTabsMenu-dropIndicator" collapsed="true"/> <vbox id="allTabsMenu-dropIndicator" collapsed="true"/>

View file

@ -7,6 +7,8 @@
ChromeUtils.defineESModuleGetters(this, { ChromeUtils.defineESModuleGetters(this, {
BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs", BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs",
GroupsPanel: "resource:///modules/GroupsList.sys.mjs",
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
TabsPanel: "resource:///modules/TabsList.sys.mjs", TabsPanel: "resource:///modules/TabsList.sys.mjs",
}); });
@ -19,6 +21,7 @@ var gTabsPanel = {
containerTabsView: "allTabsMenu-containerTabsView", containerTabsView: "allTabsMenu-containerTabsView",
hiddenTabsButton: "allTabsMenu-hiddenTabsButton", hiddenTabsButton: "allTabsMenu-hiddenTabsButton",
hiddenTabsView: "allTabsMenu-hiddenTabsView", hiddenTabsView: "allTabsMenu-hiddenTabsView",
groupsView: "allTabsMenu-groupsView",
}, },
_initialized: false, _initialized: false,
_initializedElements: false, _initializedElements: false,
@ -60,6 +63,11 @@ var gTabsPanel = {
containerNode: this.allTabsViewTabs, containerNode: this.allTabsViewTabs,
filterFn: tab => !tab.hidden, filterFn: tab => !tab.hidden,
dropIndicator: this.dropIndicator, dropIndicator: this.dropIndicator,
showGroups: true,
});
this.groupsPanel = new GroupsPanel({
view: this.allTabsView,
containerNode: this.groupsView,
}); });
this.allTabsView.addEventListener("ViewShowing", () => { this.allTabsView.addEventListener("ViewShowing", () => {

View file

@ -23,13 +23,15 @@
<image class="tab-sharing-icon-overlay" role="presentation"/> <image class="tab-sharing-icon-overlay" role="presentation"/>
<image class="tab-icon-overlay" role="presentation"/> <image class="tab-icon-overlay" role="presentation"/>
</stack> </stack>
<html:moz-button type="icon ghost" size="small" class="tab-audio-button" tabindex="-1"></html:moz-button>
<vbox class="tab-label-container" <vbox class="tab-label-container"
align="start" align="start"
pack="center" pack="center"
flex="1"> flex="1">
<label class="tab-text tab-label" role="presentation"/> <label class="tab-text tab-label" role="presentation"/>
<hbox class="tab-secondary-label"> <hbox class="tab-secondary-label">
<label class="tab-icon-sound-label tab-icon-sound-playing-label" data-l10n-id="browser-tab-audio-playing2" role="presentation"/>
<label class="tab-icon-sound-label tab-icon-sound-muted-label" data-l10n-id="browser-tab-audio-muted2" role="presentation"/>
<label class="tab-icon-sound-label tab-icon-sound-blocked-label" data-l10n-id="browser-tab-audio-blocked" role="presentation"/>
<label class="tab-icon-sound-label tab-icon-sound-pip-label" data-l10n-id="browser-tab-audio-pip" role="presentation"/> <label class="tab-icon-sound-label tab-icon-sound-pip-label" data-l10n-id="browser-tab-audio-pip" role="presentation"/>
<label class="tab-icon-sound-label tab-icon-sound-tooltip-label" role="presentation"/> <label class="tab-icon-sound-label tab-icon-sound-tooltip-label" role="presentation"/>
</hbox> </hbox>
@ -83,7 +85,7 @@
".tab-content": ".tab-content":
"pinned,selected=visuallyselected,multiselected,titlechanged,attention", "pinned,selected=visuallyselected,multiselected,titlechanged,attention",
".tab-icon-stack": ".tab-icon-stack":
"sharing,pictureinpicture,crashed,busy,soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected,activemedia-blocked", "sharing,pictureinpicture,crashed,busy,soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected,activemedia-blocked,indicator-replaces-favicon",
".tab-throbber": ".tab-throbber":
"fadein,pinned,busy,progress,selected=visuallyselected", "fadein,pinned,busy,progress,selected=visuallyselected",
".tab-icon-pending": ".tab-icon-pending":
@ -92,15 +94,13 @@
"src=image,triggeringprincipal=iconloadingprincipal,requestcontextid,fadein,pinned,selected=visuallyselected,busy,crashed,sharing,pictureinpicture", "src=image,triggeringprincipal=iconloadingprincipal,requestcontextid,fadein,pinned,selected=visuallyselected,busy,crashed,sharing,pictureinpicture",
".tab-sharing-icon-overlay": "sharing,selected=visuallyselected,pinned", ".tab-sharing-icon-overlay": "sharing,selected=visuallyselected,pinned",
".tab-icon-overlay": ".tab-icon-overlay":
"sharing,pictureinpicture,crashed,busy,soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected,activemedia-blocked", "sharing,pictureinpicture,crashed,busy,soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected,activemedia-blocked,indicator-replaces-favicon",
".tab-audio-button":
"soundplaying,soundplaying-scheduledremoval,pinned,muted,activemedia-blocked",
".tab-label-container": ".tab-label-container":
"pinned,selected=visuallyselected,labeldirection", "pinned,selected=visuallyselected,labeldirection",
".tab-label": ".tab-label":
"text=label,accesskey,fadein,pinned,selected=visuallyselected,attention", "text=label,accesskey,fadein,pinned,selected=visuallyselected,attention",
".tab-label-container .tab-secondary-label": ".tab-label-container .tab-secondary-label":
"pinned,blocked,selected=visuallyselected,pictureinpicture", "soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected,activemedia-blocked,pictureinpicture",
".tab-close-button": "fadein,pinned,selected=visuallyselected", ".tab-close-button": "fadein,pinned,selected=visuallyselected",
}; };
} }
@ -310,18 +310,10 @@
return this.overlayIcon?.matches(":hover"); return this.overlayIcon?.matches(":hover");
} }
get _overAudioButton() {
return this.audioButton?.matches(":hover");
}
get overlayIcon() { get overlayIcon() {
return this.querySelector(".tab-icon-overlay"); return this.querySelector(".tab-icon-overlay");
} }
get audioButton() {
return this.querySelector(".tab-audio-button");
}
get throbber() { get throbber() {
return this.querySelector(".tab-throbber"); return this.querySelector(".tab-throbber");
} }
@ -379,6 +371,24 @@
if (event.target.classList.contains("tab-close-button")) { if (event.target.classList.contains("tab-close-button")) {
this.mOverCloseButton = true; this.mOverCloseButton = true;
} }
if (this._overPlayingIcon) {
const selectedTabs = gBrowser.selectedTabs;
const contextTabInSelection = selectedTabs.includes(this);
const affectedTabsLength = contextTabInSelection
? selectedTabs.length
: 1;
let stringID;
if (this.hasAttribute("activemedia-blocked")) {
stringID = "browser-tab-unblock";
} else {
stringID = this.linkedBrowser.audioMuted
? "browser-tab-unmute"
: "browser-tab-mute";
}
this.setSecondaryTabTooltipLabel(stringID, {
count: affectedTabsLength,
});
}
if (!this.visible) { if (!this.visible) {
return; return;
@ -399,6 +409,9 @@
if (event.target.classList.contains("tab-close-button")) { if (event.target.classList.contains("tab-close-button")) {
this.mOverCloseButton = false; this.mOverCloseButton = false;
} }
if (event.target == this.overlayIcon) {
this.setSecondaryTabTooltipLabel(null);
}
// If the new target is not part of this tab then this is a mouseleave event. // If the new target is not part of this tab then this is a mouseleave event.
if (!this.contains(event.relatedTarget)) { if (!this.contains(event.relatedTarget)) {
@ -438,8 +451,7 @@
this.style.MozUserFocus = "ignore"; this.style.MozUserFocus = "ignore";
} else if ( } else if (
event.target.classList.contains("tab-close-button") || event.target.classList.contains("tab-close-button") ||
event.target.classList.contains("tab-icon-overlay") || event.target.classList.contains("tab-icon-overlay")
event.target.classList.contains("tab-audio-button")
) { ) {
eventMaySelectTab = false; eventMaySelectTab = false;
} }
@ -504,18 +516,14 @@
if ( if (
gBrowser.multiSelectedTabsCount > 0 && gBrowser.multiSelectedTabsCount > 0 &&
!event.target.classList.contains("tab-close-button") && !event.target.classList.contains("tab-close-button") &&
!event.target.classList.contains("tab-icon-overlay") && !event.target.classList.contains("tab-icon-overlay")
!event.target.classList.contains("tab-audio-button")
) { ) {
// Tabs were previously multi-selected and user clicks on a tab // Tabs were previously multi-selected and user clicks on a tab
// without holding Ctrl/Cmd Key // without holding Ctrl/Cmd Key
gBrowser.clearMultiSelectedTabs(); gBrowser.clearMultiSelectedTabs();
} }
if ( if (event.target.classList.contains("tab-icon-overlay")) {
event.target.classList.contains("tab-icon-overlay") ||
event.target.classList.contains("tab-audio-button")
) {
if (this.activeMediaBlocked) { if (this.activeMediaBlocked) {
if (this.multiselected) { if (this.multiselected) {
gBrowser.resumeDelayedMediaOnMultiSelectedTabs(this); gBrowser.resumeDelayedMediaOnMultiSelectedTabs(this);

View file

@ -2077,6 +2077,9 @@
// process so the browser can no longer be considered to be // process so the browser can no longer be considered to be
// crashed. // crashed.
tab.removeAttribute("crashed"); tab.removeAttribute("crashed");
// we call updatetabIndicatorAttr here, rather than _tabAttrModified, so as
// to be consistent with how "crashed" attribute changes are handled elsewhere
this.tabContainer.updateTabIndicatorAttr(tab);
} }
// If the findbar has been initialised, reset its browser reference. // If the findbar has been initialised, reset its browser reference.
@ -2966,7 +2969,6 @@
* Removes the tab group. This has the effect of closing all the tabs * Removes the tab group. This has the effect of closing all the tabs
* in the group. * in the group.
* *
*
* @param {MozTabbrowserTabGroup} [group] * @param {MozTabbrowserTabGroup} [group]
* The tab group to remove. * The tab group to remove.
* @param {object} [options] * @param {object} [options]
@ -3807,18 +3809,22 @@
tab.dispatchEvent(evt); tab.dispatchEvent(evt);
} }
getTabsToTheStartFrom(aTab) { /**
* @param {MozTabbrowserTab} aTab
* @returns {MozTabbrowserTab[]}
*/
_getTabsToTheStartFrom(aTab) {
let tabsToStart = []; let tabsToStart = [];
if (!aTab.visible) { if (!aTab.visible) {
return tabsToStart; return tabsToStart;
} }
let tabs = this.visibleTabs; let tabs = this.openTabs;
for (let i = 0; i < tabs.length; ++i) { for (let i = 0; i < tabs.length; ++i) {
if (tabs[i] == aTab) { if (tabs[i] == aTab) {
break; break;
} }
// Ignore pinned tabs. // Ignore pinned and hidden tabs.
if (tabs[i].pinned) { if (tabs[i].pinned || tabs[i].hidden) {
continue; continue;
} }
// In a multi-select context, select all unselected tabs // In a multi-select context, select all unselected tabs
@ -3831,18 +3837,22 @@
return tabsToStart; return tabsToStart;
} }
getTabsToTheEndFrom(aTab) { /**
* @param {MozTabbrowserTab} aTab
* @returns {MozTabbrowserTab[]}
*/
_getTabsToTheEndFrom(aTab) {
let tabsToEnd = []; let tabsToEnd = [];
if (!aTab.visible) { if (!aTab.visible) {
return tabsToEnd; return tabsToEnd;
} }
let tabs = this.visibleTabs; let tabs = this.openTabs;
for (let i = tabs.length - 1; i >= 0; --i) { for (let i = tabs.length - 1; i >= 0; --i) {
if (tabs[i] == aTab) { if (tabs[i] == aTab) {
break; break;
} }
// Ignore pinned tabs. // Ignore pinned and hidden tabs.
if (tabs[i].pinned) { if (tabs[i].pinned || tabs[i].hidden) {
continue; continue;
} }
// In a multi-select context, select all unselected tabs // In a multi-select context, select all unselected tabs
@ -3980,7 +3990,7 @@
* left of the leftmost selected tab will be removed. * left of the leftmost selected tab will be removed.
*/ */
removeTabsToTheStartFrom(aTab) { removeTabsToTheStartFrom(aTab) {
let tabs = this.getTabsToTheStartFrom(aTab); let tabs = this._getTabsToTheStartFrom(aTab);
if ( if (
!this.warnAboutClosingTabs(tabs.length, this.closingTabsEnum.TO_START) !this.warnAboutClosingTabs(tabs.length, this.closingTabsEnum.TO_START)
) { ) {
@ -3995,7 +4005,7 @@
* right of the rightmost selected tab will be removed. * right of the rightmost selected tab will be removed.
*/ */
removeTabsToTheEndFrom(aTab) { removeTabsToTheEndFrom(aTab) {
let tabs = this.getTabsToTheEndFrom(aTab); let tabs = this._getTabsToTheEndFrom(aTab);
if ( if (
!this.warnAboutClosingTabs(tabs.length, this.closingTabsEnum.TO_END) !this.warnAboutClosingTabs(tabs.length, this.closingTabsEnum.TO_END)
) { ) {
@ -4006,18 +4016,20 @@
} }
/** /**
* Remove all tabs but aTab. By default, in a multi-select context, all * Remove all tabs but `aTab`. By default, in a multi-select context, all
* unpinned and unselected tabs are removed. Otherwise all unpinned tabs * unpinned and unselected tabs are removed. Otherwise all unpinned tabs
* except aTab are removed. This behavior can be changed using the the bool * except aTab are removed. This behavior can be changed using the the bool
* flags below. * flags below.
* *
* @param aTab The tab we will skip removing * @param {MozTabbrowserTab} aTab
* @param aParams An optional set of parameters that will be passed to the * The tab we will skip removing
* removeTabs function. * @param {object} [aParams]
* @param {boolean} [aParams.skipWarnAboutClosingTabs=false] Skip showing * An optional set of parameters that will be passed to the
* the tab close warning prompt. * `removeTabs` function.
* @param {boolean} [aParams.skipPinnedOrSelectedTabs=true] Skip closing * @param {boolean} [aParams.skipWarnAboutClosingTabs=false]
* tabs that are selected or pinned. * Skip showing the tab close warning prompt.
* @param {boolean} [aParams.skipPinnedOrSelectedTabs=true]
* Skip closing tabs that are selected or pinned.
*/ */
removeAllTabsBut(aTab, aParams = {}) { removeAllTabsBut(aTab, aParams = {}) {
let { let {
@ -4025,21 +4037,22 @@
skipPinnedOrSelectedTabs = true, skipPinnedOrSelectedTabs = true,
} = aParams; } = aParams;
/** @type {function(MozTabbrowserTab):boolean} */
let filterFn; let filterFn;
// If enabled also filter by selected or pinned state. // If enabled also filter by selected or pinned state.
if (skipPinnedOrSelectedTabs) { if (skipPinnedOrSelectedTabs) {
if (aTab?.multiselected) { if (aTab?.multiselected) {
filterFn = tab => !tab.multiselected && !tab.pinned; filterFn = tab => !tab.multiselected && !tab.pinned && !tab.hidden;
} else { } else {
filterFn = tab => tab != aTab && !tab.pinned; filterFn = tab => tab != aTab && !tab.pinned && !tab.hidden;
} }
} else { } else {
// Exclude just aTab from being removed. // Exclude just aTab from being removed.
filterFn = tab => tab != aTab; filterFn = tab => tab != aTab;
} }
let tabsToRemove = this.visibleTabs.filter(filterFn); let tabsToRemove = this.openTabs.filter(filterFn);
// If enabled show the tab close warning. // If enabled show the tab close warning.
if ( if (
@ -4071,7 +4084,7 @@
/** /**
* @typedef {object} _startRemoveTabsReturnValue * @typedef {object} _startRemoveTabsReturnValue
* @property {Promise} beforeUnloadComplete * @property {Promise<void>} beforeUnloadComplete
* A promise that is resolved once all the beforeunload handlers have been * A promise that is resolved once all the beforeunload handlers have been
* called. * called.
* @property {object[]} tabsWithBeforeUnloadPrompt * @property {object[]} tabsWithBeforeUnloadPrompt
@ -4114,15 +4127,36 @@
) { ) {
// Note: if you change any of the unload algorithm, consider also // Note: if you change any of the unload algorithm, consider also
// changing `runBeforeUnloadForTabs` above. // changing `runBeforeUnloadForTabs` above.
/** @type {MozTabbrowserTab[]} */
let tabsWithBeforeUnloadPrompt = []; let tabsWithBeforeUnloadPrompt = [];
/** @type {MozTabbrowserTab[]} */
let tabsWithoutBeforeUnload = []; let tabsWithoutBeforeUnload = [];
/** @type {Promise<void>[]} */
let beforeUnloadPromises = []; let beforeUnloadPromises = [];
/** @type {MozTabbrowserTab|undefined} */
let lastToClose; let lastToClose;
/**
* Map of tab group to surviving tabs in the group.
* If any of the `tabs` to be removed belong to a tab group, keep track
* of how many tabs in the tab group will be left after removing `tabs`.
* For any tab group with 0 surviving tabs, we can know that that tab
* group will be removed as a consequence of removing these `tabs`.
* @type {Map<MozTabbrowserTabGroup, Set<MozTabbrowserTab>>}
*/
let tabGroupsSurvivingTabs = new Map();
for (let tab of tabs) { for (let tab of tabs) {
if (!skipRemoves) { if (!skipRemoves) {
tab._closedInGroup = true; tab._closedInGroup = true;
} }
if (!skipRemoves && !skipSessionStore) {
if (tab.group) {
if (!tabGroupsSurvivingTabs.has(tab.group)) {
tabGroupsSurvivingTabs.set(tab.group, new Set(tab.group.tabs));
}
tabGroupsSurvivingTabs.get(tab.group).delete(tab);
}
}
if (!skipRemoves && tab.selected) { if (!skipRemoves && tab.selected) {
lastToClose = tab; lastToClose = tab;
let toBlurTo = this._findTabToBlurTo(lastToClose, tabs); let toBlurTo = this._findTabToBlurTo(lastToClose, tabs);
@ -4179,6 +4213,22 @@
} }
} }
if (!skipRemoves && !skipSessionStore) {
for (let [
tabGroup,
survivingTabs,
] of tabGroupsSurvivingTabs.entries()) {
// Before removing any tabs, save tab groups that won't survive
// because all of their tabs are about to be removed. Then remove
// the tab group directly to prevent the closing tabs from being
// recorded by the session as individually closed tabs.
if (!survivingTabs.size) {
tabGroup.save();
this.removeTabGroup(tabGroup);
}
}
}
// Now that all the beforeunload IPCs have been sent to content processes, // Now that all the beforeunload IPCs have been sent to content processes,
// we can queue unload messages for all the tabs without beforeunload listeners. // we can queue unload messages for all the tabs without beforeunload listeners.
// Doing this first would cause content process main threads to be busy and delay // Doing this first would cause content process main threads to be busy and delay
@ -4249,7 +4299,7 @@
/** /**
* Removes multiple tabs from the tab browser. * Removes multiple tabs from the tab browser.
* *
* @param {object[]} tabs * @param {MozTabbrowserTab[]} tabs
* The set of tabs to remove. * The set of tabs to remove.
* @param {object} [options] * @param {object} [options]
* @param {boolean} [options.animate] * @param {boolean} [options.animate]
@ -6418,13 +6468,8 @@
event.stopPropagation(); event.stopPropagation();
let tab = event.target.triggerNode?.closest("tab"); let tab = event.target.triggerNode?.closest("tab");
if (!tab) { if (!tab) {
if (event.target.triggerNode?.getRootNode()?.host?.closest("tab")) { event.preventDefault();
// Check if triggerNode is within shadowRoot of moz-button return;
tab = event.target.triggerNode?.getRootNode().host.closest("tab");
} else {
event.preventDefault();
return;
}
} }
const tooltip = event.target; const tooltip = event.target;
@ -6433,7 +6478,7 @@
const tabCount = this.selectedTabs.includes(tab) const tabCount = this.selectedTabs.includes(tab)
? this.selectedTabs.length ? this.selectedTabs.length
: 1; : 1;
if (tab._overPlayingIcon || tab._overAudioButton) { if (tab._overPlayingIcon) {
let l10nId; let l10nId;
const l10nArgs = { tabCount }; const l10nArgs = { tabCount };
if (tab.selected) { if (tab.selected) {
@ -7047,6 +7092,7 @@
// process so the browser can no longer be considered to be // process so the browser can no longer be considered to be
// crashed. // crashed.
tab.removeAttribute("crashed"); tab.removeAttribute("crashed");
gBrowser.tabContainer.updateTabIndicatorAttr(tab);
} }
if (this.isFindBarInitialized(tab)) { if (this.isFindBarInitialized(tab)) {
@ -7371,6 +7417,7 @@
delete this.mBrowser.initialPageLoadedFromUserAction; delete this.mBrowser.initialPageLoadedFromUserAction;
// If the browser is loading it must not be crashed anymore // If the browser is loading it must not be crashed anymore
this.mTab.removeAttribute("crashed"); this.mTab.removeAttribute("crashed");
gBrowser.tabContainer.updateTabIndicatorAttr(this.mTab);
} }
if (this._shouldShowProgress(aRequest)) { if (this._shouldShowProgress(aRequest)) {
@ -8419,17 +8466,21 @@ var TabContextMenu = {
// Disable "Close Tabs to the Left/Right" if there are no tabs // Disable "Close Tabs to the Left/Right" if there are no tabs
// preceding/following it. // preceding/following it.
let noTabsToStart = !gBrowser.getTabsToTheStartFrom(this.contextTab).length; let noTabsToStart = !gBrowser._getTabsToTheStartFrom(this.contextTab)
.length;
closeTabsToTheStartItem.disabled = noTabsToStart; closeTabsToTheStartItem.disabled = noTabsToStart;
let noTabsToEnd = !gBrowser.getTabsToTheEndFrom(this.contextTab).length; let noTabsToEnd = !gBrowser._getTabsToTheEndFrom(this.contextTab).length;
closeTabsToTheEndItem.disabled = noTabsToEnd; closeTabsToTheEndItem.disabled = noTabsToEnd;
// Disable "Close other Tabs" if there are no unpinned tabs. // Disable "Close other Tabs" if there are no unpinned tabs.
let unpinnedTabsToClose = multiselectionContext let unpinnedTabsToClose = multiselectionContext
? gBrowser.visibleTabs.filter(t => !t.multiselected && !t.pinned).length ? gBrowser.openTabs.filter(
: gBrowser.visibleTabs.filter(t => t != this.contextTab && !t.pinned) t => !t.multiselected && !t.pinned && !t.hidden
.length; ).length
: gBrowser.openTabs.filter(
t => t != this.contextTab && !t.pinned && !t.hidden
).length;
let closeOtherTabsItem = document.getElementById("context_closeOtherTabs"); let closeOtherTabsItem = document.getElementById("context_closeOtherTabs");
closeOtherTabsItem.disabled = unpinnedTabsToClose < 1; closeOtherTabsItem.disabled = unpinnedTabsToClose < 1;

View file

@ -15,9 +15,13 @@
<html:slot/> <html:slot/>
`; `;
/** @type {string} */
#label;
/** @type {MozTextLabel} */ /** @type {MozTextLabel} */
#labelElement; #labelElement;
/** @type {string} */
#colorCode; #colorCode;
constructor() { constructor() {
@ -44,8 +48,8 @@
this.#labelElement = this.querySelector(".tab-group-label"); this.#labelElement = this.querySelector(".tab-group-label");
this.#labelElement.addEventListener("click", this); this.#labelElement.addEventListener("click", this);
this.#updateLabelAriaAttributes(this.label); this.#updateLabelAriaAttributes();
this.#updateCollapsedAriaAttributes(this.collapsed); this.#updateCollapsedAriaAttributes();
this.createdDate = Date.now(); this.createdDate = Date.now();
@ -121,12 +125,26 @@
} }
get label() { get label() {
return this.getAttribute("label"); return this.#label;
} }
set label(val) { set label(val) {
this.setAttribute("label", val); this.#label = val;
this.#updateLabelAriaAttributes(val);
// Add a zero width space so we always create a text node and get
// consistent layout even if the group name is empty.
this.setAttribute("label", "\u200b" + val);
this.#updateLabelAriaAttributes();
}
// alias for label
get name() {
return this.label;
}
set name(newName) {
this.label = newName;
} }
get collapsed() { get collapsed() {
@ -138,26 +156,20 @@
return; return;
} }
this.toggleAttribute("collapsed", val); this.toggleAttribute("collapsed", val);
this.#updateCollapsedAriaAttributes(val); this.#updateCollapsedAriaAttributes();
const eventName = val ? "TabGroupCollapse" : "TabGroupExpand"; const eventName = val ? "TabGroupCollapse" : "TabGroupExpand";
this.dispatchEvent(new CustomEvent(eventName, { bubbles: true })); this.dispatchEvent(new CustomEvent(eventName, { bubbles: true }));
} }
/** #updateLabelAriaAttributes() {
* @param {string} label const ariaLabel = this.#label || "unnamed";
*/
#updateLabelAriaAttributes(label) {
const ariaLabel = label == "" ? "unnamed" : label;
const ariaDescription = `${ariaLabel} tab group`; const ariaDescription = `${ariaLabel} tab group`;
this.#labelElement?.setAttribute("aria-label", ariaLabel); this.#labelElement?.setAttribute("aria-label", ariaLabel);
this.#labelElement?.setAttribute("aria-description", ariaDescription); this.#labelElement?.setAttribute("aria-description", ariaDescription);
} }
/** #updateCollapsedAriaAttributes() {
* @param {boolean} collapsed const ariaExpanded = this.collapsed ? "false" : "true";
*/
#updateCollapsedAriaAttributes(collapsed) {
const ariaExpanded = collapsed ? "false" : "true";
this.#labelElement?.setAttribute("aria-expanded", ariaExpanded); this.#labelElement?.setAttribute("aria-expanded", ariaExpanded);
} }
@ -221,6 +233,19 @@
on_TabSelect() { on_TabSelect() {
this.collapsed = false; this.collapsed = false;
} }
/**
* If one of this group's tabs is the selected tab, this will do nothing.
* Otherwise, it will expand the group if collapsed, and select the first
* tab in its list.
*/
select() {
this.collapsed = false;
if (gBrowser.selectedTab.group == this) {
return;
}
gBrowser.selectedTab = this.tabs[0];
}
} }
customElements.define("tab-group", MozTabbrowserTabGroup); customElements.define("tab-group", MozTabbrowserTabGroup);

View file

@ -55,6 +55,8 @@
this.addEventListener("TabAttrModified", this); this.addEventListener("TabAttrModified", this);
this.addEventListener("TabHide", this); this.addEventListener("TabHide", this);
this.addEventListener("TabShow", this); this.addEventListener("TabShow", this);
this.addEventListener("TabPinned", this);
this.addEventListener("TabUnpinned", this);
this.addEventListener("TabHoverStart", this); this.addEventListener("TabHoverStart", this);
this.addEventListener("TabHoverEnd", this); this.addEventListener("TabHoverEnd", this);
this.addEventListener("TabGroupExpand", this); this.addEventListener("TabGroupExpand", this);
@ -220,6 +222,17 @@
this.#updateTabMinWidth(); this.#updateTabMinWidth();
this.#updateTabMinHeight(); this.#updateTabMinHeight();
let indicatorTabs = gBrowser.visibleTabs.filter(tab => {
return (
tab.hasAttribute("soundplaying") ||
tab.hasAttribute("muted") ||
tab.hasAttribute("activemedia-blocked")
);
});
for (const indicatorTab of indicatorTabs) {
this.updateTabIndicatorAttr(indicatorTab);
}
super.attributeChangedCallback(name, oldValue, newValue); super.attributeChangedCallback(name, oldValue, newValue);
} }
@ -232,6 +245,14 @@
} }
on_TabAttrModified(event) { on_TabAttrModified(event) {
if (
["soundplaying", "muted", "activemedia-blocked", "sharing"].some(attr =>
event.detail.changed.includes(attr)
)
) {
this.updateTabIndicatorAttr(event.target);
}
if ( if (
event.detail.changed.includes("soundplaying") && event.detail.changed.includes("soundplaying") &&
!event.target.visible !event.target.visible
@ -252,6 +273,14 @@
} }
} }
on_TabPinned(event) {
this.updateTabIndicatorAttr(event.target);
}
on_TabUnpinned(event) {
this.updateTabIndicatorAttr(event.target);
}
on_TabHoverStart(event) { on_TabHoverStart(event) {
if (!this._showCardPreviews) { if (!this._showCardPreviews) {
return; return;
@ -3027,6 +3056,24 @@
} }
CustomizableUI.removeListener(this); CustomizableUI.removeListener(this);
} }
updateTabIndicatorAttr(tab) {
const theseAttributes = ["soundplaying", "muted", "activemedia-blocked"];
const notTheseAttributes = ["pinned", "sharing", "crashed"];
if (
this.verticalMode ||
notTheseAttributes.some(attr => tab.hasAttribute(attr))
) {
tab.removeAttribute("indicator-replaces-favicon");
return;
}
tab.toggleAttribute(
"indicator-replaces-favicon",
theseAttributes.some(attr => tab.hasAttribute(attr))
);
}
} }
customElements.define("tabbrowser-tabs", MozTabbrowserTabs, { customElements.define("tabbrowser-tabs", MozTabbrowserTabs, {

View file

@ -9,6 +9,7 @@ JAR_MANIFESTS += ["jar.mn"]
EXTRA_JS_MODULES += [ EXTRA_JS_MODULES += [
"AsyncTabSwitcher.sys.mjs", "AsyncTabSwitcher.sys.mjs",
"GroupsList.sys.mjs",
"NewTabPagePreloading.sys.mjs", "NewTabPagePreloading.sys.mjs",
"OpenInTabsUtils.sys.mjs", "OpenInTabsUtils.sys.mjs",
"TabsList.sys.mjs", "TabsList.sys.mjs",

View file

@ -50,7 +50,8 @@ add_task(async function mute_web_audio() {
info("- mute browser -"); info("- mute browser -");
ok(!tab.linkedBrowser.audioMuted, "Audio should not be muted by default"); ok(!tab.linkedBrowser.audioMuted, "Audio should not be muted by default");
await clickIcon(tab.audioButton); await hoverIcon(tab.overlayIcon);
await clickIcon(tab.overlayIcon);
ok(tab.linkedBrowser.audioMuted, "Audio should be muted now"); ok(tab.linkedBrowser.audioMuted, "Audio should be muted now");
info("- stop web audip -"); info("- stop web audip -");
@ -61,7 +62,8 @@ add_task(async function mute_web_audio() {
info("- unmute browser -"); info("- unmute browser -");
ok(tab.linkedBrowser.audioMuted, "Audio should be muted now"); ok(tab.linkedBrowser.audioMuted, "Audio should be muted now");
await clickIcon(tab.audioButton); await hoverIcon(tab.overlayIcon);
await clickIcon(tab.overlayIcon);
ok(!tab.linkedBrowser.audioMuted, "Audio should be unmuted now"); ok(!tab.linkedBrowser.audioMuted, "Audio should be unmuted now");
info("- tab should be audible -"); info("- tab should be audible -");

View file

@ -14,8 +14,10 @@ prefs = [
] ]
["browser_addAdjacentNewTab.js"] ["browser_addAdjacentNewTab.js"]
tags = "vertical-tabs"
["browser_addTab_index.js"] ["browser_addTab_index.js"]
tags = "vertical-tabs"
["browser_adoptTab_failure.js"] ["browser_adoptTab_failure.js"]
@ -25,7 +27,8 @@ prefs = [
support-files = ["alltabslistener.html"] support-files = ["alltabslistener.html"]
["browser_audioTabIcon.js"] ["browser_audioTabIcon.js"]
tags = "audiochannel" tags = "vertical-tabs"
fail-if = ["vertical_tab"] # Bug 1935548, fails in the "vertical-tabs" variant
["browser_beforeunload_duplicate_dialogs.js"] ["browser_beforeunload_duplicate_dialogs.js"]
https_first_disabled = true https_first_disabled = true
@ -34,20 +37,24 @@ https_first_disabled = true
run-if = ["fission"] run-if = ["fission"]
["browser_blank_tab_label.js"] ["browser_blank_tab_label.js"]
tags = "vertical-tabs"
["browser_bug580956.js"] ["browser_bug580956.js"]
["browser_bug_1387976_restore_lazy_tab_browser_muted_state.js"] ["browser_bug_1387976_restore_lazy_tab_browser_muted_state.js"]
tags = "vertical-tabs"
["browser_close_during_beforeunload.js"] ["browser_close_during_beforeunload.js"]
https_first_disabled = true https_first_disabled = true
["browser_close_tab_by_dblclick.js"] ["browser_close_tab_by_dblclick.js"]
tags = "vertical-tabs"
["browser_contextmenu_openlink_after_tabnavigated.js"] ["browser_contextmenu_openlink_after_tabnavigated.js"]
https_first_disabled = true https_first_disabled = true
skip-if = ["verify && debug && os == 'linux'"] skip-if = ["verify && debug && os == 'linux'"]
support-files = ["test_bug1358314.html"] support-files = ["test_bug1358314.html"]
tags = "vertical-tabs"
["browser_ctrlTab.js"] ["browser_ctrlTab.js"]
@ -59,6 +66,7 @@ support-files = [
["browser_double_close_tab.js"] ["browser_double_close_tab.js"]
support-files = ["file_double_close_tab.html"] support-files = ["file_double_close_tab.html"]
tags = "vertical-tabs"
["browser_e10s_about_page_triggeringprincipal.js"] ["browser_e10s_about_page_triggeringprincipal.js"]
https_first_disabled = true https_first_disabled = true
@ -67,6 +75,7 @@ support-files = [
"file_about_child.html", "file_about_child.html",
"file_about_parent.html", "file_about_parent.html",
] ]
tags = "vertical-tabs"
["browser_e10s_about_process.js"] ["browser_e10s_about_process.js"]
@ -80,6 +89,7 @@ skip-if = ["debug"] # Bug 1444565, Bug 1457887
["browser_e10s_switchbrowser.js"] ["browser_e10s_switchbrowser.js"]
["browser_exclude_fxview_hidden_tabs.js"] ["browser_exclude_fxview_hidden_tabs.js"]
tags = "vertical-tabs"
["browser_file_to_http_named_popup.js"] ["browser_file_to_http_named_popup.js"]
@ -87,13 +97,17 @@ skip-if = ["debug"] # Bug 1444565, Bug 1457887
support-files = ["tab_that_closes.html"] support-files = ["tab_that_closes.html"]
["browser_hiddentab_contextmenu.js"] ["browser_hiddentab_contextmenu.js"]
tags = "vertical-tabs"
["browser_lastAccessedTab.js"] ["browser_lastAccessedTab.js"]
skip-if = ["os == 'windows'"] # Disabled on Windows due to frequent failures (bug 969405) skip-if = ["os == 'windows'"] # Disabled on Windows due to frequent failures (bug 969405)
tags = "vertical-tabs"
["browser_lastSeenActive.js"] ["browser_lastSeenActive.js"]
tags = "vertical-tabs"
["browser_lazy_tab_browser_events.js"] ["browser_lazy_tab_browser_events.js"]
tags = "vertical-tabs"
["browser_link_in_tab_title_and_url_prefilled_blank_page.js"] ["browser_link_in_tab_title_and_url_prefilled_blank_page.js"]
support-files = [ support-files = [
@ -102,6 +116,7 @@ support-files = [
"request-timeout.sjs", "request-timeout.sjs",
"wait-a-bit.sjs", "wait-a-bit.sjs",
] ]
tags = "vertical-tabs"
["browser_link_in_tab_title_and_url_prefilled_new_window.js"] ["browser_link_in_tab_title_and_url_prefilled_new_window.js"]
support-files = [ support-files = [
@ -110,6 +125,7 @@ support-files = [
"request-timeout.sjs", "request-timeout.sjs",
"wait-a-bit.sjs", "wait-a-bit.sjs",
] ]
tags = "vertical-tabs"
["browser_link_in_tab_title_and_url_prefilled_normal_page_blank_target.js"] ["browser_link_in_tab_title_and_url_prefilled_normal_page_blank_target.js"]
support-files = [ support-files = [
@ -118,6 +134,8 @@ support-files = [
"request-timeout.sjs", "request-timeout.sjs",
"wait-a-bit.sjs", "wait-a-bit.sjs",
] ]
tags = "vertical-tabs"
fail-if = ["vertical_tab"] # Bug 1932786, fails in the "vertical-tabs" variant
["browser_link_in_tab_title_and_url_prefilled_normal_page_by_script.js"] ["browser_link_in_tab_title_and_url_prefilled_normal_page_by_script.js"]
support-files = [ support-files = [
@ -126,6 +144,8 @@ support-files = [
"request-timeout.sjs", "request-timeout.sjs",
"wait-a-bit.sjs", "wait-a-bit.sjs",
] ]
tags = "vertical-tabs"
fail-if = ["vertical_tab"] # Bug 1932786, fails in the "vertical-tabs" variant
["browser_link_in_tab_title_and_url_prefilled_normal_page_no_target.js"] ["browser_link_in_tab_title_and_url_prefilled_normal_page_no_target.js"]
support-files = [ support-files = [
@ -134,6 +154,8 @@ support-files = [
"request-timeout.sjs", "request-timeout.sjs",
"wait-a-bit.sjs", "wait-a-bit.sjs",
] ]
tags = "vertical-tabs"
fail-if = ["vertical_tab"] # Bug 1932786, fails in the "vertical-tabs" variant
["browser_link_in_tab_title_and_url_prefilled_normal_page_other_target.js"] ["browser_link_in_tab_title_and_url_prefilled_normal_page_other_target.js"]
support-files = [ support-files = [
@ -142,80 +164,117 @@ support-files = [
"request-timeout.sjs", "request-timeout.sjs",
"wait-a-bit.sjs", "wait-a-bit.sjs",
] ]
tags = "vertical-tabs"
fail-if = ["vertical_tab"] # Bug 1932786, fails in the "vertical-tabs" variant
["browser_long_data_url_label_truncation.js"] ["browser_long_data_url_label_truncation.js"]
tags = "vertical-tabs"
["browser_middle_click_new_tab_button_loads_clipboard.js"] ["browser_middle_click_new_tab_button_loads_clipboard.js"]
tags = "vertical-tabs"
fail-if = ["vertical_tab"] # Bug 1932787, fails in the "vertical-tabs" variant
["browser_multiselect_tabs_active_tab_selected_by_default.js"] ["browser_multiselect_tabs_active_tab_selected_by_default.js"]
tags = "vertical-tabs"
["browser_multiselect_tabs_bookmark.js"] ["browser_multiselect_tabs_bookmark.js"]
tags = "vertical-tabs"
["browser_multiselect_tabs_clear_selection_when_tab_switch.js"] ["browser_multiselect_tabs_clear_selection_when_tab_switch.js"]
tags = "vertical-tabs"
["browser_multiselect_tabs_close.js"] ["browser_multiselect_tabs_close.js"]
["browser_multiselect_tabs_close_duplicate_tabs.js"] ["browser_multiselect_tabs_close_duplicate_tabs.js"]
tags = "vertical-tabs"
["browser_multiselect_tabs_close_other_tabs.js"] ["browser_multiselect_tabs_close_other_tabs.js"]
tags = "vertical-tabs"
["browser_multiselect_tabs_close_tabs_to_the_left.js"] ["browser_multiselect_tabs_close_tabs_to_the_left.js"]
tags = "vertical-tabs"
["browser_multiselect_tabs_close_tabs_to_the_right.js"] ["browser_multiselect_tabs_close_tabs_to_the_right.js"]
tags = "vertical-tabs"
["browser_multiselect_tabs_close_using_shortcuts.js"] ["browser_multiselect_tabs_close_using_shortcuts.js"]
tags = "vertical-tabs"
["browser_multiselect_tabs_copy_through_drag_and_drop.js"] ["browser_multiselect_tabs_copy_through_drag_and_drop.js"]
tags = "vertical-tabs"
["browser_multiselect_tabs_drag_to_bookmarks_toolbar.js"] ["browser_multiselect_tabs_drag_to_bookmarks_toolbar.js"]
tags = "vertical-tabs"
["browser_multiselect_tabs_duplicate.js"] ["browser_multiselect_tabs_duplicate.js"]
tags = "vertical-tabs"
["browser_multiselect_tabs_event.js"] ["browser_multiselect_tabs_event.js"]
tags = "vertical-tabs"
["browser_multiselect_tabs_move.js"] ["browser_multiselect_tabs_move.js"]
tags = "vertical-tabs"
["browser_multiselect_tabs_move_to_another_window_drag.js"] ["browser_multiselect_tabs_move_to_another_window_drag.js"]
tags = "vertical-tabs"
["browser_multiselect_tabs_move_to_new_window_contextmenu.js"] ["browser_multiselect_tabs_move_to_new_window_contextmenu.js"]
https_first_disabled = true https_first_disabled = true
tags = "vertical-tabs"
["browser_multiselect_tabs_mute_unmute.js"] ["browser_multiselect_tabs_mute_unmute.js"]
tags = "vertical-tabs"
["browser_multiselect_tabs_open_related.js"] ["browser_multiselect_tabs_open_related.js"]
tags = "vertical-tabs"
["browser_multiselect_tabs_pin_unpin.js"] ["browser_multiselect_tabs_pin_unpin.js"]
tags = "vertical-tabs"
["browser_multiselect_tabs_play.js"] ["browser_multiselect_tabs_play.js"]
tags = "vertical-tabs"
["browser_multiselect_tabs_reload.js"] ["browser_multiselect_tabs_reload.js"]
tags = "vertical-tabs"
["browser_multiselect_tabs_reopen_in_container.js"] ["browser_multiselect_tabs_reopen_in_container.js"]
tags = "vertical-tabs"
["browser_multiselect_tabs_reorder.js"] ["browser_multiselect_tabs_reorder.js"]
tags = "vertical-tabs"
["browser_multiselect_tabs_unload.js"] ["browser_multiselect_tabs_unload.js"]
tags = "vertical-tabs"
["browser_multiselect_tabs_unload_telemetry.js"] ["browser_multiselect_tabs_unload_telemetry.js"]
skip-if = [ skip-if = [
"os == 'mac' && os_version == '14.70' && processor == 'x86_64' && opt", # Bug 1929417 "os == 'mac' && os_version == '14.70' && processor == 'x86_64' && opt", # Bug 1929417
] ]
tags = "vertical-tabs"
["browser_multiselect_tabs_unload_with_beforeunload.js"] ["browser_multiselect_tabs_unload_with_beforeunload.js"]
tags = "vertical-tabs"
["browser_multiselect_tabs_using_Ctrl.js"] ["browser_multiselect_tabs_using_Ctrl.js"]
tags = "vertical-tabs"
["browser_multiselect_tabs_using_Shift.js"] ["browser_multiselect_tabs_using_Shift.js"]
tags = "vertical-tabs"
["browser_multiselect_tabs_using_Shift_and_Ctrl.js"] ["browser_multiselect_tabs_using_Shift_and_Ctrl.js"]
tags = "vertical-tabs"
["browser_multiselect_tabs_using_keyboard.js"] ["browser_multiselect_tabs_using_keyboard.js"]
run-if = ["os != 'mac'"] # Skipped because macOS keyboard support requires changing system settings run-if = ["os != 'mac'"] # Skipped because macOS keyboard support requires changing system settings
tags = "vertical-tabs"
fail-if = ["vertical_tab"] # Bug 1932790, fails in the "vertical-tabs" variant
["browser_multiselect_tabs_using_selectedTabs.js"] ["browser_multiselect_tabs_using_selectedTabs.js"]
tags = "vertical-tabs"
["browser_navigatePinnedTab.js"] ["browser_navigatePinnedTab.js"]
https_first_disabled = true https_first_disabled = true
tags = "vertical-tabs"
["browser_navigate_home_focuses_addressbar.js"] ["browser_navigate_home_focuses_addressbar.js"]
@ -241,7 +300,9 @@ skip-if = [
"os == 'mac' && os_version == '10.15' && processor == 'x86_64'", # Bug 1872477 "os == 'mac' && os_version == '10.15' && processor == 'x86_64'", # Bug 1872477
"os == 'mac' && os_version == '11.20' && arch == 'aarch64'", # Bug 1872477 "os == 'mac' && os_version == '11.20' && arch == 'aarch64'", # Bug 1872477
"os == 'mac' && os_version == '14.70' && processor == 'x86_64'", # Bug 1929417 "os == 'mac' && os_version == '14.70' && processor == 'x86_64'", # Bug 1929417
"vertical_tab", # Bug 1936170
] ]
tags = "vertical-tabs"
["browser_new_tab_in_privilegedabout_process_pref.js"] ["browser_new_tab_in_privilegedabout_process_pref.js"]
https_first_disabled = true https_first_disabled = true
@ -249,11 +310,13 @@ https_first_disabled = true
["browser_new_tab_insert_position.js"] ["browser_new_tab_insert_position.js"]
https_first_disabled = true https_first_disabled = true
support-files = ["file_new_tab_page.html"] support-files = ["file_new_tab_page.html"]
tags = "vertical-tabs"
["browser_new_tab_url.js"] ["browser_new_tab_url.js"]
support-files = ["file_new_tab_page.html"] support-files = ["file_new_tab_page.html"]
["browser_newwindow_tabstrip_overflow.js"] ["browser_newwindow_tabstrip_overflow.js"]
tags = "vertical-tabs"
["browser_openURI_background.js"] ["browser_openURI_background.js"]
@ -280,15 +343,20 @@ support-files = [
] ]
["browser_overflowScroll.js"] ["browser_overflowScroll.js"]
tags = "vertical-tabs"
skip-if = ["vertical_tab"] # Bug 1932964, fails in the "vertical-tabs" variant
["browser_paste_event_at_middle_click_on_link.js"] ["browser_paste_event_at_middle_click_on_link.js"]
support-files = ["file_anchor_elements.html"] support-files = ["file_anchor_elements.html"]
["browser_pinnedTabs.js"] ["browser_pinnedTabs.js"]
tags = "vertical-tabs"
["browser_pinnedTabs_clickOpen.js"] ["browser_pinnedTabs_clickOpen.js"]
tags = "vertical-tabs"
["browser_pinnedTabs_closeByKeyboard.js"] ["browser_pinnedTabs_closeByKeyboard.js"]
tags = "vertical-tabs"
["browser_positional_attributes.js"] ["browser_positional_attributes.js"]
skip-if = [ skip-if = [
@ -296,6 +364,7 @@ skip-if = [
"os == 'mac' && os_version == '10.15' && processor == 'x86_64' && verify", "os == 'mac' && os_version == '10.15' && processor == 'x86_64' && verify",
"os == 'mac' && os_version == '11.20' && arch == 'aarch64' && verify", "os == 'mac' && os_version == '11.20' && arch == 'aarch64' && verify",
] ]
tags = "vertical-tabs"
["browser_preloadedBrowser_zoom.js"] ["browser_preloadedBrowser_zoom.js"]
@ -306,69 +375,99 @@ https_first_disabled = true
https_first_disabled = true https_first_disabled = true
["browser_relatedTabs.js"] ["browser_relatedTabs.js"]
tags = "vertical-tabs"
["browser_relatedTabs_reset.js"] ["browser_relatedTabs_reset.js"]
tags = "vertical-tabs"
["browser_reload_deleted_file.js"] ["browser_reload_deleted_file.js"]
tags = "vertical-tabs"
skip-if = ["os == 'mac' && vertical_tab"] # Bug 1936168
["browser_removeAllTabsBut.js"]
tags = "vertical-tabs"
["browser_removeTabsToTheEnd.js"] ["browser_removeTabsToTheEnd.js"]
tags = "vertical-tabs"
["browser_removeTabsToTheStart.js"] ["browser_removeTabsToTheStart.js"]
tags = "vertical-tabs"
["browser_removeTabs_order.js"] ["browser_removeTabs_order.js"]
tags = "vertical-tabs"
["browser_removeTabs_skipPermitUnload.js"] ["browser_removeTabs_skipPermitUnload.js"]
tags = "vertical-tabs"
["browser_replacewithwindow_commands.js"] ["browser_replacewithwindow_commands.js"]
tags = "vertical-tabs"
["browser_replacewithwindow_dialog.js"] ["browser_replacewithwindow_dialog.js"]
support-files = ["tab_that_opens_dialog.html"] support-files = ["tab_that_opens_dialog.html"]
tags = "vertical-tabs"
["browser_restore_isAppTab.js"] ["browser_restore_isAppTab.js"]
run-if = ["crashreporter"] # test requires crashreporter due to 1536221 run-if = ["crashreporter"] # test requires crashreporter due to 1536221
tags = "vertical-tabs"
["browser_scroll_size_determination.js"] ["browser_scroll_size_determination.js"]
["browser_selectTabAtIndex.js"] ["browser_selectTabAtIndex.js"]
tags = "vertical-tabs"
["browser_switch_by_scrolling.js"] ["browser_switch_by_scrolling.js"]
tags = "vertical-tabs"
["browser_tabCloseProbes.js"] ["browser_tabCloseProbes.js"]
tags = "vertical-tabs"
skip-if = ["os == 'mac' && vertical_tab"] # Bug 1932997
["browser_tabCloseSpacer.js"] ["browser_tabCloseSpacer.js"]
skip-if = ["true"] # Bug 1616418 Bug 1549985 skip-if = ["true"] # Bug 1616418 Bug 1549985
["browser_tabContextMenu_keyboard.js"] ["browser_tabContextMenu_keyboard.js"]
tags = "vertical-tabs"
["browser_tabDrop.js"] ["browser_tabDrop.js"]
https_first_disabled = true https_first_disabled = true
tags = "vertical-tabs"
["browser_tabReorder.js"] ["browser_tabReorder.js"]
tags = "vertical-tabs"
["browser_tabReorder_overflow.js"] ["browser_tabReorder_overflow.js"]
tags = "vertical-tabs"
["browser_tabReorder_vertical.js"] ["browser_tabReorder_vertical.js"]
tags = "vertical-tabs"
["browser_tabSpinnerProbe.js"] ["browser_tabSpinnerProbe.js"]
tags = "vertical-tabs"
["browser_tabSuccessors.js"] ["browser_tabSuccessors.js"]
["browser_tab_a11y_description.js"] ["browser_tab_a11y_description.js"]
tags = "vertical-tabs"
["browser_tab_close_dependent_window.js"] ["browser_tab_close_dependent_window.js"]
tags = "vertical-tabs"
["browser_tab_detach_restore.js"] ["browser_tab_detach_restore.js"]
https_first_disabled = true https_first_disabled = true
tags = "vertical-tabs"
["browser_tab_drag_drop_perwindow.js"] ["browser_tab_drag_drop_perwindow.js"]
tags = "vertical-tabs"
["browser_tab_dragdrop.js"] ["browser_tab_dragdrop.js"]
skip-if = ["true"] # Bug 1312436, Bug 1388973 skip-if = ["true"] # Bug 1312436, Bug 1388973
support-files = ["browser_tab_dragdrop_embed.html"] support-files = ["browser_tab_dragdrop_embed.html"]
tags = "vertical-tabs"
["browser_tab_dragdrop2.js"] ["browser_tab_dragdrop2.js"]
skip-if = ["win11_2009 && bits == 32 && !debug"] # high frequency win7 intermittent: crash skip-if = ["win11_2009 && bits == 32 && !debug"] # high frequency win7 intermittent: crash
support-files = ["browser_tab_dragdrop2_frame1.xhtml"] support-files = ["browser_tab_dragdrop2_frame1.xhtml"]
tags = "vertical-tabs"
["browser_tab_groups.js"] ["browser_tab_groups.js"]
support-files = ["file_new_tab_page.html"] support-files = ["file_new_tab_page.html"]
@ -376,51 +475,73 @@ skip-if = [
"os == 'linux' && os_version == '18.04' && processor == 'x86_64'", # Bug 1920294 "os == 'linux' && os_version == '18.04' && processor == 'x86_64'", # Bug 1920294
"os == 'mac' && os_version == '14.70' && processor == 'x86_64' && opt", # Bug 1929417 (secondary) "os == 'mac' && os_version == '14.70' && processor == 'x86_64' && opt", # Bug 1929417 (secondary)
] ]
tags = "vertical-tabs"
["browser_tab_groups_a11y.js"] ["browser_tab_groups_a11y.js"]
tags = "vertical-tabs"
["browser_tab_groups_keyboard_focus.js"] ["browser_tab_groups_keyboard_focus.js"]
tags = "vertical-tabs"
["browser_tab_label_during_reload.js"] ["browser_tab_label_during_reload.js"]
tags = "vertical-tabs"
["browser_tab_label_picture_in_picture.js"] ["browser_tab_label_picture_in_picture.js"]
tags = "vertical-tabs"
["browser_tab_manager_close.js"] ["browser_tab_manager_close.js"]
tags = "vertical-tabs"
["browser_tab_manager_drag.js"] ["browser_tab_manager_drag.js"]
tags = "vertical-tabs"
["browser_tab_manager_groups.js"]
tags = "vertical-tabs"
["browser_tab_manager_keyboard_access.js"] ["browser_tab_manager_keyboard_access.js"]
tags = "vertical-tabs"
["browser_tab_manger_synced_tabs.js"] ["browser_tab_manger_synced_tabs.js"]
tags = "vertical-tabs"
["browser_tab_move_active_tab.js"] ["browser_tab_move_active_tab.js"]
tags = "vertical-tabs"
["browser_tab_move_to_new_window_reload.js"] ["browser_tab_move_to_new_window_reload.js"]
tags = "vertical-tabs"
["browser_tab_play.js"] ["browser_tab_play.js"]
tags = "vertical-tabs"
["browser_tab_preview.js"] ["browser_tab_preview.js"]
tags = "vertical-tabs"
["browser_tab_tooltips.js"] ["browser_tab_tooltips.js"]
tags = "vertical-tabs"
["browser_tabfocus.js"] ["browser_tabfocus.js"]
tags = "vertical-tabs"
["browser_tabkeynavigation.js"] ["browser_tabkeynavigation.js"]
tags = "vertical-tabs"
["browser_tabs_close_beforeunload.js"] ["browser_tabs_close_beforeunload.js"]
support-files = [ support-files = [
"close_beforeunload_opens_second_tab.html", "close_beforeunload_opens_second_tab.html",
"close_beforeunload.html", "close_beforeunload.html",
] ]
tags = "vertical-tabs"
["browser_tabs_isActive.js"] ["browser_tabs_isActive.js"]
["browser_tabs_owner.js"] ["browser_tabs_owner.js"]
["browser_tabswitch_contextmenu.js"] ["browser_tabswitch_contextmenu.js"]
tags = "vertical-tabs"
["browser_tabswitch_select.js"] ["browser_tabswitch_select.js"]
support-files = ["open_window_in_new_tab.html"] support-files = ["open_window_in_new_tab.html"]
tags = "vertical-tabs"
["browser_tabswitch_updatecommands.js"] ["browser_tabswitch_updatecommands.js"]
@ -438,15 +559,18 @@ skip-if = ["true"] #bug 1642084
["browser_viewsource_of_data_URI_in_file_process.js"] ["browser_viewsource_of_data_URI_in_file_process.js"]
["browser_visibleTabs.js"] ["browser_visibleTabs.js"]
tags = "vertical-tabs"
["browser_visibleTabs_bookmarkAllPages.js"] ["browser_visibleTabs_bookmarkAllPages.js"]
["browser_visibleTabs_bookmarkAllTabs.js"] ["browser_visibleTabs_bookmarkAllTabs.js"]
["browser_visibleTabs_contextMenu.js"] ["browser_visibleTabs_contextMenu.js"]
tags = "vertical-tabs"
["browser_visibleTabs_tabPreview.js"] ["browser_visibleTabs_tabPreview.js"]
skip-if = ["os == 'win' && !debug"] skip-if = ["os == 'win' && !debug"]
["browser_window_open_modifiers.js"] ["browser_window_open_modifiers.js"]
support-files = ["file_window_open.html"] support-files = ["file_window_open.html"]
tags = "vertical-tabs"

View file

@ -574,6 +574,11 @@ async function test_mute_keybinding() {
let mutedPromise = get_wait_for_mute_promise(tab, true); let mutedPromise = get_wait_for_mute_promise(tab, true);
EventUtils.synthesizeKey("m", { ctrlKey: true }); EventUtils.synthesizeKey("m", { ctrlKey: true });
await mutedPromise; await mutedPromise;
is(
tab.hasAttribute("indicator-replaces-favicon"),
!tab.pinned,
"Mute indicator should replace the favicon on hover if the tab isn't pinned"
);
mutedPromise = get_wait_for_mute_promise(tab, false); mutedPromise = get_wait_for_mute_promise(tab, false);
EventUtils.synthesizeKey("m", { ctrlKey: true }); EventUtils.synthesizeKey("m", { ctrlKey: true });
await mutedPromise; await mutedPromise;

View file

@ -0,0 +1,134 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(async function test_removeAllButSingleTab() {
let win = await BrowserTestUtils.openNewBrowserWindow();
let tab1 = await addTabTo(win.gBrowser);
let tab2 = await addTabTo(win.gBrowser);
let tab3 = await addTabTo(win.gBrowser);
Assert.equal(
win.gBrowser.tabs.length,
4,
"should be 1 new tab from window + 3 added tabs from test"
);
win.gBrowser.removeAllTabsBut(tab2);
await TestUtils.waitForCondition(
() => win.gBrowser.tabs.length == 1,
"waiting for other tabs to close"
);
Assert.ok(
!win.gBrowser.tabs.some(tab => tab == tab1),
"tab1 should have been closed"
);
Assert.ok(
win.gBrowser.tabs.some(tab => tab == tab2),
"tab2 should still be present"
);
Assert.ok(
!win.gBrowser.tabs.some(tab => tab == tab3),
"tab3 should have been closed"
);
await BrowserTestUtils.closeWindow(win);
});
add_task(async function test_removesAllButMultiselectedTabs() {
let win = await BrowserTestUtils.openNewBrowserWindow();
let tab1 = await addTabTo(win.gBrowser);
let tab2 = await addTabTo(win.gBrowser);
let tab3 = await addTabTo(win.gBrowser);
let tab4 = await addTabTo(win.gBrowser);
let tab5 = await addTabTo(win.gBrowser);
Assert.equal(
win.gBrowser.tabs.length,
6,
"should be 1 new tab from window + 5 added tabs from test"
);
win.gBrowser.selectedTabs = [tab2, tab4];
win.gBrowser.removeAllTabsBut(tab2);
await TestUtils.waitForCondition(
() => win.gBrowser.tabs.length == 2,
"waiting for other tabs to close"
);
Assert.ok(
!win.gBrowser.tabs.some(tab => tab == tab1),
"tab1 should have been closed"
);
Assert.ok(
win.gBrowser.tabs.some(tab => tab == tab2),
"tab2 should still be present"
);
Assert.ok(
!win.gBrowser.tabs.some(tab => tab == tab3),
"tab3 should have been closed"
);
Assert.ok(
win.gBrowser.tabs.some(tab => tab == tab4),
"tab4 should still be present"
);
Assert.ok(
!win.gBrowser.tabs.some(tab => tab == tab5),
"tab5 should have been closed"
);
await BrowserTestUtils.closeWindow(win);
});
add_task(async function test_removesAllIncludingTabGroups() {
let win = await BrowserTestUtils.openNewBrowserWindow();
let tab1 = await addTabTo(win.gBrowser);
let tab2 = await addTabTo(win.gBrowser);
let tab3 = await addTabTo(win.gBrowser);
let tab4 = await addTabTo(win.gBrowser);
win.gBrowser.addTabGroup([tab3, tab4]);
Assert.equal(
win.gBrowser.tabs.length,
5,
"should be 1 new tab from window + 5 added tabs from test"
);
Assert.equal(win.gBrowser.tabGroups.length, 1, "should be 1 tab group");
win.gBrowser.removeAllTabsBut(tab2);
await TestUtils.waitForCondition(
() => win.gBrowser.tabs.length == 1,
"waiting for other tabs to close"
);
Assert.ok(
!win.gBrowser.tabs.some(tab => tab == tab1),
"tab1 should have been closed"
);
Assert.ok(
win.gBrowser.tabs.some(tab => tab == tab2),
"tab2 should still be present"
);
Assert.ok(
!win.gBrowser.tabs.some(tab => tab == tab3),
"tab3 should have been closed"
);
Assert.ok(
!win.gBrowser.tabs.some(tab => tab == tab4),
"tab4 should have been closed"
);
Assert.equal(
win.gBrowser.tabGroups.length,
0,
"tab group should have been deleted"
);
await BrowserTestUtils.closeWindow(win);
});

View file

@ -9,13 +9,6 @@ add_task(async function removeTabsToTheEnd() {
let lastTab = await addTab(); let lastTab = await addTab();
gBrowser.pinTab(pinnedTab); gBrowser.pinTab(pinnedTab);
// Check that there is only one closable tab from firstTab to the end
is(
gBrowser.getTabsToTheEndFrom(firstTab).length,
1,
"One unpinned tab towards the end"
);
// Remove tabs to the end // Remove tabs to the end
gBrowser.removeTabsToTheEndFrom(firstTab); gBrowser.removeTabsToTheEndFrom(firstTab);

View file

@ -13,13 +13,6 @@ add_task(async function removeTabsToTheStart() {
let lastTab = await addTab(); let lastTab = await addTab();
gBrowser.pinTab(pinnedTab); gBrowser.pinTab(pinnedTab);
// Check that there is only one closable tab from lastTab to the start
is(
gBrowser.getTabsToTheStartFrom(lastTab).length,
1,
"One unpinned tab towards the start"
);
// Remove tabs to the start // Remove tabs to the start
gBrowser.removeTabsToTheStartFrom(lastTab); gBrowser.removeTabsToTheStartFrom(lastTab);

View file

@ -590,6 +590,33 @@ add_task(async function test_moveTabBetweenGroups() {
await removeTabGroup(group2); await removeTabGroup(group2);
}); });
add_task(async function test_tabGroupSelect() {
let tab1 = BrowserTestUtils.addTab(gBrowser, "about:blank");
let tab2 = BrowserTestUtils.addTab(gBrowser, "about:blank");
let tab3 = BrowserTestUtils.addTab(gBrowser, "about:blank");
let tab1Added = BrowserTestUtils.waitForEvent(tab1, "TabGrouped");
let tab2Added = BrowserTestUtils.waitForEvent(tab2, "TabGrouped");
let group = gBrowser.addTabGroup([tab1, tab2]);
await Promise.allSettled([tab1Added, tab2Added]);
gBrowser.selectTabAtIndex(tab3._tPos);
Assert.ok(tab3.selected, "Tab 3 is selected");
group.select();
Assert.ok(group.tabs[0].selected, "First tab is selected");
gBrowser.selectTabAtIndex(group.tabs[1]._tPos);
Assert.ok(group.tabs[1].selected, "Second tab is selected");
group.select();
Assert.ok(group.tabs[1].selected, "Second tab is still selected");
group.collapsed = true;
Assert.ok(group.collapsed, "Group is collapsed");
Assert.ok(tab3.selected, "Tab 3 is selected");
group.select();
Assert.ok(!group.collapsed, "Group is no longer collapsed");
Assert.ok(group.tabs[0].selected, "First tab in group is selected");
await removeTabGroup(group);
BrowserTestUtils.removeTab(tab3);
});
// Context menu tests // Context menu tests
// --- // ---

View file

@ -0,0 +1,181 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [["browser.tabs.groups.enabled", true]],
});
const tabGroups = SessionStore.getSavedTabGroups();
tabGroups.forEach(tabGroup => SessionStore.forgetSavedTabGroup(tabGroup.id));
window.gTabsPanel.init();
});
async function openTabsMenu(win = window) {
return new Promise(resolve => {
BrowserTestUtils.waitForEvent(
win.document.getElementById("allTabsMenu-allTabsView"),
"ViewShown"
).then(event => resolve(event.target));
win.document.getElementById("alltabs-button").click();
});
}
async function closeTabsMenu(win = window) {
return new Promise(resolve => {
let panel = win.document
.getElementById("allTabsMenu-allTabsView")
.closest("panel");
BrowserTestUtils.waitForPopupEvent(panel, "hidden").then(event =>
resolve(event.target)
);
panel.hidePopup();
});
}
/**
* Tests that grouped tabs in alltabsmenu are prepended by
* a group indicator
*/
add_task(async function test_allTabsView() {
let tabs = [];
for (let i = 1; i <= 5; i++) {
tabs.push(
await addTab(`data:text/plain,tab${i}`, {
skipAnimation: true,
})
);
}
gBrowser.addTabGroup([tabs[0], tabs[1]], {
label: "Test Group",
});
gBrowser.addTabGroup([tabs[2], tabs[3]]);
let allTabsMenu = await openTabsMenu();
let tabButtons = allTabsMenu.querySelectorAll(
"#allTabsMenu-allTabsView-tabs .all-tabs-button"
);
let expectedLabels = [
"New Tab",
"data:text/plain,tab5",
"Test Group",
"data:text/plain,tab1",
"data:text/plain,tab2",
"Unnamed Group",
"data:text/plain,tab3",
"data:text/plain,tab4",
];
tabButtons.forEach((button, i) => {
Assert.equal(
button.label,
expectedLabels[i],
`Expected: ${expectedLabels[i]}`
);
});
await closeTabsMenu();
for (let tab of tabs) {
BrowserTestUtils.removeTab(tab);
}
});
/**
* Tests that groups appear in the supplementary group menu
* when they are saved (and closed,) or open in another window.
* Clicking an open group in this menu focuses it,
* and clicking on a saved group restores it.
*/
add_task(async function test_tabGroupsView() {
let tabs = [];
for (let i = 1; i <= 5; i++) {
tabs.push(
await addTab(`data:text/plain,tab${i}`, {
skipAnimation: true,
})
);
}
let group1 = gBrowser.addTabGroup([tabs[0], tabs[1]], {
id: "test-saved-group",
label: "Test Saved Group",
});
let group2 = gBrowser.addTabGroup([tabs[2], tabs[3]], {
label: "Test Open Group",
});
let newWindow = await BrowserTestUtils.openNewBrowserWindow();
newWindow.gTabsPanel.init();
let allTabsMenu = await openTabsMenu(newWindow);
Assert.equal(
allTabsMenu.querySelectorAll("#allTabsMenu-groupsView .all-tabs-button")
.length,
2,
"Both groups shown in groups list"
);
Assert.ok(
!allTabsMenu.querySelector(
"#allTabsMenu-groupsView .all-tabs-button.all-tabs-group-saved-group"
),
"Neither group is shown as saved"
);
await closeTabsMenu(newWindow);
group1.save();
await removeTabGroup(group1);
Assert.ok(!gBrowser.getTabGroupById("test-saved-group"), "Group 1 removed");
allTabsMenu = await openTabsMenu(newWindow);
Assert.equal(
allTabsMenu.querySelectorAll("#allTabsMenu-groupsView .all-tabs-button")
.length,
2,
"Both groups shown in groups list"
);
let savedGroupButton = allTabsMenu.querySelector(
"#allTabsMenu-groupsView .all-tabs-button.all-tabs-group-saved-group"
);
Assert.equal(
savedGroupButton.label,
"Test Saved Group",
"Saved group appears as saved"
);
// Clicking on an open group should select that group in the origin window
let openGroupButton = allTabsMenu.querySelector(
"#allTabsMenu-groupsView .all-tabs-button:not(.all-tabs-group-saved-group)"
);
openGroupButton.click();
Assert.equal(
gBrowser.selectedTab.group.id,
group2.id,
"Tab in group 2 is selected"
);
await BrowserTestUtils.closeWindow(newWindow, { animate: false });
// Clicking on a saved group should restore the group to the current window
allTabsMenu = await openTabsMenu();
savedGroupButton = allTabsMenu.querySelector(
"#allTabsMenu-groupsView .all-tabs-button.all-tabs-group-saved-group"
);
savedGroupButton.click();
group1 = gBrowser.getTabGroupById("test-saved-group");
Assert.ok(group1, "Group 1 has been restored");
allTabsMenu = await openTabsMenu();
Assert.ok(
!allTabsMenu.querySelector("#allTabsMenu-groupsView .all-tabs-button"),
"Groups list is now empty for this window"
);
await closeTabsMenu();
for (let tab of tabs) {
BrowserTestUtils.removeTab(tab);
}
await removeTabGroup(group1);
});

View file

@ -6,42 +6,111 @@ http://creativecommons.org/publicdomain/zero/1.0/ */
XPCOMUtils.defineLazyServiceGetters(this, { XPCOMUtils.defineLazyServiceGetters(this, {
BrowserHandler: ["@mozilla.org/browser/clh;1", "nsIBrowserHandler"], BrowserHandler: ["@mozilla.org/browser/clh;1", "nsIBrowserHandler"],
}); });
const { SpecialMessageActions } = ChromeUtils.importESModule(
"resource://messaging-system/lib/SpecialMessageActions.sys.mjs"
);
async function showAboutWelcomeModal() { const TEST_SCREEN = [
{
id: "TEST_SCREEN",
content: {
position: "split",
logo: {},
title: "test",
},
},
];
const TEST_SCREEN_SELECTOR = `.onboardingContainer .screen.${TEST_SCREEN[0].id}`;
async function waitForClick(selector, win) {
await TestUtils.waitForCondition(() => win.document.querySelector(selector));
win.document.querySelector(selector).click();
}
async function showAboutWelcomeModal(
screens = "",
modalScreens = "",
requireAction = false
) {
const PREFS_TO_SET = [
["browser.startup.homepage_override.mstone", ""],
["startup.homepage_welcome_url", "about:welcome"],
["browser.aboutwelcome.modalScreens", modalScreens],
["browser.aboutwelcome.screens", screens],
["browser.aboutwelcome.requireAction", requireAction],
["browser.aboutwelcome.showModal", true],
];
await SpecialPowers.pushPrefEnv({ await SpecialPowers.pushPrefEnv({
set: [["browser.aboutwelcome.showModal", true]], set: PREFS_TO_SET,
}); });
BrowserHandler.firstRunProfile = true; BrowserHandler.firstRunProfile = true;
await BROWSER_GLUE._maybeShowDefaultBrowserPrompt();
const data = [ registerCleanupFunction(async () => {
{ PREFS_TO_SET.forEach(pref => Services.prefs.clearUserPref(pref[0]));
id: "TEST_SCREEN", BrowserHandler.firstRunProfile = false;
content: { });
position: "split",
logo: {},
title: "test",
},
},
];
return {
data,
async cleanup() {
await SpecialPowers.popPrefEnv();
BrowserHandler.firstRunProfile = false;
},
};
} }
add_task(async function show_about_welcome_modal() { add_task(async function show_about_welcome_modal() {
const { data } = await showAboutWelcomeModal(); let messageSpy = sinon.spy(SpecialMessageActions, "handleAction");
await SpecialPowers.pushPrefEnv({ await showAboutWelcomeModal(JSON.stringify(TEST_SCREEN));
set: [["browser.aboutwelcome.screens", JSON.stringify(data)]],
});
BROWSER_GLUE._maybeShowDefaultBrowserPrompt();
const [win] = await TestUtils.topicObserved("subdialog-loaded"); const [win] = await TestUtils.topicObserved("subdialog-loaded");
const modal = win.document.querySelector(".onboardingContainer");
ok(!!modal, "About Welcome modal shown"); Assert.notEqual(
win.close(); Cc["@mozilla.org/browser/clh;1"]
.getService(Ci.nsIBrowserHandler)
.getFirstWindowArgs(),
"about:welcome",
"First window will not be about:welcome"
);
// Wait for screen content to render
await TestUtils.waitForCondition(() =>
win.document.querySelector(TEST_SCREEN_SELECTOR)
);
Assert.equal(
messageSpy.firstCall.args[0].data.content.disableEscClose,
false
);
Assert.ok(
!!win.document.querySelector(TEST_SCREEN_SELECTOR),
"Modal renders with custom about:welcome screen"
);
await win.close();
sinon.restore();
});
add_task(async function shows_modal_with_custom_screens_over_about_welcome() {
let messageSpy = sinon.spy(SpecialMessageActions, "handleAction");
await showAboutWelcomeModal("", JSON.stringify(TEST_SCREEN), true);
const [win] = await TestUtils.topicObserved("subdialog-loaded");
Assert.equal(
Cc["@mozilla.org/browser/clh;1"]
.getService(Ci.nsIBrowserHandler)
.getFirstWindowArgs(),
"about:welcome",
"First window will be about:welcome"
);
// Wait for screen content to render
await TestUtils.waitForCondition(() =>
win.document.querySelector(TEST_SCREEN_SELECTOR)
);
Assert.equal(messageSpy.firstCall.args[0].data.content.disableEscClose, true);
Assert.ok(
!!win.document.querySelector(TEST_SCREEN_SELECTOR),
"Modal renders with custom modal screen"
);
await win.close();
sinon.restore();
}); });

View file

@ -36,12 +36,19 @@ class ProviderTabGroups extends ActionsProvider {
} }
async queryActions(queryContext) { async queryActions(queryContext) {
let gBrowser = lazy.BrowserWindowTracker.getTopWindow().gBrowser; let window = lazy.BrowserWindowTracker.getTopWindow();
if (!window) {
// We're likely running xpcshell tests if this happens in automation.
if (!Cu.isInAutomation) {
console.error("Couldn't find a browser window.");
}
return null;
}
let input = queryContext.trimmedLowerCaseSearchString; let input = queryContext.trimmedLowerCaseSearchString;
let results = []; let results = [];
let i = 0; let i = 0;
for (let group of gBrowser.getAllTabGroups()) { for (let group of window.gBrowser.getAllTabGroups()) {
if (group.label.toLowerCase().startsWith(input)) { if (group.label.toLowerCase().startsWith(input)) {
results.push( results.push(
this.#makeResult({ this.#makeResult({

View file

@ -6,6 +6,7 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, { ChromeUtils.defineESModuleGetters(lazy, {
PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs", PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
SearchUIUtils: "resource:///modules/SearchUIUtils.sys.mjs", SearchUIUtils: "resource:///modules/SearchUIUtils.sys.mjs",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs", UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs", UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs",
@ -122,6 +123,8 @@ export class SearchModeSwitcher {
position: "bottomleft topleft", position: "bottomleft topleft",
triggerEvent: event, triggerEvent: event,
}).catch(console.error); }).catch(console.error);
Glean.urlbarUnifiedsearchbutton.opened.add(1);
} }
#openPreferences(event) { #openPreferences(event) {
@ -139,6 +142,8 @@ export class SearchModeSwitcher {
this.#input.window.openPreferences("paneSearch"); this.#input.window.openPreferences("paneSearch");
this.#popup.hidePopup(); this.#popup.hidePopup();
Glean.urlbarUnifiedsearchbutton.picked.settings.add(1);
} }
/** /**
@ -223,6 +228,10 @@ export class SearchModeSwitcher {
* The name of the pref relative to `browser.urlbar`. * The name of the pref relative to `browser.urlbar`.
*/ */
onPrefChanged(pref) { onPrefChanged(pref) {
if (!this.#input.window || this.#input.window.closed) {
return;
}
switch (pref) { switch (pref) {
case "scotchBonnet.enableOverride": { case "scotchBonnet.enableOverride": {
if (lazy.UrlbarPrefs.get("scotchBonnet.enableOverride")) { if (lazy.UrlbarPrefs.get("scotchBonnet.enableOverride")) {
@ -242,6 +251,10 @@ export class SearchModeSwitcher {
} }
async #updateSearchIcon() { async #updateSearchIcon() {
if (!this.#input.window || this.#input.window.closed) {
return;
}
try { try {
await lazy.UrlbarSearchUtils.init(); await lazy.UrlbarSearchUtils.init();
} catch { } catch {
@ -310,7 +323,9 @@ export class SearchModeSwitcher {
if (!searchMode || searchMode.engineName) { if (!searchMode || searchMode.engineName) {
let engine = searchMode let engine = searchMode
? lazy.UrlbarSearchUtils.getEngineByName(searchMode.engineName) ? lazy.UrlbarSearchUtils.getEngineByName(searchMode.engineName)
: lazy.UrlbarSearchUtils.getDefaultEngine(); : lazy.UrlbarSearchUtils.getDefaultEngine(
lazy.PrivateBrowsingUtils.isWindowPrivate(this.#input.window)
);
let icon = (await engine.getIconURL()) ?? SearchModeSwitcher.DEFAULT_ICON; let icon = (await engine.getIconURL()) ?? SearchModeSwitcher.DEFAULT_ICON;
return { label: engine.name, icon }; return { label: engine.name, icon };
} }
@ -431,6 +446,18 @@ export class SearchModeSwitcher {
} }
this.#popup.hidePopup(); this.#popup.hidePopup();
if (engine) {
Glean.urlbarUnifiedsearchbutton.picked[
engine.isAppProvided ? "builtin_search" : "addon_search"
].add(1);
} else if (restrict) {
Glean.urlbarUnifiedsearchbutton.picked.local_search.add(1);
} else {
console.warn(
`Unexpected search: ${JSON.stringify({ engine, restrict, openEngineHomePage })}`
);
}
} }
#enableObservers() { #enableObservers() {
@ -519,7 +546,10 @@ export class SearchModeSwitcher {
let observer = engineObj => { let observer = engineObj => {
Services.obs.removeObserver(observer, topic); Services.obs.removeObserver(observer, topic);
let eng = Services.search.getEngineByName(engineObj.wrappedJSObject.name); let eng = Services.search.getEngineByName(engineObj.wrappedJSObject.name);
this.search({ engine: eng, openEngineHomePage: e.shiftKey }); this.search({
engine: eng,
openEngineHomePage: e.shiftKey,
});
}; };
Services.obs.addObserver(observer, topic); Services.obs.addObserver(observer, topic);

View file

@ -368,7 +368,7 @@ export var UrlbarUtils = {
let dataStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( let dataStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
Ci.nsIStringInputStream Ci.nsIStringInputStream
); );
dataStream.data = postDataString; dataStream.setByteStringData(postDataString);
let mimeStream = Cc[ let mimeStream = Cc[
"@mozilla.org/network/mime-input-stream;1" "@mozilla.org/network/mime-input-stream;1"

View file

@ -513,6 +513,19 @@ urlbar.quickaction.picked
key is in the form $key-$n where $n is the number of characters the user typed key is in the form $key-$n where $n is the number of characters the user typed
in order for the suggestion to show. See bug 1783155. in order for the suggestion to show. See bug 1783155.
urlbar.unifiedsearchbutton.opened
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A uint recording the number of times the user opens search mode popup via
Unified Search Button.
See bug 1936673.
urlbar.unifiedsearchbutton.picked
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A uint recording the number of times the user selected a search mode via
Unified Search Button. See bug 1936673.
places.* places.*
~~~~~~~~ ~~~~~~~~

View file

@ -2191,3 +2191,39 @@ urlbar.quickaction:
- fx-search-telemetry@mozilla.com - fx-search-telemetry@mozilla.com
expires: never expires: never
telemetry_mirror: QUICKACTION_PICKED telemetry_mirror: QUICKACTION_PICKED
urlbar.unifiedsearchbutton:
opened:
type: counter
description: >
Counts how many times Unified Search Button popup is opened.
This metric was generated to correspond to the Legacy Telemetry
scalar urlbar.unifiedsearchbutton.opened.
bugs:
- https://bugzil.la/1936673
data_reviews:
- https://bugzil.la/1936673
notification_emails:
- fx-search-telemetry@mozilla.com
expires: never
telemetry_mirror: URLBAR_UNIFIEDSEARCHBUTTON_OPENED
picked:
type: labeled_counter
description: >
Counts how many times Unified Search Button items were selected.
The key is followings.
* builtin_search: Builtin search engine.
* addon_search: Addon search engine.
* local_search: Local search engine such as Bookmarks.
* settings: Settings menu.
This metric was generated to correspond to the Legacy Telemetry
scalar urlbar.unifiedsearchbutton.picked.
bugs:
- https://bugzil.la/1936673
data_reviews:
- https://bugzil.la/1936673
notification_emails:
- fx-search-telemetry@mozilla.com
expires: never
telemetry_mirror: URLBAR_UNIFIEDSEARCHBUTTON_PICKED

View file

@ -459,6 +459,8 @@ support-files = ["has-a-link.html"]
["browser_searchModeSwitcher_opensearchInstall.js"] ["browser_searchModeSwitcher_opensearchInstall.js"]
["browser_searchModeSwitcher_telemetry.js"]
["browser_searchMode_alias_replacement.js"] ["browser_searchMode_alias_replacement.js"]
support-files = [ support-files = [
"searchSuggestionEngine.xml", "searchSuggestionEngine.xml",
@ -701,6 +703,9 @@ tags = "search-telemetry"
["browser_urlbar_telemetry_persisted.js"] ["browser_urlbar_telemetry_persisted.js"]
tags = "search-telemetry" tags = "search-telemetry"
skip-if = [
"os == 'linux' && os_version == '18.04' && processor == 'x86_64'",
] # bug 1934362
["browser_urlbar_telemetry_places.js"] ["browser_urlbar_telemetry_places.js"]
https_first_disabled = true https_first_disabled = true

View file

@ -16,6 +16,12 @@ add_setup(async function setup() {
add_task(async function test_search_mode_app_provided_engines() { add_task(async function test_search_mode_app_provided_engines() {
let cleanup = await installPersistTestEngines(); let cleanup = await installPersistTestEngines();
let switcher = document.getElementById("urlbar-searchmode-switcher");
await BrowserTestUtils.waitForCondition(
() => BrowserTestUtils.isVisible(switcher),
`Wait until unified search button is visible`
);
let popup = await UrlbarTestUtils.openSearchModeSwitcher(window); let popup = await UrlbarTestUtils.openSearchModeSwitcher(window);
info("Press on the example menu button and enter search mode"); info("Press on the example menu button and enter search mode");

View file

@ -249,19 +249,10 @@ add_task(async function test_search_icon_change() {
}); });
let newWin = await BrowserTestUtils.openNewBrowserWindow(); let newWin = await BrowserTestUtils.openNewBrowserWindow();
let searchModeSwitcherButton = newWin.document.getElementById(
"searchmode-switcher-icon"
);
let regex = /url\("([^"]+)"\)/;
let searchModeSwitcherIconUrl = newWin
.getComputedStyle(searchModeSwitcherButton)
.listStyleImage.match(regex);
const searchGlassIconUrl = UrlbarUtils.ICON.SEARCH_GLASS; const searchGlassIconUrl = UrlbarUtils.ICON.SEARCH_GLASS;
Assert.equal( Assert.equal(
searchModeSwitcherIconUrl[1], getSeachModeSwitcherIcon(newWin),
searchGlassIconUrl, searchGlassIconUrl,
"The search mode switcher should have the search glass icon url since \ "The search mode switcher should have the search glass icon url since \
we are not in search mode." we are not in search mode."
@ -284,12 +275,8 @@ add_task(async function test_search_icon_change() {
.getEngineByName(engineName) .getEngineByName(engineName)
.getIconURL(); .getIconURL();
searchModeSwitcherIconUrl = newWin
.getComputedStyle(searchModeSwitcherButton)
.listStyleImage.match(regex);
Assert.equal( Assert.equal(
searchModeSwitcherIconUrl[1], getSeachModeSwitcherIcon(newWin),
bingSearchEngineIconUrl, bingSearchEngineIconUrl,
"The search mode switcher should have the bing icon url since we are in \ "The search mode switcher should have the bing icon url since we are in \
search mode" search mode"
@ -304,16 +291,13 @@ add_task(async function test_search_icon_change() {
newWin.document.querySelector("#searchmode-switcher-close").click(); newWin.document.querySelector("#searchmode-switcher-close").click();
await UrlbarTestUtils.assertSearchMode(newWin, null); await UrlbarTestUtils.assertSearchMode(newWin, null);
searchModeSwitcherIconUrl = await BrowserTestUtils.waitForCondition( let searchModeSwitcherIconUrl = await BrowserTestUtils.waitForCondition(
() => () => getSeachModeSwitcherIcon(newWin),
newWin
.getComputedStyle(searchModeSwitcherButton)
.listStyleImage.match(regex),
"Waiting for the search mode switcher icon to update after exiting search mode." "Waiting for the search mode switcher icon to update after exiting search mode."
); );
Assert.equal( Assert.equal(
searchModeSwitcherIconUrl[1], searchModeSwitcherIconUrl,
searchGlassIconUrl, searchGlassIconUrl,
"The search mode switcher should have the search glass icon url since \ "The search mode switcher should have the search glass icon url since \
keyword.enabled is false" keyword.enabled is false"
@ -388,13 +372,13 @@ add_task(async function test_suggestions_after_no_search_mode() {
}); });
add_task(async function open_engine_page_directly() { add_task(async function open_engine_page_directly() {
await SearchTestUtils.installSearchExtension( let searchExtension = await SearchTestUtils.installSearchExtension(
{ {
name: "MozSearch", name: "MozSearch",
search_url: "https://example.com/", search_url: "https://example.com/",
favicon_url: "https://example.com/favicon.ico", favicon_url: "https://example.com/favicon.ico",
}, },
{ setAsDefault: true } { setAsDefault: true, skipUnload: true }
); );
const TEST_DATA = [ const TEST_DATA = [
@ -466,6 +450,7 @@ add_task(async function open_engine_page_directly() {
await PlacesUtils.history.clear(); await PlacesUtils.history.clear();
await BrowserTestUtils.closeWindow(newWin); await BrowserTestUtils.closeWindow(newWin);
} }
await searchExtension.unload();
}); });
add_task(async function test_enter_searchmode_by_key_if_single_result() { add_task(async function test_enter_searchmode_by_key_if_single_result() {
@ -726,25 +711,14 @@ add_task(async function test_search_service_fail() {
set: [["keyword.enabled", false]], set: [["keyword.enabled", false]],
}); });
let searchModeSwitcherButton = newWin.document.getElementById(
"searchmode-switcher-icon"
);
const searchGlassIconUrl = UrlbarUtils.ICON.SEARCH_GLASS;
// match and capture the URL inside `url("...")`
let regex = /url\("([^"]+)"\)/;
let searchModeSwitcherIconUrl = await BrowserTestUtils.waitForCondition( let searchModeSwitcherIconUrl = await BrowserTestUtils.waitForCondition(
() => () => getSeachModeSwitcherIcon(newWin),
newWin
.getComputedStyle(searchModeSwitcherButton)
.listStyleImage.match(regex),
"Waiting for the search mode switcher icon to update after exiting search mode." "Waiting for the search mode switcher icon to update after exiting search mode."
); );
Assert.equal( Assert.equal(
searchModeSwitcherIconUrl[1], searchModeSwitcherIconUrl,
searchGlassIconUrl, UrlbarUtils.ICON.SEARCH_GLASS,
"The search mode switcher should have the search glass icon url since the search service init failed." "The search mode switcher should have the search glass icon url since the search service init failed."
); );
@ -771,6 +745,7 @@ add_task(async function test_search_service_fail() {
Services.search.wrappedJSObject.forceInitializationStatusForTests("success"); Services.search.wrappedJSObject.forceInitializationStatusForTests("success");
await BrowserTestUtils.closeWindow(newWin); await BrowserTestUtils.closeWindow(newWin);
await SpecialPowers.popPrefEnv();
}); });
add_task(async function test_search_mode_switcher_engine_no_icon() { add_task(async function test_search_mode_switcher_engine_no_icon() {
@ -784,25 +759,15 @@ add_task(async function test_search_mode_switcher_engine_no_icon() {
{ skipUnload: true } { skipUnload: true }
); );
let searchModeSwitcherButton = window.document.getElementById(
"searchmode-switcher-icon"
);
let popup = await UrlbarTestUtils.openSearchModeSwitcher(window); let popup = await UrlbarTestUtils.openSearchModeSwitcher(window);
let popupHidden = UrlbarTestUtils.searchModeSwitcherPopupClosed(window); let popupHidden = UrlbarTestUtils.searchModeSwitcherPopupClosed(window);
popup.querySelector(`toolbarbutton[label=${testEngineName}]`).click(); popup.querySelector(`toolbarbutton[label=${testEngineName}]`).click();
await popupHidden; await popupHidden;
let regex = /url\("([^"]+)"\)/;
let searchModeSwitcherIconUrl =
searchModeSwitcherButton.style.listStyleImage.match(regex);
const searchGlassIconUrl = UrlbarUtils.ICON.SEARCH_GLASS;
Assert.equal( Assert.equal(
searchModeSwitcherIconUrl[1], getSeachModeSwitcherIcon(window),
searchGlassIconUrl, UrlbarUtils.ICON.SEARCH_GLASS,
"The search mode switcher should display the default search glass icon when the engine has no icon." "The search mode switcher should display the default search glass icon when the engine has no icon."
); );
@ -812,3 +777,76 @@ add_task(async function test_search_mode_switcher_engine_no_icon() {
await searchExtension.unload(); await searchExtension.unload();
}); });
add_task(async function test_search_mode_switcher_private_engine_icon() {
await SpecialPowers.pushPrefEnv({
set: [["browser.search.separatePrivateDefault.ui.enabled", true]],
});
const testEngineName = "DefaultPrivateEngine";
let searchExtension = await SearchTestUtils.installSearchExtension(
{
name: testEngineName,
search_url: "https://www.example.com/search?q=",
icons: {
16: "private.png",
},
},
{ skipUnload: true }
);
const defaultPrivateEngine = Services.search.getEngineByName(testEngineName);
const defaultEngine = await Services.search.getDefault();
Services.search.setDefaultPrivate(
defaultPrivateEngine,
Ci.nsISearchService.CHANGE_REASON_UNKNOWN
);
Assert.notEqual(
defaultEngine.id,
defaultPrivateEngine.id,
"Default engine is not private engine."
);
Assert.equal(
(await Services.search.getDefault()).id,
defaultEngine.id,
"Default engine is still correct."
);
Assert.equal(
(await Services.search.getDefaultPrivate()).id,
defaultPrivateEngine.id,
"Default private engine is correct."
);
Assert.equal(
getSeachModeSwitcherIcon(window),
await defaultEngine.getIconURL(),
"Is the icon of the default engine."
);
info("Open a private window");
let privateWin = await BrowserTestUtils.openNewBrowserWindow({
private: true,
});
Assert.equal(
getSeachModeSwitcherIcon(privateWin),
`moz-extension://${searchExtension.uuid}/private.png`,
"Is the icon of the default private engine."
);
await BrowserTestUtils.closeWindow(privateWin);
await searchExtension.unload();
await SpecialPowers.popPrefEnv();
});
function getSeachModeSwitcherIcon(window) {
let searchModeSwitcherButton = window.document.getElementById(
"searchmode-switcher-icon"
);
// match and capture the URL inside `url("...")`
let re = /url\("([^"]+)"\)/;
return searchModeSwitcherButton.style.listStyleImage.match(re)?.[1] ?? null;
}

View file

@ -0,0 +1,144 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_setup(async function setup() {
await SpecialPowers.pushPrefEnv({
set: [["browser.urlbar.scotchBonnet.enableOverride", true]],
});
registerCleanupFunction(async () => {
await cleanUp();
});
});
add_task(async function test_opened() {
await cleanUp();
info("Open search mode switcher popup");
await UrlbarTestUtils.openSearchModeSwitcher(window);
Assert.equal(Glean.urlbarUnifiedsearchbutton.opened.testGetValue(), 1);
info("Close search mode switcher popup");
EventUtils.synthesizeKey("KEY_Escape", {});
info("Open search mode switcher popup again");
await UrlbarTestUtils.openSearchModeSwitcher(window);
Assert.equal(Glean.urlbarUnifiedsearchbutton.opened.testGetValue(), 2);
info("Close search mode switcher popup again");
EventUtils.synthesizeKey("KEY_Escape", {});
});
add_task(async function test_picked_search_engines() {
await cleanUp();
info("Open a new tab");
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
info("Start search engine tests");
await testSearchEngine("Google", "builtin_search", 1);
await testSearchEngine("Google", "builtin_search", 2);
await testSearchEngine("DuckDuckGo", "builtin_search", 3);
await testSearchEngine("Bookmarks", "local_search", 1);
await testSearchEngine("Tabs", "local_search", 2);
await testSearchEngine("DuckDuckGo", "builtin_search", 4);
await testSearchEngine("Bookmarks", "local_search", 3);
await testSearchEngine("Tabs", "local_search", 4);
await testSearchEngine("DuckDuckGo", "builtin_search", 5);
info("Add addon search engine");
await loadUri(
"http://mochi.test:8888/browser/browser/components/search/test/browser/opensearch.html"
);
info("Ensure to show Unified Search Button");
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
waitForFocus: true,
value: "",
fireInputEvent: true,
});
info("Test with addon search engine");
await testSearchEngine("engine1", "addon_search", 1);
await testSearchEngine("Foo", "addon_search", 2);
await testSearchEngine("DuckDuckGo", "builtin_search", 6);
await testSearchEngine("Bookmarks", "local_search", 5);
info("Clean up");
await removeAddonSearchEngine("Foo");
BrowserTestUtils.removeTab(tab);
});
add_task(async function test_picked_settings() {
await cleanUp();
Assert.equal(
Glean.urlbarUnifiedsearchbutton.picked.settings.testGetValue(),
null
);
info("Open a new tab");
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
let popup = await UrlbarTestUtils.openSearchModeSwitcher(window);
let popupHidden = UrlbarTestUtils.searchModeSwitcherPopupClosed(window);
let pageLoaded = BrowserTestUtils.browserLoaded(window);
popup
.querySelector("#searchmode-switcher-popup-search-settings-button")
.click();
await Promise.all([pageLoaded, popupHidden]);
Assert.equal(
Glean.urlbarUnifiedsearchbutton.picked.settings.testGetValue(),
1
);
BrowserTestUtils.removeTab(tab);
});
async function testSearchEngine(label, telemetry, expected) {
info(`Test search engine for ${{ label, telemetry, expected }}`);
let popup = await UrlbarTestUtils.openSearchModeSwitcher(window);
let popupHidden = UrlbarTestUtils.searchModeSwitcherPopupClosed(window);
popup.querySelector(`toolbarbutton[label=${label}]`).click();
await popupHidden;
Assert.equal(
Glean.urlbarUnifiedsearchbutton.picked[telemetry].testGetValue(),
expected
);
document.querySelector("#searchmode-switcher-close").click();
}
async function loadUri(uri) {
let loaded = BrowserTestUtils.browserLoaded(
gBrowser.selectedBrowser,
false,
uri
);
BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, uri);
await loaded;
}
async function removeAddonSearchEngine(name) {
let promiseEngineRemoved = SearchTestUtils.promiseSearchNotification(
SearchUtils.MODIFIED_TYPE.REMOVED,
SearchUtils.TOPIC_ENGINE_MODIFIED
);
let settingsWritten = SearchTestUtils.promiseSearchNotification(
"write-settings-to-disk-complete"
);
let engine = Services.search.getEngineByName(name);
await Promise.all([
Services.search.removeEngine(engine),
promiseEngineRemoved,
settingsWritten,
]);
}
async function cleanUp() {
await Services.fog.testFlushAllChildren();
Services.fog.testResetFOG();
Assert.equal(Glean.urlbarUnifiedsearchbutton.opened.testGetValue(), null);
}

View file

@ -357,6 +357,18 @@ onboarding-new-tabs-title = Tell us where youd like your tabs
# Setup screen for vertical tabs - "Switch it up" refers to switching between horizontal and vertical tabs. # Setup screen for vertical tabs - "Switch it up" refers to switching between horizontal and vertical tabs.
onboarding-new-tabs-subtitle = Switch it up whenever you want in the sidebar settings. onboarding-new-tabs-subtitle = Switch it up whenever you want in the sidebar settings.
# Setup screen for vertical tabs - too many tabs variation
onboarding-many-tabs-title = Your tabs, your way
# Setup screen for vertical tabs - subtitle for too many tabs variation
onboarding-many-tabs-subtitle = Keep a lot of tabs open? Try your tabs on the side for a more streamlined view. Or keep it classic with tabs on the top. Switch anytime.
# Setup screen for vertical tabs - focused variation
onboarding-focused-tabs-title = Choose your tab layout
# Setup screen for vertical tabs - subtitle for focused variation
onboarding-focused-tabs-subtitle = For a streamlined view that can help you stay focused, try your tabs on the side. Or keep it classic with tabs on the top. Switch anytime.
# Text underneath an image used for selecting browser tabs to appear on the side of the browser. # Text underneath an image used for selecting browser tabs to appear on the side of the browser.
onboarding-new-vertical-tabs-label = Tabs on the side onboarding-new-vertical-tabs-label = Tabs on the side

View file

@ -191,6 +191,7 @@ tabbrowser-manager-close-tab =
## Tab Groups ## Tab Groups
tab-group-name-default = Unnamed Group
tab-group-editor-title-create = Create tab group tab-group-editor-title-create = Create tab group
tab-group-editor-title-edit = Manage tab group tab-group-editor-title-edit = Manage tab group
tab-group-editor-name-label = Name tab-group-editor-name-label = Name
@ -200,6 +201,8 @@ tab-group-editor-cancel =
.label = Cancel .label = Cancel
.accesskey = C .accesskey = C
tab-group-menu-header = Tab groups
tab-context-unnamed-group = tab-context-unnamed-group =
.label = Unnamed group .label = Unnamed group

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