Update On Sun Apr 27 20:21:31 CEST 2025
65
Cargo.lock
generated
|
@ -916,8 +916,6 @@ version = "0.12.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"termcolor",
|
||||
"unicode-width 0.2.0",
|
||||
]
|
||||
|
||||
|
@ -1854,7 +1852,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "error-support"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
|
||||
dependencies = [
|
||||
"error-support-macros",
|
||||
"lazy_static",
|
||||
|
@ -1866,7 +1864,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "error-support-macros"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -1983,7 +1981,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "firefox-versioning"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"thiserror 1.999.999",
|
||||
|
@ -3320,7 +3318,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "interrupt-support"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"parking_lot",
|
||||
|
@ -3452,6 +3450,7 @@ dependencies = [
|
|||
"firefox-on-glean",
|
||||
"log",
|
||||
"mozbuild",
|
||||
"nserror",
|
||||
"nsstring",
|
||||
"once_cell",
|
||||
"serde",
|
||||
|
@ -4587,11 +4586,12 @@ checksum = "a2983372caf4480544083767bf2d27defafe32af49ab4df3a0b7fc90793a3664"
|
|||
[[package]]
|
||||
name = "naga"
|
||||
version = "25.0.0"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=f1c496523ff0aa10c162fd01ad606960e925a5a4#f1c496523ff0aa10c162fd01ad606960e925a5a4"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=35f131ff10153e48b790e1f156c864734063ce71#35f131ff10153e48b790e1f156c864734063ce71"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bit-set",
|
||||
"bitflags 2.9.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"codespan-reporting",
|
||||
"half 2.5.0",
|
||||
|
@ -4604,7 +4604,7 @@ dependencies = [
|
|||
"rustc-hash 1.999.999",
|
||||
"serde",
|
||||
"spirv",
|
||||
"strum 0.26.999",
|
||||
"strum 0.27.1",
|
||||
"thiserror 2.0.9",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
@ -5035,7 +5035,7 @@ checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba"
|
|||
[[package]]
|
||||
name = "payload-support"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
|
@ -5474,9 +5474,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "raw-window-handle"
|
||||
version = "0.6.0"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42a9830a0e1b9fb145ebb365b8bc4ccd75f290f98c0247deafbbe2c75cefb544"
|
||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
|
@ -5538,7 +5538,7 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
|
|||
[[package]]
|
||||
name = "relevancy"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.21.999",
|
||||
|
@ -5563,7 +5563,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "remote_settings"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"camino",
|
||||
|
@ -5692,13 +5692,6 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ron"
|
||||
version = "0.9.999"
|
||||
dependencies = [
|
||||
"ron 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ron"
|
||||
version = "0.10.1"
|
||||
|
@ -5911,7 +5904,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "search"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
|
||||
dependencies = [
|
||||
"error-support",
|
||||
"firefox-versioning",
|
||||
|
@ -6202,7 +6195,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "sql-support"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
|
||||
dependencies = [
|
||||
"interrupt-support",
|
||||
"lazy_static",
|
||||
|
@ -6408,7 +6401,7 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
|
|||
[[package]]
|
||||
name = "suggest"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
|
@ -6460,7 +6453,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "sync-guid"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
|
||||
dependencies = [
|
||||
"base64 0.21.999",
|
||||
"rand",
|
||||
|
@ -6471,7 +6464,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "sync15"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"error-support",
|
||||
|
@ -6511,7 +6504,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "tabs"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"error-support",
|
||||
|
@ -6855,7 +6848,7 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
|
|||
[[package]]
|
||||
name = "types"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
|
||||
dependencies = [
|
||||
"rusqlite 0.33.0",
|
||||
"serde",
|
||||
|
@ -7237,7 +7230,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
|||
[[package]]
|
||||
name = "viaduct"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
|
||||
dependencies = [
|
||||
"ffi-support",
|
||||
"log",
|
||||
|
@ -7407,7 +7400,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "webext-storage"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8#41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8"
|
||||
source = "git+https://github.com/mozilla/application-services?rev=280db3a3a06a8f517151ff0b84b5ce67fcc7afd6#280db3a3a06a8f517151ff0b84b5ce67fcc7afd6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"error-support",
|
||||
|
@ -7452,7 +7445,7 @@ dependencies = [
|
|||
"peek-poke",
|
||||
"plane-split",
|
||||
"rayon",
|
||||
"ron 0.10.1",
|
||||
"ron",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"svg_fmt",
|
||||
|
@ -7546,7 +7539,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "wgpu-core"
|
||||
version = "25.0.0"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=f1c496523ff0aa10c162fd01ad606960e925a5a4#f1c496523ff0aa10c162fd01ad606960e925a5a4"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=35f131ff10153e48b790e1f156c864734063ce71#35f131ff10153e48b790e1f156c864734063ce71"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bit-set",
|
||||
|
@ -7562,7 +7555,7 @@ dependencies = [
|
|||
"once_cell",
|
||||
"parking_lot",
|
||||
"profiling",
|
||||
"ron 0.9.999",
|
||||
"ron",
|
||||
"rustc-hash 1.999.999",
|
||||
"serde",
|
||||
"smallvec",
|
||||
|
@ -7576,7 +7569,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "wgpu-core-deps-apple"
|
||||
version = "25.0.0"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=f1c496523ff0aa10c162fd01ad606960e925a5a4#f1c496523ff0aa10c162fd01ad606960e925a5a4"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=35f131ff10153e48b790e1f156c864734063ce71#35f131ff10153e48b790e1f156c864734063ce71"
|
||||
dependencies = [
|
||||
"wgpu-hal",
|
||||
]
|
||||
|
@ -7584,7 +7577,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "wgpu-core-deps-windows-linux-android"
|
||||
version = "25.0.0"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=f1c496523ff0aa10c162fd01ad606960e925a5a4#f1c496523ff0aa10c162fd01ad606960e925a5a4"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=35f131ff10153e48b790e1f156c864734063ce71#35f131ff10153e48b790e1f156c864734063ce71"
|
||||
dependencies = [
|
||||
"wgpu-hal",
|
||||
]
|
||||
|
@ -7592,7 +7585,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "wgpu-hal"
|
||||
version = "25.0.0"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=f1c496523ff0aa10c162fd01ad606960e925a5a4#f1c496523ff0aa10c162fd01ad606960e925a5a4"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=35f131ff10153e48b790e1f156c864734063ce71#35f131ff10153e48b790e1f156c864734063ce71"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"arrayvec",
|
||||
|
@ -7628,7 +7621,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "wgpu-types"
|
||||
version = "25.0.0"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=f1c496523ff0aa10c162fd01ad606960e925a5a4#f1c496523ff0aa10c162fd01ad606960e925a5a4"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=35f131ff10153e48b790e1f156c864734063ce71#35f131ff10153e48b790e1f156c864734063ce71"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"bytemuck",
|
||||
|
|
21
Cargo.toml
|
@ -214,9 +214,6 @@ half = { path = "build/rust/half" }
|
|||
# Upgrade `rusqlite` 0.31 to 0.33.
|
||||
rusqlite = { path = "build/rust/rusqlite" }
|
||||
|
||||
# Patch `ron` 0.9.* to 0.10.
|
||||
ron = { path = "build/rust/ron" }
|
||||
|
||||
# Patch `strum` 0.26.* to 0.27.
|
||||
strum = { path = "build/rust/strum" }
|
||||
|
||||
|
@ -263,14 +260,14 @@ malloc_size_of_derive = { path = "xpcom/rust/malloc_size_of_derive" }
|
|||
objc = { git = "https://github.com/glandium/rust-objc", rev = "4de89f5aa9851ceca4d40e7ac1e2759410c04324" }
|
||||
|
||||
# application-services overrides to make updating them all simpler.
|
||||
interrupt-support = { git = "https://github.com/mozilla/application-services", rev = "41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8" }
|
||||
relevancy = { git = "https://github.com/mozilla/application-services", rev = "41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8" }
|
||||
search = { git = "https://github.com/mozilla/application-services", rev = "41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8" }
|
||||
sql-support = { git = "https://github.com/mozilla/application-services", rev = "41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8" }
|
||||
suggest = { git = "https://github.com/mozilla/application-services", rev = "41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8" }
|
||||
sync15 = { git = "https://github.com/mozilla/application-services", rev = "41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8" }
|
||||
tabs = { git = "https://github.com/mozilla/application-services", rev = "41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8" }
|
||||
viaduct = { git = "https://github.com/mozilla/application-services", rev = "41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8" }
|
||||
webext-storage = { git = "https://github.com/mozilla/application-services", rev = "41e0b8f5679977ff2f551eb60fa9b3288ce1f2d8" }
|
||||
interrupt-support = { git = "https://github.com/mozilla/application-services", rev = "280db3a3a06a8f517151ff0b84b5ce67fcc7afd6" }
|
||||
relevancy = { git = "https://github.com/mozilla/application-services", rev = "280db3a3a06a8f517151ff0b84b5ce67fcc7afd6" }
|
||||
search = { git = "https://github.com/mozilla/application-services", rev = "280db3a3a06a8f517151ff0b84b5ce67fcc7afd6" }
|
||||
sql-support = { git = "https://github.com/mozilla/application-services", rev = "280db3a3a06a8f517151ff0b84b5ce67fcc7afd6" }
|
||||
suggest = { git = "https://github.com/mozilla/application-services", rev = "280db3a3a06a8f517151ff0b84b5ce67fcc7afd6" }
|
||||
sync15 = { git = "https://github.com/mozilla/application-services", rev = "280db3a3a06a8f517151ff0b84b5ce67fcc7afd6" }
|
||||
tabs = { git = "https://github.com/mozilla/application-services", rev = "280db3a3a06a8f517151ff0b84b5ce67fcc7afd6" }
|
||||
viaduct = { git = "https://github.com/mozilla/application-services", rev = "280db3a3a06a8f517151ff0b84b5ce67fcc7afd6" }
|
||||
webext-storage = { git = "https://github.com/mozilla/application-services", rev = "280db3a3a06a8f517151ff0b84b5ce67fcc7afd6" }
|
||||
|
||||
allocator-api2 = { path = "third_party/rust/allocator-api2" }
|
||||
|
|
|
@ -63,9 +63,7 @@ class Settings final
|
|||
SessionAccessibility::SessionAccessibility(
|
||||
jni::NativeWeakPtr<widget::GeckoViewSupport> aWindow,
|
||||
java::SessionAccessibility::NativeProvider::Param aSessionAccessibility)
|
||||
: mWindow(aWindow), mSessionAccessibility(aSessionAccessibility) {
|
||||
SetAttached(true, nullptr);
|
||||
}
|
||||
: mWindow(aWindow), mSessionAccessibility(aSessionAccessibility) {}
|
||||
|
||||
void SessionAccessibility::SetAttached(bool aAttached,
|
||||
already_AddRefed<Runnable> aRunnable) {
|
||||
|
|
|
@ -1612,6 +1612,22 @@ bool aria::IsValidARIAHidden(nsIContent* aContent) {
|
|||
!ShouldIgnoreARIAHidden(aContent);
|
||||
}
|
||||
|
||||
bool aria::IsValidARIAHidden(DocAccessible* aDocAcc) {
|
||||
nsCOMPtr<nsIContent> docContent = aDocAcc->GetContent();
|
||||
// First, check if our Doc Accessible has aria-hidden set on its content
|
||||
bool isValid = IsValidARIAHidden(docContent);
|
||||
|
||||
// If our Doc Accessible was created using an element other than the
|
||||
// root element, we need to verify the validity of any aria-hidden on
|
||||
// the root element as well.
|
||||
auto* rootElement = aDocAcc->DocumentNode()->GetRootElement();
|
||||
if (docContent != rootElement) {
|
||||
isValid |= IsValidARIAHidden(rootElement);
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
bool aria::ShouldIgnoreARIAHidden(nsIContent* aContent) {
|
||||
if (!aContent) {
|
||||
return false;
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
#include "ARIAStateMap.h"
|
||||
#include "mozilla/a11y/AccTypes.h"
|
||||
#include "mozilla/a11y/DocAccessible.h"
|
||||
#include "mozilla/a11y/Role.h"
|
||||
|
||||
#include "nsAtom.h"
|
||||
|
@ -310,6 +311,14 @@ uint8_t AttrCharacteristicsFor(nsAtom* aAtom);
|
|||
*/
|
||||
bool IsValidARIAHidden(nsIContent* aContent);
|
||||
|
||||
/**
|
||||
* This function calls into the function above. It verifies the validity
|
||||
* of any `aria-hidden` specified on the given Doc Accessible's
|
||||
* mContent, as well as on the root element of mContent's owner
|
||||
* doc.
|
||||
*/
|
||||
bool IsValidARIAHidden(DocAccessible* aDocAcc);
|
||||
|
||||
/**
|
||||
* Return true if the element should render its subtree
|
||||
* regardless of the presence of aria-hidden.
|
||||
|
|
|
@ -38,6 +38,26 @@ bool EventQueue::PushEvent(AccEvent* aEvent) {
|
|||
return true;
|
||||
}
|
||||
|
||||
if (aEvent->mEventRule == AccEvent::eRemoveDupes && !mEvents.IsEmpty()) {
|
||||
// Check for duplicate events. If aEvent is identical to an older event, do
|
||||
// not append aEvent. We do this here rather than in CoalesceEvents because
|
||||
// CoalesceEvents never *removes* events; it only sets them to eDoNotEmit.
|
||||
// If there are many duplicate events and we appended them, this would
|
||||
// result in a massive event queue and coalescing would become increasingly
|
||||
// slow with each event queued. Doing it here, we avoid appending a
|
||||
// duplicate event in the first place.
|
||||
uint32_t last = mEvents.Length() - 1;
|
||||
for (uint32_t index = last; index <= last; --index) {
|
||||
AccEvent* checkEvent = mEvents[index];
|
||||
if (checkEvent->mEventType == aEvent->mEventType &&
|
||||
checkEvent->mEventRule == aEvent->mEventRule &&
|
||||
checkEvent->mAccessible == aEvent->mAccessible) {
|
||||
aEvent->mEventRule = AccEvent::eDoNotEmit;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// XXX(Bug 1631371) Check if this should use a fallible operation as it
|
||||
// pretended earlier, or change the return type to void.
|
||||
mEvents.AppendElement(aEvent);
|
||||
|
@ -45,10 +65,10 @@ bool EventQueue::PushEvent(AccEvent* aEvent) {
|
|||
// Filter events.
|
||||
CoalesceEvents();
|
||||
|
||||
if (aEvent->mEventRule != AccEvent::eDoNotEmit &&
|
||||
(aEvent->mEventType == nsIAccessibleEvent::EVENT_NAME_CHANGE ||
|
||||
aEvent->mEventType == nsIAccessibleEvent::EVENT_TEXT_REMOVED ||
|
||||
aEvent->mEventType == nsIAccessibleEvent::EVENT_TEXT_INSERTED)) {
|
||||
if (aEvent->mEventType == nsIAccessibleEvent::EVENT_NAME_CHANGE ||
|
||||
aEvent->mEventType == nsIAccessibleEvent::EVENT_TEXT_REMOVED ||
|
||||
aEvent->mEventType == nsIAccessibleEvent::EVENT_TEXT_INSERTED) {
|
||||
MOZ_ASSERT(aEvent->mEventRule != AccEvent::eDoNotEmit);
|
||||
PushNameOrDescriptionChange(aEvent);
|
||||
}
|
||||
return true;
|
||||
|
@ -258,23 +278,9 @@ void EventQueue::CoalesceEvents() {
|
|||
break; // eCoalesceTextSelChange
|
||||
}
|
||||
|
||||
case AccEvent::eRemoveDupes: {
|
||||
// Check for repeat events, coalesce newly appended event by more older
|
||||
// event.
|
||||
for (uint32_t index = tail - 1; index < tail; index--) {
|
||||
AccEvent* accEvent = mEvents[index];
|
||||
if (accEvent->mEventType == tailEvent->mEventType &&
|
||||
accEvent->mEventRule == tailEvent->mEventRule &&
|
||||
accEvent->mAccessible == tailEvent->mAccessible) {
|
||||
tailEvent->mEventRule = AccEvent::eDoNotEmit;
|
||||
return;
|
||||
}
|
||||
}
|
||||
break; // case eRemoveDupes
|
||||
}
|
||||
|
||||
default:
|
||||
break; // case eAllowDupes, eDoNotEmit
|
||||
// eRemoveDupes is handled in PushEvent.
|
||||
break; // case eRemoveDupes, eAllowDupes, eDoNotEmit
|
||||
} // switch
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
#include "TreeWalker.h"
|
||||
|
||||
#include "ARIAMap.h"
|
||||
#include "nsAccessibilityService.h"
|
||||
#include "DocAccessible.h"
|
||||
|
||||
|
@ -323,7 +324,8 @@ LocalAccessible* TreeWalker::AccessibleFor(nsIContent* aNode, uint32_t aFlags,
|
|||
}
|
||||
|
||||
// Create an accessible if allowed.
|
||||
if (!(aFlags & eWalkCache) && mContext->IsAcceptableChild(aNode)) {
|
||||
if (!(aFlags & eWalkCache) && mContext->IsAcceptableChild(aNode) &&
|
||||
!aria::IsValidARIAHidden(mDoc)) {
|
||||
mDoc->RelocateARIAOwnedIfNeeded(aNode);
|
||||
return GetAccService()->CreateAccessible(aNode, mContext, aSkipSubtree);
|
||||
}
|
||||
|
|
|
@ -49,3 +49,39 @@ addAccessibleTask(
|
|||
},
|
||||
{ chrome: true, topLevel: false }
|
||||
);
|
||||
|
||||
/**
|
||||
* Test that switching from a non-remote document to a remote document fires
|
||||
* focus correctly.
|
||||
*/
|
||||
addAccessibleTask(
|
||||
``,
|
||||
async function testLocalThenRemoteWithAutofocus(browser) {
|
||||
info("Loading example.com with autofocus into same tab");
|
||||
// The accessibility test harness removes maychangeremoteness when we run a
|
||||
// chrome test, but we explicitly want to change remoteness now.
|
||||
browser.setAttribute("maychangeremoteness", "true");
|
||||
const url =
|
||||
"https://example.com/document-builder.sjs?html=" +
|
||||
encodeURIComponent(`<!doctype html><input autofocus>`);
|
||||
let loaded = BrowserTestUtils.browserLoaded(browser);
|
||||
browser.loadURI(Services.io.newURI(url), {
|
||||
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
});
|
||||
await loaded;
|
||||
is(Services.focus.focusedElement, browser, "<browser> should be focused");
|
||||
is(
|
||||
Services.focus.focusedWindow,
|
||||
window,
|
||||
"window should be properly focused"
|
||||
);
|
||||
await SpecialPowers.spawn(browser, [], async () => {
|
||||
is(
|
||||
content.windowUtils.IMEStatus,
|
||||
content.windowUtils.IME_STATUS_ENABLED,
|
||||
"IME should be enabled"
|
||||
);
|
||||
});
|
||||
},
|
||||
{ chrome: true, topLevel: false }
|
||||
);
|
||||
|
|
|
@ -30,6 +30,10 @@ skip-if = [
|
|||
|
||||
["browser_test_aria_hidden.js"]
|
||||
|
||||
["browser_test_aria_hidden_iframe.js"]
|
||||
|
||||
["browser_test_aria_hidden_svg.js"]
|
||||
|
||||
["browser_searchbar.js"]
|
||||
|
||||
["browser_select.js"]
|
||||
|
|
|
@ -7,11 +7,37 @@
|
|||
loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
|
||||
|
||||
/**
|
||||
* Verify loading a root doc with aria-hidden renders the document.
|
||||
* Non-root doc elements, like embedded iframes, should continue
|
||||
* Verify loading a tab document with aria-hidden specified on the root element
|
||||
* correctly renders the root element and its content. This test is meaninfully
|
||||
* different from testTabDocument which tests aria-hidden specified on the
|
||||
* body element.
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`<html aria-hidden="true"><u>hello world`,
|
||||
async function testTabRootDocument(_, accDoc) {
|
||||
const tree = {
|
||||
DOCUMENT: [
|
||||
{
|
||||
TEXT_LEAF: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
testAccessibleTree(accDoc, tree);
|
||||
},
|
||||
{
|
||||
chrome: true,
|
||||
topLevel: true,
|
||||
iframe: false,
|
||||
remoteIframe: false,
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Verify loading a tab doc with aria-hidden on the body renders the document.
|
||||
* Body elements inside of embedded iframes, should continue
|
||||
* to respect aria-hidden when present. This test ONLY tests
|
||||
* tab documents, it should not run in iframes. There is a separate
|
||||
* test for iframes below.
|
||||
* test for iframes in browser_test_aria_hidden_iframe.js.
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`
|
||||
|
@ -35,7 +61,7 @@ addAccessibleTask(
|
|||
* Non-root doc elements, like embedded iframes, should continue
|
||||
* to respect aria-hidden when applied. This test ONLY tests
|
||||
* tab documents, it should not run in iframes. There is a separate
|
||||
* test for iframes below.
|
||||
* test for iframes in browser_test_aria_hidden_iframe.js.
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`
|
||||
|
@ -56,274 +82,3 @@ addAccessibleTask(
|
|||
},
|
||||
{ chrome: true, topLevel: true, iframe: false, remoteIframe: false }
|
||||
);
|
||||
|
||||
/**
|
||||
* Verify loading an iframe doc with aria-hidden doesn't render the document.
|
||||
* This test ONLY tests iframe documents, it should not run in tab docs.
|
||||
* There is a separate test for tab docs above.
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`
|
||||
<p id="content">I am some content in a document</p>
|
||||
`,
|
||||
async function testIframeDocument(browser, docAcc, topLevel) {
|
||||
const originalTree = { DOCUMENT: [{ INTERNAL_FRAME: [{ DOCUMENT: [] }] }] };
|
||||
testAccessibleTree(topLevel, originalTree);
|
||||
},
|
||||
{
|
||||
chrome: false,
|
||||
topLevel: false,
|
||||
iframe: true,
|
||||
remoteIframe: true,
|
||||
iframeDocBodyAttrs: { "aria-hidden": "true" },
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Verify adding aria-hidden to iframe doc elements removes
|
||||
* their subtree. This test ONLY tests iframe documents, it
|
||||
* should not run in tab documents. There is a separate test for
|
||||
* tab documents above.
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`
|
||||
<p id="content">I am some content in a document</p>
|
||||
`,
|
||||
async function testIframeDocumentMutation(browser, docAcc, topLevel) {
|
||||
const originalTree = {
|
||||
DOCUMENT: [
|
||||
{
|
||||
INTERNAL_FRAME: [
|
||||
{
|
||||
DOCUMENT: [
|
||||
{
|
||||
PARAGRAPH: [
|
||||
{
|
||||
TEXT_LEAF: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
testAccessibleTree(topLevel, originalTree);
|
||||
info("Adding aria-hidden=true to content doc");
|
||||
await contentSpawnMutation(
|
||||
browser,
|
||||
{ expected: [[EVENT_REORDER, docAcc]] },
|
||||
function () {
|
||||
const b = content.document.body;
|
||||
b.setAttribute("aria-hidden", "true");
|
||||
}
|
||||
);
|
||||
const newTree = {
|
||||
DOCUMENT: [
|
||||
{
|
||||
INTERNAL_FRAME: [
|
||||
{
|
||||
DOCUMENT: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
testAccessibleTree(topLevel, newTree);
|
||||
},
|
||||
{ chrome: false, topLevel: false, iframe: true, remoteIframe: true }
|
||||
);
|
||||
|
||||
// // ///////////////////////////////
|
||||
// // //////////////////// SVG Tests
|
||||
// // //////////////////////////////
|
||||
|
||||
const SVG_DOCUMENT_ID = "rootSVG";
|
||||
const HIDDEN_SVG_URI =
|
||||
"data:image/svg+xml,%3Csvg%20id%3D%22rootSVG%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20aria-hidden%3D%22true%22%3E%3Ctext%20x%3D%2210%22%20y%3D%2250%22%20font-size%3D%2230%22%20id%3D%22textSVG%22%3EMy%20SVG%3C%2Ftext%3E%3C%2Fsvg%3E";
|
||||
const SVG_URI =
|
||||
"data:image/svg+xml,%3Csvg%20id%3D%22rootSVG%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctext%20x%3D%2210%22%20y%3D%2250%22%20font-size%3D%2230%22%20id%3D%22textSVG%22%3EMy%20SVG%3C%2Ftext%3E%3C%2Fsvg%3E";
|
||||
|
||||
/**
|
||||
* Verify loading an SVG document with aria-hidden=true renders the
|
||||
* entire document subtree.
|
||||
* Non-root svg elements, like those in embedded iframes, should
|
||||
* continue to respect aria-hidden when applied.
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`hello world`,
|
||||
async function testSVGDocument(browser) {
|
||||
let loaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, SVG_DOCUMENT_ID);
|
||||
info("Loading SVG");
|
||||
browser.loadURI(Services.io.newURI(HIDDEN_SVG_URI), {
|
||||
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
});
|
||||
await loaded;
|
||||
|
||||
const tree = {
|
||||
DOCUMENT: [
|
||||
{
|
||||
TEXT_CONTAINER: [
|
||||
{
|
||||
TEXT_LEAF: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const root = getRootAccessible(document);
|
||||
const svgRoot = findAccessibleChildByID(root, SVG_DOCUMENT_ID);
|
||||
testAccessibleTree(svgRoot, tree);
|
||||
},
|
||||
{ chrome: true, topLevel: true, iframe: false, remoteIframe: false }
|
||||
);
|
||||
|
||||
///////////
|
||||
///// TODO: Bug 1960416
|
||||
//////////
|
||||
// /**
|
||||
// * Verify loading an SVG document with aria-hidden=true
|
||||
// * in an iframe does not render the document subtree.
|
||||
// */
|
||||
// addAccessibleTask(
|
||||
// `hello world`,
|
||||
// async function testSVGIframeDocument(browser) {
|
||||
// info("Loading SVG");
|
||||
// const loaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, SVG_DOCUMENT_ID);
|
||||
// await SpecialPowers.spawn(browser, [DEFAULT_IFRAME_ID, HIDDEN_SVG_URI], (_id,_uri) => {
|
||||
// content.document.getElementById(_id).src = _uri;
|
||||
// });
|
||||
// await loaded;
|
||||
|
||||
// const tree = {
|
||||
// DOCUMENT: [],
|
||||
// };
|
||||
// const root = getRootAccessible(document);
|
||||
// const svgRoot = findAccessibleChildByID(root, SVG_DOCUMENT_ID);
|
||||
// testAccessibleTree(svgRoot, tree);
|
||||
// },
|
||||
// { chrome: false, topLevel: false, iframe: true, remoteIframe: true }
|
||||
// );
|
||||
|
||||
/**
|
||||
* Verify adding aria-hidden to root svg elements has no effect.
|
||||
* Non-root svg elements, like those in embedded iframes, should
|
||||
* continue to respect aria-hidden when applied.
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`hello world`,
|
||||
async function testSVGDocumentMutation(browser) {
|
||||
let loaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, SVG_DOCUMENT_ID);
|
||||
info("Loading SVG");
|
||||
browser.loadURI(Services.io.newURI(SVG_URI), {
|
||||
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
});
|
||||
await loaded;
|
||||
|
||||
const originalTree = {
|
||||
DOCUMENT: [
|
||||
{
|
||||
TEXT_CONTAINER: [
|
||||
{
|
||||
TEXT_LEAF: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const root = getRootAccessible(document);
|
||||
const svgRoot = findAccessibleChildByID(root, SVG_DOCUMENT_ID);
|
||||
testAccessibleTree(svgRoot, originalTree);
|
||||
info("Adding aria-hidden=true to svg");
|
||||
// XXX Bug 1959547: We incorrectly get a reorder
|
||||
// here. The tree should be unaffected by this attribute,
|
||||
// but it seems like it isn't! Below we'll verify that
|
||||
// the tree isn't removed, despite this reorder.
|
||||
const unexpectedEvents = { expected: [[EVENT_REORDER, SVG_DOCUMENT_ID]] };
|
||||
info("Adding aria-hidden");
|
||||
await contentSpawnMutation(
|
||||
browser,
|
||||
unexpectedEvents,
|
||||
function (_id) {
|
||||
const d = content.document.getElementById(_id);
|
||||
d.setAttribute("aria-hidden", "true");
|
||||
},
|
||||
[SVG_DOCUMENT_ID]
|
||||
);
|
||||
// XXX Bug 1959547: We end up with an extra node in the
|
||||
// tree after adding aria-hidden. It seems like SVG root
|
||||
// element is splitting off / no longer behaves as the
|
||||
// document...?
|
||||
const newTree = {
|
||||
DOCUMENT: [
|
||||
{
|
||||
DIAGRAM: [
|
||||
{
|
||||
TEXT_CONTAINER: [
|
||||
{
|
||||
TEXT_LEAF: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
testAccessibleTree(svgRoot, newTree);
|
||||
},
|
||||
{ chrome: true, topLevel: true, iframe: false, remoteIframe: false }
|
||||
);
|
||||
|
||||
/**
|
||||
* Verify adding aria-hidden to root svg elements in iframes removes
|
||||
* the svg subtree.
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`hello world`,
|
||||
async function testSVGIframeDocumentMutation(browser) {
|
||||
info("Loading SVG");
|
||||
const loaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, SVG_DOCUMENT_ID);
|
||||
await SpecialPowers.spawn(
|
||||
browser,
|
||||
[DEFAULT_IFRAME_ID, SVG_URI],
|
||||
(contentId, _uri) => {
|
||||
content.document.getElementById(contentId).src = _uri;
|
||||
}
|
||||
);
|
||||
await loaded;
|
||||
const originalTree = {
|
||||
DOCUMENT: [
|
||||
{
|
||||
TEXT_CONTAINER: [
|
||||
{
|
||||
TEXT_LEAF: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const svgRoot = findAccessibleChildByID(
|
||||
getRootAccessible(document),
|
||||
SVG_DOCUMENT_ID
|
||||
);
|
||||
testAccessibleTree(svgRoot, originalTree);
|
||||
|
||||
info("Adding aria-hidden=true to svg");
|
||||
const events = { expected: [[EVENT_REORDER, SVG_DOCUMENT_ID]] };
|
||||
await contentSpawnMutation(
|
||||
browser,
|
||||
events,
|
||||
function (_id) {
|
||||
const d = content.document.getElementById(_id);
|
||||
d.setAttribute("aria-hidden", "true");
|
||||
},
|
||||
[SVG_DOCUMENT_ID]
|
||||
);
|
||||
|
||||
const newTree = { DOCUMENT: [] };
|
||||
testAccessibleTree(svgRoot, newTree);
|
||||
},
|
||||
{ chrome: false, topLevel: false, iframe: true, remoteIframe: true }
|
||||
);
|
||||
|
|
127
accessible/tests/browser/tree/browser_test_aria_hidden_iframe.js
Normal file
|
@ -0,0 +1,127 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
/* import-globals-from ../../mochitest/role.js */
|
||||
loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
|
||||
|
||||
/**
|
||||
* Verify loading an iframe document with aria-hidden specified on the root element
|
||||
* correctly hides the root element and its content. This test is meaninfully
|
||||
* different from testIframeDocument which tests aria-hidden specified on the
|
||||
* body element.
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`hello world`,
|
||||
async function testIframeRootDocument(browser) {
|
||||
info("Loading iframe document");
|
||||
const HIDDEN_IFRAME_URI =
|
||||
"data:text/html,<html id='new_html' aria-hidden='true'><body id='iframeBody'><u>hello world</u></body></html>";
|
||||
const loaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, "iframeBody");
|
||||
await SpecialPowers.spawn(
|
||||
browser,
|
||||
[DEFAULT_IFRAME_ID, HIDDEN_IFRAME_URI],
|
||||
(_id, _uri) => {
|
||||
content.document.getElementById(_id).src = _uri;
|
||||
}
|
||||
);
|
||||
await loaded;
|
||||
|
||||
const tree = {
|
||||
INTERNAL_FRAME: [
|
||||
{
|
||||
DOCUMENT: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
const root = getRootAccessible(document);
|
||||
const iframeDoc = findAccessibleChildByID(root, DEFAULT_IFRAME_ID);
|
||||
testAccessibleTree(iframeDoc, tree);
|
||||
},
|
||||
{
|
||||
chrome: false,
|
||||
topLevel: false,
|
||||
iframe: true,
|
||||
remoteIframe: true,
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Verify loading an iframe doc with aria-hidden doesn't render the document.
|
||||
* This test ONLY tests iframe documents, it should not run in tab docs.
|
||||
* There is a separate test for tab docs in browser_test_aria_hidden.js.
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`
|
||||
<p id="content">I am some content in a document</p>
|
||||
`,
|
||||
async function testIframeDocument(browser, docAcc, topLevel) {
|
||||
const originalTree = { DOCUMENT: [{ INTERNAL_FRAME: [{ DOCUMENT: [] }] }] };
|
||||
testAccessibleTree(topLevel, originalTree);
|
||||
},
|
||||
{
|
||||
chrome: false,
|
||||
topLevel: false,
|
||||
iframe: true,
|
||||
remoteIframe: true,
|
||||
iframeDocBodyAttrs: { "aria-hidden": "true" },
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Verify adding aria-hidden to iframe doc elements removes
|
||||
* their subtree. This test ONLY tests iframe documents, it
|
||||
* should not run in tab documents. There is a separate test for
|
||||
* tab documents in browser_test_aria_hidden.js.
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`
|
||||
<p id="content">I am some content in a document</p>
|
||||
`,
|
||||
async function testIframeDocumentMutation(browser, docAcc, topLevel) {
|
||||
const originalTree = {
|
||||
DOCUMENT: [
|
||||
{
|
||||
INTERNAL_FRAME: [
|
||||
{
|
||||
DOCUMENT: [
|
||||
{
|
||||
PARAGRAPH: [
|
||||
{
|
||||
TEXT_LEAF: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
testAccessibleTree(topLevel, originalTree);
|
||||
info("Adding aria-hidden=true to content doc");
|
||||
await contentSpawnMutation(
|
||||
browser,
|
||||
{ expected: [[EVENT_REORDER, docAcc]] },
|
||||
function () {
|
||||
const b = content.document.body;
|
||||
b.setAttribute("aria-hidden", "true");
|
||||
}
|
||||
);
|
||||
const newTree = {
|
||||
DOCUMENT: [
|
||||
{
|
||||
INTERNAL_FRAME: [
|
||||
{
|
||||
DOCUMENT: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
testAccessibleTree(topLevel, newTree);
|
||||
},
|
||||
{ chrome: false, topLevel: false, iframe: true, remoteIframe: true }
|
||||
);
|
197
accessible/tests/browser/tree/browser_test_aria_hidden_svg.js
Normal file
|
@ -0,0 +1,197 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
/* import-globals-from ../../mochitest/role.js */
|
||||
loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
|
||||
|
||||
const SVG_DOCUMENT_ID = "rootSVG";
|
||||
const HIDDEN_SVG_URI =
|
||||
"data:image/svg+xml,%3Csvg%20id%3D%22rootSVG%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20aria-hidden%3D%22true%22%3E%3Ctext%20x%3D%2210%22%20y%3D%2250%22%20font-size%3D%2230%22%20id%3D%22textSVG%22%3EMy%20SVG%3C%2Ftext%3E%3C%2Fsvg%3E";
|
||||
const SVG_URI =
|
||||
"data:image/svg+xml,%3Csvg%20id%3D%22rootSVG%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ctext%20x%3D%2210%22%20y%3D%2250%22%20font-size%3D%2230%22%20id%3D%22textSVG%22%3EMy%20SVG%3C%2Ftext%3E%3C%2Fsvg%3E";
|
||||
|
||||
/**
|
||||
* Verify loading an SVG document with aria-hidden=true renders the
|
||||
* entire document subtree.
|
||||
* Non-root svg elements, like those in embedded iframes, should
|
||||
* continue to respect aria-hidden when applied.
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`hello world`,
|
||||
async function testSVGDocument(browser) {
|
||||
let loaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, SVG_DOCUMENT_ID);
|
||||
info("Loading SVG");
|
||||
browser.loadURI(Services.io.newURI(HIDDEN_SVG_URI), {
|
||||
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
});
|
||||
await loaded;
|
||||
|
||||
const tree = {
|
||||
DOCUMENT: [
|
||||
{
|
||||
TEXT_CONTAINER: [
|
||||
{
|
||||
TEXT_LEAF: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const root = getRootAccessible(document);
|
||||
const svgRoot = findAccessibleChildByID(root, SVG_DOCUMENT_ID);
|
||||
testAccessibleTree(svgRoot, tree);
|
||||
},
|
||||
{ chrome: true, topLevel: true, iframe: false, remoteIframe: false }
|
||||
);
|
||||
|
||||
/**
|
||||
* Verify loading an SVG document with aria-hidden=true
|
||||
* in an iframe does not render the document subtree.
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`hello world`,
|
||||
async function testSVGIframeDocument(browser) {
|
||||
info("Loading SVG");
|
||||
const loaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, SVG_DOCUMENT_ID);
|
||||
await SpecialPowers.spawn(
|
||||
browser,
|
||||
[DEFAULT_IFRAME_ID, HIDDEN_SVG_URI],
|
||||
(_id, _uri) => {
|
||||
content.document.getElementById(_id).src = _uri;
|
||||
}
|
||||
);
|
||||
await loaded;
|
||||
|
||||
const tree = {
|
||||
DOCUMENT: [],
|
||||
};
|
||||
|
||||
const root = getRootAccessible(document);
|
||||
const svgRoot = findAccessibleChildByID(root, SVG_DOCUMENT_ID);
|
||||
testAccessibleTree(svgRoot, tree);
|
||||
},
|
||||
{ chrome: false, topLevel: false, iframe: true, remoteIframe: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* Verify adding aria-hidden to root svg elements has no effect.
|
||||
* Non-root svg elements, like those in embedded iframes, should
|
||||
* continue to respect aria-hidden when applied.
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`hello world`,
|
||||
async function testSVGDocumentMutation(browser) {
|
||||
let loaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, SVG_DOCUMENT_ID);
|
||||
info("Loading SVG");
|
||||
browser.loadURI(Services.io.newURI(SVG_URI), {
|
||||
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
});
|
||||
await loaded;
|
||||
|
||||
const originalTree = {
|
||||
DOCUMENT: [
|
||||
{
|
||||
TEXT_CONTAINER: [
|
||||
{
|
||||
TEXT_LEAF: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const root = getRootAccessible(document);
|
||||
const svgRoot = findAccessibleChildByID(root, SVG_DOCUMENT_ID);
|
||||
testAccessibleTree(svgRoot, originalTree);
|
||||
info("Adding aria-hidden=true to svg");
|
||||
// XXX Bug 1959547: We incorrectly get a reorder
|
||||
// here. The tree should be unaffected by this attribute,
|
||||
// but it seems like it isn't! Below we'll verify that
|
||||
// the tree isn't removed, despite this reorder.
|
||||
const unexpectedEvents = { expected: [[EVENT_REORDER, SVG_DOCUMENT_ID]] };
|
||||
info("Adding aria-hidden");
|
||||
await contentSpawnMutation(
|
||||
browser,
|
||||
unexpectedEvents,
|
||||
function (_id) {
|
||||
const d = content.document.getElementById(_id);
|
||||
d.setAttribute("aria-hidden", "true");
|
||||
},
|
||||
[SVG_DOCUMENT_ID]
|
||||
);
|
||||
// XXX Bug 1959547: We end up with an extra node in the
|
||||
// tree after adding aria-hidden. It seems like SVG root
|
||||
// element is splitting off / no longer behaves as the
|
||||
// document...?
|
||||
const newTree = {
|
||||
DOCUMENT: [
|
||||
{
|
||||
DIAGRAM: [
|
||||
{
|
||||
TEXT_CONTAINER: [
|
||||
{
|
||||
TEXT_LEAF: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
testAccessibleTree(svgRoot, newTree);
|
||||
},
|
||||
{ chrome: true, topLevel: true, iframe: false, remoteIframe: false }
|
||||
);
|
||||
|
||||
/**
|
||||
* Verify adding aria-hidden to root svg elements in iframes removes
|
||||
* the svg subtree.
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`hello world`,
|
||||
async function testSVGIframeDocumentMutation(browser) {
|
||||
info("Loading SVG");
|
||||
const loaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, SVG_DOCUMENT_ID);
|
||||
await SpecialPowers.spawn(
|
||||
browser,
|
||||
[DEFAULT_IFRAME_ID, SVG_URI],
|
||||
(contentId, _uri) => {
|
||||
content.document.getElementById(contentId).src = _uri;
|
||||
}
|
||||
);
|
||||
await loaded;
|
||||
const originalTree = {
|
||||
DOCUMENT: [
|
||||
{
|
||||
TEXT_CONTAINER: [
|
||||
{
|
||||
TEXT_LEAF: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const svgRoot = findAccessibleChildByID(
|
||||
getRootAccessible(document),
|
||||
SVG_DOCUMENT_ID
|
||||
);
|
||||
testAccessibleTree(svgRoot, originalTree);
|
||||
|
||||
info("Adding aria-hidden=true to svg");
|
||||
const events = { expected: [[EVENT_REORDER, SVG_DOCUMENT_ID]] };
|
||||
await contentSpawnMutation(
|
||||
browser,
|
||||
events,
|
||||
function (_id) {
|
||||
const d = content.document.getElementById(_id);
|
||||
d.setAttribute("aria-hidden", "true");
|
||||
},
|
||||
[SVG_DOCUMENT_ID]
|
||||
);
|
||||
|
||||
const newTree = { DOCUMENT: [] };
|
||||
testAccessibleTree(svgRoot, newTree);
|
||||
},
|
||||
{ chrome: false, topLevel: false, iframe: true, remoteIframe: true }
|
||||
);
|
14
aclocal.m4
vendored
|
@ -1,14 +0,0 @@
|
|||
dnl
|
||||
dnl Local autoconf macros used with mozilla
|
||||
dnl The contents of this file are under the Public Domain.
|
||||
dnl
|
||||
|
||||
builtin(include, build/autoconf/hooks.m4)dnl
|
||||
builtin(include, build/autoconf/config.status.m4)dnl
|
||||
builtin(include, build/autoconf/altoptions.m4)dnl
|
||||
|
||||
# Read the user's .mozconfig script. We can't do this in
|
||||
# configure.in: autoconf puts the argument parsing code above anything
|
||||
# expanded from configure.in, and we need to get the configure options
|
||||
# from .mozconfig in place before that argument parsing code.
|
||||
MOZ_READ_MOZCONFIG(.)
|
|
@ -1650,7 +1650,7 @@ export class SearchSERPTelemetryChild extends JSWindowActorChild {
|
|||
* @param {object} event The event details.
|
||||
*/
|
||||
handleEvent(event) {
|
||||
if (!this.#urlIsSERP(this.document.documentURI)) {
|
||||
if (!this.#urlIsSERP()) {
|
||||
return;
|
||||
}
|
||||
switch (event.type) {
|
||||
|
@ -1719,7 +1719,7 @@ export class SearchSERPTelemetryChild extends JSWindowActorChild {
|
|||
documentToSubmitMap.delete(this.document);
|
||||
}
|
||||
|
||||
#urlIsSERP(url) {
|
||||
#urlIsSERP() {
|
||||
let provider = this._getProviderInfoForUrl(this.document.documentURI);
|
||||
if (provider) {
|
||||
// Some URLs can match provider info but also be the provider's homepage
|
||||
|
@ -1727,7 +1727,7 @@ export class SearchSERPTelemetryChild extends JSWindowActorChild {
|
|||
// e.g. https://example.com/ vs. https://example.com/?foo=bar
|
||||
// To check this, we look for the presence of the query parameter
|
||||
// that contains a search term.
|
||||
let queries = new URLSearchParams(url.split("#")[0].split("?")[1]);
|
||||
let queries = URL.fromURI(this.document.documentURIObject).searchParams;
|
||||
for (let queryParamName of provider.queryParamNames) {
|
||||
if (queries.get(queryParamName)) {
|
||||
return true;
|
||||
|
|
|
@ -208,19 +208,6 @@ pref("app.update.langpack.enabled", true);
|
|||
pref("app.update.noWindowAutoRestart.delayMs", 300000);
|
||||
#endif
|
||||
|
||||
// The Multi Session Install Lockout prevents updates from being installed at
|
||||
// startup when they normally would be if there are other instances using the
|
||||
// installation. We only do this for a limited amount of time before we go ahead
|
||||
// and apply the update anyways.
|
||||
// Hopefully, at some point, updating Firefox while it is running will not break
|
||||
// things and this mechanism can be removed.
|
||||
// Note that these prefs are bit dangerous because having different values in
|
||||
// different profiles could cause erratic behavior.
|
||||
// This feature is also affected by
|
||||
// `app.update.multiSessionInstallLockout.timeoutMs`, which is in the branding
|
||||
// section.
|
||||
pref("app.update.multiSessionInstallLockout.enabled", false);
|
||||
|
||||
#if defined(MOZ_BACKGROUNDTASKS)
|
||||
// The amount of time, in seconds, before background tasks time out and exit.
|
||||
// Tasks can override this default (10 minutes).
|
||||
|
@ -388,21 +375,12 @@ pref("browser.overlink-delay", 80);
|
|||
|
||||
pref("browser.taskbarTabs.enabled", false);
|
||||
|
||||
pref("browser.theme.colorway-closet", true);
|
||||
|
||||
#if defined(MOZ_WIDGET_GTK)
|
||||
pref("browser.theme.native-theme", true);
|
||||
#else
|
||||
pref("browser.theme.native-theme", false);
|
||||
#endif
|
||||
|
||||
// Whether expired built-in colorways themes that are active or retained
|
||||
// should be allowed to check for updates and be updated to an AMO hosted
|
||||
// theme with the same id (as part of preparing to remove from mozilla-central
|
||||
// all the expired built-in colorways themes, after existing users have been
|
||||
// migrated to colorways themes hosted on AMO).
|
||||
pref("browser.theme.colorway-migration", true);
|
||||
|
||||
// Whether using `ctrl` when hitting return/enter in the URL bar
|
||||
// (or clicking 'go') should prefix 'www.' and suffix
|
||||
// browser.fixup.alternate.suffix to the URL bar value prior to
|
||||
|
@ -1063,6 +1041,9 @@ pref("browser.tabs.groups.smart.enabled", true);
|
|||
pref("browser.tabs.groups.smart.enabled", false);
|
||||
#endif
|
||||
|
||||
// KMEANS_WITH_ANCHOR or NEAREST_NEIGHBOR
|
||||
pref("browser.tabs.groups.smart.suggestOtherTabsMethod", "NEAREST_NEIGHBOR");
|
||||
|
||||
pref("browser.tabs.groups.smart.optin", false);
|
||||
|
||||
pref("browser.tabs.dragDrop.createGroup.delayMS", 240);
|
||||
|
@ -1869,8 +1850,7 @@ pref("browser.newtabpage.activity-stream.newNewtabExperience.colors", "#004CA4,#
|
|||
pref("browser.newtabpage.activity-stream.newtabLayouts.variant-a", false);
|
||||
pref("browser.newtabpage.activity-stream.newtabLayouts.variant-b", true);
|
||||
|
||||
// Shortcuts experiment
|
||||
pref("browser.newtabpage.activity-stream.newtabShortcuts.refresh", false);
|
||||
pref("browser.newtabpage.activity-stream.newtabShortcuts.refresh", true);
|
||||
|
||||
// Discovery stream ad size experiment
|
||||
pref("browser.newtabpage.activity-stream.newtabAdSize.variant-a", false);
|
||||
|
@ -1991,7 +1971,12 @@ pref("browser.newtabpage.activity-stream.discoverystream.sections.locale-content
|
|||
pref("browser.newtabpage.activity-stream.discoverystream.sections.region-content-config", "");
|
||||
|
||||
pref("browser.newtabpage.activity-stream.discoverystream.sections.cards.enabled", true);
|
||||
pref("browser.newtabpage.activity-stream.discoverystream.sections.personalization.inferred.enabled", false);
|
||||
|
||||
// List of regions that use inferred personalization.
|
||||
pref("browser.newtabpage.activity-stream.discoverystream.sections.personalization.inferred.region-config", "");
|
||||
// List of locales that use inferred personalization.
|
||||
pref("browser.newtabpage.activity-stream.discoverystream.sections.personalization.inferred.locale-config", "en-US,en-GB,en-CA");
|
||||
|
||||
pref("browser.newtabpage.activity-stream.discoverystream.sections.personalization.inferred.user.enabled", true);
|
||||
pref("browser.newtabpage.activity-stream.discoverystream.sections.personalization.inferred.blocked", false);
|
||||
|
||||
|
@ -2152,6 +2137,7 @@ pref("browser.ml.linkPreview.allowedLanguages", "en");
|
|||
pref("browser.ml.linkPreview.enabled", false);
|
||||
pref("browser.ml.linkPreview.outputSentences", 3);
|
||||
pref("browser.ml.linkPreview.blockListEnabled", true);
|
||||
pref("browser.ml.linkPreview.noKeyPointsRegions", "AD,AT,BE,BG,CH,CY,CZ,DE,DK,EE,ES,FI,FR,GR,HR,HU,IE,IS,IT,LI,LT,LU,LV,MT,NL,NO,PL,PT,RO,SE,SI,SK");
|
||||
|
||||
// Block insecure active content on https pages
|
||||
pref("security.mixed_content.block_active_content", true);
|
||||
|
|
|
@ -67,6 +67,8 @@ ChromeUtils.defineESModuleGetters(this, {
|
|||
PopupBlockerObserver: "resource:///modules/PopupBlockerObserver.sys.mjs",
|
||||
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
||||
ProcessHangMonitor: "resource:///modules/ProcessHangMonitor.sys.mjs",
|
||||
ProfilesDatastoreService:
|
||||
"resource:///modules/profiles/ProfilesDatastoreService.sys.mjs",
|
||||
PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs",
|
||||
ReaderMode: "moz-src:///toolkit/components/reader/ReaderMode.sys.mjs",
|
||||
ResetPBMPanel: "resource:///modules/ResetPBMPanel.sys.mjs",
|
||||
|
@ -75,6 +77,8 @@ ChromeUtils.defineESModuleGetters(this, {
|
|||
SaveToPocket: "chrome://pocket/content/SaveToPocket.sys.mjs",
|
||||
ScreenshotsUtils: "resource:///modules/ScreenshotsUtils.sys.mjs",
|
||||
SearchUIUtils: "moz-src:///browser/components/search/SearchUIUtils.sys.mjs",
|
||||
SelectableProfileService:
|
||||
"resource:///modules/profiles/SelectableProfileService.sys.mjs",
|
||||
SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs",
|
||||
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
|
||||
SharingUtils: "resource:///modules/SharingUtils.sys.mjs",
|
||||
|
@ -111,17 +115,6 @@ ChromeUtils.defineESModuleGetters(this, {
|
|||
ZoomUI: "resource:///modules/ZoomUI.sys.mjs",
|
||||
});
|
||||
|
||||
// Bug 1894239: We will move this up to ChromeUtils.defineESModuleGetters once
|
||||
// the MOZ_SELECTABLE_PROFILES flag is removed
|
||||
ChromeUtils.defineLazyGetter(this, "SelectableProfileService", () => {
|
||||
if (!AppConstants.MOZ_SELECTABLE_PROFILES) {
|
||||
return null;
|
||||
}
|
||||
return ChromeUtils.importESModule(
|
||||
"resource:///modules/profiles/SelectableProfileService.sys.mjs"
|
||||
).SelectableProfileService;
|
||||
});
|
||||
|
||||
ChromeUtils.defineLazyGetter(this, "fxAccounts", () => {
|
||||
return ChromeUtils.importESModule(
|
||||
"resource://gre/modules/FxAccounts.sys.mjs"
|
||||
|
|
|
@ -256,5 +256,6 @@
|
|||
"gFindBarPromise",
|
||||
"SelectableProfileService",
|
||||
"ActionsProviderContextualSearch",
|
||||
"ToolbarDropHandler"
|
||||
"ToolbarDropHandler",
|
||||
"ProfilesDatastoreService"
|
||||
]
|
||||
|
|
|
@ -32,6 +32,12 @@
|
|||
lwtProperty: "ntp_card_background",
|
||||
},
|
||||
],
|
||||
[
|
||||
"--newtab-background-card",
|
||||
{
|
||||
lwtProperty: "ntp_card_background",
|
||||
},
|
||||
],
|
||||
[
|
||||
"--newtab-text-primary-color",
|
||||
{
|
||||
|
|
|
@ -148,7 +148,7 @@ document.addEventListener(
|
|||
{
|
||||
let { tabGroupId } = event.target.parentElement.triggerNode.dataset;
|
||||
SessionStore.openSavedTabGroup(tabGroupId, window, {
|
||||
source: lazy.TabMetrics.METRIC_SOURCE.RECENT_TABS,
|
||||
source: lazy.TabMetrics.METRIC_SOURCE.TAB_OVERFLOW_MENU,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
@ -157,7 +157,7 @@ document.addEventListener(
|
|||
// TODO Bug 1940112: "Open Group in New Window" should directly restore saved tab groups into a new window
|
||||
let { tabGroupId } = event.target.parentElement.triggerNode.dataset;
|
||||
let tabGroup = SessionStore.openSavedTabGroup(tabGroupId, window, {
|
||||
source: lazy.TabMetrics.METRIC_SOURCE.RECENT_TABS,
|
||||
source: lazy.TabMetrics.METRIC_SOURCE.TAB_OVERFLOW_MENU,
|
||||
});
|
||||
gBrowser.replaceGroupWithWindow(tabGroup);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ prefs = [
|
|||
# The form autofill framescript is only used in certain locales if this
|
||||
# pref is set to 'detect', which is the default value on non-Nightly.
|
||||
"extensions.formautofill.addresses.available='on'",
|
||||
"browser.urlbar.disableExtendForTests=true",
|
||||
# For perfomance tests do not enable the remote control cue, which gets set
|
||||
# when Marionette is enabled, but users normally don't see.
|
||||
"browser.chrome.disableRemoteControlCueForTests=true",
|
||||
|
@ -105,10 +104,8 @@ skip-if = ["os == 'win'"] #Bug 1455054
|
|||
["browser_toolbariconcolor_restyles.js"]
|
||||
|
||||
["browser_urlbar_keyed_search.js"]
|
||||
skip-if = ["true"] # Bug 1926118: Disabled until the test can be revisited to measure performance under representative conditions.
|
||||
|
||||
["browser_urlbar_search.js"]
|
||||
skip-if = ["true"] # Bug 1926118: Disabled until the test can be revisited to measure performance under representative conditions.
|
||||
|
||||
["browser_vsync_accessibility.js"]
|
||||
|
||||
|
|
|
@ -108,6 +108,13 @@ const startupPhases = {
|
|||
},
|
||||
};
|
||||
|
||||
if (AppConstants.platform == "win") {
|
||||
// On Windows we call checkForLaunchOnLogin early in startup.
|
||||
startupPhases["before profile selection"].allowlist.modules.add(
|
||||
"moz-src:///browser/components/shell/StartupOSIntegration.sys.mjs"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
Services.prefs.getBoolPref("browser.startup.blankWindow") &&
|
||||
Services.prefs.getCharPref(
|
||||
|
|
|
@ -818,9 +818,18 @@ async function runUrlbarTest(
|
|||
};
|
||||
|
||||
let urlbarRect = URLBar.textbox.getBoundingClientRect();
|
||||
const SHADOW_SIZE = 17;
|
||||
// To isolate unexpected repaints, we need to filter out the rectangle of
|
||||
// pixels changed by showing the urlbar popover
|
||||
const SHADOW_SIZE = 17; // The blur/spread of the box shadow, plus 1px fudge factor
|
||||
const INLINE_MARGIN = 5; // Margin applied to the breakout-extend urlbar
|
||||
const VERTICAL_OFFSET = -4; // The popover positioning requires this offset
|
||||
let expectedRects = {
|
||||
filter: rects => {
|
||||
const referenceRect = {
|
||||
x1: Math.floor(urlbarRect.left) - INLINE_MARGIN - SHADOW_SIZE,
|
||||
x2: Math.ceil(urlbarRect.right) + INLINE_MARGIN + SHADOW_SIZE,
|
||||
y1: Math.floor(urlbarRect.top) + VERTICAL_OFFSET - SHADOW_SIZE,
|
||||
};
|
||||
// We put text into the urlbar so expect its textbox to change.
|
||||
// We expect many changes in the results view.
|
||||
// So we just allow changes anywhere in the urlbar. We don't check the
|
||||
|
@ -831,9 +840,9 @@ async function runUrlbarTest(
|
|||
return rects.filter(
|
||||
r =>
|
||||
!(
|
||||
r.x1 >= Math.floor(urlbarRect.left) - SHADOW_SIZE &&
|
||||
r.x2 <= Math.ceil(urlbarRect.right) + SHADOW_SIZE &&
|
||||
r.y1 >= Math.floor(urlbarRect.top) - SHADOW_SIZE
|
||||
r.x1 >= referenceRect.x1 &&
|
||||
r.x2 <= referenceRect.x2 &&
|
||||
r.y1 >= referenceRect.y1
|
||||
)
|
||||
);
|
||||
},
|
||||
|
|
|
@ -119,6 +119,10 @@ var gExceptionPaths = [
|
|||
"resource://builtin-addons/newtab/",
|
||||
"resource://newtab/",
|
||||
"chrome://newtab/",
|
||||
|
||||
// Bug 1957102 - Temporarily ignore until used by the URLBar provider
|
||||
"resource://gre/modules/PlacesSemanticHistoryManager.sys.mjs",
|
||||
"resource://gre/modules/PlacesSemanticHistoryDatabase.sys.mjs",
|
||||
];
|
||||
|
||||
// These are not part of the omni.ja file, so we find them only when running
|
||||
|
|
|
@ -29,14 +29,6 @@ pref("app.update.checkInstallTime.days", 2);
|
|||
// Give the user x seconds to reboot before showing a badge on the hamburger
|
||||
// button. default=4 days
|
||||
pref("app.update.badgeWaitTime", 345600);
|
||||
// This represents the duration between an update being ready and it being
|
||||
// possible to install it while other sessions are running. Note that
|
||||
// having this pref's duration differ from `app.update.badgeWaitTime` may result
|
||||
// in undefined behavior such as showing an update prompt that does not result
|
||||
// in an update when the "Restart to Update" button is clicked. Keep in mind
|
||||
// that this is in milliseconds and `app.update.badgeWaitTime` is in seconds.
|
||||
// Note that the effective value of this pref is limited to 1 week, maximum.
|
||||
pref("app.update.multiSessionInstallLockout.timeoutMs", 345600000);
|
||||
|
||||
// Number of usages of the web console.
|
||||
// If this is less than 5, then pasting code into the web console is disabled
|
||||
|
|
|
@ -30,14 +30,6 @@ pref("app.update.checkInstallTime.days", 2);
|
|||
// Give the user x seconds to reboot before showing a badge on the hamburger
|
||||
// button. default=immediately
|
||||
pref("app.update.badgeWaitTime", 0);
|
||||
// This represents the duration between an update being ready and it being
|
||||
// possible to install it while other sessions are running. Note that
|
||||
// having this pref's duration differ from `app.update.badgeWaitTime` may result
|
||||
// in undefined behavior such as showing an update prompt that does not result
|
||||
// in an update when the "Restart to Update" button is clicked. Keep in mind
|
||||
// that this is in milliseconds and `app.update.badgeWaitTime` is in seconds.
|
||||
// Note that the effective value of this pref is limited to 1 week, maximum.
|
||||
pref("app.update.multiSessionInstallLockout.timeoutMs", 0);
|
||||
|
||||
// Number of usages of the web console.
|
||||
// If this is less than 5, then pasting code into the web console is disabled
|
||||
|
|
|
@ -42,14 +42,6 @@ pref("app.update.checkInstallTime.days", 63);
|
|||
// Give the user x seconds to reboot before showing a badge on the hamburger
|
||||
// button. default=4 days
|
||||
pref("app.update.badgeWaitTime", 345600);
|
||||
// This represents the duration between an update being ready and it being
|
||||
// possible to install it while other sessions are running. Note that
|
||||
// having this pref's duration differ from `app.update.badgeWaitTime` may result
|
||||
// in undefined behavior such as showing an update prompt that does not result
|
||||
// in an update when the "Restart to Update" button is clicked. Keep in mind
|
||||
// that this is in milliseconds and `app.update.badgeWaitTime` is in seconds.
|
||||
// Note that the effective value of this pref is limited to 1 week, maximum.
|
||||
pref("app.update.multiSessionInstallLockout.timeoutMs", 345600000);
|
||||
|
||||
// Number of usages of the web console.
|
||||
// If this is less than 5, then pasting code into the web console is disabled
|
||||
|
|
|
@ -26,14 +26,6 @@ pref("app.update.checkInstallTime.days", 2);
|
|||
// Give the user x seconds to reboot before showing a badge on the hamburger
|
||||
// button. default=immediately
|
||||
pref("app.update.badgeWaitTime", 0);
|
||||
// This represents the duration between an update being ready and it being
|
||||
// possible to install it while other sessions are running. Note that
|
||||
// having this pref's duration differ from `app.update.badgeWaitTime` may result
|
||||
// in undefined behavior such as showing an update prompt that does not result
|
||||
// in an update when the "Restart to Update" button is clicked. Keep in mind
|
||||
// that this is in milliseconds and `app.update.badgeWaitTime` is in seconds.
|
||||
// Note that the effective value of this pref is limited to 1 week, maximum.
|
||||
pref("app.update.multiSessionInstallLockout.timeoutMs", 0);
|
||||
|
||||
// Number of usages of the web console.
|
||||
// If this is less than 5, then pasting code into the web console is disabled
|
||||
|
|
|
@ -47,12 +47,16 @@ category browser-idle-startup resource:///modules/UrlbarSearchTermsPersistence.s
|
|||
category browser-idle-startup resource:///modules/ShoppingUtils.sys.mjs ShoppingUtils.init
|
||||
category browser-idle-startup moz-src:///browser/components/search/SERPCategorization.sys.mjs SERPCategorization.init
|
||||
category browser-idle-startup resource://gre/modules/ContentRelevancyManager.sys.mjs ContentRelevancyManager.init
|
||||
category browser-idle-startup resource://gre/modules/ColorwayThemeMigration.sys.mjs ColorwayThemeMigration.maybeWarn
|
||||
#ifdef MOZ_UPDATER
|
||||
category browser-idle-startup resource://gre/modules/UpdateListener.sys.mjs UpdateListener.maybeShowUnsupportedNotification
|
||||
#endif
|
||||
#ifdef XP_WIN
|
||||
category browser-idle-startup resource:///modules/WindowsJumpLists.sys.mjs WinTaskbarJumpList.startup
|
||||
#endif
|
||||
#if defined(XP_WIN) || defined(XP_MACOSX)
|
||||
category browser-idle-startup moz-src:///browser/components/shell/StartupOSIntegration.sys.mjs StartupOSIntegration.onStartupIdle
|
||||
#endif
|
||||
|
||||
# Note that these telemetry entries schedule their own idle tasks,
|
||||
# so they are guaranteed to run after everything else.
|
||||
|
|
|
@ -42,8 +42,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
|||
DownloadsViewableInternally:
|
||||
"resource:///modules/DownloadsViewableInternally.sys.mjs",
|
||||
ExtensionsUI: "resource:///modules/ExtensionsUI.sys.mjs",
|
||||
FirefoxBridgeExtensionUtils:
|
||||
"resource:///modules/FirefoxBridgeExtensionUtils.sys.mjs",
|
||||
// FilePickerCrashed is used by the `listeners` object below.
|
||||
// eslint-disable-next-line mozilla/valid-lazy
|
||||
FilePickerCrashed: "resource:///modules/FilePickerCrashed.sys.mjs",
|
||||
|
@ -70,6 +68,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
|||
ProcessHangMonitor: "resource:///modules/ProcessHangMonitor.sys.mjs",
|
||||
ProfileDataUpgrader:
|
||||
"moz-src:///browser/components/ProfileDataUpgrader.sys.mjs",
|
||||
ProfilesDatastoreService:
|
||||
"resource:///modules/profiles/ProfilesDatastoreService.sys.mjs",
|
||||
RemoteSecuritySettings:
|
||||
"resource://gre/modules/psm/RemoteSecuritySettings.sys.mjs",
|
||||
RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
|
||||
|
@ -83,10 +83,11 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
|||
"resource:///modules/profiles/SelectableProfileService.sys.mjs",
|
||||
SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs",
|
||||
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
|
||||
ShellService: "resource:///modules/ShellService.sys.mjs",
|
||||
ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs",
|
||||
SpecialMessageActions:
|
||||
"resource://messaging-system/lib/SpecialMessageActions.sys.mjs",
|
||||
StartupOSIntegration:
|
||||
"moz-src:///browser/components/shell/StartupOSIntegration.sys.mjs",
|
||||
TelemetryReportingPolicy:
|
||||
"resource://gre/modules/TelemetryReportingPolicy.sys.mjs",
|
||||
TRRRacer: "resource:///modules/TRRPerformance.sys.mjs",
|
||||
|
@ -94,9 +95,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
|||
WebChannel: "resource://gre/modules/WebChannel.sys.mjs",
|
||||
WebProtocolHandlerRegistrar:
|
||||
"resource:///modules/WebProtocolHandlerRegistrar.sys.mjs",
|
||||
WindowsLaunchOnLogin: "resource://gre/modules/WindowsLaunchOnLogin.sys.mjs",
|
||||
WindowsRegistry: "resource://gre/modules/WindowsRegistry.sys.mjs",
|
||||
WindowsGPOParser: "resource://gre/modules/policies/WindowsGPOParser.sys.mjs",
|
||||
setTimeout: "resource://gre/modules/Timer.sys.mjs",
|
||||
});
|
||||
|
||||
|
@ -126,13 +125,6 @@ if (AppConstants.ENABLE_WEBDRIVER) {
|
|||
|
||||
const PREF_PDFJS_ISDEFAULT_CACHE_STATE = "pdfjs.enabledCache.state";
|
||||
|
||||
const PRIVATE_BROWSING_BINARY = "private_browsing.exe";
|
||||
// Index of Private Browsing icon in private_browsing.exe
|
||||
// Must line up with IDI_PBICON_PB_PB_EXE in nsNativeAppSupportWin.h.
|
||||
const PRIVATE_BROWSING_EXE_ICON_INDEX = 1;
|
||||
const PREF_PRIVATE_BROWSING_SHORTCUT_CREATED =
|
||||
"browser.privacySegmentation.createdShortcut";
|
||||
|
||||
ChromeUtils.defineLazyGetter(
|
||||
lazy,
|
||||
"WeaveService",
|
||||
|
@ -157,21 +149,6 @@ ChromeUtils.defineLazyGetter(lazy, "gBrowserBundle", function () {
|
|||
);
|
||||
});
|
||||
|
||||
ChromeUtils.defineLazyGetter(lazy, "log", () => {
|
||||
let { ConsoleAPI } = ChromeUtils.importESModule(
|
||||
"resource://gre/modules/Console.sys.mjs"
|
||||
);
|
||||
let consoleOptions = {
|
||||
// tip: set maxLogLevel to "debug" and use lazy.log.debug() to create
|
||||
// detailed messages during development. See LOG_LEVELS in Console.sys.mjs
|
||||
// for details.
|
||||
maxLogLevel: "error",
|
||||
maxLogLevelPref: "browser.policies.loglevel",
|
||||
prefix: "BrowserGlue.sys.mjs",
|
||||
};
|
||||
return new ConsoleAPI(consoleOptions);
|
||||
});
|
||||
|
||||
const listeners = {
|
||||
observers: {
|
||||
"file-picker-crashed": ["FilePickerCrashed"],
|
||||
|
@ -255,77 +232,6 @@ export function BrowserGlue() {
|
|||
this._init();
|
||||
}
|
||||
|
||||
function WindowsRegPoliciesGetter(wrk, root, regLocation) {
|
||||
wrk.open(root, regLocation, wrk.ACCESS_READ);
|
||||
let policies;
|
||||
if (wrk.hasChild("Mozilla\\" + Services.appinfo.name)) {
|
||||
policies = lazy.WindowsGPOParser.readPolicies(wrk, policies);
|
||||
}
|
||||
wrk.close();
|
||||
return policies;
|
||||
}
|
||||
|
||||
function isPrivateBrowsingAllowedInRegistry() {
|
||||
// If there is an attempt to open Private Browsing before
|
||||
// EnterprisePolicies are initialized the Windows registry
|
||||
// can be checked to determine if it is enabled
|
||||
if (Services.policies.status > Ci.nsIEnterprisePolicies.UNINITIALIZED) {
|
||||
// Yield to policies engine if initialized
|
||||
let privateAllowed = Services.policies.isAllowed("privatebrowsing");
|
||||
lazy.log.debug(
|
||||
`Yield to initialized policies engine: Private Browsing Allowed = ${privateAllowed}`
|
||||
);
|
||||
return privateAllowed;
|
||||
}
|
||||
if (AppConstants.platform !== "win") {
|
||||
// Not using Windows so no registry, return true
|
||||
lazy.log.debug(
|
||||
"AppConstants.platform is not 'win': Private Browsing allowed"
|
||||
);
|
||||
return true;
|
||||
}
|
||||
// If all other checks fail only then do we check registry
|
||||
let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
|
||||
Ci.nsIWindowsRegKey
|
||||
);
|
||||
let regLocation = "SOFTWARE\\Policies";
|
||||
let userPolicies, machinePolicies;
|
||||
// Only check HKEY_LOCAL_MACHINE if not in testing
|
||||
if (!Cu.isInAutomation) {
|
||||
machinePolicies = WindowsRegPoliciesGetter(
|
||||
wrk,
|
||||
wrk.ROOT_KEY_LOCAL_MACHINE,
|
||||
regLocation
|
||||
);
|
||||
}
|
||||
// Check machine policies before checking user policies
|
||||
// HKEY_LOCAL_MACHINE supersedes HKEY_CURRENT_USER so only check
|
||||
// HKEY_CURRENT_USER if the registry key is not present in
|
||||
// HKEY_LOCAL_MACHINE at all
|
||||
if (machinePolicies && "DisablePrivateBrowsing" in machinePolicies) {
|
||||
lazy.log.debug(
|
||||
`DisablePrivateBrowsing in HKEY_LOCAL_MACHINE is ${machinePolicies.DisablePrivateBrowsing}`
|
||||
);
|
||||
return !(machinePolicies.DisablePrivateBrowsing === 1);
|
||||
}
|
||||
userPolicies = WindowsRegPoliciesGetter(
|
||||
wrk,
|
||||
wrk.ROOT_KEY_CURRENT_USER,
|
||||
regLocation
|
||||
);
|
||||
if (userPolicies && "DisablePrivateBrowsing" in userPolicies) {
|
||||
lazy.log.debug(
|
||||
`DisablePrivateBrowsing in HKEY_CURRENT_USER is ${userPolicies.DisablePrivateBrowsing}`
|
||||
);
|
||||
return !(userPolicies.DisablePrivateBrowsing === 1);
|
||||
}
|
||||
// Private browsing allowed if no registry entry exists
|
||||
lazy.log.debug(
|
||||
"No DisablePrivateBrowsing registry entry: Private Browsing allowed"
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
BrowserGlue.prototype = {
|
||||
_saveSession: false,
|
||||
_migrationImportsDefaultBookmarks: false,
|
||||
|
@ -513,30 +419,8 @@ BrowserGlue.prototype = {
|
|||
"os-autostart",
|
||||
false
|
||||
);
|
||||
let launchOnLoginPref = "browser.startup.windowsLaunchOnLogin.enabled";
|
||||
let profileSvc = Cc[
|
||||
"@mozilla.org/toolkit/profile-service;1"
|
||||
].getService(Ci.nsIToolkitProfileService);
|
||||
if (
|
||||
AppConstants.platform == "win" &&
|
||||
!profileSvc.startWithLastProfile
|
||||
) {
|
||||
// If we don't start with last profile, the user
|
||||
// likely sees the profile selector on launch.
|
||||
if (Services.prefs.getBoolPref(launchOnLoginPref)) {
|
||||
Glean.launchOnLogin.lastProfileDisableStartup.record();
|
||||
// Disable launch on login messaging if we are disabling the
|
||||
// feature.
|
||||
Services.prefs.setBoolPref(
|
||||
"browser.startup.windowsLaunchOnLogin.disableLaunchOnLoginPrompt",
|
||||
true
|
||||
);
|
||||
}
|
||||
// To reduce confusion when running multiple Gecko profiles,
|
||||
// delete launch on login shortcuts and registry keys so that
|
||||
// users are not presented with the outdated profile selector
|
||||
// dialog.
|
||||
lazy.WindowsLaunchOnLogin.removeLaunchOnLogin();
|
||||
if (AppConstants.platform == "win") {
|
||||
lazy.StartupOSIntegration.checkForLaunchOnLogin();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -904,7 +788,7 @@ BrowserGlue.prototype = {
|
|||
|
||||
let makeWindowPrivate =
|
||||
cmdLine.findFlag("private-window", false) != -1 &&
|
||||
isPrivateBrowsingAllowedInRegistry();
|
||||
lazy.StartupOSIntegration.isPrivateBrowsingAllowedInRegistry();
|
||||
if (!shouldCreateWindow(makeWindowPrivate)) {
|
||||
return;
|
||||
}
|
||||
|
@ -1035,9 +919,8 @@ BrowserGlue.prototype = {
|
|||
|
||||
lazy.DoHController.init();
|
||||
|
||||
if (AppConstants.MOZ_SELECTABLE_PROFILES) {
|
||||
lazy.SelectableProfileService.init().catch(console.error);
|
||||
}
|
||||
lazy.ProfilesDatastoreService.init().catch(console.error);
|
||||
lazy.SelectableProfileService.init().catch(console.error);
|
||||
|
||||
this._firstWindowTelemetry(aWindow);
|
||||
this._firstWindowLoaded();
|
||||
|
@ -1409,129 +1292,6 @@ BrowserGlue.prototype = {
|
|||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "firefoxBridgeNativeMessaging",
|
||||
condition:
|
||||
(AppConstants.platform == "macosx" ||
|
||||
AppConstants.platform == "win") &&
|
||||
Services.prefs.getBoolPref("browser.firefoxbridge.enabled", false),
|
||||
task: async () => {
|
||||
let profileService = Cc[
|
||||
"@mozilla.org/toolkit/profile-service;1"
|
||||
].getService(Ci.nsIToolkitProfileService);
|
||||
if (
|
||||
profileService.defaultProfile &&
|
||||
profileService.currentProfile == profileService.defaultProfile
|
||||
) {
|
||||
await lazy.FirefoxBridgeExtensionUtils.ensureRegistered();
|
||||
} else {
|
||||
lazy.log.debug(
|
||||
"FirefoxBridgeExtensionUtils failed to register due to non-default current profile."
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// Kick off an idle task that will silently pin Firefox to the start menu on
|
||||
// first run when using MSIX on a new profile.
|
||||
// If not first run, check if Firefox is no longer pinned to the Start Menu
|
||||
// when it previously was and send telemetry.
|
||||
{
|
||||
name: "maybePinToStartMenuFirstRun",
|
||||
condition:
|
||||
AppConstants.platform === "win" &&
|
||||
Services.sysinfo.getProperty("hasWinPackageId"),
|
||||
task: async () => {
|
||||
if (
|
||||
lazy.BrowserHandler.firstRunProfile &&
|
||||
(await lazy.ShellService.doesAppNeedStartMenuPin())
|
||||
) {
|
||||
await lazy.ShellService.pinToStartMenu();
|
||||
return;
|
||||
}
|
||||
await lazy.ShellService.recordWasPreviouslyPinnedToStartMenu();
|
||||
},
|
||||
},
|
||||
|
||||
// Ensure a Private Browsing Shortcut exists. This is needed in case
|
||||
// a user tries to use Windows functionality to pin our Private Browsing
|
||||
// mode icon to the Taskbar (eg: the "Pin to Taskbar" context menu item).
|
||||
// This is also created by the installer, but it's possible that a user
|
||||
// has removed it, or is running out of a zip build. The consequences of not
|
||||
// having a Shortcut for this are that regular Firefox will be pinned instead
|
||||
// of the Private Browsing version -- so it's quite important we do our best
|
||||
// to make sure one is available.
|
||||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1762994 for additional
|
||||
// background.
|
||||
{
|
||||
name: "ensurePrivateBrowsingShortcutExists",
|
||||
condition:
|
||||
AppConstants.platform == "win" &&
|
||||
Services.prefs.getBoolPref(
|
||||
"browser.privateWindowSeparation.enabled",
|
||||
true
|
||||
) &&
|
||||
// We don't want a shortcut if it's been disabled, eg: by enterprise policy.
|
||||
lazy.PrivateBrowsingUtils.enabled &&
|
||||
// Private Browsing shortcuts for packaged builds come with the package,
|
||||
// if they exist at all. We shouldn't try to create our own.
|
||||
!Services.sysinfo.getProperty("hasWinPackageId") &&
|
||||
// If we've ever done this successfully before, don't try again. The
|
||||
// user may have deleted the shortcut, and we don't want to force it
|
||||
// on them.
|
||||
!Services.prefs.getBoolPref(
|
||||
PREF_PRIVATE_BROWSING_SHORTCUT_CREATED,
|
||||
false
|
||||
),
|
||||
task: async () => {
|
||||
let shellService = Cc[
|
||||
"@mozilla.org/browser/shell-service;1"
|
||||
].getService(Ci.nsIWindowsShellService);
|
||||
let winTaskbar = Cc["@mozilla.org/windows-taskbar;1"].getService(
|
||||
Ci.nsIWinTaskbar
|
||||
);
|
||||
|
||||
if (
|
||||
!(await shellService.hasPinnableShortcut(
|
||||
winTaskbar.defaultPrivateGroupId,
|
||||
true
|
||||
))
|
||||
) {
|
||||
let appdir = Services.dirsvc.get("GreD", Ci.nsIFile);
|
||||
let exe = appdir.clone();
|
||||
exe.append(PRIVATE_BROWSING_BINARY);
|
||||
let strings = new Localization(
|
||||
["branding/brand.ftl", "browser/browser.ftl"],
|
||||
true
|
||||
);
|
||||
let [desc] = await strings.formatValues([
|
||||
"private-browsing-shortcut-text-2",
|
||||
]);
|
||||
await shellService.createShortcut(
|
||||
exe,
|
||||
[],
|
||||
desc,
|
||||
exe,
|
||||
// The code we're calling indexes from 0 instead of 1
|
||||
PRIVATE_BROWSING_EXE_ICON_INDEX - 1,
|
||||
winTaskbar.defaultPrivateGroupId,
|
||||
"Programs",
|
||||
desc + ".lnk",
|
||||
appdir
|
||||
);
|
||||
}
|
||||
// We always set this as long as no exception has been thrown. This
|
||||
// ensure that it is `true` both if we created one because it didn't
|
||||
// exist, or if it already existed (most likely because it was created
|
||||
// by the installer). This avoids the need to call `hasPinnableShortcut`
|
||||
// again, which necessarily does pointless I/O.
|
||||
Services.prefs.setBoolPref(
|
||||
PREF_PRIVATE_BROWSING_SHORTCUT_CREATED,
|
||||
true
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "BrowserGlue._maybeShowDefaultBrowserPrompt",
|
||||
task: () => {
|
||||
|
@ -1740,6 +1500,15 @@ BrowserGlue.prototype = {
|
|||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "Init hasSSD for SystemInfo",
|
||||
condition: AppConstants.platform == "win",
|
||||
// Initializes diskInfo to be able to get hasSSD which is part
|
||||
// of the PageLoad event. Only runs on windows, since diskInfo
|
||||
// is a no-op on other platforms
|
||||
task: () => Services.sysinfo.diskInfo,
|
||||
},
|
||||
|
||||
{
|
||||
name: "browser-startup-idle-tasks-finished",
|
||||
task: () => {
|
||||
|
|
|
@ -8,8 +8,6 @@ const lazy = {};
|
|||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
|
||||
SelectableProfileService:
|
||||
"resource:///modules/profiles/SelectableProfileService.sys.mjs",
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -597,29 +595,6 @@ let JSWINDOWACTORS = {
|
|||
},
|
||||
matches: ["about:editprofile", "about:deleteprofile", "about:newprofile"],
|
||||
remoteTypes: ["privilegedabout"],
|
||||
onAddActor(register, unregister) {
|
||||
let registered = false;
|
||||
|
||||
const maybeRegister = () => {
|
||||
let isEnabled = lazy.SelectableProfileService.isEnabled;
|
||||
|
||||
if (isEnabled && !registered) {
|
||||
register();
|
||||
} else if (!isEnabled && registered) {
|
||||
unregister();
|
||||
}
|
||||
|
||||
registered = isEnabled;
|
||||
};
|
||||
|
||||
// Defer all this logic until a little later in startup
|
||||
Services.obs.addObserver(() => {
|
||||
// Update when the pref changes
|
||||
lazy.SelectableProfileService.on("enableChanged", maybeRegister);
|
||||
|
||||
maybeRegister();
|
||||
}, "final-ui-startup");
|
||||
},
|
||||
},
|
||||
|
||||
Prompt: {
|
||||
|
|
|
@ -1114,6 +1114,30 @@ html {
|
|||
width: 30%;
|
||||
}
|
||||
}
|
||||
|
||||
.message-text.hero-text {
|
||||
margin-block: auto;
|
||||
margin-inline: 17% auto;
|
||||
text-align: start;
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
|
||||
h1 {
|
||||
font-weight: 510;
|
||||
font-size: 80px;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
margin: 0;
|
||||
margin-block-start: 24px;
|
||||
font-weight: 510;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.addons-picker-container {
|
||||
|
|
|
@ -432,20 +432,59 @@ export class ProtonScreen extends React.PureComponent {
|
|||
{content.hero_image ? (
|
||||
<HeroImage url={content.hero_image.url} />
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<div className="message-text">
|
||||
<div className="spacer-top" />
|
||||
<Localized text={content.hero_text}>
|
||||
<h1 />
|
||||
</Localized>
|
||||
<div className="spacer-bottom" />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
this.renderHeroText(content.hero_text)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderHeroText(hero_text) {
|
||||
if (!hero_text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if hero_text is a string or an object with string_id property
|
||||
// essentially checking if we're using old or new design
|
||||
const isSimpleText =
|
||||
typeof hero_text === "string" ||
|
||||
(typeof hero_text === "object" &&
|
||||
hero_text !== null &&
|
||||
"string_id" in hero_text);
|
||||
|
||||
const HeroTextWrapper = ({ children, className = "" }) => (
|
||||
<React.Fragment>
|
||||
<div className={`message-text ${className}`}>
|
||||
<div className="spacer-top" />
|
||||
{children}
|
||||
<div className="spacer-bottom" />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
if (isSimpleText) {
|
||||
return (
|
||||
<HeroTextWrapper>
|
||||
<Localized text={hero_text}>
|
||||
<h1 />
|
||||
</Localized>
|
||||
</HeroTextWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<HeroTextWrapper className="hero-text">
|
||||
<Localized text={hero_text.title}>
|
||||
<h1 />
|
||||
</Localized>
|
||||
{hero_text.subtitle && (
|
||||
<Localized text={hero_text.subtitle}>
|
||||
<h2 />
|
||||
</Localized>
|
||||
)}
|
||||
</HeroTextWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
renderOrderedContent(content) {
|
||||
const elements = [];
|
||||
for (const item of content) {
|
||||
|
|
|
@ -1258,15 +1258,38 @@ class ProtonScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
|
|||
role: "img"
|
||||
})), content.hero_image ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_HeroImage__WEBPACK_IMPORTED_MODULE_6__.HeroImage, {
|
||||
url: content.hero_image.url
|
||||
}) : /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||
className: "message-text"
|
||||
}) : this.renderHeroText(content.hero_text));
|
||||
}
|
||||
renderHeroText(hero_text) {
|
||||
if (!hero_text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if hero_text is a string or an object with string_id property
|
||||
// essentially checking if we're using old or new design
|
||||
const isSimpleText = typeof hero_text === "string" || typeof hero_text === "object" && hero_text !== null && "string_id" in hero_text;
|
||||
const HeroTextWrapper = ({
|
||||
children,
|
||||
className = ""
|
||||
}) => /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||
className: `message-text ${className}`
|
||||
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||
className: "spacer-top"
|
||||
}), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
|
||||
text: content.hero_text
|
||||
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("h1", null)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||
}), children, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||
className: "spacer-bottom"
|
||||
}))));
|
||||
})));
|
||||
if (isSimpleText) {
|
||||
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(HeroTextWrapper, null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
|
||||
text: hero_text
|
||||
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("h1", null)));
|
||||
}
|
||||
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(HeroTextWrapper, {
|
||||
className: "hero-text"
|
||||
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
|
||||
text: hero_text.title
|
||||
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("h1", null)), hero_text.subtitle && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
|
||||
text: hero_text.subtitle
|
||||
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("h2", null)));
|
||||
}
|
||||
renderOrderedContent(content) {
|
||||
const elements = [];
|
||||
|
|
|
@ -2153,6 +2153,27 @@ html {
|
|||
.onboardingContainer .screen[pos=split][fullscreen] .section-secondary .hero-image img {
|
||||
width: 30%;
|
||||
}
|
||||
.onboardingContainer .screen[pos=split][fullscreen] .section-secondary .message-text.hero-text {
|
||||
margin-block: auto;
|
||||
margin-inline: 17% auto;
|
||||
text-align: start;
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
}
|
||||
.onboardingContainer .screen[pos=split][fullscreen] .section-secondary .message-text.hero-text h1 {
|
||||
font-weight: 510;
|
||||
font-size: 80px;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
.onboardingContainer .screen[pos=split][fullscreen] .section-secondary .message-text.hero-text h2 {
|
||||
font-size: 20px;
|
||||
margin: 0;
|
||||
margin-block-start: 24px;
|
||||
font-weight: 510;
|
||||
width: 100%;
|
||||
}
|
||||
.onboardingContainer .screen[pos=split][fullscreen] .addons-picker-container {
|
||||
background: none;
|
||||
}
|
||||
|
|
|
@ -108,6 +108,149 @@ describe("MultiStageAboutWelcomeProton module", () => {
|
|||
assert.equal(wrapper.find("main").prop("pos"), "center");
|
||||
});
|
||||
|
||||
it("should render simple hero text if hero_text is a string or object without string_id", () => {
|
||||
// test simple hero text with hero_text string
|
||||
const STRING_HERO_TEXT_PROPS = {
|
||||
content: {
|
||||
position: "split",
|
||||
hero_text: "Simple hero text string",
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<MultiStageProtonScreen {...STRING_HERO_TEXT_PROPS} />
|
||||
);
|
||||
assert.ok(wrapper.exists());
|
||||
assert.equal(
|
||||
wrapper.find(".section-secondary h1").text(),
|
||||
"Simple hero text string"
|
||||
);
|
||||
|
||||
// test with simple hero text with hero_text string_id
|
||||
const STRING_ID_HERO_TEXT_PROPS = {
|
||||
content: {
|
||||
position: "split",
|
||||
hero_text: { string_id: "hero-text-id" },
|
||||
},
|
||||
};
|
||||
const stringIdWrapper = mount(
|
||||
<MultiStageProtonScreen {...STRING_ID_HERO_TEXT_PROPS} />
|
||||
);
|
||||
assert.ok(stringIdWrapper.exists());
|
||||
assert.equal(
|
||||
stringIdWrapper.find(".section-secondary h1").prop("data-l10n-id"),
|
||||
"hero-text-id"
|
||||
);
|
||||
|
||||
// test that we're not using the hero-text class
|
||||
assert.isFalse(
|
||||
wrapper.find(".section-secondary .hero-text").exists(),
|
||||
"Simple hero text should not use hero-text class"
|
||||
);
|
||||
});
|
||||
|
||||
it("should render complex hero text if hero text is an object with title property", () => {
|
||||
const COMPLEX_HERO_TEXT_PROPS = {
|
||||
content: {
|
||||
position: "split",
|
||||
hero_text: {
|
||||
title: "Test title",
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = mount(
|
||||
<MultiStageProtonScreen {...COMPLEX_HERO_TEXT_PROPS} />
|
||||
);
|
||||
assert.ok(wrapper.exists());
|
||||
|
||||
assert.isTrue(
|
||||
wrapper.find(".section-secondary .hero-text").exists(),
|
||||
"Text container should use hero-text class"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
wrapper.find(".section-secondary .hero-text h1").text(),
|
||||
"Test title"
|
||||
);
|
||||
|
||||
assert.isFalse(
|
||||
wrapper.find(".section-secondary .hero-text h2").exists(),
|
||||
"No subtitle should be rendered"
|
||||
);
|
||||
});
|
||||
|
||||
it("should render hero text subtitle if both title and subtitle properties are present", () => {
|
||||
const HERO_TEXT_WITH_SUBTITLE_PROPS = {
|
||||
content: {
|
||||
position: "split",
|
||||
hero_text: {
|
||||
title: "Title text",
|
||||
subtitle: "Subtitle text",
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = mount(
|
||||
<MultiStageProtonScreen {...HERO_TEXT_WITH_SUBTITLE_PROPS} />
|
||||
);
|
||||
assert.ok(wrapper.exists());
|
||||
|
||||
assert.isTrue(
|
||||
wrapper.find(".section-secondary .hero-text").exists(),
|
||||
"Complex hero text should use hero-text class"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
wrapper.find(".section-secondary .hero-text h1").text(),
|
||||
"Title text"
|
||||
);
|
||||
|
||||
assert.isTrue(
|
||||
wrapper.find(".section-secondary .hero-text h2").exists(),
|
||||
"Subtitle should be rendered when provided"
|
||||
);
|
||||
assert.equal(
|
||||
wrapper.find(".section-secondary .hero-text h2").text(),
|
||||
"Subtitle text"
|
||||
);
|
||||
});
|
||||
|
||||
it("should render hero text title and subtitle with localization if string ids are present", () => {
|
||||
const LOCALIZED_HERO_TEXT_PROPS = {
|
||||
content: {
|
||||
position: "split",
|
||||
hero_text: {
|
||||
title: { string_id: "hero-title-string-id" },
|
||||
subtitle: { string_id: "hero-subtitle-string-id" },
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = mount(
|
||||
<MultiStageProtonScreen {...LOCALIZED_HERO_TEXT_PROPS} />
|
||||
);
|
||||
assert.ok(wrapper.exists());
|
||||
|
||||
assert.isTrue(
|
||||
wrapper.find(".section-secondary .hero-text").exists(),
|
||||
"Text container should use hero-text class"
|
||||
);
|
||||
|
||||
const titleElement = wrapper.find(".section-secondary .hero-text h1");
|
||||
assert.isTrue(titleElement.exists(), "Title element should exist");
|
||||
assert.equal(
|
||||
titleElement.prop("data-l10n-id"),
|
||||
"hero-title-string-id",
|
||||
"Title should have correct string ID for localization"
|
||||
);
|
||||
|
||||
const subtitleElement = wrapper.find(".section-secondary .hero-text h2");
|
||||
assert.isTrue(subtitleElement.exists(), "Subtitle element should exist");
|
||||
assert.equal(
|
||||
subtitleElement.prop("data-l10n-id"),
|
||||
"hero-subtitle-string-id",
|
||||
"Subtitle should have correct string ID for localization"
|
||||
);
|
||||
});
|
||||
|
||||
it("should not render multiple action buttons if an additional button does not exist", () => {
|
||||
const SCREEN_PROPS = {
|
||||
content: {
|
||||
|
|
|
@ -548,10 +548,11 @@
|
|||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Should the message be global (persisted across tabs) or local (disappear when switching to a different tab).",
|
||||
"description": "Specifies where the message should appear and persist: 'global' (persists across tabs in the current window), 'tab' (only visible in the current tab), or 'universal' (visible across all tabs and windows, current and future).",
|
||||
"enum": [
|
||||
"global",
|
||||
"tab"
|
||||
"tab",
|
||||
"universal"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
|
@ -714,9 +715,29 @@
|
|||
"type": "string",
|
||||
"description": "URL for image to use with the content."
|
||||
},
|
||||
"imageVerticalOffset": {
|
||||
"imageWidth": {
|
||||
"type": "number",
|
||||
"description": "The margin-block-start value to apply to the image in pixels."
|
||||
"description": "The image width in pixels. Default is 120px."
|
||||
},
|
||||
"imageVerticalTopOffset": {
|
||||
"type": "number",
|
||||
"description": "The margin-block-start value to apply to the image in pixels, used in 'column' layouts."
|
||||
},
|
||||
"imageVerticalBottomOffset": {
|
||||
"type": "number",
|
||||
"description": "The margin-block-end value to apply to the image in pixels, used in 'row' layouts."
|
||||
},
|
||||
"containerVerticalBottomOffset": {
|
||||
"type": "number",
|
||||
"description": "The container's margin-block-end value in pixels. Used to visually offset the image in 'row' layouts when 'imageVerticalBottomOffset' is applied."
|
||||
},
|
||||
"layout": {
|
||||
"type": "string",
|
||||
"description": "The layout of the message content and illustration. Row displays the image to the inline to the right of the text, column displays the image above the text.",
|
||||
"enum": [
|
||||
"row",
|
||||
"column"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -11,8 +11,8 @@
|
|||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Should the message be global (persisted across tabs) or local (disappear when switching to a different tab).",
|
||||
"enum": ["global", "tab"]
|
||||
"description": "Specifies where the message should appear and persist: 'global' (persists across tabs in the current window), 'tab' (only visible in the current tab), or 'universal' (visible across all tabs and windows, current and future).",
|
||||
"enum": ["global", "tab", "universal"]
|
||||
},
|
||||
"text": {
|
||||
"$ref": "file:///FxMSCommon.schema.json#/$defs/localizableText",
|
||||
|
|
|
@ -64,9 +64,26 @@
|
|||
"type": "string",
|
||||
"description": "URL for image to use with the content."
|
||||
},
|
||||
"imageVerticalOffset": {
|
||||
"imageWidth": {
|
||||
"type": "number",
|
||||
"description": "The margin-block-start value to apply to the image in pixels."
|
||||
"description": "The image width in pixels. Default is 120px."
|
||||
},
|
||||
"imageVerticalTopOffset": {
|
||||
"type": "number",
|
||||
"description": "The margin-block-start value to apply to the image in pixels, used in 'column' layouts."
|
||||
},
|
||||
"imageVerticalBottomOffset": {
|
||||
"type": "number",
|
||||
"description": "The margin-block-end value to apply to the image in pixels, used in 'row' layouts."
|
||||
},
|
||||
"containerVerticalBottomOffset": {
|
||||
"type": "number",
|
||||
"description": "The container's margin-block-end value in pixels. Used to visually offset the image in 'row' layouts when 'imageVerticalBottomOffset' is applied."
|
||||
},
|
||||
"layout": {
|
||||
"type": "string",
|
||||
"description": "The layout of the message content and illustration. Row displays the image to the inline to the right of the text, column displays the image above the text.",
|
||||
"enum": ["row", "column"]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
:host {
|
||||
margin-inline: var(--arrowpanel-menuitem-margin-inline, var(--space-xsmall));
|
||||
margin-block: var(--space-xsmall);
|
||||
--illustration-margin-block-offset: 0px;
|
||||
--illustration-margin-block-start-offset: 0px;
|
||||
--illustration-margin-block-end-offset: 0px;
|
||||
--container-margin-block-end-offset: 0px;
|
||||
}
|
||||
|
||||
#container {
|
||||
|
@ -20,26 +22,50 @@
|
|||
color: var(--text-color);
|
||||
}
|
||||
|
||||
#container[layout="row"] {
|
||||
margin-block-end: var(--container-margin-block-end-offset);
|
||||
}
|
||||
|
||||
#container[layout="row"] #bottom-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#container[layout="row"] #body-container {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#close-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-end: 0;
|
||||
padding-block: var(--arrowpanel-menuitem-padding-block, var(--space-small));
|
||||
padding-inline: var(--arrowpanel-menuitem-padding-inline, var(--space-small));
|
||||
}
|
||||
|
||||
#container:not([has-image]) > #illustration-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#container[has-image] > #illustration-container {
|
||||
#container[has-image][layout="column"] > #illustration-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-block-start: var(--illustration-margin-block-offset);
|
||||
margin-block-start: var(--illustration-margin-block-start-offset);
|
||||
margin-block-end: var(--space-xsmall);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#container[has-image][layout="row"] #illustration-container {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-end;
|
||||
margin-block-end: var(--illustration-margin-block-end-offset);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#container[has-image] #illustration-container img{
|
||||
width: var(--image-width, 120px);
|
||||
}
|
||||
|
||||
#primary {
|
||||
font-size: 1.154em;
|
||||
font-weight: var(--font-weight-bold);
|
||||
|
|
|
@ -21,6 +21,7 @@ export default class FxAMenuMessage extends MozLitElement {
|
|||
buttonText: { type: String },
|
||||
primaryText: { type: String },
|
||||
secondaryText: { type: String },
|
||||
layout: { type: String, reflect: true },
|
||||
};
|
||||
static queries = {
|
||||
signUpButton: "#sign-up-button",
|
||||
|
@ -29,6 +30,7 @@ export default class FxAMenuMessage extends MozLitElement {
|
|||
|
||||
constructor() {
|
||||
super();
|
||||
this.layout = "column"; // Default layout
|
||||
this.addEventListener(
|
||||
"keydown",
|
||||
event => {
|
||||
|
@ -69,39 +71,86 @@ export default class FxAMenuMessage extends MozLitElement {
|
|||
});
|
||||
}
|
||||
|
||||
get isRowLayout() {
|
||||
return this.layout === "row";
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<link
|
||||
return html`<link
|
||||
rel="stylesheet"
|
||||
href="chrome://browser/content/asrouter/components/fxa-menu-message.css"
|
||||
/>
|
||||
<div id="container" ?has-image=${this.imageURL}>
|
||||
<moz-button
|
||||
id="close-button"
|
||||
@click=${this.handleClose}
|
||||
type="ghost"
|
||||
iconsrc="chrome://global/skin/icons/close-12.svg"
|
||||
tabindex="2"
|
||||
data-l10n-id="fxa-menu-message-close-button"
|
||||
>
|
||||
</moz-button>
|
||||
<div id="illustration-container">
|
||||
<img id="illustration" role="presentation" src=${this.imageURL} />
|
||||
</div>
|
||||
<div id="primary">${this.primaryText}</div>
|
||||
<div id="secondary">${this.secondaryText}</div>
|
||||
<moz-button
|
||||
id="sign-up-button"
|
||||
@click=${this.handleSignUp}
|
||||
type="primary"
|
||||
tabindex="1"
|
||||
autofocus
|
||||
title=${this.buttonText}
|
||||
aria-label=${this.buttonText}
|
||||
>${this.buttonText}</moz-button
|
||||
>
|
||||
</div>
|
||||
`;
|
||||
<div id="container" layout=${this.layout} ?has-image=${this.imageURL}>
|
||||
${this.isRowLayout
|
||||
? html`
|
||||
<div id="top-row">
|
||||
<moz-button
|
||||
id="close-button"
|
||||
@click=${this.handleClose}
|
||||
type="ghost"
|
||||
iconsrc="chrome://global/skin/icons/close-12.svg"
|
||||
tabindex="2"
|
||||
data-l10n-id="fxa-menu-message-close-button"
|
||||
>
|
||||
</moz-button>
|
||||
<div id="primary">${this.primaryText}</div>
|
||||
</div>
|
||||
<div id="bottom-row">
|
||||
<div id="body-container">
|
||||
<div id="secondary">${this.secondaryText}</div>
|
||||
<moz-button
|
||||
id="sign-up-button"
|
||||
@click=${this.handleSignUp}
|
||||
type="primary"
|
||||
tabindex="1"
|
||||
autofocus
|
||||
title=${this.buttonText}
|
||||
aria-label=${this.buttonText}
|
||||
>
|
||||
${this.buttonText}
|
||||
</moz-button>
|
||||
</div>
|
||||
<div id="illustration-container">
|
||||
<img
|
||||
id="illustration"
|
||||
role="presentation"
|
||||
src=${this.imageURL}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<moz-button
|
||||
id="close-button"
|
||||
@click=${this.handleClose}
|
||||
type="ghost"
|
||||
iconsrc="chrome://global/skin/icons/close-12.svg"
|
||||
tabindex="2"
|
||||
data-l10n-id="fxa-menu-message-close-button"
|
||||
>
|
||||
</moz-button>
|
||||
<div id="illustration-container">
|
||||
<img
|
||||
id="illustration"
|
||||
role="presentation"
|
||||
src=${this.imageURL}
|
||||
/>
|
||||
</div>
|
||||
<div id="primary">${this.primaryText}</div>
|
||||
<div id="secondary">${this.secondaryText}</div>
|
||||
<moz-button
|
||||
id="sign-up-button"
|
||||
@click=${this.handleSignUp}
|
||||
type="primary"
|
||||
tabindex="1"
|
||||
autofocus
|
||||
title=${this.buttonText}
|
||||
aria-label=${this.buttonText}
|
||||
>
|
||||
${this.buttonText}
|
||||
</moz-button>
|
||||
`}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,11 @@ const Template = ({
|
|||
imageURL,
|
||||
primaryText,
|
||||
secondaryText,
|
||||
imageVerticalOffset,
|
||||
imageVerticalTopOffset,
|
||||
imageVerticalBottomOffset,
|
||||
containerVerticalBottomOffset,
|
||||
layout,
|
||||
imageHeight,
|
||||
}) => html`
|
||||
<moz-card style="width: 22.5rem;">
|
||||
<fxa-menu-message
|
||||
|
@ -26,7 +30,13 @@ const Template = ({
|
|||
primaryText=${primaryText}
|
||||
secondaryText=${secondaryText}
|
||||
imageURL=${imageURL}
|
||||
style="--illustration-margin-block-offset: ${imageVerticalOffset}px"
|
||||
style="
|
||||
--illustration-margin-block-start-offset: ${imageVerticalTopOffset}px;
|
||||
--illustration-margin-block-end-offset: ${imageVerticalBottomOffset}px;
|
||||
--container-margin-block-end-offset: ${containerVerticalBottomOffset}px;
|
||||
--image-height: ${imageHeight}px;
|
||||
"
|
||||
layout=${layout}
|
||||
>
|
||||
</fxa-menu-message>
|
||||
</moz-card>
|
||||
|
@ -40,5 +50,9 @@ Default.args = {
|
|||
primaryText: "Bounce between devices",
|
||||
secondaryText:
|
||||
"Sync and encrypt your bookmarks, passwords, and more on all your devices.",
|
||||
imageVerticalOffset: -20,
|
||||
imageVerticalTopOffset: -20,
|
||||
imageVerticalBottomOffset: 0,
|
||||
containerVerticalBottomOffset: 0,
|
||||
layout: "column",
|
||||
imageHeight: 120,
|
||||
};
|
||||
|
|
|
@ -177,9 +177,9 @@ XPCOMUtils.defineLazyPreferenceGetter(
|
|||
);
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
lazy,
|
||||
"profileStoreID",
|
||||
"toolkit.profiles.storeID",
|
||||
null
|
||||
"profilesCreated",
|
||||
"browser.profiles.created",
|
||||
false
|
||||
);
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
lazy,
|
||||
|
@ -616,7 +616,7 @@ const TargetingGetters = {
|
|||
return lazy.SelectableProfileService?.isEnabled ?? false;
|
||||
},
|
||||
get hasSelectableProfiles() {
|
||||
return !!lazy.profileStoreID;
|
||||
return lazy.profilesCreated;
|
||||
},
|
||||
get profileAgeCreated() {
|
||||
return lazy.ProfileAge().then(times => times.created);
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* eslint-disable no-use-before-define */
|
||||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
|
@ -11,6 +12,11 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
|||
"resource://messaging-system/lib/SpecialMessageActions.sys.mjs",
|
||||
});
|
||||
|
||||
const TYPES = {
|
||||
UNIVERSAL: "universal",
|
||||
GLOBAL: "global",
|
||||
};
|
||||
|
||||
class InfoBarNotification {
|
||||
constructor(message, dispatch) {
|
||||
this._dispatch = dispatch;
|
||||
|
@ -31,7 +37,7 @@ class InfoBarNotification {
|
|||
let { gBrowser } = browser.ownerGlobal;
|
||||
let doc = gBrowser.ownerDocument;
|
||||
let notificationContainer;
|
||||
if (content.type === "global") {
|
||||
if ([TYPES.GLOBAL, TYPES.UNIVERSAL].includes(content.type)) {
|
||||
notificationContainer = browser.ownerGlobal.gNotificationBox;
|
||||
} else {
|
||||
notificationContainer = gBrowser.getNotificationBox(browser);
|
||||
|
@ -51,8 +57,21 @@ class InfoBarNotification {
|
|||
false,
|
||||
content.dismissable
|
||||
);
|
||||
// If InfoBar is universal, only record an impression for the first
|
||||
// instance.
|
||||
if (
|
||||
content.type !== TYPES.UNIVERSAL ||
|
||||
!InfoBar._universalInfobars.length
|
||||
) {
|
||||
this.addImpression();
|
||||
}
|
||||
|
||||
this.addImpression();
|
||||
if (content.type === TYPES.UNIVERSAL) {
|
||||
InfoBar._universalInfobars.push({
|
||||
box: browser.ownerGlobal.gNotificationBox,
|
||||
notification: this.notification,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
formatMessageConfig(doc, browser, content) {
|
||||
|
@ -145,16 +164,36 @@ class InfoBarNotification {
|
|||
* Called when interacting with the toolbar (but not through the buttons)
|
||||
*/
|
||||
infobarCallback(eventType) {
|
||||
const wasUniversal =
|
||||
InfoBar._activeInfobar?.message.content.type === TYPES.UNIVERSAL;
|
||||
if (eventType === "removed") {
|
||||
this.notification = null;
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
InfoBar._activeInfobar = null;
|
||||
} else if (this.notification) {
|
||||
this.sendUserEventTelemetry("DISMISSED");
|
||||
this.notification = null;
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
InfoBar._activeInfobar = null;
|
||||
}
|
||||
// If one instance of universal infobar is removed, remove all instances and
|
||||
// the new window observer
|
||||
if (wasUniversal) {
|
||||
this.removeUniversalInfobars();
|
||||
}
|
||||
}
|
||||
|
||||
removeUniversalInfobars() {
|
||||
try {
|
||||
Services.obs.removeObserver(InfoBar, "domwindowopened");
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Error removing domwindowopened observer on InfoBar:",
|
||||
error
|
||||
);
|
||||
}
|
||||
InfoBar._universalInfobars.forEach(({ box, notification }) => {
|
||||
box.removeNotification(notification);
|
||||
});
|
||||
InfoBar._universalInfobars = [];
|
||||
}
|
||||
|
||||
sendUserEventTelemetry(event) {
|
||||
|
@ -171,6 +210,7 @@ class InfoBarNotification {
|
|||
|
||||
export const InfoBar = {
|
||||
_activeInfobar: null,
|
||||
_universalInfobars: [],
|
||||
|
||||
maybeLoadCustomElement(win) {
|
||||
if (!win.customElements.get("remote-text")) {
|
||||
|
@ -188,12 +228,22 @@ export const InfoBar = {
|
|||
);
|
||||
},
|
||||
|
||||
async showInfoBarMessage(browser, message, dispatch) {
|
||||
async showNotificationAllWindows(notification) {
|
||||
for (let win of Services.wm.getEnumerator(null)) {
|
||||
const browser = win.gBrowser.selectedBrowser;
|
||||
await notification.showNotification(browser);
|
||||
}
|
||||
},
|
||||
|
||||
async showInfoBarMessage(browser, message, dispatch, universalInNewWin) {
|
||||
// Prevent stacking multiple infobars
|
||||
if (this._activeInfobar) {
|
||||
if (this._activeInfobar && !universalInNewWin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if this is the first instance of a universal infobar
|
||||
const isFirstUniversal =
|
||||
!universalInNewWin && message.content.type === TYPES.UNIVERSAL;
|
||||
const win = browser?.ownerGlobal;
|
||||
|
||||
if (!win || lazy.PrivateBrowsingUtils.isWindowPrivate(win)) {
|
||||
|
@ -204,9 +254,39 @@ export const InfoBar = {
|
|||
this.maybeInsertFTL(win);
|
||||
|
||||
let notification = new InfoBarNotification(message, dispatch);
|
||||
await notification.showNotification(browser);
|
||||
this._activeInfobar = true;
|
||||
if (isFirstUniversal) {
|
||||
await this.showNotificationAllWindows(notification);
|
||||
Services.obs.addObserver(this, "domwindowopened");
|
||||
} else {
|
||||
await notification.showNotification(browser);
|
||||
}
|
||||
if (!universalInNewWin) {
|
||||
this._activeInfobar = { message, dispatch };
|
||||
}
|
||||
|
||||
return notification;
|
||||
},
|
||||
|
||||
observe(aSubject, aTopic) {
|
||||
const { message, dispatch } = this._activeInfobar;
|
||||
if (
|
||||
aTopic !== "domwindowopened" ||
|
||||
message?.content.type !== TYPES.UNIVERSAL
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (aSubject.document.readyState === "complete") {
|
||||
let browser = aSubject.gBrowser.selectedBrowser;
|
||||
this.showInfoBarMessage(browser, message, dispatch, true);
|
||||
} else {
|
||||
aSubject.addEventListener(
|
||||
"load",
|
||||
() => {
|
||||
let browser = aSubject.gBrowser.selectedBrowser;
|
||||
this.showInfoBarMessage(browser, message, dispatch, true);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -181,6 +181,7 @@ export const MenuMessage = {
|
|||
win.MozXULElement.insertFTLIfNeeded("browser/newtab/asrouter.ftl");
|
||||
|
||||
const msgElement = document.createElement("fxa-menu-message");
|
||||
msgElement.layout = message.content.layout ?? "column";
|
||||
msgElement.imageURL = message.content.imageURL;
|
||||
msgElement.buttonText = await lazy.RemoteL10n.formatLocalizableText(
|
||||
message.content.primaryActionText
|
||||
|
@ -192,9 +193,23 @@ export const MenuMessage = {
|
|||
message.content.secondaryText
|
||||
);
|
||||
msgElement.dataset.navigableWithTabOnly = "true";
|
||||
if (message.content.imageWidth !== undefined) {
|
||||
msgElement.style.setProperty(
|
||||
"--image-width",
|
||||
`${message.content.imageWidth}px`
|
||||
);
|
||||
}
|
||||
msgElement.style.setProperty(
|
||||
"--illustration-margin-block-offset",
|
||||
`${message.content.imageVerticalOffset}px`
|
||||
"--illustration-margin-block-start-offset",
|
||||
`${message.content.imageVerticalTopOffset}px`
|
||||
);
|
||||
msgElement.style.setProperty(
|
||||
"--illustration-margin-block-end-offset",
|
||||
`${message.content.imageVerticalBottomOffset}px`
|
||||
);
|
||||
msgElement.style.setProperty(
|
||||
"--container-margin-block-end-offset",
|
||||
`${message.content.containerVerticalBottomOffset}px`
|
||||
);
|
||||
|
||||
msgElement.addEventListener("FxAMenuMessage:Close", () => {
|
||||
|
|
|
@ -1534,7 +1534,7 @@ const MESSAGES = () => [
|
|||
},
|
||||
imageURL:
|
||||
"chrome://browser/content/asrouter/assets/fox-with-box-on-cloud.svg",
|
||||
imageVerticalOffset: -20,
|
||||
imageVerticalTopOffset: -20,
|
||||
},
|
||||
skip_in_tests: "TODO",
|
||||
trigger: {
|
||||
|
|
|
@ -45,6 +45,8 @@ tags = "remote-settings"
|
|||
tags = "remote-settings"
|
||||
skip-if = ["a11y_checks"] # Bug 1854515 and 1858041 to investigate intermittent a11y_checks results (fails on Autoland, passes on Try)
|
||||
|
||||
["browser_asrouter_universal_infobar.js"]
|
||||
|
||||
["browser_bookmarks_bar_button.js"]
|
||||
|
||||
["browser_feature_callout.js"]
|
||||
|
|
|
@ -368,3 +368,28 @@ add_task(async function test_specialMessageAction_onLinkClick() {
|
|||
|
||||
handleStub.restore();
|
||||
});
|
||||
|
||||
add_task(async function test_showInfoBarMessage_skipsPrivateWindow() {
|
||||
const { PrivateBrowsingUtils } = ChromeUtils.importESModule(
|
||||
"resource://gre/modules/PrivateBrowsingUtils.sys.mjs"
|
||||
);
|
||||
sinon.stub(PrivateBrowsingUtils, "isWindowPrivate").returns(true);
|
||||
|
||||
let browser = BrowserWindowTracker.getTopWindow().gBrowser.selectedBrowser;
|
||||
let dispatch = sinon.stub();
|
||||
|
||||
let result = await InfoBar.showInfoBarMessage(
|
||||
browser,
|
||||
{
|
||||
id: "Private Win Test",
|
||||
content: { type: "global", buttons: [], text: "t", dismissable: true },
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
Assert.equal(result, null);
|
||||
Assert.equal(dispatch.callCount, 0);
|
||||
|
||||
// Cleanup
|
||||
sinon.restore();
|
||||
});
|
||||
|
|
|
@ -122,10 +122,18 @@ async function assertMessageInMenuSource(source, message, win = window) {
|
|||
"The element should be configured for tab navigation."
|
||||
);
|
||||
|
||||
Assert.equal(
|
||||
messageEl.layout,
|
||||
"column",
|
||||
"The default layout should be 'column'."
|
||||
);
|
||||
|
||||
let messageElStyles = window.getComputedStyle(messageEl);
|
||||
Assert.equal(
|
||||
messageElStyles.getPropertyValue("--illustration-margin-block-offset"),
|
||||
`${message.content.imageVerticalOffset}px`
|
||||
messageElStyles.getPropertyValue(
|
||||
"--illustration-margin-block-start-offset"
|
||||
),
|
||||
`${message.content.imageVerticalTopOffset}px`
|
||||
);
|
||||
|
||||
if (source === MenuMessage.SOURCES.APP_MENU) {
|
||||
|
|
|
@ -221,6 +221,12 @@ add_task(async function check_canCreateSelectableProfiles() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Reset profiles prefs
|
||||
await pushPrefs(
|
||||
["browser.profiles.enabled", false],
|
||||
["browser.profiles.created", false]
|
||||
);
|
||||
|
||||
is(
|
||||
await ASRouterTargeting.Environment.canCreateSelectableProfiles,
|
||||
false,
|
||||
|
@ -228,8 +234,13 @@ add_task(async function check_canCreateSelectableProfiles() {
|
|||
);
|
||||
|
||||
// We have to fake there being a real profile available and enable the profiles feature
|
||||
await pushPrefs(["browser.profiles.enabled", "someValue"]);
|
||||
await SelectableProfileService.resetProfileService({ currentProfile: {} });
|
||||
await pushPrefs(
|
||||
["browser.profiles.enabled", true],
|
||||
["browser.profiles.created", false]
|
||||
);
|
||||
await ProfilesDatastoreService.resetProfileService({ currentProfile: {} });
|
||||
await SelectableProfileService.uninit();
|
||||
await SelectableProfileService.init();
|
||||
|
||||
is(
|
||||
await ASRouterTargeting.Environment.canCreateSelectableProfiles,
|
||||
|
@ -244,7 +255,7 @@ add_task(async function check_canCreateSelectableProfiles() {
|
|||
"should select correct item by canCreateSelectableProfiles"
|
||||
);
|
||||
|
||||
await SelectableProfileService.resetProfileService(null);
|
||||
await ProfilesDatastoreService.resetProfileService(null);
|
||||
});
|
||||
|
||||
add_task(async function check_hasSelectableProfiles() {
|
||||
|
@ -254,7 +265,7 @@ add_task(async function check_hasSelectableProfiles() {
|
|||
"should return false before the pref is set"
|
||||
);
|
||||
|
||||
await pushPrefs(["toolkit.profiles.storeID", "someValue"]);
|
||||
await pushPrefs(["browser.profiles.created", true]);
|
||||
is(
|
||||
await ASRouterTargeting.Environment.hasSelectableProfiles,
|
||||
true,
|
||||
|
|
|
@ -0,0 +1,263 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { InfoBar } = ChromeUtils.importESModule(
|
||||
"resource:///modules/asrouter/InfoBar.sys.mjs"
|
||||
);
|
||||
const { CFRMessageProvider } = ChromeUtils.importESModule(
|
||||
"resource:///modules/asrouter/CFRMessageProvider.sys.mjs"
|
||||
);
|
||||
const { ASRouter } = ChromeUtils.importESModule(
|
||||
"resource:///modules/asrouter/ASRouter.sys.mjs"
|
||||
);
|
||||
|
||||
const UNIVERSAL_MESSAGE = {
|
||||
id: "universal-infobar",
|
||||
content: {
|
||||
type: "universal",
|
||||
text: "t",
|
||||
buttons: [],
|
||||
},
|
||||
};
|
||||
|
||||
const cleanupInfobars = () => {
|
||||
InfoBar._universalInfobars = [];
|
||||
InfoBar._activeInfobar = null;
|
||||
};
|
||||
|
||||
add_task(async function showNotificationAllWindows() {
|
||||
let fakeNotification = { showNotification: sinon.stub().resolves() };
|
||||
let fakeWins = [
|
||||
{ gBrowser: { selectedBrowser: "win1" } },
|
||||
{ gBrowser: { selectedBrowser: "win2" } },
|
||||
{ gBrowser: { selectedBrowser: "win3" } },
|
||||
];
|
||||
|
||||
let origWinManager = Services.wm;
|
||||
// Using sinon.stub won’t work here, because Services.wm is a frozen,
|
||||
// non-configurable object and its methods cannot be replaced via typical JS
|
||||
// property assignment.
|
||||
Object.defineProperty(Services, "wm", {
|
||||
value: { getEnumerator: () => fakeWins[Symbol.iterator]() },
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
await InfoBar.showNotificationAllWindows(fakeNotification);
|
||||
|
||||
Assert.equal(fakeNotification.showNotification.callCount, 3);
|
||||
Assert.ok(fakeNotification.showNotification.calledWith("win1"));
|
||||
Assert.ok(fakeNotification.showNotification.calledWith("win2"));
|
||||
Assert.ok(fakeNotification.showNotification.calledWith("win3"));
|
||||
|
||||
// Cleanup
|
||||
cleanupInfobars();
|
||||
sinon.restore();
|
||||
Object.defineProperty(Services, "wm", {
|
||||
value: origWinManager,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function removeUniversalInfobars() {
|
||||
let browser = BrowserWindowTracker.getTopWindow().gBrowser.selectedBrowser;
|
||||
let origBox = browser.ownerGlobal.gNotificationBox;
|
||||
browser.ownerGlobal.gNotificationBox = {
|
||||
appendNotification: sinon.stub().resolves({}),
|
||||
removeNotification: sinon.stub(),
|
||||
};
|
||||
|
||||
sinon
|
||||
.stub(InfoBar, "showNotificationAllWindows")
|
||||
.callsFake(async notification => {
|
||||
await notification.showNotification(browser);
|
||||
});
|
||||
|
||||
let notification = await InfoBar.showInfoBarMessage(
|
||||
browser,
|
||||
UNIVERSAL_MESSAGE,
|
||||
sinon.stub()
|
||||
);
|
||||
|
||||
Assert.equal(InfoBar._universalInfobars.length, 1);
|
||||
notification.removeUniversalInfobars();
|
||||
|
||||
Assert.ok(
|
||||
browser.ownerGlobal.gNotificationBox.removeNotification.calledWith(
|
||||
notification.notification
|
||||
)
|
||||
);
|
||||
|
||||
Assert.deepEqual(InfoBar._universalInfobars, []);
|
||||
|
||||
// Cleanup
|
||||
cleanupInfobars();
|
||||
browser.ownerGlobal.gNotificationBox = origBox;
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
add_task(async function initialUniversal_showsAllWindows_andSendsTelemetry() {
|
||||
let browser = BrowserWindowTracker.getTopWindow().gBrowser.selectedBrowser;
|
||||
let origBox = browser.ownerGlobal.gNotificationBox;
|
||||
browser.ownerGlobal.gNotificationBox = {
|
||||
appendNotification: sinon.stub().resolves({}),
|
||||
removeNotification: sinon.stub(),
|
||||
};
|
||||
|
||||
let showAll = sinon
|
||||
.stub(InfoBar, "showNotificationAllWindows")
|
||||
.callsFake(async notification => {
|
||||
await notification.showNotification(browser);
|
||||
});
|
||||
|
||||
let dispatch1 = sinon.stub();
|
||||
let dispatch2 = sinon.stub();
|
||||
|
||||
await InfoBar.showInfoBarMessage(browser, UNIVERSAL_MESSAGE, dispatch1);
|
||||
await InfoBar.showInfoBarMessage(browser, UNIVERSAL_MESSAGE, dispatch2, true);
|
||||
|
||||
Assert.ok(showAll.calledOnce);
|
||||
Assert.equal(InfoBar._universalInfobars.length, 2);
|
||||
|
||||
// Dispatch impression (as this is the first universal infobar) and telemetry
|
||||
// ping
|
||||
Assert.equal(dispatch1.callCount, 2);
|
||||
|
||||
// Do not send telemetry for subsequent appearance of the message
|
||||
Assert.equal(dispatch2.callCount, 0);
|
||||
|
||||
// Cleanup
|
||||
cleanupInfobars();
|
||||
browser.ownerGlobal.gNotificationBox = origBox;
|
||||
sinon.restore();
|
||||
Services.obs.removeObserver(InfoBar, "domwindowopened");
|
||||
});
|
||||
|
||||
add_task(async function observe_domwindowopened_withLoadEvent() {
|
||||
let stub = sinon.stub(InfoBar, "showInfoBarMessage").resolves();
|
||||
|
||||
InfoBar._activeInfobar = {
|
||||
message: { content: { type: "universal" } },
|
||||
dispatch: sinon.stub(),
|
||||
};
|
||||
|
||||
let subject = {
|
||||
document: { readyState: "loading" },
|
||||
gBrowser: { selectedBrowser: "b" },
|
||||
addEventListener(event, cb) {
|
||||
subject.document.readyState = "complete";
|
||||
cb();
|
||||
},
|
||||
};
|
||||
|
||||
InfoBar.observe(subject, "domwindowopened");
|
||||
|
||||
Assert.ok(stub.calledOnce);
|
||||
// Called with universalInNewWin true
|
||||
Assert.equal(stub.firstCall.args[3], true);
|
||||
|
||||
// Cleanup
|
||||
cleanupInfobars();
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
add_task(async function observe_domwindowopened() {
|
||||
let stub = sinon.stub(InfoBar, "showInfoBarMessage").resolves();
|
||||
|
||||
InfoBar._activeInfobar = {
|
||||
message: { content: { type: "universal" } },
|
||||
dispatch: sinon.stub(),
|
||||
};
|
||||
|
||||
let win = BrowserWindowTracker.getTopWindow();
|
||||
InfoBar.observe(win, "domwindowopened");
|
||||
|
||||
Assert.ok(stub.calledOnce);
|
||||
Assert.equal(stub.firstCall.args[3], true);
|
||||
|
||||
// Cleanup
|
||||
cleanupInfobars();
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
add_task(async function observe_skips_nonUniversal() {
|
||||
let stub = sinon.stub(InfoBar, "showInfoBarMessage").resolves();
|
||||
InfoBar._activeInfobar = {
|
||||
message: { content: { type: "global" } },
|
||||
dispatch: sinon.stub(),
|
||||
};
|
||||
InfoBar.observe({}, "domwindowopened");
|
||||
Assert.ok(stub.notCalled);
|
||||
|
||||
// Cleanup
|
||||
cleanupInfobars();
|
||||
stub.restore();
|
||||
});
|
||||
|
||||
add_task(async function infobarCallback_dismissed_universal() {
|
||||
const browser = BrowserWindowTracker.getTopWindow().gBrowser.selectedBrowser;
|
||||
const dispatch = sinon.stub();
|
||||
|
||||
sinon
|
||||
.stub(InfoBar, "showNotificationAllWindows")
|
||||
.callsFake(async notif => await notif.showNotification(browser));
|
||||
|
||||
let infobar = await InfoBar.showInfoBarMessage(
|
||||
browser,
|
||||
UNIVERSAL_MESSAGE,
|
||||
dispatch
|
||||
);
|
||||
// Reset the dispatch count to just watch for the DISMISSED ping
|
||||
dispatch.reset();
|
||||
|
||||
infobar.infobarCallback("not‑removed‑event");
|
||||
|
||||
Assert.equal(dispatch.callCount, 1);
|
||||
Assert.equal(dispatch.firstCall.args[0].data.event, "DISMISSED");
|
||||
Assert.deepEqual(InfoBar._universalInfobars, []);
|
||||
|
||||
// Cleanup
|
||||
cleanupInfobars();
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
add_task(async function removeObserver_on_removeUniversalInfobars() {
|
||||
const sandbox = sinon.createSandbox();
|
||||
|
||||
sandbox.stub(InfoBar, "showNotificationAllWindows").resolves();
|
||||
|
||||
let browser = BrowserWindowTracker.getTopWindow().gBrowser.selectedBrowser;
|
||||
let dispatch = sandbox.stub();
|
||||
|
||||
// Show the universal infobar so it registers the observer
|
||||
let infobar = await InfoBar.showInfoBarMessage(
|
||||
browser,
|
||||
UNIVERSAL_MESSAGE,
|
||||
dispatch
|
||||
);
|
||||
Assert.ok(infobar, "Got an InfoBar notification");
|
||||
|
||||
// Swap out Services.obs so removeObserver is spyable
|
||||
let origObs = Services.obs;
|
||||
let removeSpy = sandbox.spy();
|
||||
Services.obs = {
|
||||
addObserver: origObs.addObserver.bind(origObs),
|
||||
removeObserver: removeSpy,
|
||||
notifyObservers: origObs.notifyObservers.bind(origObs),
|
||||
};
|
||||
|
||||
infobar.removeUniversalInfobars();
|
||||
|
||||
Assert.ok(
|
||||
removeSpy.calledWith(InfoBar, "domwindowopened"),
|
||||
"removeObserver was invoked for domwindowopened"
|
||||
);
|
||||
|
||||
// Cleanup
|
||||
Services.obs = origObs;
|
||||
sandbox.restore();
|
||||
cleanupInfobars();
|
||||
});
|
|
@ -19,7 +19,7 @@
|
|||
* Tests that the exposed properties of the component are reflected
|
||||
* as expected in the shadow DOM.
|
||||
*/
|
||||
add_task(async function test_properties() {
|
||||
add_task(async function test_exposed_properties() {
|
||||
let message = document.createElement("fxa-menu-message");
|
||||
const TEST_IMAGE_URL = "chrome://activity-stream/content/data/content/assets/fox-doodle-waving-static.png";
|
||||
message.imageURL = TEST_IMAGE_URL;
|
||||
|
@ -39,7 +39,7 @@
|
|||
is(image.src, TEST_IMAGE_URL, "The img element got the expected URL");
|
||||
|
||||
let button = shadow.querySelector("#sign-up-button");
|
||||
is(button.textContent, TEST_BUTTON_TEXT, "The sign-up button got the right text.");
|
||||
is(button.textContent.trim(), TEST_BUTTON_TEXT, "The sign-up button got the right text.");
|
||||
|
||||
let primaryText = shadow.querySelector("#primary");
|
||||
is(primaryText.textContent, PRIMARY_TEXT, "The primary text was correct.");
|
||||
|
@ -47,6 +47,14 @@
|
|||
let secondaryText = shadow.querySelector("#secondary");
|
||||
is(secondaryText.textContent, SECONDARY_TEXT, "The secondary text was correct.");
|
||||
|
||||
let container = shadow.querySelector("#container");
|
||||
is(container.getAttribute("layout"), "column", "The layout should default to 'column' when no layout is explicitly set.");
|
||||
|
||||
const TEST_ROW_LAYOUT = "row";
|
||||
message.layout = TEST_ROW_LAYOUT
|
||||
await message.updateComplete;
|
||||
is(container.getAttribute("layout"), TEST_ROW_LAYOUT, "The layout attribute should update to 'row' when set.");
|
||||
|
||||
message.remove();
|
||||
});
|
||||
|
||||
|
@ -54,7 +62,7 @@
|
|||
* Tests that the buttons exposed by the component emit the expected
|
||||
* events.
|
||||
*/
|
||||
add_task(async function test_properties() {
|
||||
add_task(async function test_button_properties() {
|
||||
let message = document.createElement("fxa-menu-message");
|
||||
const TEST_BUTTON_TEXT = "Howdy, partner! Sign up!";
|
||||
message.buttonText = TEST_BUTTON_TEXT;
|
||||
|
@ -84,7 +92,7 @@
|
|||
* Tests that the sign-up button is focused by default, and that focus
|
||||
* can be changed via the keyboard.
|
||||
*/
|
||||
add_task(async function test_focus() {
|
||||
add_task(async function test_signup_focus() {
|
||||
let message = document.createElement("fxa-menu-message");
|
||||
const TEST_BUTTON_TEXT = "Howdy, partner! Sign up!";
|
||||
message.buttonText = TEST_BUTTON_TEXT;
|
||||
|
@ -125,7 +133,7 @@
|
|||
* Tests that setting no imageURL makes it so that the image element is
|
||||
* not visible, and setting one makes it visible.
|
||||
*/
|
||||
add_task(async function test_focus() {
|
||||
add_task(async function test_image_visibility() {
|
||||
let message = document.createElement("fxa-menu-message");
|
||||
const TEST_BUTTON_TEXT = "Howdy, partner! Sign up!";
|
||||
message.buttonText = TEST_BUTTON_TEXT;
|
||||
|
@ -144,35 +152,95 @@
|
|||
message.remove();
|
||||
});
|
||||
|
||||
async function testIllustrationOffset({
|
||||
layout,
|
||||
offsetVar,
|
||||
cssProperty,
|
||||
checkContainerOffset = false,
|
||||
}) {
|
||||
const TEST_DEFAULT_VALUE = "0px";
|
||||
const TEST_CONTAINER_OFFSET = "10px";
|
||||
const TEST_ILLUSTRATION_OFFSET = "123px";
|
||||
const TEST_IMAGE_URL =
|
||||
"chrome://activity-stream/content/data/content/assets/fox-doodle-waving-static.png";
|
||||
|
||||
let message = document.createElement("fxa-menu-message");
|
||||
message.imageURL = TEST_IMAGE_URL;
|
||||
if (layout) {
|
||||
message.layout = layout;
|
||||
}
|
||||
|
||||
document.getElementById("content").appendChild(message);
|
||||
await message.updateComplete;
|
||||
|
||||
let illustrationContainer = message.shadowRoot.querySelector(
|
||||
"#illustration-container"
|
||||
);
|
||||
ok(
|
||||
!isHidden(illustrationContainer),
|
||||
"Illustration container should not be hidden."
|
||||
);
|
||||
|
||||
let computedStyle = window.getComputedStyle(illustrationContainer);
|
||||
is(
|
||||
computedStyle[cssProperty],
|
||||
TEST_DEFAULT_VALUE,
|
||||
"Illustration offset should default to 0px"
|
||||
);
|
||||
|
||||
message.style.setProperty(offsetVar, TEST_ILLUSTRATION_OFFSET);
|
||||
computedStyle = window.getComputedStyle(illustrationContainer);
|
||||
is(
|
||||
computedStyle[cssProperty],
|
||||
TEST_ILLUSTRATION_OFFSET,
|
||||
"Illustration offset should have been forwarded to the container."
|
||||
);
|
||||
|
||||
if (checkContainerOffset) {
|
||||
const container = message.shadowRoot.querySelector("#container");
|
||||
let containerStyle = window.getComputedStyle(container);
|
||||
is(
|
||||
containerStyle.marginBlockEnd,
|
||||
TEST_DEFAULT_VALUE,
|
||||
"Container offset should default to 0px."
|
||||
);
|
||||
message.style.setProperty(
|
||||
"--container-margin-block-end-offset",
|
||||
TEST_CONTAINER_OFFSET
|
||||
);
|
||||
containerStyle = window.getComputedStyle(container);
|
||||
is(
|
||||
containerStyle.marginBlockEnd,
|
||||
TEST_CONTAINER_OFFSET,
|
||||
"Container offset should have been applied."
|
||||
);
|
||||
}
|
||||
message.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that setting the --illustration-margin-block-offset forwards that
|
||||
* offset to the illustration container.
|
||||
* Tests that setting the --illustration-margin-block-start-offset forwards that
|
||||
* offset to the illustration container for 'column' layout.
|
||||
*/
|
||||
add_task(async function test_focus() {
|
||||
let message = document.createElement("fxa-menu-message");
|
||||
const TEST_IMAGE_URL = "chrome://activity-stream/content/data/content/assets/fox-doodle-waving-static.png";
|
||||
message.imageURL = TEST_IMAGE_URL;
|
||||
add_task(async function test_column_layout_illustration_offset() {
|
||||
await testIllustrationOffset({
|
||||
layout: null,
|
||||
offsetVar: "--illustration-margin-block-start-offset",
|
||||
cssProperty: "marginBlockStart",
|
||||
});
|
||||
});
|
||||
|
||||
let content = document.getElementById("content");
|
||||
content.appendChild(message);
|
||||
|
||||
await message.updateComplete;
|
||||
|
||||
let illustrationContainer = message.shadowRoot.querySelector("#illustration-container");
|
||||
ok(!isHidden(illustrationContainer), "Illustration container should not be hidden.");
|
||||
let messageStyle = window.getComputedStyle(illustrationContainer);
|
||||
is(messageStyle.marginBlockStart, "0px", "Illustration offset should default to 0px.");
|
||||
|
||||
const TEST_OFFSET = "123px";
|
||||
message.style.setProperty("--illustration-margin-block-offset", TEST_OFFSET);
|
||||
messageStyle = window.getComputedStyle(illustrationContainer);
|
||||
is(
|
||||
messageStyle.marginBlockStart,
|
||||
TEST_OFFSET,
|
||||
"Illustration offset should have been forwarded to the container."
|
||||
);
|
||||
|
||||
message.remove();
|
||||
/**
|
||||
* Tests that setting the --illustration-margin-block-end-offset forwards that
|
||||
* offset to the illustration container for 'row' layout.
|
||||
*/
|
||||
add_task(async function test_row_layout_illustration_offset() {
|
||||
await testIllustrationOffset({
|
||||
layout: "row",
|
||||
offsetVar: "--illustration-margin-block-end-offset",
|
||||
cssProperty: "marginBlockEnd",
|
||||
checkContainerOffset: true,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
|
|
@ -16,13 +16,7 @@
|
|||
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
||||
|
||||
const lazy = {};
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(
|
||||
lazy,
|
||||
"gContentAnalysis",
|
||||
"@mozilla.org/contentanalysis;1",
|
||||
Ci.nsIContentAnalysis
|
||||
);
|
||||
let internalContentAnalysisService = undefined;
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
clearTimeout: "resource://gre/modules/Timer.sys.mjs",
|
||||
|
@ -114,6 +108,41 @@ export const ContentAnalysis = {
|
|||
*/
|
||||
warnDialogRequestTokens: new Set(),
|
||||
|
||||
/**
|
||||
* The nsIContentAnalysis to use instead of lazy.gContentAnalysis. Should
|
||||
* only be used for tests.
|
||||
*
|
||||
* @type {nsIContentAnalysis?}
|
||||
*/
|
||||
mockContentAnalysisForTest: undefined,
|
||||
|
||||
/**
|
||||
* The nsIContentAnalysis to use. Nothing else in this file should
|
||||
* use lazy.gContentAnalysis.
|
||||
*
|
||||
* @returns {nsIContentAnalysis}
|
||||
*/
|
||||
get contentAnalysis() {
|
||||
if (this.mockContentAnalysisForTest) {
|
||||
return this.mockContentAnalysisForTest;
|
||||
}
|
||||
if (!internalContentAnalysisService) {
|
||||
internalContentAnalysisService = Cc[
|
||||
"@mozilla.org/contentanalysis;1"
|
||||
].getService(Ci.nsIContentAnalysis);
|
||||
}
|
||||
return internalContentAnalysisService;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the nsIContentAnalysis to use. Should only be used for tests.
|
||||
*
|
||||
* @param {nsIContentAnalysis?} contentAnalysis
|
||||
*/
|
||||
setMockContentAnalysisForTest(contentAnalysis) {
|
||||
this.mockContentAnalysisForTest = contentAnalysis;
|
||||
},
|
||||
|
||||
/**
|
||||
* Registers for various messages/events that will indicate the
|
||||
* need for communicating something to the user.
|
||||
|
@ -121,13 +150,14 @@ export const ContentAnalysis = {
|
|||
* @param {Window} window - The window to monitor
|
||||
*/
|
||||
initialize(window) {
|
||||
if (!lazy.gContentAnalysis.isActive) {
|
||||
if (!this.contentAnalysis.isActive) {
|
||||
this.uninitialize();
|
||||
return;
|
||||
}
|
||||
let doc = window.document;
|
||||
if (!this.isInitialized) {
|
||||
this.isInitialized = true;
|
||||
this.initializeDownloadCA();
|
||||
this.initializeObservers();
|
||||
|
||||
ChromeUtils.defineLazyGetter(this, "l10n", function () {
|
||||
return new Localization(
|
||||
|
@ -153,13 +183,15 @@ export const ContentAnalysis = {
|
|||
if (this.isInitialized) {
|
||||
this.isInitialized = false;
|
||||
this.requestTokenToRequestInfo.clear();
|
||||
this.userActionToBusyDialogMap.clear();
|
||||
this.uninitializeObservers();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Register UI for file download CA events.
|
||||
* Register UI for CA events.
|
||||
*/
|
||||
async initializeDownloadCA() {
|
||||
initializeObservers() {
|
||||
Services.obs.addObserver(this, "dlp-request-made");
|
||||
Services.obs.addObserver(this, "dlp-response");
|
||||
Services.obs.addObserver(this, "quit-application");
|
||||
|
@ -167,6 +199,17 @@ export const ContentAnalysis = {
|
|||
Services.obs.addObserver(this, "quit-application-requested");
|
||||
},
|
||||
|
||||
/**
|
||||
* Unregister UI for CA events.
|
||||
*/
|
||||
uninitializeObservers() {
|
||||
Services.obs.removeObserver(this, "dlp-request-made");
|
||||
Services.obs.removeObserver(this, "dlp-response");
|
||||
Services.obs.removeObserver(this, "quit-application");
|
||||
Services.obs.removeObserver(this, "quit-application-granted");
|
||||
Services.obs.removeObserver(this, "quit-application-requested");
|
||||
},
|
||||
|
||||
// nsIObserver
|
||||
async observe(aSubj, aTopic, _aData) {
|
||||
switch (aTopic) {
|
||||
|
@ -216,7 +259,7 @@ export const ContentAnalysis = {
|
|||
// DLP requests, but the "DLP busy" or "DLP blocked" dialog can block the
|
||||
// main thread, thus preventing the "quit-application" from being sent,
|
||||
// which causes a shutdownhang. (bug 1899703)
|
||||
lazy.gContentAnalysis.cancelAllRequests(true);
|
||||
this.contentAnalysis.cancelAllRequests(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -230,7 +273,7 @@ export const ContentAnalysis = {
|
|||
// to call respondToWarnDialog() again.
|
||||
this.warnDialogRequestTokens = new Set();
|
||||
for (let warnDialogRequestToken of requestTokensToCancel) {
|
||||
lazy.gContentAnalysis.respondToWarnDialog(
|
||||
this.contentAnalysis.respondToWarnDialog(
|
||||
warnDialogRequestToken,
|
||||
false
|
||||
);
|
||||
|
@ -717,12 +760,15 @@ export const ContentAnalysis = {
|
|||
// the dialog, no need to log the exception.
|
||||
})
|
||||
.finally(() => {
|
||||
// This is also be called if the tab/window is closed while a request is in progress,
|
||||
// in which case we need to cancel the request.
|
||||
// This is also called if the tab/window is closed while a request is
|
||||
// in progress, in which case we need to cancel all related requests.
|
||||
if (this.requestTokenToRequestInfo.delete(aRequestToken)) {
|
||||
// TODO: Is this useful? I think no.
|
||||
this._removeSlowCAMessage(aUserActionId, aRequestToken);
|
||||
lazy.gContentAnalysis.cancelRequestsByRequestToken(aRequestToken);
|
||||
}
|
||||
this.contentAnalysis.cancelAllRequestsAssociatedWithUserAction(
|
||||
aUserActionId
|
||||
);
|
||||
});
|
||||
return {
|
||||
dialogBrowsingContext: aBrowsingContext,
|
||||
|
@ -807,7 +853,7 @@ export const ContentAnalysis = {
|
|||
// to the request already, so don't call respondToWarnDialog()
|
||||
// if aRequestToken is not in warnDialogRequestTokens.
|
||||
if (this.warnDialogRequestTokens.delete(aRequestToken)) {
|
||||
lazy.gContentAnalysis.respondToWarnDialog(aRequestToken, allow);
|
||||
this.contentAnalysis.respondToWarnDialog(aRequestToken, allow);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -831,7 +877,7 @@ export const ContentAnalysis = {
|
|||
case Ci.nsIContentAnalysisRequest.eClipboard: {
|
||||
// Unlike the cases below, this can be shown when the DLP
|
||||
// agent is not available. We use a different message for that.
|
||||
const caInfo = await lazy.gContentAnalysis.getDiagnosticInfo();
|
||||
const caInfo = await this.contentAnalysis.getDiagnosticInfo();
|
||||
titleId = "contentanalysis-block-dialog-title-clipboard";
|
||||
bodyId = caInfo.connectedToAgent
|
||||
? "contentanalysis-block-dialog-body-clipboard"
|
||||
|
@ -980,7 +1026,7 @@ export const ContentAnalysis = {
|
|||
* @param {ResourceNameOrOperationType} aResourceNameOrOperationType
|
||||
*/
|
||||
async _warnDialogText(aResourceNameOrOperationType) {
|
||||
const caInfo = await lazy.gContentAnalysis.getDiagnosticInfo();
|
||||
const caInfo = await this.contentAnalysis.getDiagnosticInfo();
|
||||
if (caInfo.connectedToAgent) {
|
||||
return await this.l10n.formatValue("contentanalysis-warndialogtext", {
|
||||
content: this._getResourceNameFromNameOrOperationType(
|
||||
|
|
|
@ -199,6 +199,21 @@
|
|||
</popupnotificationcontent>
|
||||
</popupnotification>
|
||||
|
||||
<popupnotification id="appMenu-update-other-instance-notification"
|
||||
popupid="update-other-instance"
|
||||
data-lazy-l10n-id="appmenu-update-other-instance"
|
||||
data-l10n-attrs="buttonlabel, buttonaccesskey, secondarybuttonlabel, secondarybuttonaccesskey"
|
||||
closebuttonhidden="true"
|
||||
dropmarkerhidden="true"
|
||||
checkboxhidden="true"
|
||||
buttonhighlight="true"
|
||||
hasicon="true"
|
||||
hidden="true">
|
||||
<popupnotificationcontent id="update-other-instance-notification-content" orient="vertical">
|
||||
<description id="update-other-instance-description" data-lazy-l10n-id="appmenu-update-other-instance-message"></description>
|
||||
</popupnotificationcontent>
|
||||
</popupnotification>
|
||||
|
||||
<popupnotification id="appMenu-addon-installed-notification"
|
||||
popupid="addon-installed"
|
||||
closebuttonhidden="true"
|
||||
|
|
|
@ -1409,8 +1409,6 @@
|
|||
},
|
||||
|
||||
"SearchEngines": {
|
||||
"enterprise_only": true,
|
||||
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Add": {
|
||||
|
|
|
@ -159,6 +159,12 @@
|
|||
"manifest": ["sidebar_action"],
|
||||
"paths": [["sidebarAction"]]
|
||||
},
|
||||
"tabGroups": {
|
||||
"url": "chrome://browser/content/parent/ext-tabGroups.js",
|
||||
"schema": "chrome://browser/content/schemas/tabGroups.json",
|
||||
"scopes": ["addon_parent"],
|
||||
"paths": [["tabGroups"]]
|
||||
},
|
||||
"tabs": {
|
||||
"url": "chrome://browser/content/parent/ext-tabs.js",
|
||||
"schema": "chrome://browser/content/schemas/tabs.json",
|
||||
|
|
|
@ -25,6 +25,7 @@ browser.jar:
|
|||
content/browser/parent/ext-search.js (parent/ext-search.js)
|
||||
content/browser/parent/ext-sessions.js (parent/ext-sessions.js)
|
||||
content/browser/parent/ext-sidebarAction.js (parent/ext-sidebarAction.js)
|
||||
content/browser/parent/ext-tabGroups.js (parent/ext-tabGroups.js)
|
||||
content/browser/parent/ext-tabs.js (parent/ext-tabs.js)
|
||||
content/browser/parent/ext-topSites.js (parent/ext-topSites.js)
|
||||
content/browser/parent/ext-url-overrides.js (parent/ext-url-overrides.js)
|
||||
|
|
230
browser/components/extensions/parent/ext-tabGroups.js
Normal file
|
@ -0,0 +1,230 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
ChromeUtils.defineESModuleGetters(this, {
|
||||
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
||||
});
|
||||
|
||||
var { ExtensionError } = ExtensionUtils;
|
||||
|
||||
const spellColour = color => (color === "grey" ? "gray" : color);
|
||||
|
||||
this.tabGroups = class extends ExtensionAPIPersistent {
|
||||
queryGroups({ collapsed, color, title, windowId } = {}) {
|
||||
color = spellColour(color);
|
||||
let glob = title != null && new MatchGlob(title);
|
||||
let window =
|
||||
windowId != null && windowTracker.getWindow(windowId, null, false);
|
||||
return windowTracker
|
||||
.browserWindows()
|
||||
.filter(
|
||||
win =>
|
||||
this.extension.canAccessWindow(win) &&
|
||||
(windowId == null || win === window)
|
||||
)
|
||||
.flatMap(win => win.gBrowser.tabGroups)
|
||||
.filter(
|
||||
group =>
|
||||
(collapsed == null || group.collapsed === collapsed) &&
|
||||
(color == null || group.color === color) &&
|
||||
(title == null || glob.matches(group.name))
|
||||
);
|
||||
}
|
||||
|
||||
get(groupId) {
|
||||
let gid = getInternalTabGroupIdForExtTabGroupId(groupId);
|
||||
if (!gid) {
|
||||
throw new ExtensionError(`No group with id: ${groupId}`);
|
||||
}
|
||||
for (let group of this.queryGroups()) {
|
||||
if (group.id === gid) {
|
||||
return group;
|
||||
}
|
||||
}
|
||||
throw new ExtensionError(`No group with id: ${groupId}`);
|
||||
}
|
||||
|
||||
convert(group) {
|
||||
return {
|
||||
collapsed: !!group.collapsed,
|
||||
/** Internally we use "gray", but Chrome uses "grey" @see spellColour. */
|
||||
color: group.color === "gray" ? "grey" : group.color,
|
||||
id: getExtTabGroupIdForInternalTabGroupId(group.id),
|
||||
title: group.name,
|
||||
windowId: windowTracker.getId(group.ownerGlobal),
|
||||
};
|
||||
}
|
||||
|
||||
PERSISTENT_EVENTS = {
|
||||
onCreated({ fire }) {
|
||||
let onCreate = event => {
|
||||
if (event.detail.isAdoptingGroup) {
|
||||
// Tab group moved from a different window.
|
||||
return;
|
||||
}
|
||||
fire.async(this.convert(event.originalTarget));
|
||||
};
|
||||
windowTracker.addListener("TabGroupCreate", onCreate);
|
||||
return {
|
||||
unregister() {
|
||||
windowTracker.removeListener("TabGroupCreate", onCreate);
|
||||
},
|
||||
convert(_fire) {
|
||||
fire = _fire;
|
||||
},
|
||||
};
|
||||
},
|
||||
onMoved({ fire }) {
|
||||
let onMove = event => {
|
||||
fire.async(this.convert(event.originalTarget));
|
||||
};
|
||||
let onCreate = event => {
|
||||
if (event.detail.isAdoptingGroup) {
|
||||
// Tab group moved from a different window.
|
||||
fire.async(this.convert(event.originalTarget));
|
||||
}
|
||||
};
|
||||
windowTracker.addListener("TabGroupMoved", onMove);
|
||||
windowTracker.addListener("TabGroupCreate", onCreate);
|
||||
return {
|
||||
unregister() {
|
||||
windowTracker.removeListener("TabGroupMoved", onMove);
|
||||
windowTracker.removeListener("TabGroupCreate", onCreate);
|
||||
},
|
||||
convert(_fire) {
|
||||
fire = _fire;
|
||||
},
|
||||
};
|
||||
},
|
||||
onRemoved({ fire }) {
|
||||
let onRemove = event => {
|
||||
if (event.originalTarget.removedByAdoption) {
|
||||
// Tab group moved to a different window.
|
||||
return;
|
||||
}
|
||||
fire.async(this.convert(event.originalTarget));
|
||||
};
|
||||
windowTracker.addListener("TabGroupRemoved", onRemove);
|
||||
return {
|
||||
unregister() {
|
||||
windowTracker.removeListener("TabGroupRemoved", onRemove);
|
||||
},
|
||||
convert(_fire) {
|
||||
fire = _fire;
|
||||
},
|
||||
};
|
||||
},
|
||||
onUpdated({ fire }) {
|
||||
let onUpdate = event => {
|
||||
fire.async(this.convert(event.originalTarget));
|
||||
};
|
||||
windowTracker.addListener("TabGroupCollapse", onUpdate);
|
||||
windowTracker.addListener("TabGroupExpand", onUpdate);
|
||||
windowTracker.addListener("TabGroupUpdate", onUpdate);
|
||||
return {
|
||||
unregister() {
|
||||
windowTracker.removeListener("TabGroupCollapse", onUpdate);
|
||||
windowTracker.removeListener("TabGroupExpand", onUpdate);
|
||||
windowTracker.removeListener("TabGroupUpdate", onUpdate);
|
||||
},
|
||||
convert(_fire) {
|
||||
fire = _fire;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
getAPI(context) {
|
||||
const { windowManager } = this.extension;
|
||||
return {
|
||||
tabGroups: {
|
||||
get: groupId => {
|
||||
return this.convert(this.get(groupId));
|
||||
},
|
||||
|
||||
move: (groupId, { index, windowId }) => {
|
||||
let group = this.get(groupId);
|
||||
let win = group.ownerGlobal;
|
||||
|
||||
if (windowId != null) {
|
||||
win = windowTracker.getWindow(windowId, context);
|
||||
if (
|
||||
PrivateBrowsingUtils.isWindowPrivate(group.ownerGlobal) !==
|
||||
PrivateBrowsingUtils.isWindowPrivate(win)
|
||||
) {
|
||||
throw new ExtensionError(
|
||||
"Can't move groups between private and non-private windows"
|
||||
);
|
||||
}
|
||||
if (windowManager.getWrapper(win).type !== "normal") {
|
||||
throw new ExtensionError(
|
||||
"Groups can only be moved to normal windows."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (win !== group.ownerGlobal) {
|
||||
let last = win.gBrowser.tabContainer.ariaFocusableItems.length + 1;
|
||||
let elementIndex = index === -1 ? last : Math.min(index, last);
|
||||
group = win.gBrowser.adoptTabGroup(group, elementIndex);
|
||||
} else if (index >= 0 && index < win.gBrowser.tabs.length) {
|
||||
win.gBrowser.moveTabTo(group, { tabIndex: index });
|
||||
} else if (win.gBrowser.tabs.at(-1) !== group.tabs.at(-1)) {
|
||||
win.gBrowser.moveTabAfter(group, win.gBrowser.tabs.at(-1));
|
||||
}
|
||||
return this.convert(group);
|
||||
},
|
||||
|
||||
query: query => {
|
||||
return Array.from(this.queryGroups(query ?? {}), group =>
|
||||
this.convert(group)
|
||||
);
|
||||
},
|
||||
|
||||
update: (groupId, { collapsed, color, title }) => {
|
||||
let group = this.get(groupId);
|
||||
if (collapsed != null) {
|
||||
group.collapsed = collapsed;
|
||||
}
|
||||
if (color != null) {
|
||||
group.color = spellColour(color);
|
||||
}
|
||||
if (title != null) {
|
||||
group.name = title;
|
||||
}
|
||||
return this.convert(group);
|
||||
},
|
||||
|
||||
onCreated: new EventManager({
|
||||
context,
|
||||
module: "tabGroups",
|
||||
event: "onCreated",
|
||||
extensionApi: this,
|
||||
}).api(),
|
||||
|
||||
onMoved: new EventManager({
|
||||
context,
|
||||
module: "tabGroups",
|
||||
event: "onMoved",
|
||||
extensionApi: this,
|
||||
}).api(),
|
||||
|
||||
onRemoved: new EventManager({
|
||||
context,
|
||||
module: "tabGroups",
|
||||
event: "onRemoved",
|
||||
extensionApi: this,
|
||||
}).api(),
|
||||
|
||||
onUpdated: new EventManager({
|
||||
context,
|
||||
module: "tabGroups",
|
||||
event: "onUpdated",
|
||||
extensionApi: this,
|
||||
}).api(),
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
|
@ -267,11 +267,15 @@ this.tabs = class extends ExtensionAPIPersistent {
|
|||
let moveListener = event => {
|
||||
let nativeTab = event.originalTarget;
|
||||
let { previousTabState, currentTabState } = event.detail;
|
||||
if (tabManager.canAccessTab(nativeTab)) {
|
||||
let fromIndex = previousTabState.tabIndex;
|
||||
let toIndex = currentTabState.tabIndex;
|
||||
// TabMove also fires if its tab group changes; we should only fire
|
||||
// event if the position actually moved.
|
||||
if (fromIndex !== toIndex && tabManager.canAccessTab(nativeTab)) {
|
||||
fire.async(tabTracker.getId(nativeTab), {
|
||||
windowId: windowTracker.getId(nativeTab.ownerGlobal),
|
||||
fromIndex: previousTabState.tabIndex,
|
||||
toIndex: currentTabState.tabIndex,
|
||||
fromIndex,
|
||||
toIndex,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -20,6 +20,7 @@ browser.jar:
|
|||
content/browser/schemas/search.json
|
||||
content/browser/schemas/sessions.json
|
||||
content/browser/schemas/sidebar_action.json
|
||||
content/browser/schemas/tabGroups.json
|
||||
content/browser/schemas/tabs.json
|
||||
content/browser/schemas/top_sites.json
|
||||
content/browser/schemas/url_overrides.json
|
||||
|
|
231
browser/components/extensions/schemas/tabGroups.json
Normal file
|
@ -0,0 +1,231 @@
|
|||
[
|
||||
{
|
||||
"namespace": "manifest",
|
||||
"types": [
|
||||
{
|
||||
"$extend": "OptionalPermissionNoPrompt",
|
||||
"choices": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["tabGroups"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"namespace": "tabGroups",
|
||||
"description": "Use the browser.tabGroups API to interact with the browser's tab grouping system. You can use this API to modify, and rearrange tab groups.",
|
||||
"permissions": ["tabGroups"],
|
||||
"types": [
|
||||
{
|
||||
"id": "Color",
|
||||
"type": "string",
|
||||
"description": "The group's color, using 'grey' spelling for compatibility with Chromium.",
|
||||
"enum": [
|
||||
"blue",
|
||||
"cyan",
|
||||
"grey",
|
||||
"green",
|
||||
"orange",
|
||||
"pink",
|
||||
"purple",
|
||||
"red",
|
||||
"yellow"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "TabGroup",
|
||||
"type": "object",
|
||||
"description": "State of a tab group inside of an open window.",
|
||||
"properties": {
|
||||
"collapsed": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the tab group is collapsed or expanded in the tab strip."
|
||||
},
|
||||
"color": {
|
||||
"$ref": "Color",
|
||||
"description": "User-selected color name for the tab group's label/icons."
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"description": "Unique ID of the tab group."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"optional": true,
|
||||
"description": "User-defined name of the tab group."
|
||||
},
|
||||
"windowId": {
|
||||
"type": "integer",
|
||||
"description": "Window that the tab group is in."
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"TAB_GROUP_ID_NONE": {
|
||||
"value": -1,
|
||||
"description": "An ID that represents the absence of a group."
|
||||
}
|
||||
},
|
||||
"functions": [
|
||||
{
|
||||
"name": "get",
|
||||
"type": "function",
|
||||
"description": "Retrieves details about the specified group.",
|
||||
"async": "callback",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "groupId",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "callback",
|
||||
"parameters": [{ "name": "group", "$ref": "TabGroup" }]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "move",
|
||||
"type": "function",
|
||||
"description": "Move a group within, or to another window.",
|
||||
"async": "callback",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "groupId",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"name": "moveProperties",
|
||||
"properties": {
|
||||
"index": {
|
||||
"type": "integer",
|
||||
"minimum": -1
|
||||
},
|
||||
"windowId": {
|
||||
"type": "integer",
|
||||
"optional": true,
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "callback",
|
||||
"parameters": [{ "name": "group", "$ref": "TabGroup" }]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "query",
|
||||
"type": "function",
|
||||
"description": "Return all grups, or find groups with specified properties.",
|
||||
"async": "callback",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "object",
|
||||
"name": "queryInfo",
|
||||
"optional": true,
|
||||
"properties": {
|
||||
"collapsed": {
|
||||
"type": "boolean",
|
||||
"optional": true
|
||||
},
|
||||
"color": {
|
||||
"$ref": "Color",
|
||||
"optional": true
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"windowId": {
|
||||
"type": "integer",
|
||||
"optional": true,
|
||||
"minimum": -2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "callback",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "groups",
|
||||
"type": "array",
|
||||
"items": { "$ref": "TabGroup" }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "update",
|
||||
"type": "function",
|
||||
"description": "Modifies state of a specified group.",
|
||||
"async": "callback",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "groupId",
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"name": "updateProperties",
|
||||
"properties": {
|
||||
"collapsed": {
|
||||
"type": "boolean",
|
||||
"optional": true
|
||||
},
|
||||
"color": {
|
||||
"$ref": "Color",
|
||||
"optional": true
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "callback",
|
||||
"parameters": [{ "name": "group", "$ref": "TabGroup" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"name": "onCreated",
|
||||
"type": "function",
|
||||
"description": "Fired when a tab group is created.",
|
||||
"parameters": [{ "$ref": "TabGroup", "name": "group" }]
|
||||
},
|
||||
{
|
||||
"name": "onMoved",
|
||||
"type": "function",
|
||||
"description": "Fired when a tab group is moved, within a window or to another window.",
|
||||
"parameters": [{ "$ref": "TabGroup", "name": "group" }]
|
||||
},
|
||||
{
|
||||
"name": "onRemoved",
|
||||
"type": "function",
|
||||
"description": "Fired when a tab group is removed.",
|
||||
"parameters": [{ "$ref": "TabGroup", "name": "group" }]
|
||||
},
|
||||
{
|
||||
"name": "onUpdated",
|
||||
"type": "function",
|
||||
"description": "Fired when a tab group is updated.",
|
||||
"parameters": [{ "$ref": "TabGroup", "name": "group" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -481,6 +481,14 @@ skip-if = ["true"] # Bug 1575369
|
|||
["browser_ext_slow_script.js"]
|
||||
https_first_disabled = true
|
||||
|
||||
["browser_ext_tabGroups.js"]
|
||||
|
||||
["browser_ext_tabGroups_move_event_order.js"]
|
||||
|
||||
["browser_ext_tabGroups_move_onMoved.js"]
|
||||
|
||||
["browser_ext_tabGroups_query.js"]
|
||||
|
||||
["browser_ext_tab_runtimeConnect.js"]
|
||||
|
||||
["browser_ext_tabs_attention.js"]
|
||||
|
@ -546,6 +554,8 @@ https_first_disabled = true
|
|||
|
||||
["browser_ext_tabs_group_ungroup.js"]
|
||||
|
||||
["browser_ext_tabs_group_windowId.js"]
|
||||
|
||||
["browser_ext_tabs_hide.js"]
|
||||
https_first_disabled = true
|
||||
|
||||
|
|
|
@ -0,0 +1,211 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
const {
|
||||
Management: {
|
||||
global: { getExtTabGroupIdForInternalTabGroupId },
|
||||
},
|
||||
} = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
|
||||
|
||||
add_task(async function tabsGroups_get_private() {
|
||||
async function loadExt(allowPrivate) {
|
||||
let ext = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
permissions: ["tabGroups"],
|
||||
},
|
||||
incognitoOverride: allowPrivate ? "spanning" : undefined,
|
||||
background() {
|
||||
browser.test.onMessage.addListener(async groupId => {
|
||||
try {
|
||||
let group = await browser.tabGroups.get(groupId);
|
||||
|
||||
let color = browser.tabGroups.Color[group.color.toUpperCase()];
|
||||
browser.test.assertEq(group.color, color, "A known colour.");
|
||||
|
||||
browser.test.sendMessage("group", group);
|
||||
} catch (e) {
|
||||
browser.test.sendMessage("error", e.message);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
await ext.startup();
|
||||
return ext;
|
||||
}
|
||||
|
||||
let url = "https://example.com/?";
|
||||
let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
|
||||
let group1 = gBrowser.addTabGroup([tab1]);
|
||||
let gid1 = getExtTabGroupIdForInternalTabGroupId(group1.id);
|
||||
|
||||
let win2 = await BrowserTestUtils.openNewBrowserWindow({ private: true });
|
||||
let tab2 = await BrowserTestUtils.openNewForegroundTab(win2.gBrowser, url);
|
||||
let group2 = win2.gBrowser.addTabGroup([tab2]);
|
||||
let gid2 = getExtTabGroupIdForInternalTabGroupId(group2.id);
|
||||
|
||||
function compare(group, ext, info) {
|
||||
is(group.collapsed, ext.collapsed, `Collapsed ok - ${info}`);
|
||||
is(group.name, ext.title, `Title ok - ${info}`);
|
||||
is(group.color.replace(/^gray$/, "grey"), ext.color, `Color ok - ${info}`);
|
||||
}
|
||||
|
||||
info("Testing extension without private browsing access.");
|
||||
let ext1 = await loadExt(false);
|
||||
|
||||
ext1.sendMessage(gid1);
|
||||
let g1 = await ext1.awaitMessage("group");
|
||||
compare(group1, g1, "Ext 1 / group 1.");
|
||||
|
||||
ext1.sendMessage(gid2);
|
||||
let e2 = await ext1.awaitMessage("error");
|
||||
is(e2, `No group with id: ${gid2}`, "Expected error.");
|
||||
await ext1.unload();
|
||||
|
||||
info("Testing extension with private browsing access.");
|
||||
let ext2 = await loadExt(true);
|
||||
|
||||
ext2.sendMessage(gid1);
|
||||
g1 = await ext2.awaitMessage("group");
|
||||
compare(group1, g1, "Ext 2 / group 1.");
|
||||
|
||||
ext2.sendMessage(gid1);
|
||||
let g2 = await ext2.awaitMessage("group");
|
||||
compare(group1, g2, "Ext 2 / group 2.");
|
||||
|
||||
await ext2.unload();
|
||||
BrowserTestUtils.removeTab(tab1);
|
||||
await BrowserTestUtils.closeWindow(win2);
|
||||
});
|
||||
|
||||
add_task(async function tabsGroups_update_onUpdated() {
|
||||
let url = "https://example.com/?";
|
||||
let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, url + 1);
|
||||
let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, url + 2);
|
||||
|
||||
let group1 = gBrowser.addTabGroup([tab1]);
|
||||
let group2 = gBrowser.addTabGroup([tab2]);
|
||||
gBrowser.selectedTab = tab1;
|
||||
|
||||
info("Setup initial group colour for consistency.");
|
||||
group1.color = "red";
|
||||
group2.color = "blue";
|
||||
|
||||
let ext = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
permissions: ["tabGroups"],
|
||||
},
|
||||
async background() {
|
||||
let [tab] = await browser.tabs.query({
|
||||
lastFocusedWindow: true,
|
||||
active: true,
|
||||
});
|
||||
|
||||
let group = await browser.tabGroups.get(tab.groupId);
|
||||
browser.test.assertEq(tab.groupId, group.id, "Group id matches.");
|
||||
browser.test.assertEq(tab.windowId, group.windowId, "Window id ok.");
|
||||
|
||||
browser.tabGroups.onCreated.addListener(group => {
|
||||
browser.test.sendMessage("created", group);
|
||||
});
|
||||
browser.tabGroups.onRemoved.addListener(group => {
|
||||
browser.test.sendMessage("removed", group);
|
||||
});
|
||||
|
||||
browser.tabGroups.onUpdated.addListener(async updated => {
|
||||
let group = await browser.tabGroups.get(updated.id);
|
||||
browser.test.assertDeepEq(updated, group, "It's the same group.");
|
||||
browser.test.sendMessage("updated", updated);
|
||||
});
|
||||
|
||||
browser.test.assertThrows(
|
||||
() => browser.tabGroups.update(1e9, { color: "magenta" }),
|
||||
/Invalid enumeration value "magenta"/,
|
||||
"Magenta is not a real color."
|
||||
);
|
||||
|
||||
await browser.test.assertRejects(
|
||||
browser.tabGroups.update(1e9, { title: "blah" }),
|
||||
"No group with id: 1000000000",
|
||||
"Invalid group id rejects."
|
||||
);
|
||||
|
||||
browser.test.onMessage.addListener((_msg, update) => {
|
||||
browser.tabGroups.update(group.id, update);
|
||||
});
|
||||
browser.test.sendMessage("group", group);
|
||||
},
|
||||
});
|
||||
await ext.startup();
|
||||
|
||||
function compare(first, second, info) {
|
||||
let { color, title, collapsed } = first;
|
||||
|
||||
// XUL element has a .name, and uses "gray".
|
||||
if (XULElement.isInstance(first)) {
|
||||
color = first.color.replace(/^gray$/, "grey");
|
||||
title = first.name;
|
||||
}
|
||||
|
||||
is(collapsed, second.collapsed, `Collapsed ok - ${info}`);
|
||||
is(color, second.color, `Color ok - ${info}`);
|
||||
is(title, second.title, `Title ok - ${info}`);
|
||||
}
|
||||
|
||||
let first = await ext.awaitMessage("group");
|
||||
compare(group1, first, "Initial group.");
|
||||
|
||||
info("Updating group1 using extension api.");
|
||||
ext.sendMessage("update", { collapsed: true });
|
||||
first.collapsed = true;
|
||||
|
||||
let updated = await ext.awaitMessage("updated");
|
||||
compare(first, updated, "After collapsing 1.");
|
||||
|
||||
ext.sendMessage("update", { collapsed: false });
|
||||
first.collapsed = false;
|
||||
|
||||
updated = await ext.awaitMessage("updated");
|
||||
compare(first, updated, "After expanding 1.");
|
||||
|
||||
ext.sendMessage("update", { color: "grey" });
|
||||
first.color = "grey";
|
||||
|
||||
updated = await ext.awaitMessage("updated");
|
||||
compare(first, updated, "After coloring 1.");
|
||||
|
||||
ext.sendMessage("update", { title: "First" });
|
||||
first.title = "First";
|
||||
|
||||
updated = await ext.awaitMessage("updated");
|
||||
compare(first, updated, "After naming 1.");
|
||||
|
||||
info("Creating a new group does not trigger onUpdated event.");
|
||||
let tab3 = await BrowserTestUtils.openNewForegroundTab(gBrowser, url + 3);
|
||||
gBrowser.addTabGroup([tab3]);
|
||||
await ext.awaitMessage("created");
|
||||
|
||||
info("Closing tab1, and thus its group, does not trigger onUpdated.");
|
||||
BrowserTestUtils.removeTab(tab1);
|
||||
await ext.awaitMessage("removed");
|
||||
|
||||
info("Updating group2 directly from outside of the extension.");
|
||||
group2.collapsed = true;
|
||||
let second = await ext.awaitMessage("updated");
|
||||
compare(group2, second, "After collapsing 2.");
|
||||
|
||||
group2.color = "pink";
|
||||
second = await ext.awaitMessage("updated");
|
||||
compare(group2, second, "After coloring 2.");
|
||||
|
||||
group2.name = "Second";
|
||||
second = await ext.awaitMessage("updated");
|
||||
compare(group2, second, "After naming 2.");
|
||||
|
||||
isnot(first.id, second.id, "Groups have different ids.");
|
||||
is(first.windowId, second.windowId, "Groups in the same window.");
|
||||
|
||||
await ext.unload();
|
||||
BrowserTestUtils.removeTab(tab3);
|
||||
BrowserTestUtils.removeTab(tab2);
|
||||
});
|
|
@ -0,0 +1,175 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
// TODO bug 1938594: moving a created tab to another window sometimes triggers
|
||||
// this error. See https://bugzilla.mozilla.org/show_bug.cgi?id=1938594#c1
|
||||
PromiseTestUtils.allowMatchingRejectionsGlobally(
|
||||
/Unexpected undefined tabState for onMoveToNewWindow/
|
||||
);
|
||||
|
||||
// There is special logic for adopting tab groups at the end of a tab strip.
|
||||
// This test tests the behavior at the penultimate tab. For extra coverage,
|
||||
// this test uses extension APIs only to create windows, tabs, groups.
|
||||
// It also checks tabs.onMoved, tabs.onAttached and tabs.onDetached.
|
||||
add_task(async function tabGroups_move_to_other_window() {
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
permissions: ["tabGroups"],
|
||||
},
|
||||
async background() {
|
||||
// Setup: The tab that we are going to group.
|
||||
const { id: tabId, windowId: oldWinId } = await browser.tabs.create({});
|
||||
browser.test.log(`Tab ID: ${tabId}, old win ID: ${oldWinId}`);
|
||||
const groupId = await browser.tabs.group({ tabIds: tabId });
|
||||
// Setup: Create window with two tabs.
|
||||
const { id: windowId } = await browser.windows.create({});
|
||||
const tab2 = await browser.tabs.create({ windowId });
|
||||
browser.test.assertEq(1, tab2.index, `Two tabs in window ${windowId}`);
|
||||
|
||||
const events = [];
|
||||
browser.tabGroups.onCreated.addListener(group => {
|
||||
browser.test.fail(`Unexpected tabGroups.onCreated: ${group.id}`);
|
||||
});
|
||||
browser.tabGroups.onRemoved.addListener(group => {
|
||||
browser.test.fail(`Unexpected tabGroups.onRemoved: ${group.id}`);
|
||||
});
|
||||
browser.tabGroups.onMoved.addListener(group => {
|
||||
browser.test.assertEq(groupId, group.id, "onMoved fired for group");
|
||||
events.push("tabGroups.onMoved");
|
||||
});
|
||||
browser.tabs.onMoved.addListener((movedTabId, moveInfo) => {
|
||||
// onDetached & onAttached should fire when moving to another window.
|
||||
browser.test.fail(
|
||||
`Unexpected tabs.onMoved: ${JSON.stringify(moveInfo)}`
|
||||
);
|
||||
});
|
||||
browser.tabs.onDetached.addListener((movedTabId, detachInfo) => {
|
||||
browser.test.assertEq(tabId, movedTabId, "Our tab detached");
|
||||
browser.test.assertEq(oldWinId, detachInfo.oldWindowId, "oldWindowId");
|
||||
browser.test.assertEq(1, detachInfo.oldPosition, "oldPosition");
|
||||
events.push("tabs.onDetached");
|
||||
});
|
||||
browser.tabs.onAttached.addListener((movedTabId, attachInfo) => {
|
||||
browser.test.assertEq(tabId, movedTabId, "Our tab attached");
|
||||
browser.test.assertEq(windowId, attachInfo.newWindowId, "newWindowId");
|
||||
browser.test.assertEq(1, attachInfo.newPosition, "newPosition");
|
||||
events.push("tabs.onAttached");
|
||||
});
|
||||
|
||||
browser.test.assertDeepEq([], events, "No tabGroups event yet");
|
||||
// Move tab group between the (only) two existing tabs in the window.
|
||||
const moved = await browser.tabGroups.move(groupId, {
|
||||
windowId,
|
||||
index: 1,
|
||||
});
|
||||
browser.test.assertEq(groupId, moved.id, "Group ID did not change");
|
||||
browser.test.assertEq(windowId, moved.windowId, "Group moved to window");
|
||||
|
||||
browser.test.assertEq(
|
||||
1,
|
||||
(await browser.tabs.get(tabId)).index,
|
||||
"Tab appears at the expected index"
|
||||
);
|
||||
|
||||
await browser.windows.remove(windowId);
|
||||
browser.test.assertDeepEq(
|
||||
["tabs.onDetached", "tabs.onAttached", "tabGroups.onMoved"],
|
||||
events,
|
||||
"Expected events when moving tab group to a new window"
|
||||
);
|
||||
|
||||
browser.test.sendMessage("done");
|
||||
},
|
||||
});
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("done");
|
||||
await extension.unload();
|
||||
});
|
||||
|
||||
add_task(async function tabGroups_move_multiple_tabs_to_other_window() {
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
permissions: ["tabGroups"],
|
||||
},
|
||||
async background() {
|
||||
// Setup: The tabs that we are going to group.
|
||||
const tabs = [
|
||||
await browser.tabs.create({}),
|
||||
await browser.tabs.create({}),
|
||||
];
|
||||
const oldWinId = tabs[0].windowId;
|
||||
const tabIds = tabs.map(t => t.id);
|
||||
browser.test.log(`Tab ID: ${tabIds}, old win ID: ${oldWinId}`);
|
||||
const groupId = await browser.tabs.group({ tabIds });
|
||||
// Setup: Create window with two tabs.
|
||||
const { id: windowId } = await browser.windows.create({});
|
||||
const tab2 = await browser.tabs.create({ windowId });
|
||||
browser.test.assertEq(1, tab2.index, `Two tabs in window ${windowId}`);
|
||||
|
||||
const events = [];
|
||||
browser.tabGroups.onCreated.addListener(group => {
|
||||
browser.test.fail(`Unexpected tabGroups.onCreated: ${group.id}`);
|
||||
});
|
||||
browser.tabGroups.onRemoved.addListener(group => {
|
||||
browser.test.fail(`Unexpected tabGroups.onRemoved: ${group.id}`);
|
||||
});
|
||||
browser.tabGroups.onMoved.addListener(group => {
|
||||
browser.test.assertEq(groupId, group.id, "onMoved fired for group");
|
||||
events.push("tabGroups.onMoved");
|
||||
});
|
||||
browser.tabs.onMoved.addListener((movedTabId, moveInfo) => {
|
||||
// onDetached & onAttached should fire when moving to another window.
|
||||
browser.test.fail(
|
||||
`Unexpected tabs.onMoved: ${JSON.stringify(moveInfo)}`
|
||||
);
|
||||
});
|
||||
browser.tabs.onDetached.addListener((movedTabId, detachInfo) => {
|
||||
browser.test.assertEq(oldWinId, detachInfo.oldWindowId, "oldWindowId");
|
||||
events.push(`tabs.onDetached:${detachInfo.oldPosition}:${movedTabId}`);
|
||||
});
|
||||
browser.tabs.onAttached.addListener((movedTabId, attachInfo) => {
|
||||
browser.test.assertEq(windowId, attachInfo.newWindowId, "newWindowId");
|
||||
events.push(`tabs.onAttached:${attachInfo.newPosition}:${movedTabId}`);
|
||||
});
|
||||
|
||||
browser.test.assertDeepEq([], events, "No tabGroups event yet");
|
||||
// Move tab group to start of tab strip.
|
||||
const moved = await browser.tabGroups.move(groupId, {
|
||||
windowId,
|
||||
index: 0,
|
||||
});
|
||||
browser.test.assertEq(groupId, moved.id, "Group ID did not change");
|
||||
browser.test.assertEq(windowId, moved.windowId, "Group moved to window");
|
||||
|
||||
browser.test.assertEq(
|
||||
0,
|
||||
(await browser.tabs.get(tabIds[0])).index,
|
||||
`First tab appears at the expected index`
|
||||
);
|
||||
browser.test.assertEq(
|
||||
1,
|
||||
(await browser.tabs.get(tabIds[1])).index,
|
||||
`Second tab appears at the expected index`
|
||||
);
|
||||
|
||||
await browser.windows.remove(windowId);
|
||||
browser.test.assertDeepEq(
|
||||
[
|
||||
`tabs.onDetached:1:${tabIds[0]}`,
|
||||
`tabs.onAttached:0:${tabIds[0]}`,
|
||||
`tabs.onDetached:1:${tabIds[1]}`,
|
||||
`tabs.onAttached:1:${tabIds[1]}`,
|
||||
"tabGroups.onMoved",
|
||||
],
|
||||
events,
|
||||
"Expected events when moving tab group (2 tabs) to a new window"
|
||||
);
|
||||
|
||||
browser.test.sendMessage("done");
|
||||
},
|
||||
});
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("done");
|
||||
await extension.unload();
|
||||
});
|
|
@ -0,0 +1,155 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
const {
|
||||
Management: {
|
||||
global: { getExtTabGroupIdForInternalTabGroupId },
|
||||
},
|
||||
} = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
|
||||
|
||||
add_task(async function tabsGroups_move_onMoved() {
|
||||
async function loadExt() {
|
||||
let ext = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
permissions: ["tabGroups"],
|
||||
},
|
||||
incognitoOverride: "spanning",
|
||||
async background() {
|
||||
browser.tabGroups.onCreated.addListener(group => {
|
||||
browser.test.sendMessage("created", group);
|
||||
});
|
||||
browser.tabGroups.onRemoved.addListener(group => {
|
||||
browser.test.sendMessage("removed", group);
|
||||
});
|
||||
browser.tabGroups.onMoved.addListener(moved => {
|
||||
browser.test.sendMessage("moved", moved);
|
||||
});
|
||||
browser.test.onMessage.addListener(async (groupId, moveProps) => {
|
||||
try {
|
||||
let group = await browser.tabGroups.move(groupId, moveProps);
|
||||
browser.test.sendMessage("done", group);
|
||||
} catch (e) {
|
||||
browser.test.sendMessage("error", e.message);
|
||||
}
|
||||
});
|
||||
let [tab] = await browser.tabs.query({
|
||||
lastFocusedWindow: true,
|
||||
active: true,
|
||||
});
|
||||
browser.test.sendMessage("windowId", tab.windowId);
|
||||
},
|
||||
});
|
||||
await ext.startup();
|
||||
return ext;
|
||||
}
|
||||
|
||||
let tabs = [];
|
||||
let url = "https://example.com/foo?";
|
||||
for (let i = 1; i < 10; i++) {
|
||||
tabs.push(await BrowserTestUtils.openNewForegroundTab(gBrowser, url + i));
|
||||
}
|
||||
|
||||
let group = gBrowser.addTabGroup(gBrowser.tabs.slice(-2));
|
||||
is(group.tabs[1], gBrowser.tabs.at(-1), "Group's last tab is last.");
|
||||
|
||||
let gid = getExtTabGroupIdForInternalTabGroupId(group.id);
|
||||
|
||||
let ext = await loadExt();
|
||||
let windowId = await ext.awaitMessage("windowId");
|
||||
|
||||
ext.sendMessage(gid, { index: 6 });
|
||||
await Promise.all([ext.awaitMessage("done"), ext.awaitMessage("moved")]);
|
||||
is(group.tabs[0], gBrowser.tabs[6], "Group's first tab moved to index 6.");
|
||||
|
||||
ext.sendMessage(gid, { index: 3 });
|
||||
await Promise.all([ext.awaitMessage("done"), ext.awaitMessage("moved")]);
|
||||
is(group.tabs[0], gBrowser.tabs[3], "Group's first tab moved to index 3.");
|
||||
|
||||
ext.sendMessage(gid, { index: 3 });
|
||||
await ext.awaitMessage("done");
|
||||
is(group.tabs[0], gBrowser.tabs[3], "Using same index 3 doesn't move.");
|
||||
|
||||
ext.sendMessage(gid, { index: -1 });
|
||||
await Promise.all([ext.awaitMessage("done"), ext.awaitMessage("moved")]);
|
||||
is(group.tabs[1], gBrowser.tabs.at(-1), "Group moved to the end.");
|
||||
|
||||
ext.sendMessage(gid, { index: -1 });
|
||||
await ext.awaitMessage("done");
|
||||
is(group.tabs[1], gBrowser.tabs.at(-1), "Using same index -1 doesn't move.");
|
||||
|
||||
ext.sendMessage(gid, { index: 0 });
|
||||
await Promise.all([ext.awaitMessage("done"), ext.awaitMessage("moved")]);
|
||||
is(group.tabs[0], gBrowser.tabs[0], "Group moved to the beginning.");
|
||||
|
||||
ext.sendMessage(gid, { index: 0 });
|
||||
await ext.awaitMessage("done");
|
||||
is(group.tabs[0], gBrowser.tabs[0], "Using same index 0 doesn't move.");
|
||||
|
||||
info("Create a large second group, and try to move in the middle of it.");
|
||||
let group4 = gBrowser.addTabGroup(gBrowser.tabs.slice(5));
|
||||
let created4 = await ext.awaitMessage("created");
|
||||
let gid4 = getExtTabGroupIdForInternalTabGroupId(group4.id);
|
||||
is(created4.id, gid4, "Correct group 4 created event.");
|
||||
|
||||
ext.sendMessage(gid, { index: 8 });
|
||||
await Promise.all([ext.awaitMessage("done"), ext.awaitMessage("moved")]);
|
||||
is(group.tabs[1], gBrowser.tabs[4], "Moved before the whole group instead.");
|
||||
|
||||
for (let tab of tabs) {
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
}
|
||||
let removed1 = await ext.awaitMessage("removed");
|
||||
let removed4 = await ext.awaitMessage("removed");
|
||||
is(removed1.id, gid, "Correct group 1 removed event.");
|
||||
is(removed4.id, gid4, "Correct group 4 removed event.");
|
||||
|
||||
info("Test moving a group from a non-private to a private window.");
|
||||
let win2 = await BrowserTestUtils.openNewBrowserWindow({ private: true });
|
||||
let tab2 = await BrowserTestUtils.openNewForegroundTab(win2.gBrowser, url);
|
||||
let group2 = win2.gBrowser.addTabGroup([tab2]);
|
||||
let gid2 = getExtTabGroupIdForInternalTabGroupId(group2.id);
|
||||
let created2 = await ext.awaitMessage("created");
|
||||
is(created2.id, gid2, "Correct group 2 create event.");
|
||||
|
||||
ext.sendMessage(gid2, { index: 0, windowId });
|
||||
let error = await ext.awaitMessage("error");
|
||||
is(error, "Can't move groups between private and non-private windows");
|
||||
await BrowserTestUtils.closeWindow(win2);
|
||||
|
||||
info("Test moving a group to another window.");
|
||||
let win3 = await BrowserTestUtils.openNewBrowserWindow();
|
||||
let tab3 = await BrowserTestUtils.openNewForegroundTab(win3.gBrowser, url);
|
||||
let group3 = win3.gBrowser.addTabGroup([tab3]);
|
||||
let gid3 = getExtTabGroupIdForInternalTabGroupId(group3.id);
|
||||
let created3 = await ext.awaitMessage("created");
|
||||
is(created3.id, gid3, "Correct group 3 create event.");
|
||||
|
||||
ext.sendMessage(gid3, { index: 0, windowId });
|
||||
await ext.awaitMessage("done");
|
||||
|
||||
let moved3 = await ext.awaitMessage("moved");
|
||||
// Chrome fires onRemoved + onCreated, but we intentionally dispatch onMoved
|
||||
// because it makes more sense - bug 1962475.
|
||||
is(moved3.id, gid3, "onMoved fired after moving to another window");
|
||||
|
||||
// Add and remove a tab group, so that if onCreated/onRemoved was
|
||||
// unexpectedly fired by the move above, that we will see (unexpected) gid3
|
||||
// instead of (expected) gid4.
|
||||
let tab5 = await BrowserTestUtils.openNewForegroundTab(win3.gBrowser, url);
|
||||
let group5 = win3.gBrowser.addTabGroup([tab5]);
|
||||
let created5 = await ext.awaitMessage("created");
|
||||
let gid5 = getExtTabGroupIdForInternalTabGroupId(group5.id);
|
||||
is(created5.id, gid5, "Correct group 5 create event.");
|
||||
win3.gBrowser.removeTabGroup(group5);
|
||||
let removed5 = await ext.awaitMessage("removed");
|
||||
is(removed5.id, gid5, "Correct group 5 removed event.");
|
||||
|
||||
await BrowserTestUtils.closeWindow(win3);
|
||||
let group3b = gBrowser.getTabGroupById(group3.id);
|
||||
BrowserTestUtils.removeTab(group3b.tabs[0]);
|
||||
let removed3 = await ext.awaitMessage("removed");
|
||||
is(removed3.id, gid3, "Correct group 3 removed event.");
|
||||
|
||||
await ext.unload();
|
||||
});
|
|
@ -0,0 +1,174 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
add_task(async function tabsGroups_query() {
|
||||
async function loadExt(allowPrivate) {
|
||||
let ext = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
permissions: ["tabGroups"],
|
||||
},
|
||||
incognitoOverride: allowPrivate ? "spanning" : undefined,
|
||||
async background() {
|
||||
let [tab] = await browser.tabs.query({
|
||||
lastFocusedWindow: true,
|
||||
active: true,
|
||||
});
|
||||
browser.test.onMessage.addListener(async (_msg, tests) => {
|
||||
for (let { query, expected } of tests) {
|
||||
let groups = await browser.tabGroups.query(query);
|
||||
let titles = groups.map(group => group.title);
|
||||
browser.test.assertEq(
|
||||
expected.map(e => String(e)).join(),
|
||||
titles.sort().join(),
|
||||
`Expected groups for query - ${JSON.stringify(query)}`
|
||||
);
|
||||
}
|
||||
browser.test.sendMessage("done");
|
||||
});
|
||||
browser.test.sendMessage("windowId", tab.windowId);
|
||||
},
|
||||
});
|
||||
await ext.startup();
|
||||
return ext;
|
||||
}
|
||||
|
||||
let ext = await loadExt();
|
||||
let windowId = await ext.awaitMessage("windowId");
|
||||
|
||||
let url = "https://example.com/foo?";
|
||||
let groups = [];
|
||||
let tabs = [];
|
||||
let colors = ["red", "blue", "red", "pink", "green"];
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
tabs.push(await BrowserTestUtils.openNewForegroundTab(gBrowser, url + i));
|
||||
let group = gBrowser.addTabGroup(tabs.slice(-1));
|
||||
group.color = colors[i];
|
||||
group.name = String(i);
|
||||
groups.push(group);
|
||||
}
|
||||
groups[2].collapsed = true;
|
||||
groups[3].collapsed = true;
|
||||
|
||||
let win2 = await BrowserTestUtils.openNewBrowserWindow();
|
||||
let tab5 = await BrowserTestUtils.openNewForegroundTab(win2.gBrowser, url);
|
||||
let tab6 = await BrowserTestUtils.openNewForegroundTab(win2.gBrowser, url);
|
||||
let group5 = win2.gBrowser.addTabGroup([tab5]);
|
||||
let group6 = win2.gBrowser.addTabGroup([tab6]);
|
||||
group5.name = "5";
|
||||
group6.name = "6";
|
||||
group5.color = "red";
|
||||
group6.color = "gray";
|
||||
group6.collapsed = true;
|
||||
|
||||
ext.sendMessage("runTests", [
|
||||
{
|
||||
query: undefined,
|
||||
expected: [0, 1, 2, 3, 4, 5, 6],
|
||||
},
|
||||
{
|
||||
query: {},
|
||||
expected: [0, 1, 2, 3, 4, 5, 6],
|
||||
},
|
||||
{
|
||||
query: { collapsed: true },
|
||||
expected: [2, 3, 6],
|
||||
},
|
||||
{
|
||||
query: { collapsed: false },
|
||||
expected: [0, 1, 4, 5],
|
||||
},
|
||||
{
|
||||
query: { color: "red" },
|
||||
expected: [0, 2, 5],
|
||||
},
|
||||
{
|
||||
query: { color: "grey" },
|
||||
expected: [6],
|
||||
},
|
||||
{
|
||||
query: { title: "4" },
|
||||
expected: [4],
|
||||
},
|
||||
{
|
||||
query: { title: "*" },
|
||||
expected: [0, 1, 2, 3, 4, 5, 6],
|
||||
},
|
||||
{
|
||||
query: { windowId },
|
||||
expected: [0, 1, 2, 3, 4],
|
||||
},
|
||||
{
|
||||
query: { windowId: -2 },
|
||||
expected: [5, 6],
|
||||
},
|
||||
{
|
||||
query: { windowId: 1e9 },
|
||||
expected: [],
|
||||
},
|
||||
{
|
||||
query: { collapsed: true, color: "red" },
|
||||
expected: [2],
|
||||
},
|
||||
{
|
||||
query: { color: "red", windowId },
|
||||
expected: [0, 2],
|
||||
},
|
||||
]);
|
||||
await ext.awaitMessage("done");
|
||||
|
||||
groups[3].name = "Foo 3";
|
||||
group5.name = "FooBar 5";
|
||||
|
||||
let win3 = await BrowserTestUtils.openNewBrowserWindow({ private: true });
|
||||
let tab7 = await BrowserTestUtils.openNewForegroundTab(win3.gBrowser, url);
|
||||
let group7 = win3.gBrowser.addTabGroup([tab7]);
|
||||
group7.name = "FooBaz 7";
|
||||
|
||||
let ext2 = await loadExt(true);
|
||||
windowId = await ext2.awaitMessage("windowId");
|
||||
|
||||
info("Extension without private browsing should't see group7 from win3.");
|
||||
ext.sendMessage("runTests", [
|
||||
{
|
||||
query: { title: "Foo*" },
|
||||
expected: ["Foo 3", "FooBar 5"],
|
||||
},
|
||||
{
|
||||
query: { windowId },
|
||||
expected: [],
|
||||
},
|
||||
{
|
||||
query: { windowId: -2 },
|
||||
expected: [],
|
||||
},
|
||||
]);
|
||||
await ext.awaitMessage("done");
|
||||
|
||||
info("Extension with private browsing access should see group7 from win3.");
|
||||
ext2.sendMessage("runTests", [
|
||||
{
|
||||
query: { title: "Foo*" },
|
||||
expected: ["Foo 3", "FooBar 5", "FooBaz 7"],
|
||||
},
|
||||
{
|
||||
query: { windowId },
|
||||
expected: ["FooBaz 7"],
|
||||
},
|
||||
{
|
||||
query: { windowId: -2 },
|
||||
expected: ["FooBaz 7"],
|
||||
},
|
||||
]);
|
||||
await ext2.awaitMessage("done");
|
||||
|
||||
for (let tab of tabs) {
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
}
|
||||
await BrowserTestUtils.closeWindow(win2);
|
||||
await BrowserTestUtils.closeWindow(win3);
|
||||
|
||||
await ext.unload();
|
||||
await ext2.unload();
|
||||
});
|
|
@ -2,7 +2,7 @@
|
|||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
// TODO bug 1938594: group_across_private_browsing_windows sometimes triggers
|
||||
// TODO bug 1938594: group_pinned_tab_from_different_window sometimes triggers
|
||||
// this error. See https://bugzilla.mozilla.org/show_bug.cgi?id=1938594#c1
|
||||
PromiseTestUtils.allowMatchingRejectionsGlobally(
|
||||
/Unexpected undefined tabState for onMoveToNewWindow/
|
||||
|
@ -10,6 +10,9 @@ PromiseTestUtils.allowMatchingRejectionsGlobally(
|
|||
|
||||
add_task(async function group_ungroup_and_index() {
|
||||
const extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
permissions: ["tabGroups"],
|
||||
},
|
||||
files: {
|
||||
"tab1.htm": "<title>tab1.html</title>",
|
||||
"tab2.htm": "<title>tab2.html</title>",
|
||||
|
@ -17,9 +20,28 @@ add_task(async function group_ungroup_and_index() {
|
|||
},
|
||||
async background() {
|
||||
const { id: tabId1 } = await browser.tabs.create({ url: "tab1.htm" });
|
||||
const { id: tabId2 } = await browser.tabs.create({ url: "tab2.htm " });
|
||||
const { id: tabId2 } = await browser.tabs.create({ url: "tab2.htm" });
|
||||
const { id: tabId3 } = await browser.tabs.create({ url: "tab3.htm" });
|
||||
|
||||
let eventIds = [];
|
||||
let expected = [];
|
||||
let allEvents = Promise.withResolvers();
|
||||
|
||||
browser.tabGroups.onCreated.addListener(group => {
|
||||
eventIds.push(group.id);
|
||||
browser.test.log(`Events so far (${eventIds.length}): ${eventIds}`);
|
||||
});
|
||||
browser.tabGroups.onRemoved.addListener(group => {
|
||||
eventIds.push(-group.id);
|
||||
browser.test.log(`Events so far (${eventIds.length}): ${eventIds}`);
|
||||
if (eventIds.length === 16) {
|
||||
allEvents.resolve();
|
||||
}
|
||||
if (eventIds.length > 16) {
|
||||
browser.fail("Extra event received: " + group.id);
|
||||
}
|
||||
});
|
||||
|
||||
async function assertAllTabExpectations(expectations, desc) {
|
||||
const tabs = await Promise.all([
|
||||
browser.tabs.get(tabId1),
|
||||
|
@ -52,6 +74,7 @@ add_task(async function group_ungroup_and_index() {
|
|||
|
||||
const groupId1 = await browser.tabs.group({ tabIds: tabId1 });
|
||||
const groupId2 = await browser.tabs.group({ tabIds: [tabId2, tabId3] });
|
||||
expected.push(groupId1, groupId2);
|
||||
|
||||
await assertAllTabExpectations(
|
||||
{ indexes: [1, 2, 3], groupIds: [groupId1, groupId2, groupId2] },
|
||||
|
@ -63,6 +86,7 @@ add_task(async function group_ungroup_and_index() {
|
|||
// middle of a tab group.
|
||||
await browser.tabs.ungroup([tabId3, tabId1]);
|
||||
await browser.tabs.ungroup(tabId2);
|
||||
expected.push(-groupId1, -groupId2);
|
||||
|
||||
await browser.test.assertRejects(
|
||||
browser.tabs.group({ tabIds: tabId3, groupId: groupId1 }),
|
||||
|
@ -81,6 +105,7 @@ add_task(async function group_ungroup_and_index() {
|
|||
{ indexes: [1, 3, 2], groupIds: [groupId3, groupId3, -1] },
|
||||
"Tabs in same tab group must be next to each other"
|
||||
);
|
||||
expected.push(groupId3);
|
||||
|
||||
// Join existing tab group - now we should have three in the tab group.
|
||||
const groupId4 = await browser.tabs.group({
|
||||
|
@ -100,6 +125,7 @@ add_task(async function group_ungroup_and_index() {
|
|||
);
|
||||
|
||||
await browser.tabs.ungroup([tabId1, tabId2, tabId3]);
|
||||
expected.push(-groupId3);
|
||||
|
||||
// Ungrouping of the group should not have changed positions either,
|
||||
// despite the list of tabIds passed to ungroup() being out of order.
|
||||
|
@ -124,6 +150,7 @@ add_task(async function group_ungroup_and_index() {
|
|||
{ indexes: [1, 2, 3], groupIds: [groupId6, groupId5, groupId5] },
|
||||
"Leftmost tab should still be ordered before the original tab group"
|
||||
);
|
||||
expected.push(groupId5, groupId6);
|
||||
|
||||
// Join an existing group (from the left). Position should not change.
|
||||
await browser.tabs.group({ tabIds: [tabId1], groupId: groupId5 });
|
||||
|
@ -136,6 +163,7 @@ add_task(async function group_ungroup_and_index() {
|
|||
`No group with id: ${groupId6}`,
|
||||
"Old groupId should be invalid after last tab was moved from group"
|
||||
);
|
||||
expected.push(-groupId6);
|
||||
|
||||
// Move the middle tab to a new group. That tab should be at the right.
|
||||
const groupId7 = await browser.tabs.group({ tabIds: [tabId2] });
|
||||
|
@ -147,6 +175,7 @@ add_task(async function group_ungroup_and_index() {
|
|||
// Prepare: tabId1 and tabId2 together at the left, followed by tabId3.
|
||||
const groupId8 = await browser.tabs.group({ tabIds: [tabId1, tabId2] });
|
||||
await browser.tabs.ungroup(tabId3);
|
||||
expected.push(groupId7, groupId8);
|
||||
|
||||
// When tabId2 is moved to a new group, it should stay in the middle,
|
||||
// meaning that the tab was inserted after its original tab group.
|
||||
|
@ -156,10 +185,25 @@ add_task(async function group_ungroup_and_index() {
|
|||
{ indexes: [1, 2, 3], groupIds: [groupId8, groupId9, -1] },
|
||||
"group() on rightmost tab should appear after original tab group"
|
||||
);
|
||||
expected.push(-groupId7, -groupId5, groupId9);
|
||||
|
||||
await browser.tabs.remove(tabId1);
|
||||
await browser.tabs.remove(tabId2);
|
||||
await browser.tabs.remove(tabId3);
|
||||
|
||||
expected.push(-groupId8, -groupId9);
|
||||
|
||||
// TODO bug 1962683: Re-enable when events are no longer missing
|
||||
// await allEvents.promise;
|
||||
//
|
||||
// browser.test.assertEq(
|
||||
// eventIds.join(),
|
||||
// expected.join(),
|
||||
// "Received expected onCreated events"
|
||||
// );
|
||||
browser.test.log(`Expect: ${eventIds.join()}`);
|
||||
browser.test.log(`Actual: ${expected.join()}`);
|
||||
|
||||
browser.test.sendMessage("done");
|
||||
},
|
||||
});
|
||||
|
@ -168,221 +212,6 @@ add_task(async function group_ungroup_and_index() {
|
|||
await extension.unload();
|
||||
});
|
||||
|
||||
add_task(async function group_with_windowId() {
|
||||
const extension = ExtensionTestUtils.loadExtension({
|
||||
async background() {
|
||||
const { id: windowId, tabs: initialTabs } = await browser.windows.create(
|
||||
{}
|
||||
);
|
||||
browser.test.assertEq(1, initialTabs.length, "Got window with 1 tab");
|
||||
|
||||
const { id: tabId1 } = await browser.tabs.create({});
|
||||
const { id: tabId2 } = await browser.tabs.create({});
|
||||
|
||||
const groupId1 = await browser.tabs.group({
|
||||
tabIds: [tabId2, tabId1],
|
||||
createProperties: { windowId },
|
||||
});
|
||||
|
||||
browser.test.assertDeepEq(
|
||||
Array.from(await browser.tabs.query({ groupId: groupId1 }), t => t.id),
|
||||
[tabId2, tabId1],
|
||||
"Moved tabs to group"
|
||||
);
|
||||
browser.test.assertDeepEq(
|
||||
Array.from(await browser.tabs.query({ windowId }), t => t.id),
|
||||
[initialTabs[0].id, tabId2, tabId1],
|
||||
"Moved tabs to group in new window (next to initial tab)"
|
||||
);
|
||||
|
||||
await browser.windows.remove(windowId);
|
||||
browser.test.sendMessage("done");
|
||||
},
|
||||
});
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("done");
|
||||
await extension.unload();
|
||||
});
|
||||
|
||||
add_task(async function group_adopt_from_multiple_windows() {
|
||||
const extension = ExtensionTestUtils.loadExtension({
|
||||
incognitoOverride: "spanning",
|
||||
async background() {
|
||||
const {
|
||||
id: windowId1,
|
||||
tabs: [{ id: tabId1 }],
|
||||
} = await browser.windows.create({});
|
||||
const {
|
||||
id: windowId2,
|
||||
tabs: [{ id: tabId2 }],
|
||||
} = await browser.windows.create({});
|
||||
const {
|
||||
id: windowId3,
|
||||
tabs: [{ id: tabId3 }],
|
||||
} = await browser.windows.create({});
|
||||
|
||||
// This confirms that group() can adapt tabs from different windows.
|
||||
const groupId = await browser.tabs.group({
|
||||
tabIds: [tabId2, tabId3],
|
||||
createProperties: { windowId: windowId1 },
|
||||
});
|
||||
|
||||
await browser.test.assertRejects(
|
||||
browser.windows.get(windowId2),
|
||||
`Invalid window ID: ${windowId2}`,
|
||||
"Window closes when group() adopts the last tab of the window"
|
||||
);
|
||||
// We just confirmed that window2 is closed, do the same for window3.
|
||||
await browser.test.assertRejects(
|
||||
browser.tabs.group({
|
||||
tabIds: tabId1,
|
||||
createProperties: { windowId: windowId3 },
|
||||
}),
|
||||
`Invalid window ID: ${windowId3}`,
|
||||
"group() cannot adapt groups from a closed window"
|
||||
);
|
||||
|
||||
browser.test.assertDeepEq(
|
||||
// Note: tabId1 is missing because the above group() rejected.
|
||||
[tabId2, tabId3],
|
||||
Array.from(await browser.tabs.query({ groupId }), tab => tab.id),
|
||||
"All specified tabIds should now belong to the given group"
|
||||
);
|
||||
|
||||
await browser.windows.remove(windowId1);
|
||||
browser.test.sendMessage("done");
|
||||
},
|
||||
});
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("done");
|
||||
await extension.unload();
|
||||
});
|
||||
|
||||
add_task(async function group_across_private_browsing_windows() {
|
||||
const extension = ExtensionTestUtils.loadExtension({
|
||||
incognitoOverride: "spanning",
|
||||
async background() {
|
||||
const privateWin = await browser.windows.create({ incognito: true });
|
||||
const normalWin = await browser.windows.create({ incognito: false });
|
||||
|
||||
const otherPrivateWin = await browser.windows.create({ incognito: true });
|
||||
const otherNormalWin = await browser.windows.create({ incognito: false });
|
||||
|
||||
// Mixture of private and non-private tabIDs, so we can easily verify
|
||||
// whether we inadvertently move some of the tabs.
|
||||
const privateTab = await browser.tabs.create({ windowId: privateWin.id });
|
||||
const privateAndNonPrivateTabs = [
|
||||
privateTab,
|
||||
await browser.tabs.create({ windowId: normalWin.id }),
|
||||
await browser.tabs.create({ windowId: privateWin.id }),
|
||||
await browser.tabs.create({ windowId: normalWin.id }),
|
||||
];
|
||||
|
||||
await browser.test.assertRejects(
|
||||
browser.tabs.group({
|
||||
tabIds: privateAndNonPrivateTabs.map(t => t.id),
|
||||
}),
|
||||
"Cannot move private tabs to non-private window",
|
||||
"Should not be able to move private+non-private tabs to current window"
|
||||
);
|
||||
|
||||
await browser.test.assertRejects(
|
||||
browser.tabs.group({
|
||||
tabIds: privateAndNonPrivateTabs.map(t => t.id),
|
||||
createProperties: { windowId: otherPrivateWin.id },
|
||||
}),
|
||||
"Cannot move non-private tabs to private window",
|
||||
"Should not be able to move non-private tab to private window"
|
||||
);
|
||||
await browser.test.assertRejects(
|
||||
browser.tabs.group({
|
||||
tabIds: privateAndNonPrivateTabs.map(t => t.id),
|
||||
createProperties: { windowId: otherNormalWin.id },
|
||||
}),
|
||||
"Cannot move private tabs to non-private window",
|
||||
"Should not be able to move private tab to non-private window"
|
||||
);
|
||||
|
||||
const reply = await browser.runtime.sendMessage("@no_private", {
|
||||
privateWindowId: privateWin.id,
|
||||
privateTabId: privateTab.id,
|
||||
});
|
||||
browser.test.assertEq("no_private:done", reply, "Reply from other ext");
|
||||
|
||||
for (const tab of privateAndNonPrivateTabs) {
|
||||
const actualTab = await browser.tabs.get(tab.id);
|
||||
browser.test.assertEq(
|
||||
tab.windowId,
|
||||
actualTab.windowId,
|
||||
"Tab should not have moved to a different window"
|
||||
);
|
||||
browser.test.assertEq(
|
||||
tab.windowId,
|
||||
actualTab.windowId,
|
||||
"Tab should not have moved within its window"
|
||||
);
|
||||
browser.test.assertEq(
|
||||
tab.groupId,
|
||||
-1,
|
||||
"Tab should not have joined a group"
|
||||
);
|
||||
}
|
||||
|
||||
// Now check that we can actually group tabs in private windows.
|
||||
const groupId = await browser.tabs.group({
|
||||
tabIds: privateTab.id,
|
||||
createProperties: { windowId: otherPrivateWin.id },
|
||||
});
|
||||
const updatedPrivateTab = await browser.tabs.get(privateTab.id);
|
||||
browser.test.assertEq(
|
||||
groupId,
|
||||
updatedPrivateTab.groupId,
|
||||
"group() succeeded with private tab"
|
||||
);
|
||||
browser.test.assertEq(
|
||||
otherPrivateWin.id,
|
||||
updatedPrivateTab.windowId,
|
||||
"Private tab is now part of the destination private window"
|
||||
);
|
||||
|
||||
await browser.windows.remove(privateWin.id);
|
||||
await browser.windows.remove(normalWin.id);
|
||||
await browser.windows.remove(otherPrivateWin.id);
|
||||
await browser.windows.remove(otherNormalWin.id);
|
||||
browser.test.sendMessage("done");
|
||||
},
|
||||
});
|
||||
const extensionWithoutPrivateAccess = ExtensionTestUtils.loadExtension({
|
||||
manifest: { browser_specific_settings: { gecko: { id: "@no_private" } } },
|
||||
background() {
|
||||
browser.runtime.onMessageExternal.addListener(async data => {
|
||||
const { privateWindowId, privateTabId } = data;
|
||||
const { id: normalTabId } = await browser.tabs.create({});
|
||||
await browser.test.assertRejects(
|
||||
browser.tabs.group({ tabIds: [privateTabId] }),
|
||||
`Invalid tab ID: ${privateTabId}`,
|
||||
"@no_private should not be able to group private tabs"
|
||||
);
|
||||
await browser.test.assertRejects(
|
||||
browser.tabs.group({
|
||||
tabIds: [normalTabId],
|
||||
createProperties: { windowId: privateWindowId },
|
||||
}),
|
||||
`Invalid window ID: ${privateWindowId}`,
|
||||
"@no_private should not see private windows"
|
||||
);
|
||||
await browser.tabs.remove(normalTabId);
|
||||
return "no_private:done"; // Checked by sender.
|
||||
});
|
||||
},
|
||||
});
|
||||
await extensionWithoutPrivateAccess.startup();
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("done");
|
||||
await extensionWithoutPrivateAccess.unload();
|
||||
await extension.unload();
|
||||
});
|
||||
|
||||
add_task(async function group_pinned_tab() {
|
||||
const extension = ExtensionTestUtils.loadExtension({
|
||||
async background() {
|
||||
|
|
|
@ -0,0 +1,224 @@
|
|||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
// TODO bug 1938594: group_across_private_browsing_windows sometimes triggers
|
||||
// this error. See https://bugzilla.mozilla.org/show_bug.cgi?id=1938594#c1
|
||||
PromiseTestUtils.allowMatchingRejectionsGlobally(
|
||||
/Unexpected undefined tabState for onMoveToNewWindow/
|
||||
);
|
||||
|
||||
add_task(async function group_with_windowId() {
|
||||
const extension = ExtensionTestUtils.loadExtension({
|
||||
async background() {
|
||||
const { id: windowId, tabs: initialTabs } = await browser.windows.create(
|
||||
{}
|
||||
);
|
||||
browser.test.assertEq(1, initialTabs.length, "Got window with 1 tab");
|
||||
|
||||
const { id: tabId1 } = await browser.tabs.create({});
|
||||
const { id: tabId2 } = await browser.tabs.create({});
|
||||
|
||||
const groupId1 = await browser.tabs.group({
|
||||
tabIds: [tabId2, tabId1],
|
||||
createProperties: { windowId },
|
||||
});
|
||||
|
||||
browser.test.assertDeepEq(
|
||||
Array.from(await browser.tabs.query({ groupId: groupId1 }), t => t.id),
|
||||
[tabId2, tabId1],
|
||||
"Moved tabs to group"
|
||||
);
|
||||
browser.test.assertDeepEq(
|
||||
Array.from(await browser.tabs.query({ windowId }), t => t.id),
|
||||
[initialTabs[0].id, tabId2, tabId1],
|
||||
"Moved tabs to group in new window (next to initial tab)"
|
||||
);
|
||||
|
||||
await browser.windows.remove(windowId);
|
||||
browser.test.sendMessage("done");
|
||||
},
|
||||
});
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("done");
|
||||
await extension.unload();
|
||||
});
|
||||
|
||||
add_task(async function group_adopt_from_multiple_windows() {
|
||||
const extension = ExtensionTestUtils.loadExtension({
|
||||
incognitoOverride: "spanning",
|
||||
async background() {
|
||||
const {
|
||||
id: windowId1,
|
||||
tabs: [{ id: tabId1 }],
|
||||
} = await browser.windows.create({});
|
||||
const {
|
||||
id: windowId2,
|
||||
tabs: [{ id: tabId2 }],
|
||||
} = await browser.windows.create({});
|
||||
const {
|
||||
id: windowId3,
|
||||
tabs: [{ id: tabId3 }],
|
||||
} = await browser.windows.create({});
|
||||
|
||||
// This confirms that group() can adapt tabs from different windows.
|
||||
const groupId = await browser.tabs.group({
|
||||
tabIds: [tabId2, tabId3],
|
||||
createProperties: { windowId: windowId1 },
|
||||
});
|
||||
|
||||
await browser.test.assertRejects(
|
||||
browser.windows.get(windowId2),
|
||||
`Invalid window ID: ${windowId2}`,
|
||||
"Window closes when group() adopts the last tab of the window"
|
||||
);
|
||||
// We just confirmed that window2 is closed, do the same for window3.
|
||||
await browser.test.assertRejects(
|
||||
browser.tabs.group({
|
||||
tabIds: tabId1,
|
||||
createProperties: { windowId: windowId3 },
|
||||
}),
|
||||
`Invalid window ID: ${windowId3}`,
|
||||
"group() cannot adapt groups from a closed window"
|
||||
);
|
||||
|
||||
browser.test.assertDeepEq(
|
||||
// Note: tabId1 is missing because the above group() rejected.
|
||||
[tabId2, tabId3],
|
||||
Array.from(await browser.tabs.query({ groupId }), tab => tab.id),
|
||||
"All specified tabIds should now belong to the given group"
|
||||
);
|
||||
|
||||
await browser.windows.remove(windowId1);
|
||||
browser.test.sendMessage("done");
|
||||
},
|
||||
});
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("done");
|
||||
await extension.unload();
|
||||
});
|
||||
|
||||
add_task(async function group_across_private_browsing_windows() {
|
||||
const extension = ExtensionTestUtils.loadExtension({
|
||||
incognitoOverride: "spanning",
|
||||
async background() {
|
||||
const privateWin = await browser.windows.create({ incognito: true });
|
||||
const normalWin = await browser.windows.create({ incognito: false });
|
||||
|
||||
const otherPrivateWin = await browser.windows.create({ incognito: true });
|
||||
const otherNormalWin = await browser.windows.create({ incognito: false });
|
||||
|
||||
// Mixture of private and non-private tabIDs, so we can easily verify
|
||||
// whether we inadvertently move some of the tabs.
|
||||
const privateTab = await browser.tabs.create({ windowId: privateWin.id });
|
||||
const privateAndNonPrivateTabs = [
|
||||
privateTab,
|
||||
await browser.tabs.create({ windowId: normalWin.id }),
|
||||
await browser.tabs.create({ windowId: privateWin.id }),
|
||||
await browser.tabs.create({ windowId: normalWin.id }),
|
||||
];
|
||||
|
||||
await browser.test.assertRejects(
|
||||
browser.tabs.group({
|
||||
tabIds: privateAndNonPrivateTabs.map(t => t.id),
|
||||
}),
|
||||
"Cannot move private tabs to non-private window",
|
||||
"Should not be able to move private+non-private tabs to current window"
|
||||
);
|
||||
|
||||
await browser.test.assertRejects(
|
||||
browser.tabs.group({
|
||||
tabIds: privateAndNonPrivateTabs.map(t => t.id),
|
||||
createProperties: { windowId: otherPrivateWin.id },
|
||||
}),
|
||||
"Cannot move non-private tabs to private window",
|
||||
"Should not be able to move non-private tab to private window"
|
||||
);
|
||||
await browser.test.assertRejects(
|
||||
browser.tabs.group({
|
||||
tabIds: privateAndNonPrivateTabs.map(t => t.id),
|
||||
createProperties: { windowId: otherNormalWin.id },
|
||||
}),
|
||||
"Cannot move private tabs to non-private window",
|
||||
"Should not be able to move private tab to non-private window"
|
||||
);
|
||||
|
||||
const reply = await browser.runtime.sendMessage("@no_private", {
|
||||
privateWindowId: privateWin.id,
|
||||
privateTabId: privateTab.id,
|
||||
});
|
||||
browser.test.assertEq("no_private:done", reply, "Reply from other ext");
|
||||
|
||||
for (const tab of privateAndNonPrivateTabs) {
|
||||
const actualTab = await browser.tabs.get(tab.id);
|
||||
browser.test.assertEq(
|
||||
tab.windowId,
|
||||
actualTab.windowId,
|
||||
"Tab should not have moved to a different window"
|
||||
);
|
||||
browser.test.assertEq(
|
||||
tab.windowId,
|
||||
actualTab.windowId,
|
||||
"Tab should not have moved within its window"
|
||||
);
|
||||
browser.test.assertEq(
|
||||
tab.groupId,
|
||||
-1,
|
||||
"Tab should not have joined a group"
|
||||
);
|
||||
}
|
||||
|
||||
// Now check that we can actually group tabs in private windows.
|
||||
const groupId = await browser.tabs.group({
|
||||
tabIds: privateTab.id,
|
||||
createProperties: { windowId: otherPrivateWin.id },
|
||||
});
|
||||
const updatedPrivateTab = await browser.tabs.get(privateTab.id);
|
||||
browser.test.assertEq(
|
||||
groupId,
|
||||
updatedPrivateTab.groupId,
|
||||
"group() succeeded with private tab"
|
||||
);
|
||||
browser.test.assertEq(
|
||||
otherPrivateWin.id,
|
||||
updatedPrivateTab.windowId,
|
||||
"Private tab is now part of the destination private window"
|
||||
);
|
||||
|
||||
await browser.windows.remove(privateWin.id);
|
||||
await browser.windows.remove(normalWin.id);
|
||||
await browser.windows.remove(otherPrivateWin.id);
|
||||
await browser.windows.remove(otherNormalWin.id);
|
||||
browser.test.sendMessage("done");
|
||||
},
|
||||
});
|
||||
const extensionWithoutPrivateAccess = ExtensionTestUtils.loadExtension({
|
||||
manifest: { browser_specific_settings: { gecko: { id: "@no_private" } } },
|
||||
background() {
|
||||
browser.runtime.onMessageExternal.addListener(async data => {
|
||||
const { privateWindowId, privateTabId } = data;
|
||||
const { id: normalTabId } = await browser.tabs.create({});
|
||||
await browser.test.assertRejects(
|
||||
browser.tabs.group({ tabIds: [privateTabId] }),
|
||||
`Invalid tab ID: ${privateTabId}`,
|
||||
"@no_private should not be able to group private tabs"
|
||||
);
|
||||
await browser.test.assertRejects(
|
||||
browser.tabs.group({
|
||||
tabIds: [normalTabId],
|
||||
createProperties: { windowId: privateWindowId },
|
||||
}),
|
||||
`Invalid window ID: ${privateWindowId}`,
|
||||
"@no_private should not see private windows"
|
||||
);
|
||||
await browser.tabs.remove(normalTabId);
|
||||
return "no_private:done"; // Checked by sender.
|
||||
});
|
||||
},
|
||||
});
|
||||
await extensionWithoutPrivateAccess.startup();
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("done");
|
||||
await extensionWithoutPrivateAccess.unload();
|
||||
await extension.unload();
|
||||
});
|
|
@ -27,6 +27,7 @@
|
|||
flex-direction: column;
|
||||
font-size: 0.95rem;
|
||||
gap: 6px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.import-history-banner .banner-text span:first-child {
|
||||
|
@ -37,7 +38,7 @@
|
|||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 16px;
|
||||
padding: 8px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.import-history-banner .buttons {
|
||||
|
|
|
@ -386,75 +386,52 @@ export const GenAI = {
|
|||
);
|
||||
return context;
|
||||
},
|
||||
/**
|
||||
* Handle messages from content to show or hide shortcuts.
|
||||
*
|
||||
* @param {string} name of message
|
||||
* @param {{
|
||||
inputType: string,
|
||||
selection: string,
|
||||
delay: number,
|
||||
x: number,
|
||||
y: number,
|
||||
}} data for the message
|
||||
* @param {MozBrowser} browser that provided the message
|
||||
*/
|
||||
handleShortcutsMessage(name, data, browser) {
|
||||
const isInBrowserStack = browser?.closest(".browserStack");
|
||||
|
||||
if (
|
||||
!isInBrowserStack ||
|
||||
!browser ||
|
||||
this.ignoredInputs.has(data.inputType) ||
|
||||
!lazy.chatShortcuts ||
|
||||
!this.canShowChatEntrypoint
|
||||
) {
|
||||
/**
|
||||
* Setup helpers and callbacks for ai shortcut button.
|
||||
*
|
||||
* @param {MozButton} aiActionButton instance for the browser window
|
||||
*/
|
||||
initializeAIShortcut(aiActionButton) {
|
||||
if (aiActionButton.initialized) {
|
||||
return;
|
||||
}
|
||||
aiActionButton.initialized = true;
|
||||
|
||||
const window = browser.ownerGlobal;
|
||||
const { document, devicePixelRatio } = window;
|
||||
|
||||
// Get Panel elements
|
||||
const document = aiActionButton.ownerDocument;
|
||||
const buttonActiveState = "icon";
|
||||
const buttonDefaultState = "icon ghost";
|
||||
const chatShortcutsOptionsPanel = document.getElementById(
|
||||
"chat-shortcuts-options-panel"
|
||||
);
|
||||
const selectionShortcutActionPanel = document.getElementById(
|
||||
"selection-shortcut-action-panel"
|
||||
);
|
||||
|
||||
if (!chatShortcutsOptionsPanel || !selectionShortcutActionPanel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const aiActionButton =
|
||||
selectionShortcutActionPanel.querySelector("#ai-action-button");
|
||||
aiActionButton.iconSrc = "chrome://global/skin/icons/highlights.svg";
|
||||
const buttonActiveState = "icon";
|
||||
const buttonDefaultState = "icon ghost";
|
||||
|
||||
// Hide shortcuts and panel
|
||||
const hide = () => {
|
||||
aiActionButton.setAttribute("type", buttonDefaultState);
|
||||
aiActionButton.removeEventListener("mouseover", aiActionButton.listener);
|
||||
aiActionButton.listener = null;
|
||||
aiActionButton.hide = () => {
|
||||
chatShortcutsOptionsPanel.hidePopup();
|
||||
selectionShortcutActionPanel.hidePopup();
|
||||
};
|
||||
aiActionButton.iconSrc = "chrome://global/skin/icons/highlights.svg";
|
||||
aiActionButton.setAttribute("type", buttonDefaultState);
|
||||
chatShortcutsOptionsPanel.addEventListener("popuphidden", () =>
|
||||
aiActionButton.setAttribute("type", buttonDefaultState)
|
||||
);
|
||||
chatShortcutsOptionsPanel.firstChild.id = "ask-chat-shortcuts";
|
||||
|
||||
// Helper to show rounded warning numbers
|
||||
const roundDownToNearestHundred = number => {
|
||||
return Math.floor(number / 100) * 100;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a warning message bar.
|
||||
*
|
||||
* @param {{
|
||||
name: string,
|
||||
maxLength: number,
|
||||
}} chatProvider attributes for the warning
|
||||
* @returns { mozMessageBarEl } MozMessageBar warning message bar
|
||||
*/
|
||||
* Create a warning message bar.
|
||||
*
|
||||
* @param {{
|
||||
* name: string,
|
||||
* maxLength: number,
|
||||
* }} chatProvider attributes for the warning
|
||||
* @returns { mozMessageBarEl } MozMessageBar warning message bar
|
||||
*/
|
||||
const createMessageBarWarning = chatProvider => {
|
||||
const mozMessageBarEl = document.createElement("moz-message-bar");
|
||||
|
||||
|
@ -481,133 +458,150 @@ export const GenAI = {
|
|||
return mozMessageBarEl;
|
||||
};
|
||||
|
||||
// Detect hover to build and open the popup
|
||||
aiActionButton.addEventListener("mouseover", async () => {
|
||||
if (chatShortcutsOptionsPanel.state != "closed") {
|
||||
return;
|
||||
}
|
||||
|
||||
aiActionButton.setAttribute("type", buttonActiveState);
|
||||
const vbox = chatShortcutsOptionsPanel.querySelector("vbox");
|
||||
vbox.innerHTML = "";
|
||||
|
||||
const chatProvider = this.chatProviders.get(lazy.chatProvider);
|
||||
const selectionLength = aiActionButton.data.selection.length;
|
||||
const showWarning =
|
||||
this.estimateSelectionLimit(chatProvider?.maxLength) < selectionLength;
|
||||
|
||||
// Show warning if selection is too long
|
||||
if (showWarning) {
|
||||
vbox.appendChild(createMessageBarWarning(chatProvider));
|
||||
}
|
||||
|
||||
const addItem = () => {
|
||||
const button = vbox.appendChild(
|
||||
document.createXULElement("toolbarbutton")
|
||||
);
|
||||
button.className = "subviewbutton";
|
||||
button.setAttribute("tabindex", "0");
|
||||
return button;
|
||||
};
|
||||
|
||||
const browser = document.ownerGlobal.gBrowser.selectedBrowser;
|
||||
const context = await this.addAskChatItems(
|
||||
browser,
|
||||
aiActionButton.data,
|
||||
promptObj => {
|
||||
const button = addItem();
|
||||
button.textContent = promptObj.label;
|
||||
return button;
|
||||
},
|
||||
"shortcuts",
|
||||
aiActionButton.hide
|
||||
);
|
||||
|
||||
// Add custom textarea box if configured
|
||||
if (lazy.chatShortcutsCustom) {
|
||||
const textAreaEl = vbox.appendChild(document.createElement("textarea"));
|
||||
document.l10n.setAttributes(
|
||||
textAreaEl,
|
||||
chatProvider?.name
|
||||
? "genai-input-ask-provider"
|
||||
: "genai-input-ask-generic",
|
||||
{ provider: chatProvider?.name }
|
||||
);
|
||||
|
||||
textAreaEl.className = "ask-chat-shortcuts-custom-prompt";
|
||||
textAreaEl.addEventListener("mouseover", () => textAreaEl.focus());
|
||||
textAreaEl.addEventListener("keydown", event => {
|
||||
if (event.key == "Enter" && !event.shiftKey) {
|
||||
this.handleAskChat({ value: textAreaEl.value }, context);
|
||||
aiActionButton.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// For Content Analysis, we need to specify the URL that the data is being sent to.
|
||||
// In this case it's not the URL in the browsingContext (like it is in other cases),
|
||||
// but the URL of the chatProvider is close enough to where the content will eventually
|
||||
// be sent.
|
||||
lazy.ContentAnalysisUtils.setupContentAnalysisEventsForTextElement(
|
||||
textAreaEl,
|
||||
browser.browsingContext,
|
||||
Services.io.newURI(lazy.chatProvider)
|
||||
);
|
||||
|
||||
const resetHeight = () => {
|
||||
textAreaEl.style.height = "auto";
|
||||
textAreaEl.style.height = textAreaEl.scrollHeight + "px";
|
||||
};
|
||||
|
||||
textAreaEl.addEventListener("input", resetHeight);
|
||||
chatShortcutsOptionsPanel.addEventListener("popupshown", resetHeight, {
|
||||
once: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Allow hiding these shortcuts
|
||||
vbox.appendChild(document.createXULElement("toolbarseparator"));
|
||||
const hider = addItem();
|
||||
document.l10n.setAttributes(hider, "genai-shortcuts-hide");
|
||||
hider.addEventListener("command", () => {
|
||||
Services.prefs.setBoolPref("browser.ml.chat.shortcuts", false);
|
||||
Glean.genaiChatbot.shortcutsHideClick.record({
|
||||
selection: aiActionButton.data.selection.length,
|
||||
});
|
||||
});
|
||||
|
||||
chatShortcutsOptionsPanel.openPopup(
|
||||
selectionShortcutActionPanel,
|
||||
"after_start",
|
||||
0,
|
||||
10
|
||||
);
|
||||
Glean.genaiChatbot.shortcutsExpanded.record({
|
||||
selection: aiActionButton.data.selection.length,
|
||||
provider: this.getProviderId(),
|
||||
warning: showWarning,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle messages from content to show or hide shortcuts.
|
||||
*
|
||||
* @param {string} name of message
|
||||
* @param {{
|
||||
* inputType: string,
|
||||
* selection: string,
|
||||
* delay: number,
|
||||
* x: number,
|
||||
* y: number,
|
||||
* }} data for the message
|
||||
* @param {MozBrowser} browser that provided the message
|
||||
*/
|
||||
handleShortcutsMessage(name, data, browser) {
|
||||
const isInBrowserStack = browser?.closest(".browserStack");
|
||||
|
||||
if (
|
||||
!isInBrowserStack ||
|
||||
!browser ||
|
||||
this.ignoredInputs.has(data.inputType) ||
|
||||
!lazy.chatShortcuts ||
|
||||
!this.canShowChatEntrypoint
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const window = browser.ownerGlobal;
|
||||
const { document, devicePixelRatio } = window;
|
||||
const aiActionButton = document.getElementById("ai-action-button");
|
||||
this.initializeAIShortcut(aiActionButton);
|
||||
|
||||
switch (name) {
|
||||
case "GenAI:HideShortcuts":
|
||||
hide();
|
||||
aiActionButton.hide();
|
||||
break;
|
||||
case "GenAI:ShowShortcuts": {
|
||||
aiActionButton.setAttribute("type", buttonDefaultState);
|
||||
|
||||
// Detect hover to build and open the popup
|
||||
aiActionButton.listener = async () => {
|
||||
if (aiActionButton.hasAttribute("active")) {
|
||||
return;
|
||||
}
|
||||
|
||||
aiActionButton.toggleAttribute("active");
|
||||
aiActionButton.setAttribute("type", buttonActiveState);
|
||||
const vbox = chatShortcutsOptionsPanel.querySelector("vbox");
|
||||
vbox.innerHTML = "";
|
||||
|
||||
const chatProvider = this.chatProviders.get(lazy.chatProvider);
|
||||
const selectionLength = aiActionButton.data.selection.length;
|
||||
const showWarning =
|
||||
this.estimateSelectionLimit(chatProvider?.maxLength) <
|
||||
selectionLength;
|
||||
|
||||
// Show warning if selection is too long
|
||||
if (showWarning) {
|
||||
vbox.appendChild(createMessageBarWarning(chatProvider));
|
||||
}
|
||||
|
||||
const addItem = () => {
|
||||
const button = vbox.appendChild(
|
||||
document.createXULElement("toolbarbutton")
|
||||
);
|
||||
button.className = "subviewbutton";
|
||||
button.setAttribute("tabindex", "0");
|
||||
return button;
|
||||
};
|
||||
|
||||
const context = await this.addAskChatItems(
|
||||
browser,
|
||||
aiActionButton.data,
|
||||
promptObj => {
|
||||
const button = addItem();
|
||||
button.textContent = promptObj.label;
|
||||
return button;
|
||||
},
|
||||
"shortcuts",
|
||||
hide
|
||||
);
|
||||
|
||||
// Add custom textarea box if configured
|
||||
if (lazy.chatShortcutsCustom) {
|
||||
const textAreaEl = vbox.appendChild(
|
||||
document.createElement("textarea")
|
||||
);
|
||||
document.l10n.setAttributes(
|
||||
textAreaEl,
|
||||
chatProvider?.name
|
||||
? "genai-input-ask-provider"
|
||||
: "genai-input-ask-generic",
|
||||
{ provider: chatProvider?.name }
|
||||
);
|
||||
|
||||
textAreaEl.className = "ask-chat-shortcuts-custom-prompt";
|
||||
textAreaEl.addEventListener("mouseover", () => textAreaEl.focus());
|
||||
textAreaEl.addEventListener("keydown", event => {
|
||||
if (event.key == "Enter" && !event.shiftKey) {
|
||||
this.handleAskChat({ value: textAreaEl.value }, context);
|
||||
hide();
|
||||
}
|
||||
});
|
||||
|
||||
// For Content Analysis, we need to specify the URL that the data is being sent to.
|
||||
// In this case it's not the URL in the browsingContext (like it is in other cases),
|
||||
// but the URL of the chatProvider is close enough to where the content will eventually
|
||||
// be sent.
|
||||
lazy.ContentAnalysisUtils.setupContentAnalysisEventsForTextElement(
|
||||
textAreaEl,
|
||||
browser.browsingContext,
|
||||
Services.io.newURI(lazy.chatProvider)
|
||||
);
|
||||
|
||||
const resetHeight = () => {
|
||||
textAreaEl.style.height = "auto";
|
||||
textAreaEl.style.height = textAreaEl.scrollHeight + "px";
|
||||
};
|
||||
|
||||
textAreaEl.addEventListener("input", resetHeight);
|
||||
chatShortcutsOptionsPanel.addEventListener(
|
||||
"popupshown",
|
||||
resetHeight,
|
||||
{
|
||||
once: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Allow hiding these shortcuts
|
||||
vbox.appendChild(document.createXULElement("toolbarseparator"));
|
||||
const hider = addItem();
|
||||
document.l10n.setAttributes(hider, "genai-shortcuts-hide");
|
||||
hider.addEventListener("command", () => {
|
||||
Services.prefs.setBoolPref("browser.ml.chat.shortcuts", false);
|
||||
Glean.genaiChatbot.shortcutsHideClick.record({
|
||||
selection: aiActionButton.data.selection.length,
|
||||
});
|
||||
});
|
||||
|
||||
chatShortcutsOptionsPanel.openPopup(
|
||||
selectionShortcutActionPanel,
|
||||
"after_start",
|
||||
0,
|
||||
10
|
||||
);
|
||||
chatShortcutsOptionsPanel.addEventListener(
|
||||
"popuphidden",
|
||||
() => aiActionButton.removeAttribute("active"),
|
||||
{ once: true }
|
||||
);
|
||||
Glean.genaiChatbot.shortcutsExpanded.record({
|
||||
selection: aiActionButton.data.selection.length,
|
||||
provider: this.getProviderId(),
|
||||
warning: showWarning,
|
||||
});
|
||||
};
|
||||
aiActionButton.addEventListener("mouseover", aiActionButton.listener);
|
||||
|
||||
// Save the latest selection so it can be used by popup
|
||||
aiActionButton.data = data;
|
||||
|
||||
|
@ -625,12 +619,14 @@ export const GenAI = {
|
|||
const screenX = data.screenXDevPx / devicePixelRatio;
|
||||
const screenY = screenYBase + bottomPadding;
|
||||
|
||||
selectionShortcutActionPanel.openPopup(
|
||||
browser,
|
||||
"before_start",
|
||||
screenX - browser.screenX,
|
||||
screenY - browser.screenY
|
||||
);
|
||||
aiActionButton
|
||||
.closest("panel")
|
||||
.openPopup(
|
||||
browser,
|
||||
"before_start",
|
||||
screenX - browser.screenX,
|
||||
screenY - browser.screenY
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ const lazy = {};
|
|||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
LinkPreviewModel:
|
||||
"moz-src:///browser/components/genai/LinkPreviewModel.sys.mjs",
|
||||
Region: "resource://gre/modules/Region.sys.mjs",
|
||||
});
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
lazy,
|
||||
|
@ -28,6 +29,11 @@ XPCOMUtils.defineLazyPreferenceGetter(
|
|||
"browser.ml.linkPreview.prefetchOnEnable",
|
||||
true
|
||||
);
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
lazy,
|
||||
"noKeyPointsRegions",
|
||||
"browser.ml.linkPreview.noKeyPointsRegions"
|
||||
);
|
||||
|
||||
export const LinkPreview = {
|
||||
// Shared downloading state to use across multiple previews
|
||||
|
@ -50,7 +56,7 @@ export const LinkPreview = {
|
|||
}
|
||||
|
||||
// Prefetch the model when enabling by simulating a request.
|
||||
if (enabled && lazy.prefetchOnEnable) {
|
||||
if (enabled && lazy.prefetchOnEnable && this._isRegionSupported()) {
|
||||
this.generateKeyPoints();
|
||||
}
|
||||
|
||||
|
@ -176,6 +182,20 @@ export const LinkPreview = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the user's region is supported for key points generation.
|
||||
*
|
||||
* @returns {boolean} True if the region is supported, false otherwise.
|
||||
*/
|
||||
_isRegionSupported() {
|
||||
const disallowedRegions = lazy.noKeyPointsRegions
|
||||
.split(",")
|
||||
.map(region => region.trim().toUpperCase());
|
||||
|
||||
const userRegion = lazy.Region.home?.toUpperCase();
|
||||
return !disallowedRegions.includes(userRegion);
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates an Open Graph (OG) card using meta information from the page.
|
||||
*
|
||||
|
@ -208,6 +228,7 @@ export const LinkPreview = {
|
|||
// Generate key points if we have content, language and configured for any
|
||||
// language or restricted.
|
||||
if (
|
||||
this._isRegionSupported() &&
|
||||
pageData.article.textContent &&
|
||||
pageData.article.detectedLanguage &&
|
||||
(!lazy.allowedLanguages ||
|
||||
|
|
|
@ -16,7 +16,7 @@ const DEFAULT_INPUT_SENTENCES = 6;
|
|||
const MIN_SENTENCE_LENGTH = 14;
|
||||
const MIN_WORD_COUNT = 5;
|
||||
const DEFAULT_INPUT_PROMPT =
|
||||
"Provide a concise, objective summary of the input text in up to three sentences, focusing on key actions and intentions without using second or third person pronouns.";
|
||||
"You're an AI assistant for text re-writing and summarization. Rewrite the input text focusing on the main key point in at most three very short sentences.";
|
||||
|
||||
// All tokens taken from the model's vocabulary at https://huggingface.co/HuggingFaceTB/SmolLM2-360M-Instruct/raw/main/vocab.json
|
||||
// Token id for end of text
|
||||
|
@ -528,7 +528,10 @@ export class SentencePostProcessor {
|
|||
// If the sentence contains a block word, abort
|
||||
if (
|
||||
this.blockListManager &&
|
||||
this.blockListManager.matchAtWordBoundary({ text: sentence })
|
||||
this.blockListManager.matchAtWordBoundary({
|
||||
// Blocklist is always lowercase
|
||||
text: sentence.toLowerCase(),
|
||||
})
|
||||
) {
|
||||
sentence = "";
|
||||
abort = true;
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
<!-- may be protected as a trademark in some jurisdictions -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" fill="context-fill"><path d="M32.022 16.242a7.574 7.574 0 0 0-.651-6.221 7.66 7.66 0 0 0-8.25-3.675 7.577 7.577 0 0 0-5.714-2.547A7.661 7.661 0 0 0 10.1 9.102a7.578 7.578 0 0 0-5.065 3.674 7.663 7.663 0 0 0 .942 8.983 7.574 7.574 0 0 0 .651 6.221 7.66 7.66 0 0 0 8.25 3.675 7.571 7.571 0 0 0 5.714 2.546 7.662 7.662 0 0 0 7.31-5.307 7.577 7.577 0 0 0 5.065-3.674 7.663 7.663 0 0 0-.944-8.98l-.001.002ZM20.594 32.215a5.68 5.68 0 0 1-3.648-1.32c.047-.024.128-.069.18-.1l6.054-3.497c.31-.176.5-.506.498-.862v-8.535l2.558 1.478a.09.09 0 0 1 .05.07v7.068a5.704 5.704 0 0 1-5.692 5.698ZM8.352 26.986a5.673 5.673 0 0 1-.679-3.817c.045.026.124.075.18.107l6.054 3.496c.307.18.687.18.995 0l7.39-4.267v2.954a.095.095 0 0 1-.036.08l-6.12 3.533a5.705 5.705 0 0 1-7.783-2.086ZM6.76 13.771a5.679 5.679 0 0 1 2.965-2.498l-.002.21v6.993a.985.985 0 0 0 .497.86l7.39 4.268-2.558 1.477a.09.09 0 0 1-.087.008l-6.12-3.537a5.704 5.704 0 0 1-2.086-7.78l.001-.001Zm21.022 4.892-7.39-4.268 2.558-1.476a.09.09 0 0 1 .087-.008l6.12 3.534a5.7 5.7 0 0 1-.88 10.282v-7.203a.983.983 0 0 0-.494-.86Zm2.547-3.833a8.018 8.018 0 0 0-.18-.107l-6.054-3.496a.986.986 0 0 0-.995 0l-7.39 4.268V12.54a.095.095 0 0 1 .035-.08l6.12-3.53a5.697 5.697 0 0 1 8.462 5.9h.002Zm-16.01 5.267-2.56-1.478a.09.09 0 0 1-.05-.07v-7.068a5.699 5.699 0 0 1 9.345-4.376c-.047.025-.127.07-.18.102l-6.054 3.496a.983.983 0 0 0-.498.86l-.004 8.532v.002Zm1.39-2.997L19 15.2l3.292 1.9v3.802l-3.293 1.9-3.292-1.9v-3.8Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" fill="none"><g clip-path="url(#a)"><mask id="b" width="38" height="38" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:luminance"><path fill="#fff" d="M38 0H0v37.66h38V0Z"/></mask><g mask="url(#b)"><path fill="context-fill" d="M14.575 13.709V10.13c0-.302.113-.528.376-.678l7.194-4.143c.979-.565 2.146-.828 3.351-.828 4.52 0 7.382 3.502 7.382 7.23 0 .264 0 .566-.038.867L25.383 8.21c-.451-.263-.904-.263-1.355 0l-9.453 5.498ZM31.37 27.642v-8.55c0-.526-.226-.903-.677-1.167l-9.453-5.498 3.088-1.77c.263-.15.49-.15.753 0l7.193 4.142c2.072 1.206 3.465 3.767 3.465 6.252 0 2.862-1.694 5.498-4.369 6.59v.001Zm-19.018-7.532-3.088-1.808c-.264-.15-.377-.376-.377-.678V9.34c0-4.03 3.088-7.08 7.269-7.08 1.581 0 3.05.527 4.293 1.469l-7.419 4.293c-.452.264-.678.64-.678 1.168V20.11ZM19 23.952l-4.425-2.485v-5.273L19 13.71l4.425 2.485v5.273L19 23.952Zm2.843 11.45c-1.582 0-3.05-.528-4.293-1.47l7.419-4.293c.452-.264.678-.64.678-1.168V17.55l3.126 1.808c.263.15.377.376.377.677v8.286c0 4.03-3.127 7.08-7.307 7.08Zm-8.925-8.399L5.724 22.86C3.653 21.655 2.26 19.094 2.26 16.61c0-2.9 1.733-5.499 4.407-6.59v8.586c0 .527.226.904.678 1.167l9.415 5.46-3.088 1.771c-.264.15-.49.15-.753 0Zm-.414 6.176c-4.256 0-7.382-3.2-7.382-7.155 0-.302.038-.603.075-.904l7.42 4.293c.451.264.903.264 1.355 0l9.453-5.46v3.578c0 .3-.113.527-.377.677l-7.193 4.143c-.98.565-2.147.828-3.352.828Zm9.34 4.482a9.416 9.416 0 0 0 9.226-7.532c4.218-1.093 6.93-5.047 6.93-9.077 0-2.636-1.13-5.197-3.163-7.042.188-.791.3-1.582.3-2.373 0-5.385-4.368-9.415-9.415-9.415-1.016 0-1.995.15-2.975.49C21.052 1.054 18.717 0 16.157 0A9.416 9.416 0 0 0 6.93 7.532C2.712 8.624 0 12.58 0 16.608c0 2.637 1.13 5.197 3.163 7.043-.188.79-.3 1.582-.3 2.372 0 5.386 4.368 9.416 9.415 9.416 1.016 0 1.995-.15 2.975-.49 1.694 1.657 4.03 2.712 6.59 2.712Z"/></g></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h38v38H0z"/></clipPath></defs></svg>
|
||||
|
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2 KiB |
|
@ -1,2 +1,2 @@
|
|||
<!-- may be protected as a trademark in some jurisdictions -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" fill="none"><path fill="#D97757" d="m9.886 23.95 5.903-3.31.098-.29-.098-.161h-.292l-.989-.06-3.373-.09-2.919-.121-2.838-.15-.713-.151L4 18.729l.065-.436.6-.406.86.075 1.897.135 2.854.196 2.06.12 3.064.316h.486l.065-.195-.162-.12-.13-.121-2.95-2.001-3.195-2.106-1.67-1.22-.893-.616-.454-.572-.194-1.264.81-.902 1.103.075.276.075 1.119.858 2.384 1.85 3.113 2.287.454.376.183-.123.028-.087-.21-.346-1.687-3.054-1.8-3.115-.811-1.294-.21-.767a3.51 3.51 0 0 1-.13-.918l.924-1.264.519-.165 1.248.165.52.452.778 1.775 1.248 2.784 1.946 3.79.568 1.13.308 1.037.113.316h.195v-.18l.162-2.137.292-2.617.292-3.37.097-.948.47-1.144.94-.617.73.346.6.858-.08.557-.357 2.317-.697 3.625-.454 2.438h.259l.308-.316 1.233-1.625 2.059-2.588.908-1.023 1.07-1.128.681-.542h1.298l.94 1.414-.421 1.46-1.33 1.685-1.103 1.429-1.58 2.118-.982 1.704.088.14.236-.02 3.568-.767 1.93-.346 2.302-.392 1.038.482.114.496-.406 1.008-2.465.602-2.886.587-4.298 1.012-.048.038.056.083 1.939.176.827.045h2.027l3.778.286.99.647.583.797-.097.617-1.525.767-2.043-.481-4.784-1.144-1.637-.406h-.228v.136l1.363 1.339 2.513 2.256 3.13 2.92.162.721-.405.572-.422-.06L27.27 27.2l-1.07-.933-2.4-2.031h-.162v.21l.551.813 2.935 4.408.146 1.354-.21.436-.763.271-.827-.15-1.735-2.422-1.767-2.709-1.427-2.437-.173.109-.85 9.069-.388.466-.908.346-.763-.572-.405-.933.405-1.85.487-2.407.39-1.911.356-2.377.218-.794-.02-.053-.174.029-1.792 2.458-2.724 3.686-2.157 2.302-.519.21-.892-.466.082-.828.502-.737 2.984-3.791 1.8-2.362 1.16-1.356-.011-.196-.064-.006-7.928 5.169-1.411.18-.616-.572.08-.932.293-.301 2.383-1.64Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" fill="none"><g clip-path="url(#a)"><path fill="#D97757" d="m7.456 25.27 7.477-4.193.124-.367-.124-.204h-.37l-1.253-.076-4.272-.114-3.698-.153-3.595-.19-.903-.191L0 18.657l.082-.552.76-.515 1.09.095 2.403.171 3.615.248 2.609.152 3.881.4h.616l.082-.246-.205-.152-.165-.153-3.737-2.535-4.047-2.668-2.115-1.545-1.131-.78-.575-.725-.246-1.6 1.026-1.143 1.397.095.35.095 1.417 1.086 3.02 2.344 3.943 2.897.575.476.232-.156.035-.11-.266-.438-2.136-3.869-2.28-3.946-1.028-1.639-.266-.971a4.446 4.446 0 0 1-.164-1.163L9.942.21 10.6 0l1.58.209.659.573.985 2.248 1.581 3.526 2.465 4.8.72 1.432.39 1.314.143.4h.247v-.228l.205-2.707.37-3.315.37-4.268.123-1.201.595-1.45 1.19-.78.925.438.76 1.086-.101.706-.452 2.935-.883 4.591-.575 3.089h.328l.39-.4 1.562-2.06 2.608-3.277 1.15-1.296 1.355-1.429.863-.686h1.644l1.19 1.79-.532 1.85-1.685 2.134-1.397 1.81-2.002 2.683-1.243 2.159.111.177.299-.025 4.52-.972 2.444-.438 2.916-.497 1.315.61.144.63-.514 1.276-3.123.762-3.655.744-5.444 1.282-.061.048.07.105 2.457.223 1.047.057h2.568l4.785.362 1.255.82.738 1.01-.123.78-1.931.973-2.588-.61-6.06-1.449-2.074-.514h-.288v.172l1.726 1.696 3.183 2.858 3.965 3.698.205.914-.513.724-.535-.076-3.492-2.63-1.355-1.181-3.04-2.573h-.205v.266l.698 1.03 3.717 5.583.185 1.716-.266.552-.966.343-1.048-.19-2.197-3.068-2.239-3.431-1.807-3.087-.22.138-1.076 11.487-.491.59-1.15.439-.967-.724-.513-1.182.513-2.344.617-3.049.494-2.42.45-3.011.277-1.006-.025-.067-.22.037-2.27 3.113-3.451 4.67-2.732 2.915-.658.266-1.13-.59.104-1.049.636-.933 3.78-4.802 2.28-2.992 1.47-1.718-.015-.248-.08-.008-10.043 6.548-1.787.228-.78-.725.101-1.18.371-.382 3.019-2.077Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h38v38H0z"/></clipPath></defs></svg>
|
||||
|
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.8 KiB |
|
@ -1,2 +1,2 @@
|
|||
<!-- may be protected as a trademark in some jurisdictions -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" fill="none" class="mb-4 size-12"><path fill="url(#a)" d="M85.855 18.313A11.574 11.574 0 0 0 74.75 10h-3.379a11.574 11.574 0 0 0-11.384 9.485L54.2 51.018l1.436-4.913a11.574 11.574 0 0 1 11.11-8.327H86.38l8.235 3.207 7.937-3.207h-2.316a11.574 11.574 0 0 1-11.105-8.313z"/><path fill="url(#b)" d="M36.326 101.64A11.574 11.574 0 0 0 47.445 110h7.176c6.276 0 11.409-5.002 11.57-11.277l.781-30.405-1.634 5.583a11.574 11.574 0 0 1-11.108 8.321H34.432l-7.058-3.829-7.641 3.83h2.278c5.154 0 9.687 3.408 11.119 8.36z"/><path fill="url(#c)" d="M74.248 10H34.15c-11.457 0-18.33 15.142-22.913 30.283-5.43 17.939-12.534 41.93 8.02 41.93H36.57c5.174 0 9.716-3.421 11.138-8.396 3.01-10.531 8.286-28.903 12.43-42.889 2.105-7.107 3.86-13.211 6.551-17.012C68.2 11.785 70.715 10 74.248 10"/><path fill="url(#d)" d="M74.248 10H34.15c-11.457 0-18.33 15.142-22.913 30.283-5.43 17.939-12.534 41.93 8.02 41.93H36.57c5.174 0 9.716-3.421 11.138-8.396 3.01-10.531 8.286-28.903 12.43-42.889 2.105-7.107 3.86-13.211 6.551-17.012C68.2 11.785 70.715 10 74.248 10"/><path fill="url(#e)" d="M46.744 110h40.099c11.456 0 18.33-15.144 22.913-30.288 5.429-17.942 12.533-41.937-8.02-41.937H84.422a11.576 11.576 0 0 0-11.138 8.396c-3.01 10.533-8.286 28.909-12.43 42.897-2.106 7.109-3.86 13.214-6.552 17.016-1.51 2.131-4.025 3.916-7.558 3.916"/><path fill="url(#f)" d="M46.744 110h40.099c11.456 0 18.33-15.144 22.913-30.288 5.429-17.942 12.533-41.937-8.02-41.937H84.422a11.576 11.576 0 0 0-11.138 8.396c-3.01 10.533-8.286 28.909-12.43 42.897-2.106 7.109-3.86 13.214-6.552 17.016-1.51 2.131-4.025 3.916-7.558 3.916"/><defs><radialGradient id="a" cx="0" cy="0" r="1" gradientTransform="matrix(-27.40125 -33.47302 31.47539 -25.76598 95.512 51.286)" gradientUnits="userSpaceOnUse"><stop offset=".096" stop-color="#00AEFF"/><stop offset=".773" stop-color="#2253CE"/><stop offset="1" stop-color="#0736C4"/></radialGradient><radialGradient id="b" cx="0" cy="0" r="1" gradientTransform="rotate(51.84 -70.254 70.14) scale(39.9779 38.7796)" gradientUnits="userSpaceOnUse"><stop stop-color="#FFB657"/><stop offset=".634" stop-color="#FF5F3D"/><stop offset=".923" stop-color="#C02B3C"/></radialGradient><radialGradient id="e" cx="0" cy="0" r="1" gradientTransform="matrix(-31.67773 90.58917 -108.5232 -37.949 103.796 30.703)" gradientUnits="userSpaceOnUse"><stop offset=".066" stop-color="#8C48FF"/><stop offset=".5" stop-color="#F2598A"/><stop offset=".896" stop-color="#FFB152"/></radialGradient><linearGradient id="c" x1="31.75" x2="37.471" y1="18.75" y2="84.938" gradientUnits="userSpaceOnUse"><stop offset=".156" stop-color="#0D91E1"/><stop offset=".487" stop-color="#52B471"/><stop offset=".652" stop-color="#98BD42"/><stop offset=".937" stop-color="#FFC800"/></linearGradient><linearGradient id="d" x1="36.75" x2="39.874" y1="10" y2="82.213" gradientUnits="userSpaceOnUse"><stop stop-color="#3DCBFF"/><stop offset=".247" stop-color="#0588F7" stop-opacity="0"/></linearGradient><linearGradient id="f" x1="106.964" x2="106.923" y1="33.365" y2="53.037" gradientUnits="userSpaceOnUse"><stop offset=".058" stop-color="#F8ADFA"/><stop offset=".708" stop-color="#A86EDD" stop-opacity="0"/></linearGradient></defs></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" fill="none"><path fill="url(#a)" d="M27.53 4.977a3.893 3.893 0 0 0-3.736-2.796h-1.136a3.893 3.893 0 0 0-3.83 3.19l-1.946 10.607.483-1.652a3.893 3.893 0 0 1 3.737-2.801h6.604l2.77 1.079 2.67-1.08h-.779a3.893 3.893 0 0 1-3.735-2.795L27.53 4.977Z"/><path fill="url(#b)" d="M10.871 33.006a3.894 3.894 0 0 0 3.74 2.812h2.414a3.893 3.893 0 0 0 3.892-3.793l.263-10.227-.55 1.878a3.893 3.893 0 0 1-3.736 2.799h-6.66L7.86 25.185l-2.57 1.289h.766a3.893 3.893 0 0 1 3.74 2.812l1.075 3.72Z"/><path fill="url(#c)" d="M23.625 2.181H10.137c-3.854 0-6.166 5.093-7.707 10.186-1.827 6.035-4.216 14.104 2.697 14.104h5.824c1.74 0 3.268-1.15 3.747-2.824 1.012-3.542 2.787-9.722 4.18-14.426.709-2.39 1.299-4.444 2.204-5.723.508-.716 1.354-1.317 2.543-1.317Z"/><path fill="url(#d)" d="M23.625 2.181H10.137c-3.854 0-6.166 5.093-7.707 10.186-1.827 6.035-4.216 14.104 2.697 14.104h5.824c1.74 0 3.268-1.15 3.747-2.824 1.012-3.542 2.787-9.722 4.18-14.426.709-2.39 1.299-4.444 2.204-5.723.508-.716 1.354-1.317 2.543-1.317Z"/><path fill="url(#e)" d="M14.375 35.818h13.488c3.854 0 6.166-5.094 7.707-10.188 1.827-6.035 4.216-14.106-2.697-14.106h-5.824a3.894 3.894 0 0 0-3.747 2.824 1748.2 1748.2 0 0 1-4.18 14.43c-.71 2.39-1.3 4.444-2.205 5.723-.508.717-1.354 1.317-2.542 1.317Z"/><path fill="url(#f)" d="M14.375 35.818h13.488c3.854 0 6.166-5.094 7.707-10.188 1.827-6.035 4.216-14.106-2.697-14.106h-5.824a3.894 3.894 0 0 0-3.747 2.824 1748.2 1748.2 0 0 1-4.18 14.43c-.71 2.39-1.3 4.444-2.205 5.723-.508.717-1.354 1.317-2.542 1.317Z"/><defs><radialGradient id="a" cx="0" cy="0" r="1" gradientTransform="rotate(-129.304 19.195 .744) scale(14.5508 13.6824)" gradientUnits="userSpaceOnUse"><stop offset=".096" stop-color="#00AEFF"/><stop offset=".773" stop-color="#2253CE"/><stop offset="1" stop-color="#0736C4"/></radialGradient><radialGradient id="b" cx="0" cy="0" r="1" gradientTransform="rotate(51.84 -23.089 21.615) scale(13.4474 13.0443)" gradientUnits="userSpaceOnUse"><stop stop-color="#FFB657"/><stop offset=".634" stop-color="#FF5F3D"/><stop offset=".923" stop-color="#C02B3C"/></radialGradient><radialGradient id="e" cx="0" cy="0" r="1" gradientTransform="matrix(-10.65548 30.47158 -36.504 -12.76492 33.566 9.145)" gradientUnits="userSpaceOnUse"><stop offset=".066" stop-color="#8C48FF"/><stop offset=".5" stop-color="#F2598A"/><stop offset=".896" stop-color="#FFB152"/></radialGradient><linearGradient id="c" x1="9.33" x2="11.254" y1="5.124" y2="27.388" gradientUnits="userSpaceOnUse"><stop offset=".156" stop-color="#0D91E1"/><stop offset=".487" stop-color="#52B471"/><stop offset=".652" stop-color="#98BD42"/><stop offset=".937" stop-color="#FFC800"/></linearGradient><linearGradient id="d" x1="11.012" x2="12.062" y1="2.181" y2="26.471" gradientUnits="userSpaceOnUse"><stop stop-color="#3DCBFF"/><stop offset=".247" stop-color="#0588F7" stop-opacity="0"/></linearGradient><linearGradient id="f" x1="34.631" x2="34.617" y1="10.04" y2="16.658" gradientUnits="userSpaceOnUse"><stop offset=".058" stop-color="#F8ADFA"/><stop offset=".708" stop-color="#A86EDD" stop-opacity="0"/></linearGradient></defs></svg>
|
||||
|
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.1 KiB |
|
@ -1,2 +1,2 @@
|
|||
<!-- may be protected as a trademark in some jurisdictions -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" fill="none"><path fill="url(#a)" d="M36.928 18.928c-2.468 0-4.749-.468-6.919-1.388-2.172-.951-4.087-2.25-5.692-3.856-1.606-1.605-2.902-3.522-3.856-5.692-.922-2.17-1.388-4.453-1.388-6.921A.071.071 0 0 0 19 .999a.071.071 0 0 0-.072.072c0 2.468-.482 4.749-1.433 6.921-.923 2.172-2.205 4.087-3.81 5.692-1.606 1.606-3.523 2.903-5.693 3.856-2.17.922-4.453 1.389-6.921 1.389A.072.072 0 0 0 1 19c0 .039.033.072.072.072 2.468 0 4.749.482 6.921 1.433 2.172.923 4.087 2.205 5.692 3.81 1.606 1.607 2.888 3.523 3.81 5.695.952 2.17 1.434 4.45 1.434 6.92 0 .038.033.071.072.071.039 0 .072-.03.072-.072 0-2.468.466-4.749 1.388-6.919.952-2.172 2.248-4.087 3.856-5.694 1.605-1.606 3.52-2.888 5.692-3.81 2.17-.952 4.45-1.434 6.92-1.434.038 0 .071-.031.071-.072a.071.071 0 0 0-.072-.072Z"/><defs><linearGradient id="a" x1="12.431" x2="28.716" y1="24.537" y2="10.806" gradientUnits="userSpaceOnUse"><stop stop-color="#217BFE"/><stop offset=".27" stop-color="#078EFB"/><stop offset=".78" stop-color="#A190FF"/><stop offset="1" stop-color="#BD99FE"/></linearGradient></defs></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="none"><path fill="url(#a)" d="M39.92 19.921c-2.742 0-5.277-.52-7.688-1.542-2.413-1.057-4.54-2.5-6.324-4.285-1.785-1.783-3.225-3.913-4.285-6.324C20.6 5.359 20.081 2.822 20.081.08a.077.077 0 0 0-.023-.057A.08.08 0 0 0 20 0a.079.079 0 0 0-.08.08c0 2.742-.536 5.277-1.592 7.69-1.026 2.413-2.45 4.541-4.234 6.325-1.784 1.784-3.914 3.225-6.325 4.284-2.411 1.024-4.948 1.543-7.69 1.543a.08.08 0 0 0-.079.08c0 .043.037.08.08.08 2.742 0 5.277.535 7.69 1.591 2.413 1.026 4.541 2.45 6.324 4.234 1.785 1.785 3.21 3.914 4.234 6.327 1.058 2.412 1.593 4.945 1.593 7.69a.08.08 0 0 0 .08.078c.043 0 .08-.033.08-.08 0-2.742.518-5.276 1.542-7.688 1.058-2.413 2.498-4.54 4.285-6.326 1.783-1.785 3.91-3.21 6.324-4.234 2.411-1.057 4.945-1.593 7.69-1.593a.08.08 0 0 0 .078-.08.08.08 0 0 0-.05-.074.081.081 0 0 0-.03-.006Z"/><defs><linearGradient id="a" x1="12.701" x2="30.796" y1="26.153" y2="10.897" gradientUnits="userSpaceOnUse"><stop stop-color="#217BFE"/><stop offset=".27" stop-color="#078EFB"/><stop offset=".78" stop-color="#A190FF"/><stop offset="1" stop-color="#BD99FE"/></linearGradient></defs></svg>
|
||||
|
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
@ -1,2 +1,2 @@
|
|||
<!-- may be protected as a trademark in some jurisdictions -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" fill="none"><path fill="#FFD21E" d="M3 18.678c0-7.77 6.3-14.07 14.071-14.07h5.478C29.426 4.607 35 10.181 35 17.057c0 6.876-5.574 12.45-12.45 12.45H7.774l-3.848 3.73A.546.546 0 0 1 3 32.845V18.678Z"/><path fill="#32343D" d="M23.836 14.586c.498.177.696 1.205 1.2.936a1.966 1.966 0 0 0 .807-2.653 1.95 1.95 0 0 0-2.643-.812 1.966 1.966 0 0 0-.808 2.654c.24.451.998-.283 1.444-.125ZM14.628 14.586c-.498.177-.696 1.205-1.2.936a1.966 1.966 0 0 1-.807-2.653 1.95 1.95 0 0 1 2.643-.812 1.966 1.966 0 0 1 .808 2.654c-.24.451-.998-.283-1.444-.125ZM19.326 24.003c3.842 0 5.082-3.439 5.082-5.204 0-1.766-2.275.935-5.082.935-2.806 0-5.08-2.701-5.08-.935 0 1.765 1.239 5.204 5.08 5.204Z"/><path fill="#FF323D" d="M22.41 22.999c-.76.601-1.77 1.005-3.086 1.005-1.236 0-2.202-.357-2.945-.898a3.403 3.403 0 0 1 2.074-1.761c.157-.047.317.223.482.5.16.266.322.54.487.54.176 0 .35-.27.518-.532.177-.275.349-.542.515-.489l.145.05c.778.29 1.422.858 1.81 1.585Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" fill="none"><path fill="#FFD21E" d="M0 19.068C0 9.841 7.481 2.36 16.71 2.36h6.504C31.381 2.359 38 8.978 38 17.143s-6.62 14.784-14.784 14.784H5.669l-4.57 4.43A.649.649 0 0 1 0 35.89V19.068Z"/><path fill="#32343D" d="M24.091 13.897c.576.205.805 1.393 1.388 1.082a2.273 2.273 0 0 0-.4-4.172 2.255 2.255 0 0 0-2.824 1.508 2.273 2.273 0 0 0 .167 1.727c.277.521 1.154-.328 1.67-.145Zm-10.646 0c-.576.205-.805 1.393-1.388 1.082a2.273 2.273 0 0 1 .4-4.172 2.255 2.255 0 0 1 2.824 1.508 2.273 2.273 0 0 1-.167 1.727c-.277.521-1.154-.328-1.67-.145Zm5.432 10.889c4.442 0 5.876-3.977 5.876-6.018s-2.63 1.081-5.876 1.081c-3.245 0-5.874-3.122-5.874-1.08 0 2.04 1.433 6.017 5.874 6.017Z"/><path fill="#FF323D" d="M22.443 23.624c-.879.695-2.047 1.163-3.568 1.163-1.43 0-2.546-.413-3.405-1.039a3.936 3.936 0 0 1 2.398-2.036c.181-.054.366.258.557.578.185.308.372.625.563.625.204 0 .405-.313.6-.616.204-.318.403-.626.595-.565l.167.058c.9.335 1.644.992 2.093 1.832Z"/></svg>
|
||||
|
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 1.1 KiB |
|
@ -1,2 +1,2 @@
|
|||
<!-- may be protected as a trademark in some jurisdictions -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1.1em" height="1em" viewBox="0 0 256 233" shape-rendering="crispEdges"><path d="M186.182 0h46.545v46.545h-46.545z"/><path fill="#f7d046" d="M209.455 0H256v46.545h-46.545z"/><path d="M0 0h46.545v46.545H0zm0 46.545h46.545V93.09H0zm0 46.546h46.545v46.545H0zm0 46.545h46.545v46.545H0zm0 46.546h46.545v46.545H0z"/><path fill="#f7d046" d="M23.273 0h46.545v46.545H23.273z"/><path fill="#f2a73b" d="M209.455 46.545H256V93.09h-46.545zm-186.182 0h46.545V93.09H23.273z"/><path d="M139.636 46.545h46.545V93.09h-46.545z"/><path fill="#f2a73b" d="M162.909 46.545h46.545V93.09h-46.545zm-93.091 0h46.545V93.09H69.818z"/><path fill="#ee792f" d="M116.364 93.091h46.545v46.545h-46.545zm46.545 0h46.545v46.545h-46.545zm-93.091 0h46.545v46.545H69.818z"/><path d="M93.091 139.636h46.545v46.545H93.091z"/><path fill="#eb5829" d="M116.364 139.636h46.545v46.545h-46.545z"/><path fill="#ee792f" d="M209.455 93.091H256v46.545h-46.545zm-186.182 0h46.545v46.545H23.273z"/><path d="M186.182 139.636h46.545v46.545h-46.545z"/><path fill="#eb5829" d="M209.455 139.636H256v46.545h-46.545z"/><path d="M186.182 186.182h46.545v46.545h-46.545z"/><path fill="#eb5829" d="M23.273 139.636h46.545v46.545H23.273z"/><path fill="#ea3326" d="M209.455 186.182H256v46.545h-46.545zm-186.182 0h46.545v46.545H23.273z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" fill="none"><path fill="#000" d="M27.18 2.638h6.546v6.545h-6.545V2.638Z"/><path fill="#F7D046" d="M30.454 2.638H37v6.545h-6.546V2.638Z"/><path fill="#000" d="M1 2.638h6.546v6.545H1V2.638Zm0 6.545h6.546v6.544H1V9.183Zm0 6.544h6.546v6.545H1v-6.544Zm0 6.545h6.546v6.545H1v-6.545Zm0 6.545h6.546v6.545H1v-6.545Z"/><path fill="#F7D046" d="M4.271 2.638h6.546v6.545H4.271V2.638Z"/><path fill="#F2A73B" d="M30.454 9.182H37v6.545h-6.546V9.182Zm-26.183 0h6.546v6.545H4.271V9.182Z"/><path fill="#000" d="M20.638 9.182h6.545v6.545h-6.545V9.182Z"/><path fill="#F2A73B" d="M23.909 9.182h6.545v6.545H23.91V9.182Zm-13.092 0h6.546v6.545h-6.546V9.182Z"/><path fill="#EE792F" d="M17.363 15.728h6.546v6.545h-6.546v-6.545Zm6.546 0h6.545v6.545H23.91v-6.545Zm-13.092 0h6.546v6.545h-6.546v-6.545Z"/><path fill="#000" d="M14.089 22.273h6.545v6.544H14.09v-6.544Z"/><path fill="#EB5829" d="M17.362 22.273h6.546v6.544h-6.546v-6.544Z"/><path fill="#EE792F" d="M30.454 15.728H37v6.545h-6.546v-6.545Zm-26.183 0h6.546v6.545H4.271v-6.545Z"/><path fill="#000" d="M27.18 22.273h6.546v6.544h-6.545v-6.544Z"/><path fill="#EB5829" d="M30.454 22.273H37v6.544h-6.546v-6.544Z"/><path fill="#000" d="M27.18 28.817h6.546v6.545h-6.545v-6.545Z"/><path fill="#EB5829" d="M4.271 22.273h6.546v6.544H4.271v-6.544Z"/><path fill="#EA3326" d="M30.454 28.817H37v6.545h-6.546v-6.545Zm-26.183 0h6.546v6.545H4.271v-6.545Z"/></svg>
|
||||
|
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.5 KiB |
|
@ -171,12 +171,15 @@ browser {
|
|||
}
|
||||
|
||||
.icon {
|
||||
--icon-size: 30px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
border-radius: 0;
|
||||
height: var(--icon-size);
|
||||
margin-inline: var(--space-small);
|
||||
min-width: 40px;
|
||||
max-width: var(--icon-size);
|
||||
min-width: var(--icon-size);
|
||||
outline: none;
|
||||
|
||||
&.claude {
|
||||
|
|
|
@ -12,6 +12,18 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
|||
BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
|
||||
});
|
||||
|
||||
ChromeUtils.defineLazyGetter(
|
||||
lazy,
|
||||
"numberFormat",
|
||||
() => new Services.intl.NumberFormat()
|
||||
);
|
||||
|
||||
ChromeUtils.defineLazyGetter(
|
||||
lazy,
|
||||
"pluralRules",
|
||||
() => new Services.intl.PluralRules()
|
||||
);
|
||||
|
||||
const FEEDBACK_LINK =
|
||||
"https://connect.mozilla.org/t5/discussions/try-out-link-previews-on-firefox-labs/td-p/92012";
|
||||
|
||||
|
@ -107,6 +119,12 @@ class LinkPreviewCard extends MozLitElement {
|
|||
|
||||
const readingTimeMinsFast = articleData.readingTimeMinsFast || "";
|
||||
const readingTimeMinsSlow = articleData.readingTimeMinsSlow || "";
|
||||
const readingTimeMinsFastStr =
|
||||
lazy.numberFormat.format(readingTimeMinsFast);
|
||||
const readingTimeRange = lazy.numberFormat.formatRange(
|
||||
readingTimeMinsFast,
|
||||
readingTimeMinsSlow
|
||||
);
|
||||
|
||||
// Check if both metadata and article text content are missing
|
||||
const isMissingAllContent = !description && !articleData.textContent;
|
||||
|
@ -156,11 +174,25 @@ class LinkPreviewCard extends MozLitElement {
|
|||
? html`<p class="og-card-description">${description}</p>`
|
||||
: ""}
|
||||
${readingTimeMinsFast && readingTimeMinsSlow
|
||||
? html`<div class="og-card-reading-time">
|
||||
${readingTimeMinsFast === readingTimeMinsSlow
|
||||
? `${readingTimeMinsFast} min${readingTimeMinsFast > 1 ? "s" : ""} reading time`
|
||||
: `${readingTimeMinsFast}-${readingTimeMinsSlow} mins reading time`}
|
||||
</div>`
|
||||
? html`
|
||||
<div
|
||||
class="og-card-reading-time"
|
||||
data-l10n-id="link-preview-reading-time"
|
||||
data-l10n-args=${JSON.stringify({
|
||||
range:
|
||||
readingTimeMinsFast === readingTimeMinsSlow
|
||||
? `~${readingTimeMinsFastStr}`
|
||||
: `${readingTimeRange}`,
|
||||
rangePlural:
|
||||
readingTimeMinsFast === readingTimeMinsSlow
|
||||
? lazy.pluralRules.select(readingTimeMinsFast)
|
||||
: lazy.pluralRules.selectRange(
|
||||
readingTimeMinsFast,
|
||||
readingTimeMinsSlow
|
||||
),
|
||||
})}
|
||||
></div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
${this.generating || this.keyPoints.length
|
||||
|
|
|
@ -141,6 +141,8 @@ add_task(async function test_show_shortcuts_second_tab() {
|
|||
|
||||
Assert.equal(stub.callCount, 1, "Shortcuts added on select");
|
||||
Assert.equal(stub.firstCall.args[0], browser, "Got correct browser");
|
||||
|
||||
sandbox.restore();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -211,6 +213,23 @@ add_task(async function test_show_warning_label() {
|
|||
"true",
|
||||
"Warning lable value is correct"
|
||||
);
|
||||
|
||||
Assert.ok(
|
||||
!aiActionButton.getAttribute("type").includes("ghost"),
|
||||
"button is active"
|
||||
);
|
||||
|
||||
const hidden = BrowserTestUtils.waitForEvent(
|
||||
chatShortcutsOptionsPanel,
|
||||
"popuphidden"
|
||||
);
|
||||
chatShortcutsOptionsPanel.hidePopup();
|
||||
await hidden;
|
||||
|
||||
Assert.ok(
|
||||
aiActionButton.getAttribute("type").includes("ghost"),
|
||||
"button is inactive"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
const { LinkPreview } = ChromeUtils.importESModule(
|
||||
"moz-src:///browser/components/genai/LinkPreview.sys.mjs"
|
||||
);
|
||||
const { Region } = ChromeUtils.importESModule(
|
||||
"resource://gre/modules/Region.sys.mjs"
|
||||
);
|
||||
const { LinkPreviewModel } = ChromeUtils.importESModule(
|
||||
"moz-src:///browser/components/genai/LinkPreviewModel.sys.mjs"
|
||||
);
|
||||
|
@ -403,3 +406,52 @@ add_task(async function test_skip_keypoints_generation_if_url_not_readable() {
|
|||
generateStub.restore();
|
||||
LinkPreview.keyboardComboActive = false;
|
||||
});
|
||||
|
||||
/**
|
||||
* Test that the Link Preview feature does not generate key points in disallowed regions.
|
||||
*
|
||||
* This test performs the following steps:
|
||||
* 1. Sets the current region as a disallowed region for key point generation via the preference `"browser.ml.linkPreview.noKeyPointsRegions"`.
|
||||
* 2. Enables the Link Preview feature via the preference `"browser.ml.linkPreview.enabled"`.
|
||||
* 3. Stubs the `generateTextAI` method to track if it's called.
|
||||
* 4. Activates the keyboard combination for link preview and sets an over link.
|
||||
* 5. Verifies that the link preview panel is shown but no AI generation is attempted.
|
||||
* 6. Ensures that the `generateTextAI` method is not called when the region is disallowed.
|
||||
* 7. Cleans up by removing the panel, restoring the stub, and clearing the user preference
|
||||
* `"browser.ml.linkPreview.noKeyPointsRegions"` to avoid affecting other tests.
|
||||
*/
|
||||
add_task(async function test_no_key_points_in_disallowed_region() {
|
||||
const currentRegion = Region.home;
|
||||
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["browser.ml.linkPreview.enabled", true],
|
||||
["browser.ml.linkPreview.noKeyPointsRegions", currentRegion],
|
||||
],
|
||||
});
|
||||
|
||||
const generateStub = sinon.stub(LinkPreviewModel, "generateTextAI");
|
||||
|
||||
LinkPreview.keyboardComboActive = true;
|
||||
XULBrowserWindow.setOverLink(
|
||||
"https://example.com/browser/browser/components/genai/tests/browser/data/readableEn.html",
|
||||
{}
|
||||
);
|
||||
|
||||
let panel = await TestUtils.waitForCondition(() =>
|
||||
document.getElementById("link-preview-panel")
|
||||
);
|
||||
await BrowserTestUtils.waitForEvent(panel, "popupshown");
|
||||
|
||||
is(
|
||||
generateStub.callCount,
|
||||
0,
|
||||
"generateTextAI should not be called when region is disallowed"
|
||||
);
|
||||
|
||||
panel.remove();
|
||||
LinkPreview.keyboardComboActive = false;
|
||||
generateStub.restore();
|
||||
|
||||
Services.prefs.clearUserPref("browser.ml.linkPreview.noKeyPointsRegions");
|
||||
});
|
||||
|
|
|
@ -151,7 +151,7 @@ add_task(async function test_generateAI_with_blocklist() {
|
|||
|
||||
// Mocked Blocked List Manager
|
||||
let manager = new BlockListManager({
|
||||
blockNgrams: [BlockListManager.encodeBase64("Hello")],
|
||||
blockNgrams: [BlockListManager.encodeBase64("hello")],
|
||||
language: "en",
|
||||
});
|
||||
|
||||
|
|
|
@ -611,18 +611,25 @@ export class ChromeProfileMigrator extends MigratorBase {
|
|||
|
||||
let cards = [];
|
||||
for (let row of rows) {
|
||||
cards.push({
|
||||
"cc-name": row.getResultByName("name_on_card"),
|
||||
"cc-number": await loginCrypto.decryptData(
|
||||
row.getResultByName("card_number_encrypted"),
|
||||
null
|
||||
),
|
||||
"cc-exp-month": parseInt(
|
||||
row.getResultByName("expiration_month"),
|
||||
10
|
||||
),
|
||||
"cc-exp-year": parseInt(row.getResultByName("expiration_year"), 10),
|
||||
});
|
||||
try {
|
||||
cards.push({
|
||||
"cc-name": row.getResultByName("name_on_card"),
|
||||
"cc-number": await loginCrypto.decryptData(
|
||||
row.getResultByName("card_number_encrypted"),
|
||||
null
|
||||
),
|
||||
"cc-exp-month": parseInt(
|
||||
row.getResultByName("expiration_month"),
|
||||
10
|
||||
),
|
||||
"cc-exp-year": parseInt(
|
||||
row.getResultByName("expiration_year"),
|
||||
10
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
await MigrationUtils.insertCreditCardsWrapper(cards);
|
||||
|
|
|
@ -1002,6 +1002,38 @@ newtab:
|
|||
send_in_pings:
|
||||
- newtab
|
||||
|
||||
metric_registered:
|
||||
type: labeled_boolean
|
||||
description: >
|
||||
Records technical data about whether the metric registration
|
||||
at runtime succeeded
|
||||
bugs:
|
||||
- https://bugzil.la/1961989
|
||||
data_reviews:
|
||||
- https://bugzil.la/1961989
|
||||
data_sensitivity:
|
||||
- technical
|
||||
notification_emails:
|
||||
- pdahiya@mozilla.com
|
||||
- mconley@mozilla.com
|
||||
expires: never
|
||||
|
||||
ping_registered:
|
||||
type: labeled_boolean
|
||||
description: >
|
||||
Records technical data about whether the ping registration
|
||||
at runtime succeeded
|
||||
bugs:
|
||||
- https://bugzil.la/1961989
|
||||
data_reviews:
|
||||
- https://bugzil.la/1961989
|
||||
data_sensitivity:
|
||||
- technical
|
||||
notification_emails:
|
||||
- pdahiya@mozilla.com
|
||||
- mconley@mozilla.com
|
||||
expires: never
|
||||
|
||||
newtab.search:
|
||||
enabled:
|
||||
lifetime: application
|
||||
|
@ -1503,22 +1535,6 @@ pocket:
|
|||
description: >
|
||||
If click belongs in a section, the numeric position of the section
|
||||
type: string
|
||||
title: &title
|
||||
description: >
|
||||
Title of the article
|
||||
type: string
|
||||
url: &url
|
||||
description: >
|
||||
Url of the article
|
||||
type: string
|
||||
time_sensitive: &time_sensitive
|
||||
description: >
|
||||
Indicates whether the article is time sensitive. This is passed down from merino
|
||||
type: boolean
|
||||
publisher: &publisher
|
||||
description: >
|
||||
derived publisher name of article
|
||||
type: string
|
||||
is_section_followed: *is_section_followed
|
||||
send_in_pings:
|
||||
- newtab
|
||||
|
@ -1586,10 +1602,6 @@ pocket:
|
|||
If click belongs in a section, the numeric position of the section
|
||||
type: string
|
||||
is_section_followed: *is_section_followed
|
||||
title: *title
|
||||
url: *url
|
||||
time_sensitive: *time_sensitive
|
||||
publisher: *publisher
|
||||
send_in_pings:
|
||||
- newtab
|
||||
|
||||
|
@ -1632,10 +1644,6 @@ pocket:
|
|||
If click belongs in a section, the numeric position of the section
|
||||
type: string
|
||||
is_section_followed: *is_section_followed
|
||||
title: *title
|
||||
url: *url
|
||||
time_sensitive: *time_sensitive
|
||||
publisher: *publisher
|
||||
send_in_pings:
|
||||
- newtab
|
||||
|
||||
|
@ -1846,10 +1854,6 @@ pocket:
|
|||
If event belongs in a section, the numeric position of the section
|
||||
type: string
|
||||
is_section_followed: *is_section_followed
|
||||
title: *title
|
||||
url: *url
|
||||
time_sensitive: *time_sensitive
|
||||
publisher: *publisher
|
||||
send_in_pings:
|
||||
- newtab
|
||||
|
||||
|
@ -2073,10 +2077,6 @@ newtab_content:
|
|||
description: >
|
||||
If click belongs in a section, if that section is followed
|
||||
type: boolean
|
||||
title: *title
|
||||
url: *url
|
||||
time_sensitive: *time_sensitive
|
||||
publisher: *publisher
|
||||
send_in_pings:
|
||||
- newtab-content
|
||||
|
||||
|
@ -2134,10 +2134,6 @@ newtab_content:
|
|||
description: >
|
||||
If click belongs in a section, if that section is followed
|
||||
type: boolean
|
||||
title: *title
|
||||
url: *url
|
||||
time_sensitive: *time_sensitive
|
||||
publisher: *publisher
|
||||
send_in_pings:
|
||||
- newtab-content
|
||||
|
||||
|
@ -2179,10 +2175,6 @@ newtab_content:
|
|||
description: >
|
||||
If click belongs in a section, if that section is followed
|
||||
type: boolean
|
||||
title: *title
|
||||
url: *url
|
||||
time_sensitive: *time_sensitive
|
||||
publisher: *publisher
|
||||
send_in_pings:
|
||||
- newtab-content
|
||||
|
||||
|
@ -2237,10 +2229,6 @@ newtab_content:
|
|||
description: >
|
||||
If event belongs in a section, if that section is followed
|
||||
type: boolean
|
||||
title: *title
|
||||
url: *url
|
||||
time_sensitive: *time_sensitive
|
||||
publisher: *publisher
|
||||
send_in_pings:
|
||||
- newtab-content
|
||||
|
||||
|
|
|
@ -214,9 +214,7 @@
|
|||
<html:h2 data-l10n-id="preferences-web-appearance-header"/>
|
||||
<html:div id="webAppearanceSettings">
|
||||
<description class="description-deemphasized" data-l10n-id="preferences-web-appearance-description"/>
|
||||
<html:moz-message-bar id="web-appearance-override-warning" data-l10n-id="preferences-web-appearance-override-warning2">
|
||||
<button slot="actions" class="accessory-button" data-l10n-id="preferences-colors-manage-button" id="web-appearance-manage-colors-button"/>
|
||||
</html:moz-message-bar>
|
||||
<html:moz-message-bar id="web-appearance-override-warning" data-l10n-id="preferences-web-appearance-override-warning3"/>
|
||||
<form xmlns="http://www.w3.org/1999/xhtml" id="web-appearance-chooser" autocomplete="off">
|
||||
<label class="web-appearance-choice" data-l10n-id="preferences-web-appearance-choice-tooltip-auto">
|
||||
<div class="web-appearance-choice-image-container"><img role="presentation" alt="" width="54" height="42" /></div>
|
||||
|
|
|
@ -4448,11 +4448,6 @@ const AppearanceChooser = {
|
|||
handleEvent(e) {
|
||||
if (e.type == "click") {
|
||||
switch (e.target.id) {
|
||||
// Forward the click to the "colors" button.
|
||||
case "web-appearance-manage-colors-button":
|
||||
document.getElementById("colors").click();
|
||||
e.preventDefault();
|
||||
break;
|
||||
case "web-appearance-manage-themes-link":
|
||||
window.browsingContext.topChromeWindow.BrowserAddonUI.openAddonsMgr(
|
||||
"addons://list/theme"
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
DIRS += ["dialogs"]
|
||||
|
||||
BROWSER_CHROME_MANIFESTS += ["tests/browser.toml", "tests/siteData/browser.toml"]
|
||||
MOCHITEST_CHROME_MANIFESTS += ["tests/chrome/chrome.toml"]
|
||||
|
||||
for var in ("MOZ_APP_NAME", "MOZ_MACBUNDLE_NAME"):
|
||||
DEFINES[var] = CONFIG[var]
|
||||
|
|
|
@ -439,10 +439,10 @@ var gSyncPane = {
|
|||
},
|
||||
|
||||
_getEntryPoint() {
|
||||
let params = new URLSearchParams(
|
||||
document.URL.split("#")[0].split("?")[1] || ""
|
||||
);
|
||||
return params.get("entrypoint") || "preferences";
|
||||
let params = URL.fromURI(document.documentURIObject).searchParams;
|
||||
let entryPoint = params.get("entrypoint") || "preferences";
|
||||
entryPoint = entryPoint.replace(/[^-.\w]/g, "");
|
||||
return entryPoint;
|
||||
},
|
||||
|
||||
openContentInBrowser(url, options) {
|
||||
|
|
|
@ -76,7 +76,7 @@ add_setup(async function () {
|
|||
Ci.nsIWebHandlerApp
|
||||
);
|
||||
handler2.name = "Handler 2";
|
||||
handler2.uriTemplate = "http://example.org/second/%s";
|
||||
handler2.uriTemplate = "https://example.org/second/%s";
|
||||
gDummyHandlers.push(handler1, handler2);
|
||||
|
||||
function substituteWebHandlers(handlerInfo) {
|
||||
|
|
|
@ -25,10 +25,7 @@ add_task(async function testFilterFeatures() {
|
|||
{
|
||||
...DEFAULT_LABS_RECIPES[3],
|
||||
slug: "test-featureD",
|
||||
bucketConfig: {
|
||||
...ExperimentFakes.recipe.bucketConfig,
|
||||
count: 1000,
|
||||
},
|
||||
bucketConfig: NimbusTestUtils.factories.recipe.bucketConfig,
|
||||
},
|
||||
];
|
||||
const cleanup = await setupLabsTest(recipes);
|
||||
|
|
|
@ -58,6 +58,11 @@ const EXPECTED = {
|
|||
// run through, so request a longer timeout.
|
||||
requestLongerTimeout(10);
|
||||
|
||||
add_setup(async function () {
|
||||
// Suggest needs to be initialized in order to dismiss a suggestion.
|
||||
await QuickSuggestTestUtils.ensureQuickSuggestInit();
|
||||
});
|
||||
|
||||
// The following tasks check the initial visibility of the Firefox Suggest UI
|
||||
// and the visibility after installing a Nimbus experiment.
|
||||
|
||||
|
@ -199,9 +204,11 @@ add_task(async function initiallyEnabled_settingsUiOfflineOnly() {
|
|||
|
||||
// Tests the "Restore" button for dismissed suggestions.
|
||||
add_task(async function restoreDismissedSuggestions() {
|
||||
// Start with no dismissed suggestions.
|
||||
await QuickSuggest.clearDismissedSuggestions();
|
||||
Assert.ok(
|
||||
!(await QuickSuggest.canClearDismissedSuggestions()),
|
||||
"Sanity check: This test expects canClearDismissedSuggestions to return false initially"
|
||||
"canClearDismissedSuggestions should be false after clearing suggestions"
|
||||
);
|
||||
|
||||
await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
|
||||
|
@ -213,27 +220,120 @@ add_task(async function restoreDismissedSuggestions() {
|
|||
let button = doc.getElementById(BUTTON_RESTORE_DISMISSED_ID);
|
||||
Assert.ok(button.disabled, "Restore button is disabled initially.");
|
||||
|
||||
await QuickSuggest.blockedSuggestions.add("https://example.com/");
|
||||
await QuickSuggest.dismissResult(QuickSuggestTestUtils.ampResult());
|
||||
|
||||
Assert.ok(
|
||||
await QuickSuggest.canClearDismissedSuggestions(),
|
||||
"canClearDismissedSuggestions should return true after dismissing a suggestion"
|
||||
);
|
||||
Assert.ok(!button.disabled, "Restore button is enabled after blocking URL.");
|
||||
await TestUtils.waitForCondition(
|
||||
() => !button.disabled,
|
||||
"Waiting for Restore button to become enabled after dismissing suggestion"
|
||||
);
|
||||
Assert.ok(
|
||||
!button.disabled,
|
||||
"Restore button should be enabled after dismissing suggestion"
|
||||
);
|
||||
|
||||
let clearPromise = TestUtils.topicObserved("quicksuggest-dismissals-cleared");
|
||||
button.click();
|
||||
await clearPromise;
|
||||
|
||||
Assert.ok(
|
||||
await QuickSuggest.blockedSuggestions.isEmpty(),
|
||||
"blockedSuggestions.isEmpty() should return true after restoring dismissals"
|
||||
);
|
||||
Assert.ok(
|
||||
!(await QuickSuggest.canClearDismissedSuggestions()),
|
||||
"canClearDismissedSuggestions should return false after restoring dismissals"
|
||||
);
|
||||
Assert.ok(button.disabled, "Restore button is disabled after clicking it.");
|
||||
await TestUtils.waitForCondition(
|
||||
() => button.disabled,
|
||||
"Waiting for Restore button to become disabled after clicking it"
|
||||
);
|
||||
Assert.ok(
|
||||
button.disabled,
|
||||
"Restore button should be disabled after clearing suggestions"
|
||||
);
|
||||
|
||||
gBrowser.removeCurrentTab();
|
||||
});
|
||||
|
||||
// If the pane is open while Suggest is still initializing and there are
|
||||
// dismissed suggestions, the "Restore" button should become enabled when init
|
||||
// finishes.
|
||||
add_task(async function restoreDismissedSuggestions_init_enabled() {
|
||||
// Dismiss a suggestion.
|
||||
await QuickSuggest.dismissResult(QuickSuggestTestUtils.ampResult());
|
||||
Assert.ok(
|
||||
await QuickSuggest.canClearDismissedSuggestions(),
|
||||
"canClearDismissedSuggestions should be true after dismissing suggestion"
|
||||
);
|
||||
|
||||
await doRestoreInitTest(async button => {
|
||||
// The button should become enabled since we dismissed a suggestion above.
|
||||
await TestUtils.waitForCondition(
|
||||
() => !button.disabled,
|
||||
"Waiting for Restore button to become enabled after re-enabling Rust backend"
|
||||
);
|
||||
Assert.ok(
|
||||
!button.disabled,
|
||||
"Restore button should be enabled after re-enabling Rust backend"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// If the pane is open while Suggest is still initializing and there are no
|
||||
// dismissed suggestions, the "Restore" button should remain disabled when init
|
||||
// finishes.
|
||||
add_task(async function restoreDismissedSuggestions_init_disabled() {
|
||||
// Clear dismissed suggestions.
|
||||
await QuickSuggest.clearDismissedSuggestions();
|
||||
Assert.ok(
|
||||
!(await QuickSuggest.canClearDismissedSuggestions()),
|
||||
"canClearDismissedSuggestions should be false after clearing suggestions"
|
||||
);
|
||||
|
||||
await doRestoreInitTest(async button => {
|
||||
// The button should remain disabled since there are no dismissed
|
||||
// suggestions.
|
||||
await TestUtils.waitForTick();
|
||||
Assert.ok(
|
||||
button.disabled,
|
||||
"Restore button should remain disabled after re-enabling Rust backend"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
async function doRestoreInitTest(checkButton) {
|
||||
// Disable the Suggest Rust backend, which manages individually dismissed
|
||||
// suggestions. While Rust is disabled, Suggest won't be able to tell whether
|
||||
// there are any individually dismissed suggestions.
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["browser.urlbar.quicksuggest.rustEnabled", false]],
|
||||
});
|
||||
|
||||
// Open the pane.
|
||||
await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
|
||||
|
||||
let doc = gBrowser.selectedBrowser.contentDocument;
|
||||
let addressBarSection = doc.getElementById("locationBarGroup");
|
||||
addressBarSection.scrollIntoView();
|
||||
|
||||
let button = doc.getElementById(BUTTON_RESTORE_DISMISSED_ID);
|
||||
Assert.ok(button.disabled, "Restore button is disabled initially.");
|
||||
|
||||
// Re-enable the Rust backend. It will send `quicksuggest-dismissals-changed`
|
||||
// when it finishes initialization.
|
||||
let changedPromise = TestUtils.topicObserved(
|
||||
"quicksuggest-dismissals-changed"
|
||||
);
|
||||
await SpecialPowers.popPrefEnv();
|
||||
|
||||
info(
|
||||
"Waiting for quicksuggest-dismissals-changed after re-enabling Rust backend"
|
||||
);
|
||||
await changedPromise;
|
||||
|
||||
await checkButton(button);
|
||||
|
||||
// Clean up.
|
||||
await QuickSuggest.clearDismissedSuggestions();
|
||||
gBrowser.removeCurrentTab();
|
||||
}
|
||||
|
|
8
browser/components/preferences/tests/chrome/chrome.toml
Normal file
|
@ -0,0 +1,8 @@
|
|||
[DEFAULT]
|
||||
support-files = [
|
||||
"../../../../../toolkit/content/tests/widgets/lit-test-helpers.js",
|
||||
]
|
||||
|
||||
["test_setting_control.html"]
|
||||
|
||||
["test_setting_group.html"]
|
|
@ -0,0 +1,195 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>setting-control test</title>
|
||||
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="chrome://mochikit/content/tests/SimpleTest/test.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="chrome://global/skin/global.css" />
|
||||
<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
|
||||
<script src="../../../../../toolkit/content/tests/widgets/lit-test-helpers.js"></script>
|
||||
<script
|
||||
type="module"
|
||||
src="chrome://browser/content/preferences/widgets/setting-group.mjs"
|
||||
></script>
|
||||
<script
|
||||
type="module"
|
||||
src="chrome://browser/content/preferences/widgets/setting-control.mjs"
|
||||
></script>
|
||||
<script
|
||||
type="module"
|
||||
src="chrome://global/content/elements/moz-support-link.mjs"
|
||||
></script>
|
||||
<script
|
||||
type="application/javascript"
|
||||
src="chrome://global/content/preferencesBindings.js"
|
||||
></script>
|
||||
<script>
|
||||
/* import-globals-from /toolkit/content/preferencesBindings.js */
|
||||
let html, testHelpers;
|
||||
|
||||
const LABEL_L10N_ID = "browsing-use-autoscroll";
|
||||
const GROUP_L10N_ID = "pane-experimental-reset";
|
||||
|
||||
function renderTemplate(itemConfig, setting) {
|
||||
return testHelpers.renderTemplate(html`
|
||||
<setting-control
|
||||
.config=${itemConfig}
|
||||
.setting=${setting}
|
||||
></setting-group>
|
||||
`);
|
||||
}
|
||||
|
||||
function waitForSettingChange(setting) {
|
||||
return new Promise(resolve => {
|
||||
setting.on("change", function handler() {
|
||||
setting.off("change", handler);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
add_setup(async function setup() {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["settings.revamp.design", false]],
|
||||
});
|
||||
testHelpers = new InputTestHelpers();
|
||||
({ html } = await testHelpers.setupLit());
|
||||
testHelpers.setupTests({
|
||||
templateFn: () => html`<setting-group></setting-group>`,
|
||||
});
|
||||
MozXULElement.insertFTLIfNeeded("branding/brand.ftl");
|
||||
MozXULElement.insertFTLIfNeeded("browser/preferences/preferences.ftl");
|
||||
});
|
||||
|
||||
add_task(async function testSimpleCheckbox() {
|
||||
const PREF = "test.setting-control.one";
|
||||
const SETTING = "setting-control-one";
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [[PREF, true]],
|
||||
});
|
||||
Preferences.addAll([{ id: PREF, type: "bool" }]);
|
||||
Preferences.addSetting({
|
||||
id: SETTING,
|
||||
pref: PREF,
|
||||
});
|
||||
let itemConfig = { l10nId: LABEL_L10N_ID, id: SETTING };
|
||||
let setting = Preferences.getSetting(SETTING);
|
||||
let result = await renderTemplate(itemConfig, setting);
|
||||
let control = result.firstElementChild;
|
||||
is(
|
||||
control.localName,
|
||||
"setting-control",
|
||||
"It rendered the setting-control"
|
||||
);
|
||||
is(
|
||||
control.inputEl.localName,
|
||||
"moz-checkbox",
|
||||
"The control rendered a checkbox"
|
||||
);
|
||||
is(control.inputEl.dataset.l10nId, LABEL_L10N_ID, "Label is set");
|
||||
is(control.inputEl.checked, true, "checkbox is checked");
|
||||
is(Services.prefs.getBoolPref(PREF), true, "pref is true");
|
||||
|
||||
let settingChanged = waitForSettingChange(setting);
|
||||
synthesizeMouseAtCenter(control, {});
|
||||
await settingChanged;
|
||||
is(
|
||||
control.inputEl.checked,
|
||||
false,
|
||||
"checkbox becomes unchecked after click"
|
||||
);
|
||||
is(Services.prefs.getBoolPref(PREF), false, "pref is false");
|
||||
|
||||
settingChanged = waitForSettingChange(setting);
|
||||
Services.prefs.setBoolPref(PREF, true);
|
||||
await settingChanged;
|
||||
is(
|
||||
control.inputEl.checked,
|
||||
true,
|
||||
"checkbox becomes checked after pfef change"
|
||||
);
|
||||
is(Services.prefs.getBoolPref(PREF), true, "pref is true");
|
||||
});
|
||||
|
||||
add_task(async function testSupportLinkCheckbox() {
|
||||
const SETTING = "setting-control-support-link";
|
||||
Preferences.addSetting({
|
||||
id: SETTING,
|
||||
get: () => true,
|
||||
});
|
||||
let itemConfig = {
|
||||
l10nId: LABEL_L10N_ID,
|
||||
id: SETTING,
|
||||
supportPage: "foo",
|
||||
};
|
||||
let result = await renderTemplate(
|
||||
itemConfig,
|
||||
Preferences.getSetting(SETTING)
|
||||
);
|
||||
let control = result.firstElementChild;
|
||||
ok(control, "Got a control");
|
||||
let checkbox = control.inputEl;
|
||||
is(checkbox.localName, "moz-checkbox", "moz-checkbox is rendered");
|
||||
is(
|
||||
checkbox.supportPage,
|
||||
"foo",
|
||||
"The checkbox receives the supportPage"
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function testSupportLinkSubcategory() {
|
||||
const SETTING = "setting-control-subcategory";
|
||||
Preferences.addSetting({
|
||||
id: SETTING,
|
||||
get: () => true,
|
||||
});
|
||||
|
||||
let configOne = {
|
||||
l10nId: LABEL_L10N_ID,
|
||||
id: SETTING,
|
||||
subcategory: "exsubcategory",
|
||||
};
|
||||
let result = await renderTemplate(
|
||||
configOne,
|
||||
Preferences.getSetting(SETTING)
|
||||
);
|
||||
let control = result.firstElementChild;
|
||||
ok(control, "Got the control");
|
||||
is(
|
||||
control.inputEl.dataset.subcategory,
|
||||
"exsubcategory",
|
||||
"Subcategory is set"
|
||||
);
|
||||
|
||||
let configTwo = {
|
||||
l10nId: LABEL_L10N_ID,
|
||||
id: SETTING,
|
||||
subcategory: "exsubcategory2",
|
||||
supportPage: "foo",
|
||||
};
|
||||
result = await renderTemplate(
|
||||
configTwo,
|
||||
Preferences.getSetting(SETTING)
|
||||
);
|
||||
control = result.firstElementChild;
|
||||
ok(control, "Got the control");
|
||||
is(
|
||||
control.inputEl.dataset.subcategory,
|
||||
"exsubcategory2",
|
||||
"Subcategory is set"
|
||||
);
|
||||
|
||||
is(control.inputEl.supportPage, "foo", "Input got the supportPage");
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none"></div>
|
||||
<pre id="test"></pre>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,284 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>setting-group Tests</title>
|
||||
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="chrome://mochikit/content/tests/SimpleTest/test.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="chrome://global/skin/global.css" />
|
||||
<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
|
||||
<script src="../../../../../toolkit/content/tests/widgets/lit-test-helpers.js"></script>
|
||||
<script
|
||||
type="module"
|
||||
src="chrome://browser/content/preferences/widgets/setting-group.mjs"
|
||||
></script>
|
||||
<script
|
||||
type="module"
|
||||
src="chrome://browser/content/preferences/widgets/setting-control.mjs"
|
||||
></script>
|
||||
<script
|
||||
type="module"
|
||||
src="chrome://global/content/elements/moz-support-link.mjs"
|
||||
></script>
|
||||
<script
|
||||
type="application/javascript"
|
||||
src="chrome://global/content/preferencesBindings.js"
|
||||
></script>
|
||||
<script>
|
||||
/* import-globals-from /toolkit/content/preferencesBindings.js */
|
||||
let html, testHelpers;
|
||||
|
||||
const LABEL_L10N_ID = "browsing-use-autoscroll";
|
||||
const GROUP_L10N_ID = "pane-experimental-reset";
|
||||
|
||||
function renderTemplate(config) {
|
||||
return testHelpers.renderTemplate(html`
|
||||
<setting-group
|
||||
.config=${config}
|
||||
.getSetting=${(...args) => Preferences.getSetting(...args)}
|
||||
></setting-group>
|
||||
`);
|
||||
}
|
||||
|
||||
function waitForSettingChange(setting) {
|
||||
return new Promise(resolve => {
|
||||
setting.on("change", function handler() {
|
||||
setting.off("change", handler);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
add_setup(async function setup() {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["settings.revamp.design", false]],
|
||||
});
|
||||
testHelpers = new InputTestHelpers();
|
||||
({ html } = await testHelpers.setupLit());
|
||||
testHelpers.setupTests({
|
||||
templateFn: () => html`<setting-group></setting-group>`,
|
||||
});
|
||||
MozXULElement.insertFTLIfNeeded("branding/brand.ftl");
|
||||
MozXULElement.insertFTLIfNeeded("browser/preferences/preferences.ftl");
|
||||
});
|
||||
|
||||
add_task(async function testSimpleXulPrefCheckboxes() {
|
||||
const PREF_ONE = "test.settings-group.one";
|
||||
const SETTING_ONE = "setting-one";
|
||||
const PREF_TWO = "test.settings-group.two";
|
||||
const SETTING_TWO = "setting-two";
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
[PREF_ONE, true],
|
||||
[PREF_TWO, false],
|
||||
],
|
||||
});
|
||||
Preferences.addAll([
|
||||
{ id: PREF_ONE, type: "bool" },
|
||||
{ id: PREF_TWO, type: "bool" },
|
||||
]);
|
||||
Preferences.addSetting({
|
||||
id: SETTING_ONE,
|
||||
pref: PREF_ONE,
|
||||
});
|
||||
Preferences.addSetting({
|
||||
id: SETTING_TWO,
|
||||
pref: PREF_TWO,
|
||||
});
|
||||
let config = {
|
||||
items: [
|
||||
{ l10nId: LABEL_L10N_ID, id: SETTING_ONE },
|
||||
{ l10nId: LABEL_L10N_ID, id: SETTING_TWO },
|
||||
],
|
||||
};
|
||||
let result = await renderTemplate(config);
|
||||
let group = result.querySelector("setting-group");
|
||||
ok(group, "setting-group is created");
|
||||
ok(group.config, "it got a config");
|
||||
let checkboxes = result.querySelectorAll("checkbox");
|
||||
is(checkboxes.length, 2, "Rendered two checkboxes");
|
||||
is(checkboxes[0].dataset.l10nId, LABEL_L10N_ID, "Label is set");
|
||||
is(checkboxes[0].checked, true, "First checkbox is checked");
|
||||
is(Services.prefs.getBoolPref(PREF_ONE), true, "First pref is true");
|
||||
is(checkboxes[1].checked, false, "Second checkbox is unchecked");
|
||||
is(Services.prefs.getBoolPref(PREF_TWO), false, "Second pref is false");
|
||||
|
||||
let settingChanged = waitForSettingChange(
|
||||
Preferences.getSetting(SETTING_ONE)
|
||||
);
|
||||
synthesizeMouseAtCenter(
|
||||
checkboxes[0].querySelector(".checkbox-check"),
|
||||
{}
|
||||
);
|
||||
await settingChanged;
|
||||
is(checkboxes[0].checked, false, "First checkbox is unchecked");
|
||||
is(Services.prefs.getBoolPref(PREF_ONE), false, "First pref is false");
|
||||
|
||||
settingChanged = waitForSettingChange(
|
||||
Preferences.getSetting(SETTING_TWO)
|
||||
);
|
||||
synthesizeMouseAtCenter(
|
||||
checkboxes[1].querySelector(".checkbox-check"),
|
||||
{}
|
||||
);
|
||||
await settingChanged;
|
||||
is(checkboxes[1].checked, true, "Second checkbox is checked");
|
||||
is(Services.prefs.getBoolPref(PREF_TWO), true, "Second pref is true");
|
||||
|
||||
settingChanged = waitForSettingChange(
|
||||
Preferences.getSetting(SETTING_ONE)
|
||||
);
|
||||
Services.prefs.setBoolPref(PREF_ONE, true);
|
||||
await settingChanged;
|
||||
is(
|
||||
checkboxes[0].checked,
|
||||
true,
|
||||
"First checkbox becomes checked after pref change"
|
||||
);
|
||||
is(Services.prefs.getBoolPref(PREF_ONE), true, "First pref is true");
|
||||
|
||||
settingChanged = waitForSettingChange(
|
||||
Preferences.getSetting(SETTING_TWO)
|
||||
);
|
||||
Services.prefs.setBoolPref(PREF_TWO, false);
|
||||
await settingChanged;
|
||||
is(
|
||||
checkboxes[1].checked,
|
||||
false,
|
||||
"Second checkbox becomes unchecked after pref change"
|
||||
);
|
||||
is(Services.prefs.getBoolPref(PREF_TWO), false, "Second pref is false");
|
||||
});
|
||||
|
||||
add_task(async function testSupportLinkXulCheckbox() {
|
||||
const SETTING = "setting-support-link";
|
||||
Preferences.addSetting({
|
||||
id: SETTING,
|
||||
get: () => true,
|
||||
});
|
||||
let config = {
|
||||
items: [{ l10nId: LABEL_L10N_ID, id: SETTING, supportPage: "foo" }],
|
||||
};
|
||||
let result = await renderTemplate(config);
|
||||
let checkbox = result.querySelector("checkbox");
|
||||
ok(checkbox, "Got a checkbox");
|
||||
ok(
|
||||
checkbox.classList.contains("tail-with-learn-more"),
|
||||
"Checkbox gets the correct class"
|
||||
);
|
||||
let container = checkbox.parentElement;
|
||||
is(container.localName, "hbox", "Checkbox is in an hbox");
|
||||
let supportLink = container.querySelector("a");
|
||||
ok(supportLink, "The support link was created");
|
||||
is(
|
||||
supportLink.constructor,
|
||||
customElements.get("moz-support-link"),
|
||||
"It's a support link"
|
||||
);
|
||||
is(supportLink.supportPage, "foo", "The support page is set");
|
||||
});
|
||||
|
||||
add_task(async function testSupportLinkXulSubcategory() {
|
||||
const SETTING = "setting-subcategory";
|
||||
Preferences.addSetting({
|
||||
id: SETTING,
|
||||
get: () => true,
|
||||
});
|
||||
let config = {
|
||||
items: [
|
||||
{
|
||||
l10nId: LABEL_L10N_ID,
|
||||
id: SETTING,
|
||||
subcategory: "exsubcategory",
|
||||
},
|
||||
{
|
||||
l10nId: LABEL_L10N_ID,
|
||||
id: SETTING,
|
||||
subcategory: "exsubcategory2",
|
||||
supportPage: "foo",
|
||||
},
|
||||
],
|
||||
};
|
||||
let result = await renderTemplate(config);
|
||||
let [basic, supportLink] = result.querySelectorAll("checkbox");
|
||||
ok(basic, "Got the basic checkbox");
|
||||
is(
|
||||
basic.dataset.subcategory,
|
||||
"exsubcategory",
|
||||
"Subcategory is set for basic"
|
||||
);
|
||||
|
||||
ok(supportLink, "Got the support link checkbox");
|
||||
let container = supportLink.parentElement;
|
||||
is(container.localName, "hbox", "Support link is in a container");
|
||||
is(
|
||||
container.dataset.subcategory,
|
||||
"exsubcategory2",
|
||||
"Support link container has subcategory"
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function testSettingGroupRevamp() {
|
||||
const PREF_ONE = "test.settings-group.itemone";
|
||||
const SETTING_ONE = "setting-item-one";
|
||||
const PREF_TWO = "test.settings-group.itemtwo";
|
||||
const SETTING_TWO = "setting-item-two";
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
[PREF_ONE, true],
|
||||
[PREF_TWO, false],
|
||||
["settings.revamp.design", true],
|
||||
],
|
||||
});
|
||||
Preferences.addAll([
|
||||
{ id: PREF_ONE, type: "bool" },
|
||||
{ id: PREF_TWO, type: "bool" },
|
||||
]);
|
||||
Preferences.addSetting({
|
||||
id: SETTING_ONE,
|
||||
pref: PREF_ONE,
|
||||
});
|
||||
Preferences.addSetting({
|
||||
id: SETTING_TWO,
|
||||
pref: PREF_TWO,
|
||||
});
|
||||
let config = {
|
||||
l10nId: GROUP_L10N_ID,
|
||||
items: [
|
||||
{ l10nId: LABEL_L10N_ID, id: SETTING_ONE },
|
||||
{ l10nId: LABEL_L10N_ID, id: SETTING_TWO },
|
||||
],
|
||||
};
|
||||
|
||||
let result = await renderTemplate(config);
|
||||
let group = result.querySelector("setting-group");
|
||||
ok(group, "setting-group is created");
|
||||
ok(group.config, "it got a config");
|
||||
let fieldset = group.children[0];
|
||||
is(fieldset.localName, "moz-fieldset", "First child is a fieldset");
|
||||
is(
|
||||
fieldset.dataset.l10nId,
|
||||
GROUP_L10N_ID,
|
||||
"Fieldset has the group label"
|
||||
);
|
||||
let [item1, item2] = fieldset.children;
|
||||
is(item1.localName, "setting-control", "First setting-control");
|
||||
is(item1.config.id, SETTING_ONE, "First setting-control id is correct");
|
||||
is(item2.localName, "setting-control", "Second setting-control");
|
||||
is(
|
||||
item2.config.id,
|
||||
SETTING_TWO,
|
||||
"Second setting-control id is correct"
|
||||
);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none"></div>
|
||||
<pre id="test"></pre>
|
||||
</body>
|
||||
</html>
|
|
@ -15,7 +15,7 @@ ChromeUtils.defineLazyGetter(this, "QuickSuggestTestUtils", () => {
|
|||
|
||||
ChromeUtils.defineESModuleGetters(this, {
|
||||
ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
|
||||
ExperimentFakes: "resource://testing-common/NimbusTestUtils.sys.mjs",
|
||||
NimbusTestUtils: "resource://testing-common/NimbusTestUtils.sys.mjs",
|
||||
});
|
||||
|
||||
const kDefaultWait = 2000;
|
||||
|
@ -403,11 +403,7 @@ async function assertSuggestVisibility(expectedByElementId) {
|
|||
}
|
||||
|
||||
const DEFAULT_LABS_RECIPES = [
|
||||
ExperimentFakes.recipe("nimbus-qa-1", {
|
||||
bucketConfig: {
|
||||
...ExperimentFakes.recipe.bucketConfig,
|
||||
count: 1000,
|
||||
},
|
||||
NimbusTestUtils.factories.recipe("nimbus-qa-1", {
|
||||
targeting: "true",
|
||||
isRollout: true,
|
||||
isFirefoxLabsOptIn: true,
|
||||
|
@ -432,11 +428,7 @@ const DEFAULT_LABS_RECIPES = [
|
|||
],
|
||||
}),
|
||||
|
||||
ExperimentFakes.recipe("nimbus-qa-2", {
|
||||
bucketConfig: {
|
||||
...ExperimentFakes.recipe.bucketConfig,
|
||||
count: 1000,
|
||||
},
|
||||
NimbusTestUtils.factories.recipe("nimbus-qa-2", {
|
||||
targeting: "true",
|
||||
isRollout: true,
|
||||
isFirefoxLabsOptIn: true,
|
||||
|
@ -462,11 +454,7 @@ const DEFAULT_LABS_RECIPES = [
|
|||
],
|
||||
}),
|
||||
|
||||
ExperimentFakes.recipe("targeting-false", {
|
||||
bucketConfig: {
|
||||
...ExperimentFakes.recipe.bucketConfig,
|
||||
count: 1000,
|
||||
},
|
||||
NimbusTestUtils.factories.recipe("targeting-false", {
|
||||
targeting: "false",
|
||||
isRollout: true,
|
||||
isFirefoxLabsOptIn: true,
|
||||
|
@ -477,11 +465,12 @@ const DEFAULT_LABS_RECIPES = [
|
|||
requiresRestart: false,
|
||||
}),
|
||||
|
||||
ExperimentFakes.recipe("bucketing-false", {
|
||||
NimbusTestUtils.factories.recipe("bucketing-false", {
|
||||
bucketConfig: {
|
||||
...ExperimentFakes.recipe.bucketConfig,
|
||||
...NimbusTestUtils.factories.recipe.bucketConfig,
|
||||
count: 0,
|
||||
},
|
||||
isRollout: true,
|
||||
targeting: "true",
|
||||
isFirefoxLabsOptIn: true,
|
||||
firefoxLabsTitle: "experimental-features-ime-search",
|
||||
|
|
|
@ -14,6 +14,10 @@ export class SettingControl extends MozLitElement {
|
|||
value: {},
|
||||
};
|
||||
|
||||
static queries = {
|
||||
inputEl: "#input",
|
||||
};
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
@ -48,6 +52,7 @@ export class SettingControl extends MozLitElement {
|
|||
case "checkbox":
|
||||
default:
|
||||
return html`<moz-checkbox
|
||||
id="input"
|
||||
data-l10n-id=${config.l10nId}
|
||||
.iconSrc=${config.iconSrc}
|
||||
.checked=${this.value}
|
||||
|
|
|
@ -36,6 +36,7 @@ export class SettingGroup extends MozLitElement {
|
|||
checkbox.addEventListener("command", e =>
|
||||
setting.userChange(e.target.checked)
|
||||
);
|
||||
setting.on("change", () => (checkbox.checked = setting.value));
|
||||
checkbox.checked = setting.value;
|
||||
if (item.supportPage) {
|
||||
let container = document.createXULElement("hbox");
|
||||
|
|
352
browser/components/profiles/ProfilesDatastoreService.sys.mjs
Normal file
|
@ -0,0 +1,352 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { DeferredTask } from "resource://gre/modules/DeferredTask.sys.mjs";
|
||||
|
||||
const NOTIFY_TIMEOUT = 200;
|
||||
const STOREID_PREF_NAME = "toolkit.profiles.storeID";
|
||||
|
||||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
|
||||
Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
|
||||
});
|
||||
|
||||
class ProfilesDatastoreServiceClass {
|
||||
#connection = null;
|
||||
#asyncShutdownBlocker = null;
|
||||
#initialized = false;
|
||||
#storeID = null;
|
||||
#initPromise = null;
|
||||
#notifyTask = null;
|
||||
#profileService = null;
|
||||
static #dirSvc = null;
|
||||
|
||||
/**
|
||||
* Gets a connection to the database.
|
||||
*
|
||||
* Use this connection to query existing tables, but never to create or
|
||||
* modify schemas. Any schema changes should be added as a new migration,
|
||||
* see `createTables()`.
|
||||
*
|
||||
* Rethrows errors thrown by `Sqlite.openConnection()`; see details in
|
||||
* `Sqlite.sys.mjs`. TODO: document and handle errors (bug 1960963).
|
||||
*/
|
||||
async getConnection() {
|
||||
await this.init();
|
||||
return this.#connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update tables in this shared cross-profile database.
|
||||
*
|
||||
* Includes simple forward-only migration support which applies any new
|
||||
* migrations based on the schema version.
|
||||
*
|
||||
* Notes for migration authors:
|
||||
*
|
||||
* Since a mix of Firefox versions may access the database at any time, all
|
||||
* schema changes must be backwards-compatible.
|
||||
*
|
||||
* Please keep your schemas as simple as possible, to reduce the odds of
|
||||
* corruption affecting all users of the database.
|
||||
*/
|
||||
async createTables() {
|
||||
// TODO: (Bug 1902320) Handle exceptions on connection opening
|
||||
let currentVersion = await this.#connection.getSchemaVersion();
|
||||
if (currentVersion == 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentVersion < 1) {
|
||||
// Brand new database or created prior to migration support.
|
||||
await this.#connection.executeTransaction(async () => {
|
||||
const createProfilesTable = `
|
||||
CREATE TABLE IF NOT EXISTS "Profiles" (
|
||||
id INTEGER NOT NULL,
|
||||
path TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
avatar TEXT NOT NULL,
|
||||
themeId TEXT NOT NULL,
|
||||
themeFg TEXT NOT NULL,
|
||||
themeBg TEXT NOT NULL,
|
||||
PRIMARY KEY(id)
|
||||
);`;
|
||||
|
||||
await this.#connection.execute(createProfilesTable);
|
||||
|
||||
const createSharedPrefsTable = `
|
||||
CREATE TABLE IF NOT EXISTS "SharedPrefs" (
|
||||
id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
value BLOB,
|
||||
isBoolean INTEGER,
|
||||
PRIMARY KEY(id)
|
||||
);`;
|
||||
|
||||
await this.#connection.execute(createSharedPrefsTable);
|
||||
});
|
||||
}
|
||||
|
||||
await this.#connection.setSchemaVersion(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger an async cross-instance notification that the data in the
|
||||
* datastore has been changed.
|
||||
*
|
||||
* Two different nsIObserver events may be fired, depending on whether the
|
||||
* change originated in the current Firefox instance or in another instance.
|
||||
*
|
||||
* Changes to the datastore made by the current instance will trigger the
|
||||
* "pds-datastore-changed" nsIObserver event; see `#datastoreChanged` for
|
||||
* details. These events are fired whether or not the multiple profiles
|
||||
* feature is enabled.
|
||||
*
|
||||
* If the multiple profiles feature is enabled, then changes to the datastore
|
||||
* made either by the current instance or another instance in the profile
|
||||
* group will trigger the "sps-profiles-updated" nsIObserver event. See
|
||||
* `SelectableProfileService.databaseChanged` for details.
|
||||
*/
|
||||
notify() {
|
||||
this.#notifyTask.arm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify datastore observers that the data has changed by firing
|
||||
* the "pds-datastore-changed" nsIObserver signal.
|
||||
*
|
||||
*@param {"local"|"shutdown"} source The source of the
|
||||
* notification. Either "local" meaning that the change was made in this
|
||||
* process and "shutdown" meaning we are closing the connection and
|
||||
* shutting down.
|
||||
*/
|
||||
#datastoreChanged(source) {
|
||||
Services.obs.notifyObservers(null, "pds-datastore-changed", source);
|
||||
}
|
||||
|
||||
get storeID() {
|
||||
return new Promise(resolve => {
|
||||
this.init().then(() => {
|
||||
resolve(this.#storeID);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
get initialized() {
|
||||
return this.#initialized;
|
||||
}
|
||||
|
||||
static get PROFILE_GROUPS_DIR() {
|
||||
if (this.#dirSvc && "ProfileGroups" in this.#dirSvc) {
|
||||
return this.#dirSvc.ProfileGroups;
|
||||
}
|
||||
|
||||
return PathUtils.join(
|
||||
ProfilesDatastoreServiceClass.getDirectory("UAppData").path,
|
||||
"Profile Groups"
|
||||
);
|
||||
}
|
||||
|
||||
overrideDirectoryService(dirSvc) {
|
||||
if (!Cu.isInAutomation) {
|
||||
return;
|
||||
}
|
||||
|
||||
ProfilesDatastoreServiceClass.#dirSvc = dirSvc;
|
||||
}
|
||||
|
||||
static getDirectory(id) {
|
||||
if (this.#dirSvc) {
|
||||
if (id in this.#dirSvc) {
|
||||
return this.#dirSvc[id].clone();
|
||||
}
|
||||
}
|
||||
|
||||
return Services.dirsvc.get(id, Ci.nsIFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* For use in testing only, override the profile service with a mock version
|
||||
* and reset state accordingly.
|
||||
*
|
||||
* @param {Ci.nsIToolkitProfileService} profileService The mock profile service
|
||||
*/
|
||||
async resetProfileService(profileService) {
|
||||
if (!Cu.isInAutomation) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.uninit();
|
||||
this.#profileService =
|
||||
profileService ??
|
||||
Cc["@mozilla.org/toolkit/profile-service;1"].getService(
|
||||
Ci.nsIToolkitProfileService
|
||||
);
|
||||
await this.init();
|
||||
}
|
||||
|
||||
get toolkitProfileService() {
|
||||
return this.#profileService;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.#asyncShutdownBlocker = () => this.uninit();
|
||||
this.#profileService = Cc[
|
||||
"@mozilla.org/toolkit/profile-service;1"
|
||||
].getService(Ci.nsIToolkitProfileService);
|
||||
}
|
||||
|
||||
/**
|
||||
* At startup, get the groupDBPath from the prefs, and connect to it.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
init() {
|
||||
if (!this.#initPromise) {
|
||||
this.#initPromise = this.#init().finally(
|
||||
() => (this.#initPromise = null)
|
||||
);
|
||||
}
|
||||
|
||||
return this.#initPromise;
|
||||
}
|
||||
|
||||
async #init() {
|
||||
if (this.#initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We read the store ID from prefs but in early startup (for example in the profile selector)
|
||||
// this is not available so we get it from the current profile.
|
||||
this.#storeID = Services.startup.startingUp
|
||||
? this.#profileService.currentProfile?.storeID
|
||||
: Services.prefs.getStringPref(STOREID_PREF_NAME, "");
|
||||
|
||||
// This could fail if we're adding it during shutdown. In this case,
|
||||
// don't throw but don't continue initialization.
|
||||
try {
|
||||
lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
|
||||
"ProfilesDatastoreService uninit",
|
||||
this.#asyncShutdownBlocker
|
||||
);
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
this.#notifyTask = new DeferredTask(async () => {
|
||||
this.#datastoreChanged("local");
|
||||
}, NOTIFY_TIMEOUT);
|
||||
|
||||
try {
|
||||
await this.#initConnection();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
await this.uninit();
|
||||
return;
|
||||
}
|
||||
|
||||
this.#initialized = true;
|
||||
}
|
||||
|
||||
async uninit() {
|
||||
lazy.AsyncShutdown.profileChangeTeardown.removeBlocker(
|
||||
this.#asyncShutdownBlocker
|
||||
);
|
||||
|
||||
// During shutdown we don't need to notify ourselves, just other instances
|
||||
// so rather than finalizing the task just disarm it and do the notification
|
||||
// manually.
|
||||
if (this.#notifyTask.isArmed) {
|
||||
this.#notifyTask.disarm();
|
||||
this.#datastoreChanged("shutdown");
|
||||
}
|
||||
|
||||
await this.closeConnection();
|
||||
|
||||
this.#storeID = null;
|
||||
|
||||
this.#initialized = false;
|
||||
}
|
||||
|
||||
async #initConnection() {
|
||||
if (this.#connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
let path = await this.getProfilesStorePath();
|
||||
|
||||
// TODO: (Bug 1902320) Handle exceptions on connection opening
|
||||
// This could fail if the store is corrupted.
|
||||
this.#connection = await lazy.Sqlite.openConnection({
|
||||
path,
|
||||
openNotExclusive: true,
|
||||
});
|
||||
|
||||
await this.#connection.execute("PRAGMA journal_mode = WAL");
|
||||
await this.#connection.execute("PRAGMA wal_autocheckpoint = 16");
|
||||
|
||||
await this.createTables();
|
||||
}
|
||||
|
||||
async closeConnection() {
|
||||
if (!this.#connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
// An error could occur while closing the connection. We suppress the
|
||||
// error since it is not a critical part of the browser.
|
||||
try {
|
||||
await this.#connection.close();
|
||||
} catch (ex) {}
|
||||
this.#connection = null;
|
||||
}
|
||||
|
||||
async maybeCreateProfilesStorePath() {
|
||||
if (this.#storeID) {
|
||||
return;
|
||||
}
|
||||
|
||||
await IOUtils.makeDirectory(
|
||||
ProfilesDatastoreServiceClass.PROFILE_GROUPS_DIR
|
||||
);
|
||||
|
||||
const storageID = Services.uuid
|
||||
.generateUUID()
|
||||
.toString()
|
||||
.replace("{", "")
|
||||
.split("-")[0];
|
||||
|
||||
this.#storeID = storageID;
|
||||
Services.prefs.setStringPref(STOREID_PREF_NAME, storageID);
|
||||
}
|
||||
|
||||
async getProfilesStorePath() {
|
||||
await this.maybeCreateProfilesStorePath();
|
||||
|
||||
// If we are not running in a named nsIToolkitProfile, the datastore path
|
||||
// should be in the profile directory. This is true in a local build or a
|
||||
// CI test build, for example.
|
||||
if (
|
||||
!this.#profileService.currentProfile &&
|
||||
!this.#profileService.groupProfile
|
||||
) {
|
||||
return PathUtils.join(
|
||||
ProfilesDatastoreServiceClass.getDirectory("ProfD").path,
|
||||
`${this.#storeID}.sqlite`
|
||||
);
|
||||
}
|
||||
|
||||
return PathUtils.join(
|
||||
ProfilesDatastoreServiceClass.PROFILE_GROUPS_DIR,
|
||||
`${this.#storeID}.sqlite`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const ProfilesDatastoreService = new ProfilesDatastoreServiceClass();
|
||||
export { ProfilesDatastoreService };
|
|
@ -2,6 +2,9 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { ProfilesDatastoreService } from "resource:///modules/profiles/ProfilesDatastoreService.sys.mjs";
|
||||
import { SelectableProfileService } from "resource:///modules/profiles/SelectableProfileService.sys.mjs";
|
||||
|
||||
/**
|
||||
* The selectable profile
|
||||
*/
|
||||
|
@ -26,9 +29,7 @@ export class SelectableProfile {
|
|||
#themeFg;
|
||||
#themeBg;
|
||||
|
||||
#selectableProfileService = null;
|
||||
|
||||
constructor(row, selectableProfileService) {
|
||||
constructor(row) {
|
||||
this.#id = row.getResultByName("id");
|
||||
this.#path = row.getResultByName("path");
|
||||
this.#name = row.getResultByName("name");
|
||||
|
@ -36,8 +37,6 @@ export class SelectableProfile {
|
|||
this.#themeId = row.getResultByName("themeId");
|
||||
this.#themeFg = row.getResultByName("themeFg");
|
||||
this.#themeBg = row.getResultByName("themeBg");
|
||||
|
||||
this.#selectableProfileService = selectableProfileService;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -81,7 +80,7 @@ export class SelectableProfile {
|
|||
*/
|
||||
get path() {
|
||||
return PathUtils.joinRelative(
|
||||
this.#selectableProfileService.constructor.getDirectory("UAppData").path,
|
||||
ProfilesDatastoreService.constructor.getDirectory("UAppData").path,
|
||||
this.#path
|
||||
);
|
||||
}
|
||||
|
@ -105,10 +104,10 @@ export class SelectableProfile {
|
|||
get localDir() {
|
||||
return this.rootDir.then(root => {
|
||||
let relative = root.getRelativePath(
|
||||
this.#selectableProfileService.constructor.getDirectory("DefProfRt")
|
||||
ProfilesDatastoreService.constructor.getDirectory("DefProfRt")
|
||||
);
|
||||
let local =
|
||||
this.#selectableProfileService.constructor.getDirectory("DefProfLRt");
|
||||
ProfilesDatastoreService.constructor.getDirectory("DefProfLRt");
|
||||
local.appendRelativePath(relative);
|
||||
return local;
|
||||
});
|
||||
|
@ -203,7 +202,7 @@ export class SelectableProfile {
|
|||
}
|
||||
|
||||
saveUpdatesToDB() {
|
||||
this.#selectableProfileService.updateProfile(this);
|
||||
SelectableProfileService.updateProfile(this);
|
||||
}
|
||||
|
||||
toObject() {
|
||||
|
|
|
@ -2,11 +2,12 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { SelectableProfile } from "./SelectableProfile.sys.mjs";
|
||||
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
||||
import { DeferredTask } from "resource://gre/modules/DeferredTask.sys.mjs";
|
||||
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
||||
import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
|
||||
import { ProfilesDatastoreService } from "resource:///modules/profiles/ProfilesDatastoreService.sys.mjs";
|
||||
import { SelectableProfile } from "resource:///modules/profiles/SelectableProfile.sys.mjs";
|
||||
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
||||
|
||||
const lazy = {};
|
||||
|
||||
|
@ -15,16 +16,15 @@ const TASKBAR_ICON_CONTROLLERS = new WeakMap();
|
|||
const PROFILES_PREF_NAME = "browser.profiles.enabled";
|
||||
const GROUPID_PREF_NAME = "toolkit.telemetry.cachedProfileGroupID";
|
||||
const DEFAULT_THEME_ID = "default-theme@mozilla.org";
|
||||
const PROFILES_CREATED_PREF_NAME = "browser.profiles.created";
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
|
||||
ClientID: "resource://gre/modules/ClientID.sys.mjs",
|
||||
CryptoUtils: "resource://services-crypto/utils.sys.mjs",
|
||||
EveryWindow: "resource:///modules/EveryWindow.sys.mjs",
|
||||
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
|
||||
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
||||
setTimeout: "resource://gre/modules/Timer.sys.mjs",
|
||||
Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
|
||||
TelemetryUtils: "resource://gre/modules/TelemetryUtils.sys.mjs",
|
||||
});
|
||||
|
||||
|
@ -40,8 +40,14 @@ XPCOMUtils.defineLazyPreferenceGetter(
|
|||
() => SelectableProfileService.updateEnabledState()
|
||||
);
|
||||
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
lazy,
|
||||
"PROFILES_CREATED",
|
||||
PROFILES_CREATED_PREF_NAME,
|
||||
false
|
||||
);
|
||||
|
||||
const PROFILES_CRYPTO_SALT_LENGTH_BYTES = 16;
|
||||
const NOTIFY_TIMEOUT = 200;
|
||||
|
||||
const COMMAND_LINE_UPDATE = "profiles-updated";
|
||||
const COMMAND_LINE_ACTIVATE = "profiles-activate";
|
||||
|
@ -87,7 +93,6 @@ function loadImage(url) {
|
|||
class SelectableProfileServiceClass extends EventEmitter {
|
||||
#profileService = null;
|
||||
#connection = null;
|
||||
#asyncShutdownBlocker = null;
|
||||
#initialized = false;
|
||||
#groupToolkitProfile = null;
|
||||
#storeID = null;
|
||||
|
@ -102,10 +107,8 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
"star",
|
||||
];
|
||||
#initPromise = null;
|
||||
#notifyTask = null;
|
||||
#observedPrefs = null;
|
||||
#badge = null;
|
||||
static #dirSvc = null;
|
||||
#windowActivated = null;
|
||||
#isEnabled = false;
|
||||
|
||||
|
@ -142,11 +145,7 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
this.matchMediaObserver = this.matchMediaObserver.bind(this);
|
||||
this.prefObserver = (subject, topic, prefName) =>
|
||||
this.flushSharedPrefToDatabase(prefName);
|
||||
this.#profileService = Cc[
|
||||
"@mozilla.org/toolkit/profile-service;1"
|
||||
].getService(Ci.nsIToolkitProfileService);
|
||||
|
||||
this.#asyncShutdownBlocker = () => this.uninit();
|
||||
this.#observedPrefs = new Set();
|
||||
|
||||
this.#isEnabled = this.#getEnabledState();
|
||||
|
@ -158,15 +157,26 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
);
|
||||
}
|
||||
|
||||
// Migrate any early users who created profiles before the datastore service
|
||||
// was split out, and the PROFILES_CREATED pref replaced storeID as our check
|
||||
// for whether the profiles feature had been used.
|
||||
migrateToProfilesCreatedPref() {
|
||||
if (this.groupToolkitProfile?.storeID && !lazy.PROFILES_CREATED) {
|
||||
Services.prefs.setBoolPref(PROFILES_CREATED_PREF_NAME, true);
|
||||
}
|
||||
}
|
||||
|
||||
#getEnabledState() {
|
||||
if (!Services.policies.isAllowed("profileManagement")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.migrateToProfilesCreatedPref();
|
||||
|
||||
// If a storeID has been assigned then profiles may have been created so force us on. Also
|
||||
// covers the case when the selector is shown at startup and we don't have preferences
|
||||
// available.
|
||||
if (this.storeID) {
|
||||
if (this.groupToolkitProfile?.storeID) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -185,44 +195,6 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
return this.#isEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* For use in testing only, override the profile service with a mock version
|
||||
* and reset state accordingly.
|
||||
*
|
||||
* @param {Ci.nsIToolkitProfileService} profileService The mock profile service
|
||||
*/
|
||||
async resetProfileService(profileService) {
|
||||
if (!Cu.isInAutomation) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.uninit();
|
||||
this.#profileService =
|
||||
profileService ??
|
||||
Cc["@mozilla.org/toolkit/profile-service;1"].getService(
|
||||
Ci.nsIToolkitProfileService
|
||||
);
|
||||
await this.init();
|
||||
}
|
||||
|
||||
overrideDirectoryService(dirSvc) {
|
||||
if (!Cu.isInAutomation) {
|
||||
return;
|
||||
}
|
||||
|
||||
SelectableProfileServiceClass.#dirSvc = dirSvc;
|
||||
}
|
||||
|
||||
static getDirectory(id) {
|
||||
if (this.#dirSvc) {
|
||||
if (id in this.#dirSvc) {
|
||||
return this.#dirSvc[id].clone();
|
||||
}
|
||||
}
|
||||
|
||||
return Services.dirsvc.get(id, Ci.nsIFile);
|
||||
}
|
||||
|
||||
async #attemptFlushProfileService() {
|
||||
try {
|
||||
await this.#profileService.asyncFlush();
|
||||
|
@ -253,16 +225,8 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
return this.#initialized;
|
||||
}
|
||||
|
||||
static get PROFILE_GROUPS_DIR() {
|
||||
if (this.#dirSvc && "ProfileGroups" in this.#dirSvc) {
|
||||
return this.#dirSvc.ProfileGroups;
|
||||
}
|
||||
|
||||
return PathUtils.join(this.getDirectory("UAppData").path, "Profile Groups");
|
||||
}
|
||||
|
||||
async maybeCreateProfilesStorePath() {
|
||||
if (this.storeID) {
|
||||
async initProfilesData() {
|
||||
if (lazy.PROFILES_CREATED) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -270,29 +234,15 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
throw new Error("Cannot create a store without a group profile.");
|
||||
}
|
||||
|
||||
await IOUtils.makeDirectory(
|
||||
SelectableProfileServiceClass.PROFILE_GROUPS_DIR
|
||||
);
|
||||
Services.prefs.setBoolPref(PROFILES_CREATED_PREF_NAME, true);
|
||||
|
||||
const storageID = Services.uuid
|
||||
.generateUUID()
|
||||
.toString()
|
||||
.replace("{", "")
|
||||
.split("-")[0];
|
||||
this.#groupToolkitProfile.storeID = storageID;
|
||||
this.#storeID = storageID;
|
||||
let storeID = await ProfilesDatastoreService.storeID;
|
||||
|
||||
this.#groupToolkitProfile.storeID = storeID;
|
||||
this.#storeID = storeID;
|
||||
await this.#attemptFlushProfileService();
|
||||
}
|
||||
|
||||
async getProfilesStorePath() {
|
||||
await this.maybeCreateProfilesStorePath();
|
||||
|
||||
return PathUtils.join(
|
||||
SelectableProfileServiceClass.PROFILE_GROUPS_DIR,
|
||||
`${this.storeID}.sqlite`
|
||||
);
|
||||
}
|
||||
|
||||
onNimbusUpdate() {
|
||||
if (lazy.NimbusFeatures.selectableProfiles.getVariable("enabled")) {
|
||||
Services.prefs.setBoolPref(PROFILES_PREF_NAME, true);
|
||||
|
@ -303,11 +253,13 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
* At startup, store the nsToolkitProfile for the group.
|
||||
* Get the groupDBPath from the nsToolkitProfile, and connect to it.
|
||||
*
|
||||
* @param {boolean} isInitial true if this is an init prior to creating a new profile.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
init() {
|
||||
init(isInitial = false) {
|
||||
if (!this.#initPromise) {
|
||||
this.#initPromise = this.#init().finally(
|
||||
this.#initPromise = this.#init(isInitial).finally(
|
||||
() => (this.#initPromise = null)
|
||||
);
|
||||
}
|
||||
|
@ -315,95 +267,76 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
return this.#initPromise;
|
||||
}
|
||||
|
||||
async #init() {
|
||||
async #init(isInitial = false) {
|
||||
if (this.#initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
lazy.NimbusFeatures.selectableProfiles.onUpdate(this.onNimbusUpdate);
|
||||
|
||||
this.#profileService = ProfilesDatastoreService.toolkitProfileService;
|
||||
|
||||
this.#groupToolkitProfile =
|
||||
this.#profileService.currentProfile ?? this.#profileService.groupProfile;
|
||||
this.#storeID = this.#groupToolkitProfile?.storeID;
|
||||
|
||||
if (!this.storeID) {
|
||||
this.#storeID = Services.prefs.getCharPref(
|
||||
"toolkit.profiles.storeID",
|
||||
""
|
||||
);
|
||||
}
|
||||
this.#storeID = await ProfilesDatastoreService.storeID;
|
||||
|
||||
this.updateEnabledState();
|
||||
|
||||
if (!this.isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the storeID doesn't exist, we don't want to create the db until we
|
||||
// need to so we early return.
|
||||
if (!this.storeID) {
|
||||
if (!lazy.PROFILES_CREATED) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This could fail if we're adding it during shutdown. In this case,
|
||||
// don't throw but don't continue initialization.
|
||||
try {
|
||||
lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
|
||||
"SelectableProfileService uninit",
|
||||
this.#asyncShutdownBlocker
|
||||
);
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
this.#connection = await ProfilesDatastoreService.getConnection();
|
||||
if (!this.#connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#notifyTask = new DeferredTask(async () => {
|
||||
// Notify ourselves.
|
||||
await this.databaseChanged("local");
|
||||
// Notify other instances.
|
||||
await this.#notifyRunningInstances();
|
||||
}, NOTIFY_TIMEOUT);
|
||||
|
||||
try {
|
||||
await this.initConnection();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
// If this was an attempt to recover the storeID then reset it.
|
||||
if (!this.#groupToolkitProfile?.storeID) {
|
||||
Services.prefs.clearUserPref("toolkit.profiles.storeID");
|
||||
}
|
||||
|
||||
await this.uninit();
|
||||
return;
|
||||
}
|
||||
|
||||
// This can happen if profiles.ini has been reset by a version of Firefox
|
||||
// prior to 67 and the current profile is not the current default for the
|
||||
// group. We can recover by attempting to find the group profile from the
|
||||
// database.
|
||||
if (this.#groupToolkitProfile?.storeID != this.storeID) {
|
||||
await this.#restoreStoreID();
|
||||
|
||||
if (!this.#groupToolkitProfile) {
|
||||
// If we were unable to find a matching toolkit profile then assume the
|
||||
// store ID is bogus so clear it and uninit.
|
||||
Services.prefs.clearUserPref("toolkit.profiles.storeID");
|
||||
await this.uninit();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// When we launch into the startup window, the `ProfD` is not defined so
|
||||
// getting the directory will throw. Leaving the `currentProfile` as null
|
||||
// is fine for the startup window.
|
||||
// The current profile will be null now that we are eagerly initing the db.
|
||||
try {
|
||||
// Get the SelectableProfile by the profile directory
|
||||
this.#currentProfile = await this.getProfileByPath(
|
||||
SelectableProfileServiceClass.getDirectory("ProfD")
|
||||
ProfilesDatastoreService.constructor.getDirectory("ProfD")
|
||||
);
|
||||
} catch {}
|
||||
|
||||
// If this isn't the first init prior to creating the first new profile and
|
||||
// the app is started up we should have found a current profile.
|
||||
if (!isInitial && !Services.startup.startingUp && !this.#currentProfile) {
|
||||
let count = await this.getProfileCount();
|
||||
|
||||
if (count) {
|
||||
// There are other profiles, re-create the current profile.
|
||||
this.#currentProfile = await this.#createProfile(
|
||||
ProfilesDatastoreService.constructor.getDirectory("ProfD")
|
||||
);
|
||||
} else {
|
||||
// No other profiles. Reset our state.
|
||||
this.#groupToolkitProfile.storeID = null;
|
||||
this.#attemptFlushProfileService();
|
||||
Services.prefs.setBoolPref(PROFILES_CREATED_PREF_NAME, false);
|
||||
|
||||
this.#connection = null;
|
||||
this.updateEnabledState();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// This can happen if profiles.ini has been reset by a version of Firefox
|
||||
// prior to 67 and the current profile is not the current default for the
|
||||
// group. We can recover by overwriting this.#groupToolkitProfile.storeID
|
||||
// with the current storeID.
|
||||
if (this.#groupToolkitProfile.storeID != this.storeID) {
|
||||
this.#groupToolkitProfile.storeID = this.storeID;
|
||||
this.#attemptFlushProfileService();
|
||||
}
|
||||
|
||||
// On macOS when other applications request we open a url the most recent
|
||||
// window becomes activated first. This would cause the default profile to
|
||||
// change before we determine which profile to open the url in. By
|
||||
|
@ -430,6 +363,8 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
let prefersDarkQuery = window?.matchMedia("(prefers-color-scheme: dark)");
|
||||
prefersDarkQuery?.addEventListener("change", this.matchMediaObserver);
|
||||
|
||||
Services.obs.addObserver(this, "pds-datastore-changed");
|
||||
|
||||
this.#initialized = true;
|
||||
|
||||
// this.#currentProfile is unset in the case that the database has only just been created. We
|
||||
|
@ -445,41 +380,21 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
return;
|
||||
}
|
||||
|
||||
lazy.AsyncShutdown.profileChangeTeardown.removeBlocker(
|
||||
this.#asyncShutdownBlocker
|
||||
);
|
||||
|
||||
try {
|
||||
Services.obs.removeObserver(
|
||||
this.themeObserver,
|
||||
"lightweight-theme-styling-update"
|
||||
);
|
||||
Services.obs.removeObserver(this, "lightweight-theme-styling-update");
|
||||
} catch (e) {}
|
||||
|
||||
for (let prefName of this.#observedPrefs) {
|
||||
Services.prefs.removeObserver(prefName, this.prefObserver);
|
||||
}
|
||||
this.#observedPrefs.clear();
|
||||
|
||||
lazy.NimbusFeatures.selectableProfiles.offUpdate(this.onNimbusUpdate);
|
||||
|
||||
// During shutdown we don't need to notify ourselves, just other instances
|
||||
// so rather than finalizing the task just disarm it and do the notification
|
||||
// manually.
|
||||
if (this.#notifyTask.isArmed) {
|
||||
this.#notifyTask.disarm();
|
||||
await this.#notifyRunningInstances();
|
||||
}
|
||||
|
||||
await this.closeConnection();
|
||||
|
||||
this.#currentProfile = null;
|
||||
this.#groupToolkitProfile = null;
|
||||
this.#storeID = null;
|
||||
this.#badge = null;
|
||||
this.#connection = null;
|
||||
|
||||
lazy.EveryWindow.unregisterCallback(this.#everyWindowCallbackId);
|
||||
|
||||
Services.obs.removeObserver(this, "pds-datastore-changed");
|
||||
|
||||
this.#initialized = false;
|
||||
}
|
||||
|
||||
|
@ -524,60 +439,6 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
);
|
||||
}
|
||||
|
||||
async initConnection() {
|
||||
if (this.#connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
let path = await this.getProfilesStorePath();
|
||||
|
||||
// TODO: (Bug 1902320) Handle exceptions on connection opening
|
||||
// This could fail if the store is corrupted.
|
||||
this.#connection = await lazy.Sqlite.openConnection({
|
||||
path,
|
||||
openNotExclusive: true,
|
||||
});
|
||||
|
||||
await this.#connection.execute("PRAGMA journal_mode = WAL");
|
||||
await this.#connection.execute("PRAGMA wal_autocheckpoint = 16");
|
||||
|
||||
await this.createProfilesDBTables();
|
||||
}
|
||||
|
||||
async closeConnection() {
|
||||
if (!this.#connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
// An error could occur while closing the connection. We suppress the
|
||||
// error since it is not a critical part of the browser.
|
||||
try {
|
||||
await this.#connection.close();
|
||||
} catch (ex) {}
|
||||
this.#connection = null;
|
||||
}
|
||||
|
||||
async #restoreStoreID() {
|
||||
try {
|
||||
// Finds the first nsIToolkitProfile that matches the path of a
|
||||
// SelectableProfile in the database.
|
||||
for (let profile of await this.getAllProfiles()) {
|
||||
let groupProfile = this.#profileService.getProfileByDir(
|
||||
await profile.rootDir
|
||||
);
|
||||
|
||||
if (groupProfile && !groupProfile.storeID) {
|
||||
groupProfile.storeID = this.storeID;
|
||||
await this.#profileService.asyncFlush();
|
||||
this.#groupToolkitProfile = groupProfile;
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async handleEvent(event) {
|
||||
switch (event.type) {
|
||||
case "activate": {
|
||||
|
@ -596,97 +457,32 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes the value of a preference to the database.
|
||||
*
|
||||
* @param {string} prefName the name of the preference.
|
||||
*/
|
||||
async flushSharedPrefToDatabase(prefName) {
|
||||
if (!this.#observedPrefs.has(prefName)) {
|
||||
Services.prefs.addObserver(prefName, this.prefObserver);
|
||||
this.#observedPrefs.add(prefName);
|
||||
}
|
||||
|
||||
if (
|
||||
!SelectableProfileServiceClass.permanentSharedPrefs.includes(prefName) &&
|
||||
!Services.prefs.prefHasUserValue(prefName)
|
||||
) {
|
||||
await this.#deleteDBPref(prefName);
|
||||
return;
|
||||
}
|
||||
|
||||
let value;
|
||||
|
||||
switch (Services.prefs.getPrefType(prefName)) {
|
||||
case Ci.nsIPrefBranch.PREF_BOOL:
|
||||
value = Services.prefs.getBoolPref(prefName);
|
||||
observe(subject, topic, data) {
|
||||
switch (topic) {
|
||||
case "pds-datastore-changed": {
|
||||
this.databaseChanged(data);
|
||||
break;
|
||||
case Ci.nsIPrefBranch.PREF_INT:
|
||||
value = Services.prefs.getIntPref(prefName);
|
||||
break;
|
||||
case Ci.nsIPrefBranch.PREF_STRING:
|
||||
value = Services.prefs.getCharPref(prefName);
|
||||
}
|
||||
case "lightweight-theme-styling-update": {
|
||||
this.themeObserver(subject, topic);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await this.#setDBPref(prefName, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create tables for Selectable Profiles if they don't already exist
|
||||
*/
|
||||
async createProfilesDBTables() {
|
||||
// TODO: (Bug 1902320) Handle exceptions on connection opening
|
||||
await this.#connection.executeTransaction(async () => {
|
||||
const createProfilesTable = `
|
||||
CREATE TABLE IF NOT EXISTS "Profiles" (
|
||||
id INTEGER NOT NULL,
|
||||
path TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
avatar TEXT NOT NULL,
|
||||
themeId TEXT NOT NULL,
|
||||
themeFg TEXT NOT NULL,
|
||||
themeBg TEXT NOT NULL,
|
||||
PRIMARY KEY(id)
|
||||
);`;
|
||||
|
||||
await this.#connection.execute(createProfilesTable);
|
||||
|
||||
const createSharedPrefsTable = `
|
||||
CREATE TABLE IF NOT EXISTS "SharedPrefs" (
|
||||
id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
value BLOB,
|
||||
isBoolean INTEGER,
|
||||
PRIMARY KEY(id)
|
||||
);`;
|
||||
|
||||
await this.#connection.execute(createSharedPrefsTable);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the SQLite DB for the profile group.
|
||||
* Init shared prefs for the group and add to DB.
|
||||
* Create the Group DB path to aNamedProfile entry in profiles.ini.
|
||||
* Import aNamedProfile into DB.
|
||||
*/
|
||||
createProfileGroup() {}
|
||||
|
||||
/**
|
||||
* When the last selectable profile in a group is deleted,
|
||||
* also remove the profile group's named profile entry from profiles.ini
|
||||
* and vacuum the group DB.
|
||||
* and set the profiles created pref to false.
|
||||
*/
|
||||
async deleteProfileGroup() {
|
||||
if ((await this.getAllProfiles()).length) {
|
||||
return;
|
||||
}
|
||||
|
||||
Services.prefs.setBoolPref(PROFILES_CREATED_PREF_NAME, false);
|
||||
this.#groupToolkitProfile.storeID = null;
|
||||
this.#storeID = null;
|
||||
await this.#attemptFlushProfileService();
|
||||
await this.vacuumAndCloseGroupDB();
|
||||
}
|
||||
|
||||
// App session lifecycle methods and multi-process support
|
||||
|
@ -696,7 +492,8 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
* unit testing.
|
||||
*/
|
||||
execProcess(aArgs) {
|
||||
let executable = SelectableProfileServiceClass.getDirectory("XREExeF");
|
||||
let executable =
|
||||
ProfilesDatastoreService.constructor.getDirectory("XREExeF");
|
||||
|
||||
if (AppConstants.platform == "macosx") {
|
||||
// Use the application bundle if possible.
|
||||
|
@ -752,7 +549,7 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
let profiles = await this.getAllProfiles();
|
||||
for (let profile of profiles) {
|
||||
// The current profile was notified above.
|
||||
if (profile.id === this.#currentProfile?.id) {
|
||||
if (profile.id === this.currentProfile?.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -831,13 +628,22 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
* Invoked when changes have been made to the database. Sends the observer
|
||||
* notification "sps-profiles-updated" indicating that something has changed.
|
||||
*
|
||||
* @param {"local"|"remote"|"startup"} source The source of the notification.
|
||||
* Either "local" meaning that the change was made in this process, "remote"
|
||||
* meaning the change was made by a different Firefox instance or "startup"
|
||||
* meaning the application has just launched and we may need to reload
|
||||
* changes from the database.
|
||||
* @param {"local"|"remote"|"startup"|"shutdown"} source The source of the
|
||||
* notification. Either "local" meaning that the change was made in this
|
||||
* process, "remote" meaning the change was made by a different Firefox
|
||||
* instance, "startup" meaning the application has just launched and we may
|
||||
* need to reload changes from the database, or "shutdown" meaning we are
|
||||
* closing the connection and shutting down.
|
||||
*/
|
||||
async databaseChanged(source) {
|
||||
if (source === "local" || source === "shutdown") {
|
||||
this.#notifyRunningInstances();
|
||||
}
|
||||
|
||||
if (source === "shutdown") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (source != "local") {
|
||||
await this.loadSharedPrefsFromDatabase();
|
||||
}
|
||||
|
@ -950,6 +756,48 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
};
|
||||
}
|
||||
|
||||
async flushAllSharedPrefsToDatabase() {
|
||||
for (let prefName of SelectableProfileServiceClass.permanentSharedPrefs) {
|
||||
await this.flushSharedPrefToDatabase(prefName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes the value of a preference to the database.
|
||||
*
|
||||
* @param {string} prefName the name of the preference.
|
||||
*/
|
||||
async flushSharedPrefToDatabase(prefName) {
|
||||
if (!this.#observedPrefs.has(prefName)) {
|
||||
Services.prefs.addObserver(prefName, this.prefObserver);
|
||||
this.#observedPrefs.add(prefName);
|
||||
}
|
||||
|
||||
if (
|
||||
!SelectableProfileServiceClass.permanentSharedPrefs.includes(prefName) &&
|
||||
!Services.prefs.prefHasUserValue(prefName)
|
||||
) {
|
||||
await this.#deleteDBPref(prefName);
|
||||
return;
|
||||
}
|
||||
|
||||
let value;
|
||||
|
||||
switch (Services.prefs.getPrefType(prefName)) {
|
||||
case Ci.nsIPrefBranch.PREF_BOOL:
|
||||
value = Services.prefs.getBoolPref(prefName);
|
||||
break;
|
||||
case Ci.nsIPrefBranch.PREF_INT:
|
||||
value = Services.prefs.getIntPref(prefName);
|
||||
break;
|
||||
case Ci.nsIPrefBranch.PREF_STRING:
|
||||
value = Services.prefs.getCharPref(prefName);
|
||||
break;
|
||||
}
|
||||
|
||||
await this.#setDBPref(prefName, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all prefs from the DB and write to the current instance.
|
||||
*/
|
||||
|
@ -1057,7 +905,7 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
await Promise.all([
|
||||
IOUtils.makeDirectory(
|
||||
PathUtils.join(
|
||||
SelectableProfileServiceClass.getDirectory("DefProfRt").path,
|
||||
ProfilesDatastoreService.constructor.getDirectory("DefProfRt").path,
|
||||
profileDir
|
||||
),
|
||||
{
|
||||
|
@ -1066,7 +914,7 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
),
|
||||
IOUtils.makeDirectory(
|
||||
PathUtils.join(
|
||||
SelectableProfileServiceClass.getDirectory("DefProfLRt").path,
|
||||
ProfilesDatastoreService.constructor.getDirectory("DefProfLRt").path,
|
||||
profileDir
|
||||
),
|
||||
{
|
||||
|
@ -1077,7 +925,7 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
|
||||
return IOUtils.getDirectory(
|
||||
PathUtils.join(
|
||||
SelectableProfileServiceClass.getDirectory("DefProfRt").path,
|
||||
ProfilesDatastoreService.constructor.getDirectory("DefProfRt").path,
|
||||
profileDir
|
||||
)
|
||||
);
|
||||
|
@ -1137,6 +985,7 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
|
||||
// Preferences that must be set in newly created profiles.
|
||||
prefsJs.push(`user_pref("browser.profiles.enabled", true);`);
|
||||
prefsJs.push(`user_pref("browser.profiles.created", true);`);
|
||||
prefsJs.push(`user_pref("toolkit.profiles.storeID", "${this.storeID}");`);
|
||||
|
||||
await IOUtils.writeUTF8(prefsJsFilePath, prefsJs.join(LINEBREAK));
|
||||
|
@ -1151,7 +1000,7 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
*/
|
||||
getRelativeProfilePath(aProfilePath) {
|
||||
let relativePath = aProfilePath.getRelativePath(
|
||||
SelectableProfileServiceClass.getDirectory("UAppData")
|
||||
ProfilesDatastoreService.constructor.getDirectory("UAppData")
|
||||
);
|
||||
|
||||
if (AppConstants.platform === "win") {
|
||||
|
@ -1207,24 +1056,19 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
}
|
||||
|
||||
/**
|
||||
* If the user has never created a SelectableProfile before, the group
|
||||
* datastore will be created and the currently running toolkit profile will
|
||||
* be added to the datastore.
|
||||
* If the user has never created a SelectableProfile before, the currently
|
||||
* running toolkit profile will be added to the datastore and will finish
|
||||
* initing the service for profiles.
|
||||
*/
|
||||
async maybeSetupDataStore() {
|
||||
if (this.#connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the profiles db and set the storeID on the toolkit profile if it
|
||||
// doesn't exist so we can init the service.
|
||||
await this.maybeCreateProfilesStorePath();
|
||||
await this.init();
|
||||
await this.initProfilesData();
|
||||
await this.init(true);
|
||||
|
||||
// Flush our shared prefs into the database.
|
||||
for (let prefName of SelectableProfileServiceClass.permanentSharedPrefs) {
|
||||
await this.flushSharedPrefToDatabase(prefName);
|
||||
}
|
||||
await this.flushAllSharedPrefsToDatabase();
|
||||
|
||||
// If this is the first time the user has created a selectable profile,
|
||||
// add the current toolkit profile to the datastore.
|
||||
|
@ -1260,31 +1104,6 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and launch a new SelectableProfile and add it to the group datastore.
|
||||
* This is an unmanaged profile from the nsToolkitProfile perspective.
|
||||
*
|
||||
* If the user has never created a SelectableProfile before, the group
|
||||
* datastore will be lazily created and the currently running toolkit profile
|
||||
* will be added to the datastore along with the newly created profile.
|
||||
*
|
||||
* Launches the new SelectableProfile in a new instance after creating it.
|
||||
*
|
||||
* @param {boolean} [launchProfile=true] Whether or not this should launch
|
||||
* the newly created profile.
|
||||
*
|
||||
* @returns {SelectableProfile} The profile just created.
|
||||
*/
|
||||
async createNewProfile(launchProfile = true) {
|
||||
await this.maybeSetupDataStore();
|
||||
|
||||
let profile = await this.#createProfile();
|
||||
if (launchProfile) {
|
||||
this.launchInstance(profile, "about:newprofile");
|
||||
}
|
||||
return profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a profile to the profile group datastore.
|
||||
*
|
||||
|
@ -1316,7 +1135,7 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
profileData
|
||||
);
|
||||
|
||||
this.#notifyTask.arm();
|
||||
ProfilesDatastoreService.notify();
|
||||
|
||||
return this.getProfileByName(profileData.name);
|
||||
}
|
||||
|
@ -1342,7 +1161,7 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
id: aProfile.id,
|
||||
});
|
||||
|
||||
this.#notifyTask.arm();
|
||||
ProfilesDatastoreService.notify();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1407,7 +1226,32 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
this.#currentProfile = aSelectableProfile;
|
||||
}
|
||||
|
||||
this.#notifyTask.arm();
|
||||
ProfilesDatastoreService.notify();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and launch a new SelectableProfile and add it to the group datastore.
|
||||
* This is an unmanaged profile from the nsToolkitProfile perspective.
|
||||
*
|
||||
* If the user has never created a SelectableProfile before, the currently
|
||||
* running toolkit profile will be added to the datastore along with the
|
||||
* newly created profile.
|
||||
*
|
||||
* Launches the new SelectableProfile in a new instance after creating it.
|
||||
*
|
||||
* @param {boolean} [launchProfile=true] Whether or not this should launch
|
||||
* the newly created profile.
|
||||
*
|
||||
* @returns {SelectableProfile} The profile just created.
|
||||
*/
|
||||
async createNewProfile(launchProfile = true) {
|
||||
await this.maybeSetupDataStore();
|
||||
|
||||
let profile = await this.#createProfile();
|
||||
if (launchProfile) {
|
||||
this.launchInstance(profile, "about:newprofile");
|
||||
}
|
||||
return profile;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1423,7 +1267,7 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
|
||||
return (await this.#connection.executeCached("SELECT * FROM Profiles;"))
|
||||
.map(row => {
|
||||
return new SelectableProfile(row, this);
|
||||
return new SelectableProfile(row);
|
||||
})
|
||||
.sort((p1, p2) => p1.name.localeCompare(p2.name));
|
||||
}
|
||||
|
@ -1467,7 +1311,7 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
)
|
||||
)[0];
|
||||
|
||||
return row ? new SelectableProfile(row, this) : null;
|
||||
return row ? new SelectableProfile(row) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1491,7 +1335,7 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
)
|
||||
)[0];
|
||||
|
||||
return row ? new SelectableProfile(row, this) : null;
|
||||
return row ? new SelectableProfile(row) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1515,7 +1359,7 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
)
|
||||
)[0];
|
||||
|
||||
return row ? new SelectableProfile(row, this) : null;
|
||||
return row ? new SelectableProfile(row) : null;
|
||||
}
|
||||
|
||||
// Shared Prefs management
|
||||
|
@ -1585,7 +1429,7 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
}
|
||||
);
|
||||
|
||||
this.#notifyTask.arm();
|
||||
ProfilesDatastoreService.notify();
|
||||
}
|
||||
|
||||
// Starts tracking a new shared pref across the profiles.
|
||||
|
@ -1608,23 +1452,7 @@ class SelectableProfileServiceClass extends EventEmitter {
|
|||
}
|
||||
);
|
||||
|
||||
this.#notifyTask.arm();
|
||||
}
|
||||
|
||||
// DB lifecycle
|
||||
|
||||
/**
|
||||
* Create the SQLite DB for the profile group at groupDBPath.
|
||||
* Init shared prefs for the group and add to DB.
|
||||
*/
|
||||
createGroupDB() {}
|
||||
|
||||
/**
|
||||
* Vacuum the SQLite DB.
|
||||
*/
|
||||
async vacuumAndCloseGroupDB() {
|
||||
await this.#connection.execute("VACUUM;");
|
||||
await this.closeConnection();
|
||||
ProfilesDatastoreService.notify();
|
||||
}
|
||||
}
|
||||
|
||||
|
|