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",
]
[[package]]
name = "debug_tree"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d1ec383f2d844902d3c34e4253ba11ae48513cdaddc565cf1a6518db09a8e57"
dependencies = [
"once_cell",
]
[[package]]
name = "debugid"
version = "0.8.0"
@ -2334,8 +2343,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
@ -2440,6 +2451,7 @@ dependencies = [
"mdns_service",
"midir_impl",
"mime-guess-ffi",
"mls_gk",
"moz_asserts",
"mozannotation_client",
"mozannotation_server",
@ -3779,6 +3791,17 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "md-5"
version = "0.10.5"
@ -4032,6 +4055,169 @@ dependencies = [
"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]]
name = "moz_asserts"
version = "0.1.0"
@ -4493,10 +4679,10 @@ dependencies = [
[[package]]
name = "nss-gk-api"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c17aec6d4e1822c023689899f09311592a36cbf6de8f85dfaf5f01976790d8d"
source = "git+https://github.com/beurdouche/nss-gk-api?rev=e48a946811ffd64abc78de3ee284957d8d1c0d63#e48a946811ffd64abc78de3ee284957d8d1c0d63"
dependencies = [
"bindgen 0.69.4",
"log",
"mozbuild",
"once_cell",
"pkcs11-bindings",
@ -5075,9 +5261,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quinn-udp"
version = "0.5.8"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52cd4b1eff68bf27940dd39811292c49e007f4d0b4c357358dc9b0197be6b527"
checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904"
dependencies = [
"cfg_aliases 0.2.1",
"libc",
@ -7546,6 +7732,27 @@ dependencies = [
"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]]
name = "zerovec"
version = "0.10.4"

View file

@ -14,6 +14,7 @@ members = [
"security/manager/ssl/tests/unit/test_builtins",
"security/manager/ssl/ipcclientcerts",
"security/manager/ssl/osclientcerts",
"security/mls/mls_gk",
"testing/geckodriver",
"toolkit/components/uniffi-bindgen-gecko-js",
"toolkit/crashreporter/client/app",
@ -199,6 +200,7 @@ plist = { path = "third_party/rust/plist" }
# To-be-published changes.
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
any_all_workaround = { git = "https://github.com/hsivonen/any_all_workaround", rev = "7fb1b7034c9f172aade21ee1c8554e8d8a48af80" }

View file

@ -459,6 +459,20 @@ void EventQueue::ProcessEventQueue() {
if (!mDocument) {
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() &&

View file

@ -1007,6 +1007,12 @@ void NotificationController::WillRefresh(mozilla::TimeStamp aTime) {
CoalesceMutationEvents();
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
// eRefreshProcessing. Any calls to ScheduleProcessing() that
// occur before mObservingState is reset will be dropped because we only
@ -1028,6 +1034,13 @@ void NotificationController::WillRefresh(mozilla::TimeStamp aTime) {
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()) {
size_t newDocCount = newChildDocs.Length();
for (size_t i = 0; i < newDocCount; i++) {

View file

@ -280,7 +280,13 @@ class NotificationController final : public EventQueue,
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();

View file

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

View file

@ -872,13 +872,15 @@ nsresult LocalAccessible::HandleAccEvent(AccEvent* aEvent) {
break;
case nsIAccessibleEvent::EVENT_HIDE:
ipcDoc->SendHideEvent(id, aEvent->IsFromUserInput());
ipcDoc->AppendMutationEventData(
HideEventData{id, aEvent->IsFromUserInput()});
break;
case nsIAccessibleEvent::EVENT_INNER_REORDER:
case nsIAccessibleEvent::EVENT_REORDER:
if (IsTable()) {
SendCache(CacheDomain::Table, CacheUpdateType::Update);
SendCache(CacheDomain::Table, CacheUpdateType::Update,
/*aAppendEventData*/ true);
}
#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
// parent about new top level documents.
if (!aEvent->GetAccessible()->IsApplication()) {
ipcDoc->SendEvent(id, aEvent->GetEventType());
ipcDoc->AppendMutationEventData(
ReorderEventData{id, aEvent->GetEventType()});
}
break;
case nsIAccessibleEvent::EVENT_STATE_CHANGE: {
@ -917,10 +920,10 @@ nsresult LocalAccessible::HandleAccEvent(AccEvent* aEvent) {
case nsIAccessibleEvent::EVENT_TEXT_INSERTED:
case nsIAccessibleEvent::EVENT_TEXT_REMOVED: {
AccTextChangeEvent* event = downcast_accEvent(aEvent);
const nsString& text = event->ModifiedText();
ipcDoc->SendTextChangeEvent(
id, text, event->GetStartOffset(), event->GetLength(),
event->IsTextInserted(), event->IsFromUserInput());
ipcDoc->AppendMutationEventData(TextChangeEventData{
id, event->ModifiedText(), event->GetStartOffset(),
event->GetLength(), event->IsTextInserted(),
event->IsFromUserInput()});
break;
}
case nsIAccessibleEvent::EVENT_SELECTION:
@ -3317,7 +3320,8 @@ AccGroupInfo* LocalAccessible::GetOrCreateGroupInfo() {
}
void LocalAccessible::SendCache(uint64_t aCacheDomain,
CacheUpdateType aUpdateType) {
CacheUpdateType aUpdateType,
bool aAppendEventData) {
if (!IPCAccessibilityActive() || !Document()) {
return;
}
@ -3347,7 +3351,12 @@ void LocalAccessible::SendCache(uint64_t aCacheDomain,
}
nsTArray<CacheData> data;
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()) {
nsAutoCString updateTypeStr;
@ -4184,21 +4193,37 @@ void LocalAccessible::MaybeQueueCacheUpdateForStyleChanges() {
if (nsIFrame* frame = GetFrame()) {
const ComputedStyle* newStyle = frame->Style();
nsAutoCString oldOverflow, newOverflow;
mOldComputedStyle->GetComputedPropertyValue(eCSSProperty_overflow,
oldOverflow);
newStyle->GetComputedPropertyValue(eCSSProperty_overflow, newOverflow);
const auto overflowProps =
nsCSSPropertyIDSet({eCSSProperty_overflow_x, eCSSProperty_overflow_y});
if (oldOverflow != newOverflow) {
if (oldOverflow.Equals("hidden"_ns) || newOverflow.Equals("hidden"_ns)) {
mDoc->QueueCacheUpdate(this, CacheDomain::Style);
}
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);
for (nsCSSPropertyID overflowProp : overflowProps) {
nsAutoCString oldOverflow, newOverflow;
mOldComputedStyle->GetComputedPropertyValue(overflowProp, oldOverflow);
newStyle->GetComputedPropertyValue(overflowProp, newOverflow);
if (oldOverflow != newOverflow) {
if (oldOverflow.Equals("hidden"_ns) ||
newOverflow.Equals("hidden"_ns)) {
mDoc->QueueCacheUpdate(this, CacheDomain::Style);
}
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>
AccessibleHashtable;
#define NS_ACCESSIBLE_IMPL_IID \
{ /* 133c8bf4-4913-4355-bd50-426bd1d6e1ad */ \
0x133c8bf4, 0x4913, 0x4355, { \
0xbd, 0x50, 0x42, 0x6b, 0xd1, 0xd6, 0xe1, 0xad \
} \
}
#define NS_ACCESSIBLE_IMPL_IID \
{/* 133c8bf4-4913-4355-bd50-426bd1d6e1ad */ \
0x133c8bf4, \
0x4913, \
0x4355, \
{0xbd, 0x50, 0x42, 0x6b, 0xd1, 0xd6, 0xe1, 0xad}}
/**
* 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.
* aCacheDomain - describes which fields to bundle and ultimately send
* 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();

View file

@ -20,6 +20,13 @@
namespace mozilla {
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 */
void DocAccessibleChild::FlattenTree(LocalAccessible* aRoot,
nsTArray<LocalAccessible*>& aTree) {
@ -80,34 +87,62 @@ void DocAccessibleChild::InsertIntoIpcTree(LocalAccessible* aChild,
nsTArray<LocalAccessible*> shownTree;
FlattenTree(aChild, shownTree);
uint32_t totalAccs = shownTree.Length();
// Exceeding the IPDL maximum message size will cause a crash. Try to avoid
// this by only including kMaxAccsPerMessage Accessibels in a single IPDL
// call. If there are Accessibles beyond this, they will be split across
// multiple calls.
constexpr uint32_t kMaxAccsPerMessage =
IPC::Channel::kMaximumMessageSize / (2 * 1024);
nsTArray<AccessibleData> data(std::min(kMaxAccsPerMessage, totalAccs));
for (LocalAccessible* child : shownTree) {
if (data.Length() == kMaxAccsPerMessage) {
nsTArray<AccessibleData> data(std::min(
kMaxAccsPerMessage - mMutationEventBatcher.GetCurrentBatchAccCount(),
totalAccs));
for (uint32_t accIndex = 0; accIndex < totalAccs; ++accIndex) {
// This batch of mutation events has no more room left without exceeding our
// limit. Write the show event data to the queue.
if (data.Length() + mMutationEventBatcher.GetCurrentBatchAccCount() ==
kMaxAccsPerMessage) {
if (ipc::ProcessChild::ExpectingShutdown()) {
return;
}
SendShowEvent(data, aSuppressShowEvent, false, false);
data.ClearAndRetainStorage();
// Note: std::move used on aSuppressShowEvent to force selection of the
// 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));
}
if (ipc::ProcessChild::ExpectingShutdown()) {
return;
}
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) {
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) {
@ -428,5 +463,54 @@ HyperTextAccessible* DocAccessibleChild::IdToHyperTextAccessible(
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 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 ShowEvent(AccShowEvent* aShowEvent);
void AppendMutationEventData(MutationEventData aData, uint32_t aAccCount = 1);
void SendQueuedMutationEvents();
size_t MutationEventQueueLength() const;
virtual void ActorDestroy(ActorDestroyReason) override {
if (!mDoc) {
return;
@ -169,6 +175,30 @@ class DocAccessibleChild : public PDocAccessibleChild {
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();
};

View file

@ -78,11 +78,10 @@ void DocAccessibleParent::SetBrowsingContext(
mBrowsingContext = aBrowsingContext;
}
mozilla::ipc::IPCResult DocAccessibleParent::RecvShowEvent(
mozilla::ipc::IPCResult DocAccessibleParent::ProcessShowEvent(
nsTArray<AccessibleData>&& aNewTree, const bool& aEventSuppressed,
const bool& aComplete, const bool& aFromUser) {
ACQUIRE_ANDROID_LOCK
if (mShutdown) return IPC_OK();
MOZ_ASSERT(CheckDocTree());
@ -290,10 +289,9 @@ void DocAccessibleParent::ShutdownOrPrepareForMove(RemoteAccessible* aAcc) {
mMovingIDs.EnsureRemoved(id);
}
mozilla::ipc::IPCResult DocAccessibleParent::RecvHideEvent(
mozilla::ipc::IPCResult DocAccessibleParent::ProcessHideEvent(
const uint64_t& aRootID, const bool& aFromUser) {
ACQUIRE_ANDROID_LOCK
if (mShutdown) return IPC_OK();
MOZ_ASSERT(CheckDocTree());
@ -486,13 +484,10 @@ mozilla::ipc::IPCResult DocAccessibleParent::RecvCaretMoveEvent(
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 uint32_t& aLen, const bool& aIsInsert, const bool& aFromUser) {
ACQUIRE_ANDROID_LOCK
if (mShutdown) {
return IPC_OK();
}
RemoteAccessible* target = GetAccessible(aID);
if (!target) {
@ -518,6 +513,59 @@ mozilla::ipc::IPCResult DocAccessibleParent::RecvTextChangeEvent(
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(
const uint64_t& aID, const uint64_t& aWidgetID, const uint32_t& aType) {
ACQUIRE_ANDROID_LOCK

View file

@ -98,11 +98,6 @@ class DocAccessibleParent : public RemoteAccessible,
virtual mozilla::ipc::IPCResult RecvEvent(const uint64_t& aID,
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,
const uint64_t& aState,
const bool& aEnabled) final;
@ -113,10 +108,8 @@ class DocAccessibleParent : public RemoteAccessible,
const bool& aIsAtEndOfLine, const int32_t& aGranularity,
const bool& aFromUser) final;
virtual mozilla::ipc::IPCResult RecvTextChangeEvent(
const uint64_t& aID, const nsAString& aStr, const int32_t& aStart,
const uint32_t& aLen, const bool& aIsInsert,
const bool& aFromUser) override;
virtual mozilla::ipc::IPCResult RecvMutationEvents(
nsTArray<MutationEventData>&& aData) override;
virtual mozilla::ipc::IPCResult RecvFocusEvent(
const uint64_t& aID, const LayoutDeviceIntRect& aCaretRect) override;
@ -343,6 +336,16 @@ class DocAccessibleParent : public RemoteAccessible,
*/
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;
#if defined(XP_WIN)

View file

@ -35,6 +35,45 @@ struct AccessibleData
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
{
uint64_t StartID;
@ -56,17 +95,13 @@ parent:
* event.
*/
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 CaretMoveEvent(uint64_t aID,
LayoutDeviceIntRect aCaretRect,
int32_t aOffset,
bool aIsSelectionCollapsed, bool aIsAtEndOfLine,
int32_t aGranularity, bool aFromUser);
async TextChangeEvent(uint64_t aID, nsString aStr, int32_t aStart, uint32_t aLen,
bool aIsInsert, bool aFromUser);
async MutationEvents(MutationEventData[] aData);
async SelectionEvent(uint64_t aID, uint64_t aWidgetID, uint32_t aType);
async RoleChangedEvent(role aRole, uint8_t aRoleMapEntryIndex);
async FocusEvent(uint64_t aID, LayoutDeviceIntRect aCaretRect);

View file

@ -103,3 +103,64 @@ addAccessibleTask(
},
{ 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
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) {
const container = findAccessibleChildByID(accDoc, "container");
await untilCacheOk(

View file

@ -1045,7 +1045,11 @@ pref("browser.tabs.tooltipsShowPidAndActiveness", false);
pref("browser.tabs.hoverPreview.enabled", true);
pref("browser.tabs.hoverPreview.showThumbnails", true);
#ifdef NIGHTLY_BUILD
pref("browser.tabs.groups.enabled", true);
#else
pref("browser.tabs.groups.enabled", false);
#endif
pref("browser.tabs.groups.dragOverThresholdPercent", 20);
pref("browser.tabs.groups.dragOverDelayMS", 30);
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
// for users who don't have sync enabled
pref("identity.fxaccounts.toolbar.syncSetup.enabled", false);
pref("identity.fxaccounts.toolbar.syncSetup.panelAccessed", false);
// Toolbox preferences
pref("devtools.toolbox.footer.height", 250);

View file

@ -753,7 +753,11 @@ var gSync = {
document,
"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
// if the user is not signed in
@ -1028,10 +1032,7 @@ var gSync = {
}
},
async toggleAccountPanel(
anchor = document.getElementById("fxa-toolbar-menu-button"),
aEvent
) {
async toggleAccountPanel(anchor = null, aEvent) {
// Don't show the panel if the window is in customization mode.
if (document.documentElement.hasAttribute("customizing")) {
return;
@ -1046,10 +1047,15 @@ var gSync = {
return;
}
if (
anchor == document.getElementById("fxa-toolbar-menu-button") &&
anchor.getAttribute("open") != "true"
) {
const fxaToolbarMenuBtn = document.getElementById(
"fxa-toolbar-menu-button"
);
if (anchor === null) {
anchor = fxaToolbarMenuBtn;
}
if (anchor == fxaToolbarMenuBtn && anchor.getAttribute("open") != "true") {
if (ASRouter.initialized) {
await ASRouter.sendTriggerMessage({
browser: gBrowser.selectedBrowser,
@ -1080,7 +1086,7 @@ var gSync = {
this.updateFxAPanel(UIState.get());
this.updateCTAPanel(anchor);
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
// clicks it so instead of going straight to the login page,
// 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 = {}) {
const isNewSyncSetupFlowEnabled =
NimbusFeatures.syncDecouplingUpdates.getVariable("syncSetup");
NimbusFeatures.syncSetupFlow.getVariable("enabled");
const mainWindowEl = document.documentElement;
const menuHeaderTitleEl = PanelMultiView.getViewNode(
@ -1222,6 +1253,9 @@ var gSync = {
if (state.syncEnabled) {
syncNowButtonEl.removeAttribute("hidden");
syncSetupEl.hidden = true;
} else if (this._shouldShowSyncOffIndicator()) {
let fxaButton = document.getElementById("fxa-toolbar-menu-button");
fxaButton?.setAttribute("badge-status", "sync-disabled");
}
break;

View file

@ -6,7 +6,7 @@ function makeInputStream(aString) {
let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
Ci.nsIStringInputStream
);
stream.data = aString;
stream.setByteStringData(aString);
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
await ExperimentAPI.ready();
let doCleanup = await ExperimentFakes.enrollWithFeatureConfig({
featureId: NimbusFeatures.syncDecouplingUpdates.featureId,
featureId: NimbusFeatures.syncSetupFlow.featureId,
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
await ExperimentAPI.ready();
let doCleanup = await ExperimentFakes.enrollWithFeatureConfig({
featureId: NimbusFeatures.syncDecouplingUpdates.featureId,
featureId: NimbusFeatures.syncSetupFlow.featureId,
value: {
syncSetup: false,
enabled: false,
},
});

View file

@ -817,7 +817,12 @@ nsBrowserContentHandler.prototype = {
case OVERRIDE_NEW_PROFILE:
// New profile.
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;
}
overridePage = Services.urlFormatter.formatURLPref(

View file

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

View file

@ -482,7 +482,7 @@ const StepsIndicator = props => {
let steps = [];
for (let i = 0; i < props.totalNumberOfScreens; i++) {
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,
className: `indicator ${className}`,
role: "presentation"
@ -817,7 +817,7 @@ const Localized = ({
// Add zap style and content in a way that allows fluent to insert too.
if (text.zap) {
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",
"data-l10n-name": "zap",
ref: zapRef
@ -1243,7 +1243,7 @@ class ProtonScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
for (const item of content) {
switch (item.type) {
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,
handleAction: this.props.handleAction
}));
@ -2999,7 +2999,7 @@ ReturnToAMO.defaultProps = _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_
/******/
/************************************************************************/
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__);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
@ -3135,7 +3135,7 @@ async function mount() {
messageId,
UTMTerm
} = 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,
UTMTerm: UTMTerm
}, 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-dom": "16.13.1",
"react-redux": "7.2.6",
"react-transition-group": "4.4.2",
"react-transition-group": "4.4.5",
"redux": "4.1.2"
},
"devDependencies": {
"@babel/preset-react": "7.23.3",
"@babel/preset-react": "7.26.3",
"@jsdevtools/coverage-istanbul-loader": "^3.0.5",
"babel-loader": "9.1.3",
"babel-loader": "9.2.1",
"chai": "4.3.4",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.8",
"karma": "6.4.2",
"karma": "6.4.4",
"karma-chai": "0.1.0",
"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-mocha": "2.0.1",
"karma-mocha-reporter": "2.2.5",
@ -30,9 +30,9 @@
"karma-sourcemap-loader": "0.4.0",
"karma-webpack": "5.0.1",
"npm-run-all": "4.1.5",
"sass": "1.71.1",
"sass": "1.72.0",
"sinon": "17.0.1",
"webpack": "5.90.3",
"webpack": "5.97.1",
"webpack-cli": "5.1.4",
"yamscripts": "0.1.0"
},

View file

@ -500,7 +500,7 @@ const ImpressionsItem = ({
/******/
/************************************************************************/
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__);
/* 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));
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-10",
"fxms-message-11",
"fxms-message-12",
"fxms-message-13",
"fxms-message-14",
"fxms-message-15",
"infobar",
"moments-page",
"pbNewtab",

File diff suppressed because it is too large Load diff

View file

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

View file

@ -19,6 +19,12 @@ const PanelUI = {
get kEvents() {
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
* getters are set in init, and memoizing happens after the first retrieval.
@ -165,6 +171,9 @@ const PanelUI = {
for (let event of this.kEvents) {
this.notificationPanel.removeEventListener(event, this);
}
for (let event of this.kNotificationEvents) {
this.notificationPanel.removeEventListener(event, this);
}
}
Services.obs.removeObserver(this, "fullscreen-nav-toolbox");
@ -323,6 +332,16 @@ const PanelUI = {
case "command":
this.onCommand(aEvent);
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) {
this._notificationPanel.addEventListener(event, this);
}
for (let event of this.kNotificationEvents) {
this._notificationPanel.addEventListener(event, this);
}
}
return this._notificationPanel;
},
@ -1006,14 +1028,6 @@ const PanelUI = {
let popupnotification = document.getElementById(popupnotificationID);
popupnotification.setAttribute("id", popupnotificationID);
popupnotification.setAttribute(
"buttoncommand",
"PanelUI._onNotificationButtonEvent(event, 'buttoncommand');"
);
popupnotification.setAttribute(
"secondarybuttoncommand",
"PanelUI._onNotificationButtonEvent(event, 'secondarybuttoncommand');"
);
if (notification.options.message) {
let desc = this._formatDescriptionMessage(notification);
@ -1082,6 +1096,8 @@ const PanelUI = {
},
_onNotificationButtonEvent(event, type) {
event.preventDefault();
let notificationEl = getNotificationFromElement(event.originalTarget);
if (!notificationEl) {

View file

@ -334,6 +334,8 @@ skip-if = ["(verify && debug && (os == 'linux' || os == 'mac'))"]
["browser_remove_customized_specials.js"]
["browser_remove_sidebar_button_and_sidebar.js"]
["browser_reset_builtin_widget_currentArea.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);
this.populateDescription(doc, addon);
// Setup the command handler.
let handleCommand = async event => {
// Setup the buttoncommand handler.
let handleButtonCommand = async event => {
event.preventDefault();
panel.hidePopup();
if (event.originalTarget == popupnotification.button) {
// Main action is to keep changes.
await this.setConfirmation(extensionId);
} else if (this.preferencesLocation) {
// Main action is to keep changes.
await this.setConfirmation(extensionId);
// 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.
let options = this.Entrypoint
? { urlParams: { entrypoint: this.Entrypoint } }
@ -275,20 +289,24 @@ export class ExtensionControlledPopup {
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) {
win.gURLBar.focus();
}
};
panel.addEventListener("command", handleCommand);
panel.addEventListener("buttoncommand", handleButtonCommand);
panel.addEventListener(
"secondarybuttoncommand",
handleSecondaryButtonCommand
);
panel.addEventListener(
"popuphidden",
() => {
popupnotification.hidden = true;
panel.removeEventListener("command", handleCommand);
panel.removeEventListener("buttoncommand", handleButtonCommand);
panel.removeEventListener(
"secondarybuttoncommand",
handleSecondaryButtonCommand
);
},
{ once: true }
);

View file

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

View file

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

View file

@ -1,13 +1,13 @@
@mixin section-card-small {
grid-row: span 1;
grid-column: span 1;
display: flex;
flex-direction: row;
position: relative;
align-items: center;
gap: var(--space-medium);
padding: var(--space-large);
&.ds-card.sections-card-ui {
padding: unset;
}
.stp-context-menu {
display: block;
}
@ -16,8 +16,18 @@
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 {
width: 120px;
width: 125px;
flex-shrink: 0;
flex-grow: 0;
aspect-ratio: 1/1;
@ -44,10 +54,6 @@
}
}
.ds-card-link {
inset-inline-end: 0;
}
.meta {
padding: var(--space-xsmall) 0;
align-self: flex-start;
@ -72,7 +78,7 @@
}
.card-stp-button-position-wrapper {
inset-inline-end: 28px;
inset-inline-end: 10px;
.card-stp-button {
display: none;
@ -80,6 +86,7 @@
}
}
@mixin section-card-medium {
grid-row: span 2;
grid-column: span 1;
@ -88,6 +95,8 @@
align-items: initial;
gap: initial;
.stp-context-menu {
display: none;
}
@ -104,12 +113,19 @@
display: block;
}
.ds-card-link {
display: flex;
flex-direction: column;
gap: 0;
padding: 0;
}
.img-wrapper {
width: 100%;
position: relative;
// reset values inherited from small card mixin
flex-grow: initial;
flex-shrink: initial;
flex-grow: 0;
flex-shrink: 0;
aspect-ratio: initial;
}
@ -137,6 +153,7 @@
}
}
// DS Large Card
@mixin section-card-large {
grid-row: span 2;
@ -145,15 +162,16 @@
&.ds-card.sections-card-ui {
@media (min-width: $break-point-layout-variant ) and (max-width: $break-point-widest ),
(min-width: $break-point-sections-variant ) {
display: grid;
grid-template-columns: auto 1fr;
gap: var(--space-xlarge);
padding: var(--space-xxlarge);
align-items: center;
align-content: flex-start;
.ds-card-link {
flex-direction: row;
gap: var(--space-xlarge);
padding: var(--space-xxlarge);
}
.img-wrapper {
width: 220px;
height: 220px;
width: 265px;
}
.ds-image.img {
@ -182,6 +200,10 @@
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
.has-sections-grid .ds-outer-wrapper-breakpoint-override {
.ds-layout-topsites {
width: 100vw;
margin-inline-start: 50%;
transform: translate3d(-50%, 0, 0);
> div {
width: 266px;
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
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
const currentState = this.state.isThumbsDownActive;
this.setState({ isThumbsDownActive: !currentState });
@ -801,27 +807,6 @@ export class _DSCard extends React.PureComponent {
data-position-three={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
className="ds-card-link"
dispatch={this.props.dispatch}
@ -829,6 +814,27 @@ export class _DSCard extends React.PureComponent {
url={this.props.url}
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
flightId={this.props.flightId}
rows={[
@ -863,46 +869,48 @@ export class _DSCard extends React.PureComponent {
source={this.props.type}
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
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 {
position: absolute;
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
align-items: initial;
text-decoration: none;
grid-template-columns: auto 1fr;
gap: inherit;
flex-grow: 1;
&: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;
z-index: 1;
background: light-dark(#F0F0F4, var(--newtab-background-color-secondary));
border-radius: var(--border-radius-small);
color: var(--newtab-text-primary-color);
padding: var(--space-small);
margin: var(--space-small);
font-size: 14px;
@ -537,6 +541,9 @@ $ds-card-image-gradient-solid: rgba(0, 0, 0, 100%);
height: 28px;
font-size: var(--newtab-font-size-xsmall);
color: var(--newtab-text-topic-label-color);
margin: initial;
padding: initial;
background-color: initial;
}
.card-stp-button-hover-background {

View file

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

View file

@ -27,11 +27,8 @@ export const selectLayoutRender = ({ state = {}, prefs = {} }) => {
const results = [...data];
for (let position of spocsPositions) {
const spoc = spocsData[spocIndexPlacementMap[placementName]];
const format = spoc?.format;
// 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,
// dont combine with content
if (!spoc || format === "billboard" || format === "leaderboard") {
if (!spoc) {
break;
}
@ -139,12 +136,19 @@ export const selectLayoutRender = ({ state = {}, prefs = {} }) => {
const placement = spocsPlacement || {};
const placementName = placement.name || "newtab_spocs";
const spocsData = spocs.data[placementName];
// We expect a spoc, spocs are loaded, and the server returned spocs.
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,
spocsPositions,
spocsData.items,
filteredSpocs,
placementName
);
}

View file

@ -4485,11 +4485,6 @@ main section {
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 {
width: 266px;
margin-inline: auto;
@ -4723,21 +4718,27 @@ main section {
.ds-section-grid.ds-card-grid .col-1-small {
grid-row: span 1;
grid-column: span 1;
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.ds-card.sections-card-ui {
padding: unset;
}
.ds-section-grid.ds-card-grid .col-1-small .stp-context-menu {
display: block;
}
.ds-section-grid.ds-card-grid .col-1-small .card-stp-thumbs-buttons-wrapper {
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 {
width: 120px;
width: 125px;
flex-shrink: 0;
flex-grow: 0;
aspect-ratio: 1/1;
@ -4758,9 +4759,6 @@ main section {
width: 100%;
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 {
padding: var(--space-xsmall) 0;
align-self: flex-start;
@ -4780,7 +4778,7 @@ main section {
padding-block-start: unset;
}
.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 {
display: none;
@ -4805,11 +4803,17 @@ main section {
.ds-section-grid.ds-card-grid .col-1-medium .card-stp-thumbs-buttons-wrapper {
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 {
width: 100%;
position: relative;
flex-grow: initial;
flex-shrink: initial;
flex-grow: 0;
flex-shrink: 0;
aspect-ratio: initial;
}
.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) {
.ds-section-grid.ds-card-grid .col-1-large.ds-card.sections-card-ui {
display: grid;
grid-template-columns: auto 1fr;
align-content: flex-start;
}
.ds-section-grid.ds-card-grid .col-1-large.ds-card.sections-card-ui .ds-card-link {
flex-direction: row;
gap: var(--space-xlarge);
padding: var(--space-xxlarge);
align-items: center;
}
.ds-section-grid.ds-card-grid .col-1-large.ds-card.sections-card-ui .img-wrapper {
width: 220px;
height: 220px;
width: 265px;
}
.ds-section-grid.ds-card-grid .col-1-large.ds-card.sections-card-ui .ds-image.img {
aspect-ratio: 1/1;
@ -4864,6 +4868,9 @@ main section {
-webkit-line-clamp: 4;
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) {
.ds-section-grid.ds-card-grid {
@ -4893,21 +4900,27 @@ main section {
.ds-section-grid.ds-card-grid .col-2-small {
grid-row: span 1;
grid-column: span 1;
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.ds-card.sections-card-ui {
padding: unset;
}
.ds-section-grid.ds-card-grid .col-2-small .stp-context-menu {
display: block;
}
.ds-section-grid.ds-card-grid .col-2-small .card-stp-thumbs-buttons-wrapper {
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 {
width: 120px;
width: 125px;
flex-shrink: 0;
flex-grow: 0;
aspect-ratio: 1/1;
@ -4928,9 +4941,6 @@ main section {
width: 100%;
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 {
padding: var(--space-xsmall) 0;
align-self: flex-start;
@ -4950,7 +4960,7 @@ main section {
padding-block-start: unset;
}
.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 {
display: none;
@ -4975,11 +4985,17 @@ main section {
.ds-section-grid.ds-card-grid .col-2-medium .card-stp-thumbs-buttons-wrapper {
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 {
width: 100%;
position: relative;
flex-grow: initial;
flex-shrink: initial;
flex-grow: 0;
flex-shrink: 0;
aspect-ratio: initial;
}
.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) {
.ds-section-grid.ds-card-grid .col-2-large.ds-card.sections-card-ui {
display: grid;
grid-template-columns: auto 1fr;
align-content: flex-start;
}
.ds-section-grid.ds-card-grid .col-2-large.ds-card.sections-card-ui .ds-card-link {
flex-direction: row;
gap: var(--space-xlarge);
padding: var(--space-xxlarge);
align-items: center;
}
.ds-section-grid.ds-card-grid .col-2-large.ds-card.sections-card-ui .img-wrapper {
width: 220px;
height: 220px;
width: 265px;
}
.ds-section-grid.ds-card-grid .col-2-large.ds-card.sections-card-ui .ds-image.img {
aspect-ratio: 1/1;
@ -5034,6 +5050,9 @@ main section {
-webkit-line-clamp: 4;
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) {
.ds-section-grid.ds-card-grid {
@ -5064,21 +5083,27 @@ main section {
.ds-section-grid.ds-card-grid .col-3-small {
grid-row: span 1;
grid-column: span 1;
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.ds-card.sections-card-ui {
padding: unset;
}
.ds-section-grid.ds-card-grid .col-3-small .stp-context-menu {
display: block;
}
.ds-section-grid.ds-card-grid .col-3-small .card-stp-thumbs-buttons-wrapper {
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 {
width: 120px;
width: 125px;
flex-shrink: 0;
flex-grow: 0;
aspect-ratio: 1/1;
@ -5099,9 +5124,6 @@ main section {
width: 100%;
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 {
padding: var(--space-xsmall) 0;
align-self: flex-start;
@ -5121,7 +5143,7 @@ main section {
padding-block-start: unset;
}
.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 {
display: none;
@ -5146,11 +5168,17 @@ main section {
.ds-section-grid.ds-card-grid .col-3-medium .card-stp-thumbs-buttons-wrapper {
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 {
width: 100%;
position: relative;
flex-grow: initial;
flex-shrink: initial;
flex-grow: 0;
flex-shrink: 0;
aspect-ratio: initial;
}
.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) {
.ds-section-grid.ds-card-grid .col-3-large.ds-card.sections-card-ui {
display: grid;
grid-template-columns: auto 1fr;
align-content: flex-start;
}
.ds-section-grid.ds-card-grid .col-3-large.ds-card.sections-card-ui .ds-card-link {
flex-direction: row;
gap: var(--space-xlarge);
padding: var(--space-xxlarge);
align-items: center;
}
.ds-section-grid.ds-card-grid .col-3-large.ds-card.sections-card-ui .img-wrapper {
width: 220px;
height: 220px;
width: 265px;
}
.ds-section-grid.ds-card-grid .col-3-large.ds-card.sections-card-ui .ds-image.img {
aspect-ratio: 1/1;
@ -5205,6 +5233,9 @@ main section {
-webkit-line-clamp: 4;
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) {
.ds-section-grid.ds-card-grid {
@ -5234,21 +5265,27 @@ main section {
.ds-section-grid.ds-card-grid .col-4-small {
grid-row: span 1;
grid-column: span 1;
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.ds-card.sections-card-ui {
padding: unset;
}
.ds-section-grid.ds-card-grid .col-4-small .stp-context-menu {
display: block;
}
.ds-section-grid.ds-card-grid .col-4-small .card-stp-thumbs-buttons-wrapper {
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 {
width: 120px;
width: 125px;
flex-shrink: 0;
flex-grow: 0;
aspect-ratio: 1/1;
@ -5269,9 +5306,6 @@ main section {
width: 100%;
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 {
padding: var(--space-xsmall) 0;
align-self: flex-start;
@ -5291,7 +5325,7 @@ main section {
padding-block-start: unset;
}
.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 {
display: none;
@ -5316,11 +5350,17 @@ main section {
.ds-section-grid.ds-card-grid .col-4-medium .card-stp-thumbs-buttons-wrapper {
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 {
width: 100%;
position: relative;
flex-grow: initial;
flex-shrink: initial;
flex-grow: 0;
flex-shrink: 0;
aspect-ratio: initial;
}
.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) {
.ds-section-grid.ds-card-grid .col-4-large.ds-card.sections-card-ui {
display: grid;
grid-template-columns: auto 1fr;
align-content: flex-start;
}
.ds-section-grid.ds-card-grid .col-4-large.ds-card.sections-card-ui .ds-card-link {
flex-direction: row;
gap: var(--space-xlarge);
padding: var(--space-xxlarge);
align-items: center;
}
.ds-section-grid.ds-card-grid .col-4-large.ds-card.sections-card-ui .img-wrapper {
width: 220px;
height: 220px;
width: 265px;
}
.ds-section-grid.ds-card-grid .col-4-large.ds-card.sections-card-ui .ds-image.img {
aspect-ratio: 1/1;
@ -5375,6 +5415,9 @@ main section {
-webkit-line-clamp: 4;
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 {
@ -6003,21 +6046,25 @@ main section {
box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.15);
}
.ds-card .ds-card-link {
position: absolute;
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
align-items: initial;
text-decoration: none;
grid-template-columns: auto 1fr;
gap: inherit;
flex-grow: 1;
}
.ds-card .ds-card-link:focus {
border: 0;
outline: var(--focus-outline);
transition: none;
}
.ds-card > .ds-card-topic {
.ds-card .ds-card-topic {
position: absolute;
z-index: 1;
background: light-dark(#F0F0F4, var(--newtab-background-color-secondary));
border-radius: var(--border-radius-small);
color: var(--newtab-text-primary-color);
padding: var(--space-small);
margin: var(--space-small);
font-size: 14px;
@ -6248,6 +6295,9 @@ main section {
height: 28px;
font-size: var(--newtab-font-size-xsmall);
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 {
border-radius: var(--border-radius-large) var(--border-radius-large) 0 0;
@ -6451,6 +6501,7 @@ main section {
.impression-observer {
position: absolute;
top: 0;
right: 0;
width: 100%;
height: 100%;
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
const currentState = this.state.isThumbsUpActive;
@ -3449,7 +3452,10 @@ class _DSCard extends (external_React_default()).PureComponent {
}
}, "ActivityStream:Content"));
}
onThumbsDownClick() {
onThumbsDownClick(event) {
event.stopPropagation();
event.preventDefault();
// Toggle active state for thumbs down button to show CSS animation
const currentState = this.state.isThumbsDownActive;
this.setState({
@ -3682,6 +3688,12 @@ class _DSCard extends (external_React_default()).PureComponent {
"data-position-two": this.props["data-position-one"],
"data-position-three": 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", {
className: "ds-card-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,
isRecentSave: isRecentSave,
alt_text: alt_text
})), /*#__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
}, /*#__PURE__*/external_React_default().createElement(ImpressionStats_ImpressionStats, {
})), /*#__PURE__*/external_React_default().createElement(ImpressionStats_ImpressionStats, {
flightId: this.props.flightId,
rows: [{
id: this.props.id,
@ -3733,7 +3739,7 @@ class _DSCard extends (external_React_default()).PureComponent {
isFakespot: isFakespot,
source: this.props.type,
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"
}, "Shop Now"), isFakespot ? /*#__PURE__*/external_React_default().createElement("div", {
className: "meta"
@ -3765,7 +3771,7 @@ class _DSCard extends (external_React_default()).PureComponent {
isSectionsCard: this.props.mayHaveSectionsCards && this.props.topic && !isListCard,
format: format,
topic: this.props.topic
}), /*#__PURE__*/external_React_default().createElement("div", {
})), /*#__PURE__*/external_React_default().createElement("div", {
className: `card-stp-button-hover-background ${compactPocketSavedButtonClassName}`
}, /*#__PURE__*/external_React_default().createElement("div", {
className: "card-stp-button-position-wrapper"
@ -9520,11 +9526,8 @@ const selectLayoutRender = ({ state = {}, prefs = {} }) => {
const results = [...data];
for (let position of spocsPositions) {
const spoc = spocsData[spocIndexPlacementMap[placementName]];
const format = spoc?.format;
// 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,
// dont combine with content
if (!spoc || format === "billboard" || format === "leaderboard") {
if (!spoc) {
break;
}
@ -9632,12 +9635,19 @@ const selectLayoutRender = ({ state = {}, prefs = {} }) => {
const placement = spocsPlacement || {};
const placementName = placement.name || "newtab_spocs";
const spocsData = spocs.data[placementName];
// We expect a spoc, spocs are loaded, and the server returned spocs.
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,
spocsPositions,
spocsData.items,
filteredSpocs,
placementName
);
}
@ -10025,10 +10035,11 @@ function CardSection({
type: actionTypes.CARD_SECTION_IMPRESSION,
data: {
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
const sectionRefs = useIntersectionObserver(handleIntersection);

View file

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

View file

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

View file

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

View file

@ -150,6 +150,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
formAutofillStorage: "resource://autofill/FormAutofillStorage.sys.mjs",
LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
PlacesDBUtils: "resource://gre/modules/PlacesDBUtils.sys.mjs",
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
});
@ -248,10 +249,20 @@ export class ProfilesParent extends JSWindowActorParent {
let loginCount = (await lazy.LoginHelper.getAllUserFacingLogins())
.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 bookmarkCount = stats.find(
item => item.entity == "moz_bookmarks"
).count;
let visitCount = stats.find(
item => item.entity == "moz_historyvisits"
).count;

View file

@ -180,7 +180,7 @@ class SelectableProfileServiceClass {
await this.#profileService.asyncFlush();
} catch (e) {
try {
await this.#profileService.asyncFlushCurrentProfile();
await this.#profileService.asyncFlushGroupProfile();
} catch (ex) {
console.error(
`Failed to flush changes to the profiles database: ${ex}`
@ -841,6 +841,7 @@ class SelectableProfileServiceClass {
}
this.#groupToolkitProfile.rootDir = await aProfile.rootDir;
await this.#attemptFlushProfileService();
Glean.profilesDefault.updated.record();
}
/**
@ -1066,6 +1067,9 @@ class SelectableProfileServiceClass {
if (!this.#currentProfile) {
let path = this.#profileService.currentProfile.rootDir;
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";
let lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
});
add_task(async function test_serviceInitialized() {
await initGroupDatabase();
await BrowserTestUtils.withNewTab(
@ -33,7 +38,7 @@ add_task(async function test_serviceInitialized() {
"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 () => {
let deleteProfileCard = content.document.querySelector(
"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,
`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`
);
Services.telemetry.clearEvents();
Services.fog.testResetFOG();
is(
null,
Glean.profilesDefault.updated.testGetValue(),
"We have not recorded any Glean data yet"
);
// Override
gProfileService.currentProfile.rootDir = "bad";
@ -40,7 +48,17 @@ add_task(async function test_updateDefaultProfileOnWindowSwitch() {
`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();
});

View file

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

View file

@ -425,7 +425,7 @@ export var SessionStore = {
Override the value of the closedTabsFromAllWindows preference.
* @param {boolean} [aOptions.closedTabsFromClosedWindows]
Override the value of the closedTabsFromClosedWindows preference.
* @returns {TabGroupStateData[]}
* @returns {ClosedTabGroupStateData[]}
*/
getClosedTabGroups: function ss_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.
*
* @param {string} tabGroupId
* @returns {TabGroupStateData|undefined}
* @returns {SavedTabGroupStateData|undefined}
*/
getSavedTabGroup(tabGroupId) {
return SessionStoreInternal.getSavedTabGroup(tabGroupId);
@ -832,7 +832,7 @@ export var SessionStore = {
/**
* Returns all tab groups that were saved in this session.
* @returns {TabGroupStateData[]}
* @returns {SavedTabGroupStateData[]}
*/
getSavedTabGroups() {
return SessionStoreInternal.getSavedTabGroups();
@ -977,7 +977,7 @@ var SessionStoreInternal = {
// states for all recently closed windows
_closedWindows: [],
/** @type {TabGroupStateData[]} states for all closed tab groups */
/** @type {SavedTabGroupStateData[]} states for all saved+closed tab groups */
_savedGroups: [],
// collection of session states yet to be restored
@ -2431,6 +2431,7 @@ var SessionStoreInternal = {
// Insert winData at the right position.
this._closedWindows.splice(index, 0, winData);
this._capClosedWindows();
this._saveOpenTabGroupsOnClose(winData);
this._closedObjectsChanged = true;
// The first time we close a window, ensure it can be restored from the
// 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
*/
@ -2980,7 +3047,8 @@ var SessionStoreInternal = {
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
// 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
* tab group state data, but the session store has additional requirements
* for closed tabs that are currently only implemented in
* `SessionStoreInternal.maybeSaveClosedTab`. This method uses `TabGroupState`
* to collect information and then enriches it with metadata specific to tab
* groups that are saved or closed.
* `SessionStoreInternal.maybeSaveClosedTab`. This method converts the tabs
* in a tab group into the closed tab data schema format required for
* closed or saved groups.
*
* @param {MozTabbrowserTabGroup} tabGroup
* @param {MozTabbrowserTab[]} tabs
* @param {Window} win
* @returns {TabGroupStateData}
* Returns tab group state data from `TabGroupState` but enriched with
* metadata specific to saved or closed tab groups.
* @returns {ClosedTabStateData[]}
*/
buildClosedTabGroupState(tabGroup, win) {
let tabGroupState = lazy.TabGroupState.collect(tabGroup);
tabGroupState.closedAt = Date.now();
// Only save tab state for tabs that qualify
tabGroupState.tabs = [];
tabGroup.tabs.forEach(tab => {
_collectClosedTabsForTabGroup(tabs, win) {
let closedTabs = [];
tabs.forEach(tab => {
let tabState = lazy.TabState.collect(tab, TAB_CUSTOM_VALUES.get(tab));
this.maybeSaveClosedTab(win, tab, tabState, {
closedTabsArray: tabGroupState.tabs,
closedTabsArray: closedTabs,
});
});
return tabGroupState;
return closedTabs;
},
/**
@ -3108,6 +3170,7 @@ var SessionStoreInternal = {
* Tab reference
*/
resetBrowserToLazyState(aTab) {
const gBrowser = aTab.ownerGlobal.gBrowser;
let browser = aTab.linkedBrowser;
// Browser is already lazy so don't do anything.
if (!browser.isConnected) {
@ -3121,6 +3184,7 @@ var SessionStoreInternal = {
this._lastKnownFrameLoader.delete(browser.permanentKey);
this._crashedBrowsers.delete(browser.permanentKey);
aTab.removeAttribute("crashed");
gBrowser.tabContainer.updateTabIndicatorAttr(aTab);
let { userTypedValue = null, userTypedClear = 0 } = browser;
let hasStartedLoad = browser.didStartLoadSinceLastUserTyping();
@ -3657,6 +3721,10 @@ var SessionStoreInternal = {
return this._LAST_ACTION_CLOSED_TAB;
},
/**
* @param {number} aClosedId
* @returns {WindowStateData|undefined}
*/
getClosedWindowDataByClosedId: function ssi_getClosedWindowDataByClosedId(
aClosedId
) {
@ -3948,7 +4016,7 @@ var SessionStoreInternal = {
* @param {boolean} [aOptions.private = false]
* @param {boolean} [aOptions.closedTabsFromAllWindows]
* @param {boolean} [aOptions.closedTabsFromClosedWindows]
* @returns {TabGroupStateData[]}
* @returns {ClosedTabGroupStateData[]}
*/
getClosedTabGroups: function ssi_getClosedTabGroups(aOptions) {
const sourceOptions = this._prepareClosedTabOptions(aOptions);
@ -4291,7 +4359,7 @@ var SessionStoreInternal = {
);
if (savedGroupIndex < 0) {
throw Components.Exception(
"Closed tab group not found",
"Saved tab group not found",
Cr.NS_ERROR_INVALID_ARG
);
}
@ -4301,6 +4369,9 @@ var SessionStoreInternal = {
this.removeClosedTabData({}, savedGroup.tabs, i);
}
this._savedGroups.splice(savedGroupIndex, 1);
// Notify of changes to closed objects.
this._closedObjectsChanged = true;
this._notifyOfClosedObjectsChange();
},
@ -4366,8 +4437,32 @@ var SessionStoreInternal = {
return this._closedWindows.length;
},
/**
* @returns {WindowStateData[]}
*/
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) {
@ -4394,6 +4489,17 @@ var SessionStoreInternal = {
let state = { windows: this._removeClosedWindow(aIndex) };
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);
this.windowToFocus = window;
WINDOW_SHOWING_PROMISES.get(window).promise.then(win =>
@ -4818,6 +4924,7 @@ var SessionStoreInternal = {
);
}
const gBrowser = aTab.ownerGlobal.gBrowser;
let browser = aTab.linkedBrowser;
if (!this._crashedBrowsers.has(browser.permanentKey)) {
return;
@ -4837,6 +4944,7 @@ var SessionStoreInternal = {
// a flash of the about:tabcrashed page after selecting
// the revived tab.
aTab.removeAttribute("crashed");
gBrowser.tabContainer.updateTabIndicatorAttr(aTab);
browser.loadURI(lazy.blankURI, {
triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({
@ -7597,19 +7705,31 @@ var SessionStoreInternal = {
* @param {MozTabbrowserTabGroup} tabGroup
*/
addSavedTabGroup(tabGroup) {
if (this.getSavedTabGroup(tabGroup.id)) {
return;
}
let tabGroupState = this.buildClosedTabGroupState(
let tabGroupState = lazy.TabGroupState.savedInOpenWindow(
tabGroup,
tabGroup.ownerGlobal.__SSi
);
tabGroupState.tabs = this._collectClosedTabsForTabGroup(
tabGroup.tabs,
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
* @returns {TabGroupStateData|undefined}
* @returns {SavedTabGroupStateData|undefined}
*/
getSavedTabGroup(tabGroupId) {
return this._savedGroups.find(
@ -7619,7 +7739,7 @@ var SessionStoreInternal = {
/**
* Returns all tab groups that were saved in this session.
* @returns {TabGroupStateData[]}
* @returns {SavedTabGroupStateData[]}
*/
getSavedTabGroups() {
return Cu.cloneInto(this._savedGroups, {});
@ -7628,7 +7748,7 @@ var SessionStoreInternal = {
/**
* @param {Window|{sourceWindowId: string}|{sourceClosedId: number}} source
* @param {string} tabGroupId
* @returns {TabGroupStateData|undefined}
* @returns {ClosedTabGroupStateData|undefined}
*/
getClosedTabGroup(source, tabGroupId) {
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(
tabGroupData,
targetWindow
@ -7719,6 +7855,11 @@ var SessionStoreInternal = {
return group;
},
/**
* @param {ClosedTabGroupStateData|SavedTabGroupStateData} tabGroupData
* @param {Window} targetWindow
* @returns {MozTabbrowserTabGroup}
*/
_createTabsForSavedOrClosedTabGroup(tabGroupData, targetWindow) {
let tabDataList = tabGroupData.tabs.map(tab => tab.state);
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
export const _LastSession = LastSession;

View file

@ -4,6 +4,7 @@
/**
* @typedef {object} TabGroupStateData
* State of a tab group inside of an open window.
* @property {string} id
* Unique ID of the tab group.
* @property {string} name
@ -14,6 +15,39 @@
* 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.
*/
@ -34,6 +68,88 @@ class _TabGroupState {
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();

View file

@ -296,6 +296,14 @@ tags = "os_integration"
["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_state.js"]

View file

@ -62,6 +62,7 @@ add_task(async function test_RestoreSingleGroup() {
"tab group collapsed state should be restored"
);
win.gBrowser.removeTabGroup(tabGroup);
await BrowserTestUtils.closeWindow(win);
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 = {};
ChromeUtils.defineESModuleGetters(lazy, {
isProductURL: "chrome://global/content/shopping/ShoppingProduct.mjs",
isSupportedSiteURL: "chrome://global/content/shopping/ShoppingProduct.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.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", {
url: uri?.spec ?? null,
isReload: !!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_RELOAD),
isSupportedSite,
});
}
currentProductUrl() {
getCurrentURL() {
let window = this.browsingContext.topChromeWindow;
let { selectedBrowser } = window.gBrowser;
let uri = selectedBrowser.currentURI;
let isProduct = lazy.isProductURL(uri);
return isProduct ? uri.spec : null;
return uri.spec ?? null;
}
async receiveMessage(message) {
@ -56,13 +56,16 @@ export class ReviewCheckerParent extends JSWindowActorParent {
}
switch (message.name) {
case "GetProductURL":
return this.currentProductUrl();
return this.getCurrentURL();
case "DisableShopping":
Services.prefs.setIntPref(
ReviewCheckerParent.SHOPPING_OPTED_IN_PREF,
2
);
break;
case "CloseShoppingSidebar":
this.closeSidebarPanel();
break;
}
return null;
}
@ -87,11 +90,19 @@ export class ReviewCheckerParent extends JSWindowActorParent {
lazy.ShoppingUtils.onLocationChange(aLocationURI, aFlags);
let isProduct = lazy.isProductURL(aLocationURI);
if (isProduct) {
this.updateProductURL(aLocationURI, aFlags);
} else {
this.updateProductURL(null);
this.updateCurrentURL(
aLocationURI,
aFlags,
lazy.isSupportedSiteURL(aLocationURI)
);
}
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 { 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 = {};
@ -56,6 +60,12 @@ XPCOMUtils.defineLazyPreferenceGetter(
}
}
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"isIntegratedSidebar",
"browser.shopping.experience2023.integratedSidebar",
false
);
export class ShoppingSidebarChild extends RemotePageChild {
constructor() {
@ -84,21 +94,7 @@ export class ShoppingSidebarChild extends RemotePageChild {
}
switch (message.name) {
case "ShoppingSidebar:UpdateProductURL":
let { url, isReload } = 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 });
this.handleURLUpdate(message.data);
break;
case "ShoppingSidebar:ShowKeepClosedMessage":
this.sendToContent("ShowKeepClosedMessage");
@ -113,6 +109,32 @@ export class ShoppingSidebarChild extends RemotePageChild {
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) {
if (!newURI || !currentURI) {
return false;
@ -167,6 +189,9 @@ export class ShoppingSidebarChild extends RemotePageChild {
case "DisableShopping":
this.sendAsyncMessage("DisableShopping");
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`
* is current. Defaults to false.
* @param {bool} options.isPolledRequest = false
* @param {bool} options.focusCloseButton = false
* @param {bool} options.isProductPage = true
* @param {bool} options.isSupportedSite = false
*/
async updateContent({
haveUpdatedURI = false,
isPolledRequest = false,
focusCloseButton = false,
isProductPage = true,
isSupportedSite = false,
} = {}) {
// updateContent is an async function, and when we're off making requests or doing
// other things asynchronously, the actor can be destroyed, the user
@ -287,8 +317,17 @@ export class ShoppingSidebarChild extends RemotePageChild {
data: null,
recommendationData: null,
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.#productURI) {
// If we already have a URI and it's just null, bail immediately.
@ -296,16 +335,23 @@ export class ShoppingSidebarChild extends RemotePageChild {
return;
}
let url = await this.sendQuery("GetProductURL");
if (!url) {
return;
}
// Bail out if we opted out in the meantime, or don't have a URI.
if (!canContinue(null, false)) {
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;
@ -385,14 +431,12 @@ export class ShoppingSidebarChild extends RemotePageChild {
}
this.sendToContent("Update", {
adsEnabled: this.adsEnabled,
adsEnabledByUser: this.adsEnabledByUser,
autoOpenEnabled: this.autoOpenEnabled,
autoOpenEnabledByUser: this.autoOpenEnabledByUser,
showOnboarding: false,
data,
productUrl: this.#productURI.spec,
isAnalysisInProgress,
isProductPage,
isSupportedSite,
});
if (!data || data.error) {
@ -410,9 +454,6 @@ export class ShoppingSidebarChild extends RemotePageChild {
return;
}
let url = await this.sendQuery("GetProductURL");
if (!url) {
return;
}
// Similar to canContinue() above, check to see if things
// have changed while we were waiting. Bail out if the user
@ -421,12 +462,21 @@ export class ShoppingSidebarChild extends RemotePageChild {
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
this.sendToContent("Update", {
showOnboarding: true,
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 =
"browser.shopping.experience2023.integratedSidebar";
updateProductURL(uri, flags) {
updateCurrentURL(uri, flags) {
this.sendAsyncMessage("ShoppingSidebar:UpdateProductURL", {
url: uri?.spec ?? null,
isReload: !!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_RELOAD),
@ -50,12 +50,7 @@ export class ShoppingSidebarParent extends JSWindowActorParent {
}
switch (message.name) {
case "GetProductURL":
let sidebarBrowser = this.browsingContext.top.embedderElement;
let panel = sidebarBrowser.closest(".browserSidebarContainer");
let associatedTabbedBrowser = panel.querySelector(
"browser[messagemanagergroup=browsers]"
);
return associatedTabbedBrowser.currentURI?.spec ?? null;
return this.getCurrentURL();
case "DisableShopping":
Services.prefs.setBoolPref(
ShoppingSidebarParent.SHOPPING_ACTIVE_PREF,
@ -70,6 +65,18 @@ export class ShoppingSidebarParent extends JSWindowActorParent {
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.
*/
@ -368,13 +375,13 @@ class ShoppingSidebarManagerClass {
browserPanel.appendChild(splitter);
browserPanel.appendChild(sidebar);
} else {
actor?.updateProductURL(aLocationURI, aFlags);
actor?.updateCurrentURL(aLocationURI, aFlags);
sidebar.hidden = false;
let splitter = browserPanel.querySelector(".shopping-sidebar-splitter");
splitter.hidden = false;
}
} else if (sidebar && !sidebar.hidden) {
actor?.updateProductURL(null);
actor?.updateCurrentURL(null);
sidebar.hidden = true;
let splitter = browserPanel.querySelector(".shopping-sidebar-splitter");
splitter.hidden = true;

View file

@ -51,6 +51,8 @@ export class ShoppingContainer extends MozLitElement {
autoOpenEnabled: { type: Boolean },
autoOpenEnabledByUser: { type: Boolean },
showingKeepClosedMessage: { type: Boolean },
isProductPage: { type: Boolean },
isSupportedSite: { type: Boolean },
};
static get queries() {
@ -114,22 +116,27 @@ export class ShoppingContainer extends MozLitElement {
focusCloseButton,
autoOpenEnabled,
autoOpenEnabledByUser,
isProductPage,
isSupportedSite,
}) {
// 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
// content in the sidebar.
this.data = data;
this.showOnboarding = showOnboarding;
this.showOnboarding = showOnboarding ?? this.showOnboarding;
this.productUrl = productUrl;
this.recommendationData = recommendationData;
this.isOffline = !navigator.onLine;
this.isAnalysisInProgress = isAnalysisInProgress;
this.adsEnabled = adsEnabled;
this.adsEnabledByUser = adsEnabledByUser;
this.adsEnabled = adsEnabled ?? this.adsEnabled;
this.adsEnabledByUser = adsEnabledByUser ?? this.adsEnabledByUser;
this.analysisProgress = analysisProgress;
this.focusCloseButton = focusCloseButton;
this.autoOpenEnabled = autoOpenEnabled;
this.autoOpenEnabledByUser = autoOpenEnabledByUser;
this.autoOpenEnabled = autoOpenEnabled ?? this.autoOpenEnabled;
this.autoOpenEnabledByUser =
autoOpenEnabledByUser ?? this.autoOpenEnabledByUser;
this.isProductPage = isProductPage ?? true;
this.isSupportedSite = isSupportedSite;
}
_updateRecommendations({ recommendationData }) {
@ -200,7 +207,7 @@ export class ShoppingContainer extends MozLitElement {
hostname = new URL(this.productUrl)?.hostname;
return hostname;
} catch (e) {
console.error(`Unknown product url ${this.productUrl}.`);
console.warn(`Unknown product url ${this.productUrl}.`);
return null;
}
}
@ -464,6 +471,9 @@ export class ShoppingContainer extends MozLitElement {
}
RPMSetPref(SHOPPING_SIDEBAR_ACTIVE_PREF, false);
window.dispatchEvent(
new CustomEvent("CloseShoppingSidebar", { bubbles: true, composed: true })
);
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);
});
});
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"
);
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
* 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 SpecialPowers.pushPrefEnv({
set: [["browser.shopping.experience2023.integratedSidebar", false]],
set: [
["browser.shopping.experience2023.integratedSidebar", false],
["browser.shopping.experience2023.shoppingSidebar", true],
],
});
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
let actor =
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 () => {
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.");
}
}
await SpecialPowers.popPrefEnv();
});
/**
@ -172,7 +182,10 @@ add_task(async function test_showOnboarding_notOptedIn_integrated_sidebar() {
await Services.fog.testFlushAllChildren();
await SpecialPowers.pushPrefEnv({
set: [["browser.shopping.experience2023.integratedSidebar", true]],
set: [
["browser.shopping.experience2023.integratedSidebar", true],
["browser.shopping.experience2023.shoppingSidebar", false],
],
});
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
let actor =
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 () => {
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.");
}
}
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
let actor =
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 ContentTaskUtils.waitForCondition(
@ -281,7 +295,10 @@ add_task(async function test_hideOnboarding_onClose() {
setOnboardingPrefs({ active: false, optedIn: 0, telemetryEnabled: true });
await SpecialPowers.pushPrefEnv({
set: [["browser.shopping.experience2023.integratedSidebar", false]],
set: [
["browser.shopping.experience2023.integratedSidebar", false],
["browser.shopping.experience2023.shoppingSidebar", true],
],
});
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
let actor =
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 () => {
let shoppingContainer = await ContentTaskUtils.waitForCondition(
@ -329,6 +346,7 @@ add_task(async function test_hideOnboarding_onClose() {
Assert.greater(events.length, 0);
Assert.equal(events[0].category, "shopping");
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 => {
let actor =
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 () => {
let shoppingContainer = await ContentTaskUtils.waitForCondition(

View file

@ -205,9 +205,24 @@ export class SidebarState {
return this.#props.launcherVisible;
}
updateVisibility(visible, openedByToolbarButton = false) {
updateVisibility(
visible,
openedByToolbarButton = false,
onToolbarButtonRemoval = false
) {
switch (this.revampVisibility) {
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) {
// 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.

View file

@ -349,6 +349,8 @@ var SidebarController = {
this._handleLauncherResize(entry)
);
CustomizableUI.addListener(this);
if (this.sidebarRevampEnabled) {
if (!customElements.get("sidebar-main")) {
ChromeUtils.importESModule(
@ -455,6 +457,8 @@ var SidebarController = {
Services.obs.removeObserver(this, "tabstrip-orientation-change");
delete this._tabstripOrientationObserverAdded;
CustomizableUI.removeListener(this);
if (this._observer) {
this._observer.disconnect();
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() {
let toVerticalTabs = CustomizableUI.verticalTabsEnabled;
let tabStrip = gBrowser.tabContainer;

View file

@ -14,7 +14,10 @@ const SIDEBAR_VISIBILITY_PREF = "sidebar.visibility";
add_setup(async () => {
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 hadSavedState = !!CustomizableUI.getTestOnlyInternalProp("gSavedState");
@ -78,14 +81,9 @@ add_task(async function test_expanded_state_for_always_show() {
info(
`Current window's sidebarMain.expanded: ${window.SidebarController.sidebarMain?.expanded}`
);
await SpecialPowers.pushPrefEnv({
set: [[SIDEBAR_VISIBILITY_PREF, "always-show"]],
});
const win = await BrowserTestUtils.openNewBrowserWindow();
const { SidebarController, document } = win;
const { sidebarMain, toolbarButton } = SidebarController;
await SidebarController.promiseInitialized;
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,
"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",
"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({
set: [
["sidebar.verticalTabs", true],
["sidebar.visibility", "always-show"],
["browser.ml.chat.enabled", true],
["browser.shopping.experience2023.integratedSidebar", true],
["sidebar.main.tools", "aichat,reviewchecker,syncedtabs,history"],

View file

@ -15,7 +15,10 @@ const { NonPrivateTabs } = ChromeUtils.importESModule(
add_setup(async () => {
await SpecialPowers.pushPrefEnv({
set: [["sidebar.verticalTabs", false]],
set: [
["sidebar.verticalTabs", false],
["sidebar.visibility", "always-show"],
],
});
Services.telemetry.clearScalars();
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
*/
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(() => {
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() {
let fragment = this.doc.createDocumentFragment();
let currentGroupId;
for (let tab of this.gBrowser.tabs) {
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));
}
}
@ -121,6 +126,10 @@ class TabsListBase {
* Remove the menuitems from the DOM, cleanup internal state and listeners.
*/
_cleanup() {
this.doc
.querySelectorAll(".all-tabs-group-button")
.forEach(node => node.remove());
for (let item of this.rows) {
item.remove();
}
@ -262,6 +271,9 @@ export class TabsPanel extends TabsListBase {
this.gBrowser.removeTab(event.target.tab);
break;
}
if (event.target.classList.contains("all-tabs-group-button")) {
this.gBrowser.getTabGroupById(event.target.groupId).select();
}
// fall through
default:
super.handleEvent(event);
@ -275,7 +287,10 @@ export class TabsPanel extends TabsListBase {
// 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.
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);
let muteButton = doc.createXULElement("toolbarbutton");
@ -352,6 +371,49 @@ export class TabsPanel extends TabsListBase {
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) {
setAttributes(row, { selected: tab.selected });

View file

@ -23,6 +23,9 @@
class="subviewbutton subviewbutton-nav"
closemenu="none"
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"/>
<vbox id="allTabsMenu-dropIndicatorHolder">
<vbox id="allTabsMenu-dropIndicator" collapsed="true"/>

View file

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

View file

@ -23,13 +23,15 @@
<image class="tab-sharing-icon-overlay" role="presentation"/>
<image class="tab-icon-overlay" role="presentation"/>
</stack>
<html:moz-button type="icon ghost" size="small" class="tab-audio-button" tabindex="-1"></html:moz-button>
<vbox class="tab-label-container"
align="start"
pack="center"
flex="1">
<label class="tab-text tab-label" role="presentation"/>
<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-tooltip-label" role="presentation"/>
</hbox>
@ -83,7 +85,7 @@
".tab-content":
"pinned,selected=visuallyselected,multiselected,titlechanged,attention",
".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":
"fadein,pinned,busy,progress,selected=visuallyselected",
".tab-icon-pending":
@ -92,15 +94,13 @@
"src=image,triggeringprincipal=iconloadingprincipal,requestcontextid,fadein,pinned,selected=visuallyselected,busy,crashed,sharing,pictureinpicture",
".tab-sharing-icon-overlay": "sharing,selected=visuallyselected,pinned",
".tab-icon-overlay":
"sharing,pictureinpicture,crashed,busy,soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected,activemedia-blocked",
".tab-audio-button":
"soundplaying,soundplaying-scheduledremoval,pinned,muted,activemedia-blocked",
"sharing,pictureinpicture,crashed,busy,soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected,activemedia-blocked,indicator-replaces-favicon",
".tab-label-container":
"pinned,selected=visuallyselected,labeldirection",
".tab-label":
"text=label,accesskey,fadein,pinned,selected=visuallyselected,attention",
".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",
};
}
@ -310,18 +310,10 @@
return this.overlayIcon?.matches(":hover");
}
get _overAudioButton() {
return this.audioButton?.matches(":hover");
}
get overlayIcon() {
return this.querySelector(".tab-icon-overlay");
}
get audioButton() {
return this.querySelector(".tab-audio-button");
}
get throbber() {
return this.querySelector(".tab-throbber");
}
@ -379,6 +371,24 @@
if (event.target.classList.contains("tab-close-button")) {
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) {
return;
@ -399,6 +409,9 @@
if (event.target.classList.contains("tab-close-button")) {
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 (!this.contains(event.relatedTarget)) {
@ -438,8 +451,7 @@
this.style.MozUserFocus = "ignore";
} else if (
event.target.classList.contains("tab-close-button") ||
event.target.classList.contains("tab-icon-overlay") ||
event.target.classList.contains("tab-audio-button")
event.target.classList.contains("tab-icon-overlay")
) {
eventMaySelectTab = false;
}
@ -504,18 +516,14 @@
if (
gBrowser.multiSelectedTabsCount > 0 &&
!event.target.classList.contains("tab-close-button") &&
!event.target.classList.contains("tab-icon-overlay") &&
!event.target.classList.contains("tab-audio-button")
!event.target.classList.contains("tab-icon-overlay")
) {
// Tabs were previously multi-selected and user clicks on a tab
// without holding Ctrl/Cmd Key
gBrowser.clearMultiSelectedTabs();
}
if (
event.target.classList.contains("tab-icon-overlay") ||
event.target.classList.contains("tab-audio-button")
) {
if (event.target.classList.contains("tab-icon-overlay")) {
if (this.activeMediaBlocked) {
if (this.multiselected) {
gBrowser.resumeDelayedMediaOnMultiSelectedTabs(this);

View file

@ -2077,6 +2077,9 @@
// process so the browser can no longer be considered to be
// 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.
@ -2966,7 +2969,6 @@
* Removes the tab group. This has the effect of closing all the tabs
* in the group.
*
*
* @param {MozTabbrowserTabGroup} [group]
* The tab group to remove.
* @param {object} [options]
@ -3807,18 +3809,22 @@
tab.dispatchEvent(evt);
}
getTabsToTheStartFrom(aTab) {
/**
* @param {MozTabbrowserTab} aTab
* @returns {MozTabbrowserTab[]}
*/
_getTabsToTheStartFrom(aTab) {
let tabsToStart = [];
if (!aTab.visible) {
return tabsToStart;
}
let tabs = this.visibleTabs;
let tabs = this.openTabs;
for (let i = 0; i < tabs.length; ++i) {
if (tabs[i] == aTab) {
break;
}
// Ignore pinned tabs.
if (tabs[i].pinned) {
// Ignore pinned and hidden tabs.
if (tabs[i].pinned || tabs[i].hidden) {
continue;
}
// In a multi-select context, select all unselected tabs
@ -3831,18 +3837,22 @@
return tabsToStart;
}
getTabsToTheEndFrom(aTab) {
/**
* @param {MozTabbrowserTab} aTab
* @returns {MozTabbrowserTab[]}
*/
_getTabsToTheEndFrom(aTab) {
let tabsToEnd = [];
if (!aTab.visible) {
return tabsToEnd;
}
let tabs = this.visibleTabs;
let tabs = this.openTabs;
for (let i = tabs.length - 1; i >= 0; --i) {
if (tabs[i] == aTab) {
break;
}
// Ignore pinned tabs.
if (tabs[i].pinned) {
// Ignore pinned and hidden tabs.
if (tabs[i].pinned || tabs[i].hidden) {
continue;
}
// In a multi-select context, select all unselected tabs
@ -3980,7 +3990,7 @@
* left of the leftmost selected tab will be removed.
*/
removeTabsToTheStartFrom(aTab) {
let tabs = this.getTabsToTheStartFrom(aTab);
let tabs = this._getTabsToTheStartFrom(aTab);
if (
!this.warnAboutClosingTabs(tabs.length, this.closingTabsEnum.TO_START)
) {
@ -3995,7 +4005,7 @@
* right of the rightmost selected tab will be removed.
*/
removeTabsToTheEndFrom(aTab) {
let tabs = this.getTabsToTheEndFrom(aTab);
let tabs = this._getTabsToTheEndFrom(aTab);
if (
!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
* except aTab are removed. This behavior can be changed using the the bool
* flags below.
*
* @param aTab The tab we will skip removing
* @param aParams An optional set of parameters that will be passed to the
* removeTabs function.
* @param {boolean} [aParams.skipWarnAboutClosingTabs=false] Skip showing
* the tab close warning prompt.
* @param {boolean} [aParams.skipPinnedOrSelectedTabs=true] Skip closing
* tabs that are selected or pinned.
* @param {MozTabbrowserTab} aTab
* The tab we will skip removing
* @param {object} [aParams]
* An optional set of parameters that will be passed to the
* `removeTabs` function.
* @param {boolean} [aParams.skipWarnAboutClosingTabs=false]
* Skip showing the tab close warning prompt.
* @param {boolean} [aParams.skipPinnedOrSelectedTabs=true]
* Skip closing tabs that are selected or pinned.
*/
removeAllTabsBut(aTab, aParams = {}) {
let {
@ -4025,21 +4037,22 @@
skipPinnedOrSelectedTabs = true,
} = aParams;
/** @type {function(MozTabbrowserTab):boolean} */
let filterFn;
// If enabled also filter by selected or pinned state.
if (skipPinnedOrSelectedTabs) {
if (aTab?.multiselected) {
filterFn = tab => !tab.multiselected && !tab.pinned;
filterFn = tab => !tab.multiselected && !tab.pinned && !tab.hidden;
} else {
filterFn = tab => tab != aTab && !tab.pinned;
filterFn = tab => tab != aTab && !tab.pinned && !tab.hidden;
}
} else {
// Exclude just aTab from being removed.
filterFn = tab => tab != aTab;
}
let tabsToRemove = this.visibleTabs.filter(filterFn);
let tabsToRemove = this.openTabs.filter(filterFn);
// If enabled show the tab close warning.
if (
@ -4071,7 +4084,7 @@
/**
* @typedef {object} _startRemoveTabsReturnValue
* @property {Promise} beforeUnloadComplete
* @property {Promise<void>} beforeUnloadComplete
* A promise that is resolved once all the beforeunload handlers have been
* called.
* @property {object[]} tabsWithBeforeUnloadPrompt
@ -4114,15 +4127,36 @@
) {
// Note: if you change any of the unload algorithm, consider also
// changing `runBeforeUnloadForTabs` above.
/** @type {MozTabbrowserTab[]} */
let tabsWithBeforeUnloadPrompt = [];
/** @type {MozTabbrowserTab[]} */
let tabsWithoutBeforeUnload = [];
/** @type {Promise<void>[]} */
let beforeUnloadPromises = [];
/** @type {MozTabbrowserTab|undefined} */
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) {
if (!skipRemoves) {
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) {
lastToClose = tab;
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,
// 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
@ -4249,7 +4299,7 @@
/**
* Removes multiple tabs from the tab browser.
*
* @param {object[]} tabs
* @param {MozTabbrowserTab[]} tabs
* The set of tabs to remove.
* @param {object} [options]
* @param {boolean} [options.animate]
@ -6418,13 +6468,8 @@
event.stopPropagation();
let tab = event.target.triggerNode?.closest("tab");
if (!tab) {
if (event.target.triggerNode?.getRootNode()?.host?.closest("tab")) {
// Check if triggerNode is within shadowRoot of moz-button
tab = event.target.triggerNode?.getRootNode().host.closest("tab");
} else {
event.preventDefault();
return;
}
event.preventDefault();
return;
}
const tooltip = event.target;
@ -6433,7 +6478,7 @@
const tabCount = this.selectedTabs.includes(tab)
? this.selectedTabs.length
: 1;
if (tab._overPlayingIcon || tab._overAudioButton) {
if (tab._overPlayingIcon) {
let l10nId;
const l10nArgs = { tabCount };
if (tab.selected) {
@ -7047,6 +7092,7 @@
// process so the browser can no longer be considered to be
// crashed.
tab.removeAttribute("crashed");
gBrowser.tabContainer.updateTabIndicatorAttr(tab);
}
if (this.isFindBarInitialized(tab)) {
@ -7371,6 +7417,7 @@
delete this.mBrowser.initialPageLoadedFromUserAction;
// If the browser is loading it must not be crashed anymore
this.mTab.removeAttribute("crashed");
gBrowser.tabContainer.updateTabIndicatorAttr(this.mTab);
}
if (this._shouldShowProgress(aRequest)) {
@ -8419,17 +8466,21 @@ var TabContextMenu = {
// Disable "Close Tabs to the Left/Right" if there are no tabs
// preceding/following it.
let noTabsToStart = !gBrowser.getTabsToTheStartFrom(this.contextTab).length;
let noTabsToStart = !gBrowser._getTabsToTheStartFrom(this.contextTab)
.length;
closeTabsToTheStartItem.disabled = noTabsToStart;
let noTabsToEnd = !gBrowser.getTabsToTheEndFrom(this.contextTab).length;
let noTabsToEnd = !gBrowser._getTabsToTheEndFrom(this.contextTab).length;
closeTabsToTheEndItem.disabled = noTabsToEnd;
// Disable "Close other Tabs" if there are no unpinned tabs.
let unpinnedTabsToClose = multiselectionContext
? gBrowser.visibleTabs.filter(t => !t.multiselected && !t.pinned).length
: gBrowser.visibleTabs.filter(t => t != this.contextTab && !t.pinned)
.length;
? gBrowser.openTabs.filter(
t => !t.multiselected && !t.pinned && !t.hidden
).length
: gBrowser.openTabs.filter(
t => t != this.contextTab && !t.pinned && !t.hidden
).length;
let closeOtherTabsItem = document.getElementById("context_closeOtherTabs");
closeOtherTabsItem.disabled = unpinnedTabsToClose < 1;

View file

@ -15,9 +15,13 @@
<html:slot/>
`;
/** @type {string} */
#label;
/** @type {MozTextLabel} */
#labelElement;
/** @type {string} */
#colorCode;
constructor() {
@ -44,8 +48,8 @@
this.#labelElement = this.querySelector(".tab-group-label");
this.#labelElement.addEventListener("click", this);
this.#updateLabelAriaAttributes(this.label);
this.#updateCollapsedAriaAttributes(this.collapsed);
this.#updateLabelAriaAttributes();
this.#updateCollapsedAriaAttributes();
this.createdDate = Date.now();
@ -121,12 +125,26 @@
}
get label() {
return this.getAttribute("label");
return this.#label;
}
set label(val) {
this.setAttribute("label", val);
this.#updateLabelAriaAttributes(val);
this.#label = 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() {
@ -138,26 +156,20 @@
return;
}
this.toggleAttribute("collapsed", val);
this.#updateCollapsedAriaAttributes(val);
this.#updateCollapsedAriaAttributes();
const eventName = val ? "TabGroupCollapse" : "TabGroupExpand";
this.dispatchEvent(new CustomEvent(eventName, { bubbles: true }));
}
/**
* @param {string} label
*/
#updateLabelAriaAttributes(label) {
const ariaLabel = label == "" ? "unnamed" : label;
#updateLabelAriaAttributes() {
const ariaLabel = this.#label || "unnamed";
const ariaDescription = `${ariaLabel} tab group`;
this.#labelElement?.setAttribute("aria-label", ariaLabel);
this.#labelElement?.setAttribute("aria-description", ariaDescription);
}
/**
* @param {boolean} collapsed
*/
#updateCollapsedAriaAttributes(collapsed) {
const ariaExpanded = collapsed ? "false" : "true";
#updateCollapsedAriaAttributes() {
const ariaExpanded = this.collapsed ? "false" : "true";
this.#labelElement?.setAttribute("aria-expanded", ariaExpanded);
}
@ -221,6 +233,19 @@
on_TabSelect() {
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);

View file

@ -55,6 +55,8 @@
this.addEventListener("TabAttrModified", this);
this.addEventListener("TabHide", this);
this.addEventListener("TabShow", this);
this.addEventListener("TabPinned", this);
this.addEventListener("TabUnpinned", this);
this.addEventListener("TabHoverStart", this);
this.addEventListener("TabHoverEnd", this);
this.addEventListener("TabGroupExpand", this);
@ -220,6 +222,17 @@
this.#updateTabMinWidth();
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);
}
@ -232,6 +245,14 @@
}
on_TabAttrModified(event) {
if (
["soundplaying", "muted", "activemedia-blocked", "sharing"].some(attr =>
event.detail.changed.includes(attr)
)
) {
this.updateTabIndicatorAttr(event.target);
}
if (
event.detail.changed.includes("soundplaying") &&
!event.target.visible
@ -252,6 +273,14 @@
}
}
on_TabPinned(event) {
this.updateTabIndicatorAttr(event.target);
}
on_TabUnpinned(event) {
this.updateTabIndicatorAttr(event.target);
}
on_TabHoverStart(event) {
if (!this._showCardPreviews) {
return;
@ -3027,6 +3056,24 @@
}
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, {

View file

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

View file

@ -50,7 +50,8 @@ add_task(async function mute_web_audio() {
info("- mute browser -");
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");
info("- stop web audip -");
@ -61,7 +62,8 @@ add_task(async function mute_web_audio() {
info("- unmute browser -");
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");
info("- tab should be audible -");

View file

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

View file

@ -13,13 +13,6 @@ add_task(async function removeTabsToTheStart() {
let lastTab = await addTab();
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
gBrowser.removeTabsToTheStartFrom(lastTab);

View file

@ -590,6 +590,33 @@ add_task(async function test_moveTabBetweenGroups() {
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
// ---

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, {
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({
set: [["browser.aboutwelcome.showModal", true]],
set: PREFS_TO_SET,
});
BrowserHandler.firstRunProfile = true;
await BROWSER_GLUE._maybeShowDefaultBrowserPrompt();
const data = [
{
id: "TEST_SCREEN",
content: {
position: "split",
logo: {},
title: "test",
},
},
];
return {
data,
async cleanup() {
await SpecialPowers.popPrefEnv();
BrowserHandler.firstRunProfile = false;
},
};
registerCleanupFunction(async () => {
PREFS_TO_SET.forEach(pref => Services.prefs.clearUserPref(pref[0]));
BrowserHandler.firstRunProfile = false;
});
}
add_task(async function show_about_welcome_modal() {
const { data } = await showAboutWelcomeModal();
await SpecialPowers.pushPrefEnv({
set: [["browser.aboutwelcome.screens", JSON.stringify(data)]],
});
BROWSER_GLUE._maybeShowDefaultBrowserPrompt();
let messageSpy = sinon.spy(SpecialMessageActions, "handleAction");
await showAboutWelcomeModal(JSON.stringify(TEST_SCREEN));
const [win] = await TestUtils.topicObserved("subdialog-loaded");
const modal = win.document.querySelector(".onboardingContainer");
ok(!!modal, "About Welcome modal shown");
win.close();
Assert.notEqual(
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) {
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 results = [];
let i = 0;
for (let group of gBrowser.getAllTabGroups()) {
for (let group of window.gBrowser.getAllTabGroups()) {
if (group.label.toLowerCase().startsWith(input)) {
results.push(
this.#makeResult({

View file

@ -6,6 +6,7 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
SearchUIUtils: "resource:///modules/SearchUIUtils.sys.mjs",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs",
@ -122,6 +123,8 @@ export class SearchModeSwitcher {
position: "bottomleft topleft",
triggerEvent: event,
}).catch(console.error);
Glean.urlbarUnifiedsearchbutton.opened.add(1);
}
#openPreferences(event) {
@ -139,6 +142,8 @@ export class SearchModeSwitcher {
this.#input.window.openPreferences("paneSearch");
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`.
*/
onPrefChanged(pref) {
if (!this.#input.window || this.#input.window.closed) {
return;
}
switch (pref) {
case "scotchBonnet.enableOverride": {
if (lazy.UrlbarPrefs.get("scotchBonnet.enableOverride")) {
@ -242,6 +251,10 @@ export class SearchModeSwitcher {
}
async #updateSearchIcon() {
if (!this.#input.window || this.#input.window.closed) {
return;
}
try {
await lazy.UrlbarSearchUtils.init();
} catch {
@ -310,7 +323,9 @@ export class SearchModeSwitcher {
if (!searchMode || searchMode.engineName) {
let engine = searchMode
? 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;
return { label: engine.name, icon };
}
@ -431,6 +446,18 @@ export class SearchModeSwitcher {
}
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() {
@ -519,7 +546,10 @@ export class SearchModeSwitcher {
let observer = engineObj => {
Services.obs.removeObserver(observer, topic);
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);

View file

@ -368,7 +368,7 @@ export var UrlbarUtils = {
let dataStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
Ci.nsIStringInputStream
);
dataStream.data = postDataString;
dataStream.setByteStringData(postDataString);
let mimeStream = Cc[
"@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
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.*
~~~~~~~~

View file

@ -2191,3 +2191,39 @@ urlbar.quickaction:
- fx-search-telemetry@mozilla.com
expires: never
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_telemetry.js"]
["browser_searchMode_alias_replacement.js"]
support-files = [
"searchSuggestionEngine.xml",
@ -701,6 +703,9 @@ tags = "search-telemetry"
["browser_urlbar_telemetry_persisted.js"]
tags = "search-telemetry"
skip-if = [
"os == 'linux' && os_version == '18.04' && processor == 'x86_64'",
] # bug 1934362
["browser_urlbar_telemetry_places.js"]
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() {
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);
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 searchModeSwitcherButton = newWin.document.getElementById(
"searchmode-switcher-icon"
);
let regex = /url\("([^"]+)"\)/;
let searchModeSwitcherIconUrl = newWin
.getComputedStyle(searchModeSwitcherButton)
.listStyleImage.match(regex);
const searchGlassIconUrl = UrlbarUtils.ICON.SEARCH_GLASS;
Assert.equal(
searchModeSwitcherIconUrl[1],
getSeachModeSwitcherIcon(newWin),
searchGlassIconUrl,
"The search mode switcher should have the search glass icon url since \
we are not in search mode."
@ -284,12 +275,8 @@ add_task(async function test_search_icon_change() {
.getEngineByName(engineName)
.getIconURL();
searchModeSwitcherIconUrl = newWin
.getComputedStyle(searchModeSwitcherButton)
.listStyleImage.match(regex);
Assert.equal(
searchModeSwitcherIconUrl[1],
getSeachModeSwitcherIcon(newWin),
bingSearchEngineIconUrl,
"The search mode switcher should have the bing icon url since we are in \
search mode"
@ -304,16 +291,13 @@ add_task(async function test_search_icon_change() {
newWin.document.querySelector("#searchmode-switcher-close").click();
await UrlbarTestUtils.assertSearchMode(newWin, null);
searchModeSwitcherIconUrl = await BrowserTestUtils.waitForCondition(
() =>
newWin
.getComputedStyle(searchModeSwitcherButton)
.listStyleImage.match(regex),
let searchModeSwitcherIconUrl = await BrowserTestUtils.waitForCondition(
() => getSeachModeSwitcherIcon(newWin),
"Waiting for the search mode switcher icon to update after exiting search mode."
);
Assert.equal(
searchModeSwitcherIconUrl[1],
searchModeSwitcherIconUrl,
searchGlassIconUrl,
"The search mode switcher should have the search glass icon url since \
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() {
await SearchTestUtils.installSearchExtension(
let searchExtension = await SearchTestUtils.installSearchExtension(
{
name: "MozSearch",
search_url: "https://example.com/",
favicon_url: "https://example.com/favicon.ico",
},
{ setAsDefault: true }
{ setAsDefault: true, skipUnload: true }
);
const TEST_DATA = [
@ -466,6 +450,7 @@ add_task(async function open_engine_page_directly() {
await PlacesUtils.history.clear();
await BrowserTestUtils.closeWindow(newWin);
}
await searchExtension.unload();
});
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]],
});
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(
() =>
newWin
.getComputedStyle(searchModeSwitcherButton)
.listStyleImage.match(regex),
() => getSeachModeSwitcherIcon(newWin),
"Waiting for the search mode switcher icon to update after exiting search mode."
);
Assert.equal(
searchModeSwitcherIconUrl[1],
searchGlassIconUrl,
searchModeSwitcherIconUrl,
UrlbarUtils.ICON.SEARCH_GLASS,
"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");
await BrowserTestUtils.closeWindow(newWin);
await SpecialPowers.popPrefEnv();
});
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 }
);
let searchModeSwitcherButton = window.document.getElementById(
"searchmode-switcher-icon"
);
let popup = await UrlbarTestUtils.openSearchModeSwitcher(window);
let popupHidden = UrlbarTestUtils.searchModeSwitcherPopupClosed(window);
popup.querySelector(`toolbarbutton[label=${testEngineName}]`).click();
await popupHidden;
let regex = /url\("([^"]+)"\)/;
let searchModeSwitcherIconUrl =
searchModeSwitcherButton.style.listStyleImage.match(regex);
const searchGlassIconUrl = UrlbarUtils.ICON.SEARCH_GLASS;
Assert.equal(
searchModeSwitcherIconUrl[1],
searchGlassIconUrl,
getSeachModeSwitcherIcon(window),
UrlbarUtils.ICON.SEARCH_GLASS,
"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();
});
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.
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.
onboarding-new-vertical-tabs-label = Tabs on the side

View file

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

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