Update On Thu Dec 19 19:53:27 CET 2024
This commit is contained in:
parent
8f87d3e70c
commit
158ed24d8e
1108 changed files with 96120 additions and 5853 deletions
215
Cargo.lock
generated
215
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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() &&
|
||||
|
|
|
@ -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++) {
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -1750,6 +1750,7 @@ void DocAccessible::DoInitialUpdate() {
|
|||
for (auto idx = 0U; idx < mChildren.Length(); idx++) {
|
||||
ipcDoc->InsertIntoIpcTree(mChildren.ElementAt(idx), true);
|
||||
}
|
||||
ipcDoc->SendQueuedMutationEvents();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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"));
|
||||
|
|
613
browser/components/aboutwelcome/package-lock.json
generated
613
browser/components/aboutwelcome/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
})();
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
760
browser/components/asrouter/package-lock.json
generated
760
browser/components/asrouter/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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.");
|
||||
});
|
|
@ -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 }
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}`}
|
||||
>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
.impression-observer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
33
browser/components/profiles/metrics.yaml
Normal file
33
browser/components/profiles/metrics.yaml
Normal 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
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
);
|
|
@ -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();
|
||||
}
|
||||
);
|
|
@ -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();
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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" });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"));
|
||||
|
|
132
browser/components/tabbrowser/GroupsList.sys.mjs
Normal file
132
browser/components/tabbrowser/GroupsList.sys.mjs
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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 });
|
||||
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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", () => {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 -");
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
// ---
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.*
|
||||
~~~~~~~~
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -357,6 +357,18 @@ onboarding-new-tabs-title = Tell us where you’d 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
|
||||
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue