mirror of
https://github.com/zen-browser/desktop.git
synced 2026-08-01 12:48:59 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca0d12a9a1 | ||
|
|
8a7f4f9b6c | ||
|
|
0cc0ff0489 | ||
|
|
55fe27e77b | ||
|
|
40917eff21 | ||
|
|
0b56b81f1c | ||
|
|
45a40ee347 | ||
|
|
bb1bd9c722 | ||
|
|
282b649c01 | ||
|
|
ccd08e6cf3 | ||
|
|
fe6625cd32 | ||
|
|
b69b5c3cb7 | ||
|
|
7b8582bbd7 | ||
|
|
dbe471853c | ||
|
|
07989660ee | ||
|
|
ddad0cdc77 | ||
|
|
797ff70b25 | ||
|
|
0af54cad95 | ||
|
|
d2068cd814 | ||
|
|
e3d7aaa66d | ||
|
|
cd4c36732a | ||
|
|
c3e1160a54 | ||
|
|
c874635c7e |
@@ -34,8 +34,8 @@ Zen is a firefox-based browser with the aim of pushing your productivity to a ne
|
|||||||
|
|
||||||
### Firefox Versions
|
### Firefox Versions
|
||||||
|
|
||||||
- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `153.0`! 🚀
|
- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `153.0.1`! 🚀
|
||||||
- [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 153.0`!
|
- [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 153.0.1`!
|
||||||
|
|
||||||
### Contributing
|
### Contributing
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
b1d3fe8f65751e6d695fa9ef94b11ad81e70dac1
|
30959a45eaaf0bc499f3054793c76c6d7f0c8bc2
|
||||||
@@ -82,6 +82,7 @@ zen-workspace-creation-profile = Profile
|
|||||||
.tooltiptext = Profiles are used to separate cookies and site data between spaces.
|
.tooltiptext = Profiles are used to separate cookies and site data between spaces.
|
||||||
zen-workspace-creation-header = Create a Space
|
zen-workspace-creation-header = Create a Space
|
||||||
zen-workspace-creation-label = Spaces are used to organize your tabs and sessions.
|
zen-workspace-creation-label = Spaces are used to organize your tabs and sessions.
|
||||||
|
zen-workspace-default-profile = Default
|
||||||
|
|
||||||
zen-workspaces-delete-workspace-title = Delete Space?
|
zen-workspaces-delete-workspace-title = Delete Space?
|
||||||
zen-workspaces-delete-workspace-body = Are you sure you want to delete { $name }? This action cannot be undone.
|
zen-workspaces-delete-workspace-body = Are you sure you want to delete { $name }? This action cannot be undone.
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
# 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/.
|
|
||||||
|
|
||||||
- name: services.sync.engine.spaces
|
|
||||||
value: true
|
|
||||||
condition: "@IS_TWILIGHT@"
|
|
||||||
|
|
||||||
- name: services.sync.engine.spaces
|
|
||||||
value: false
|
|
||||||
locked: true
|
|
||||||
condition: "!@IS_TWILIGHT@"
|
|
||||||
@@ -32,6 +32,9 @@
|
|||||||
- name: zen.workspaces.scroll-modifier-key
|
- name: zen.workspaces.scroll-modifier-key
|
||||||
value: ctrl
|
value: ctrl
|
||||||
|
|
||||||
|
- name: services.sync.engine.workspaces
|
||||||
|
value: false
|
||||||
|
|
||||||
- name: zen.workspaces.separate-essentials
|
- name: zen.workspaces.separate-essentials
|
||||||
value: true
|
value: true
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
diff --git a/browser/components/preferences/config/account-sync.mjs b/browser/components/preferences/config/account-sync.mjs
|
|
||||||
index b503987a08e2ce64bfc01c4e3a21e5e19ef834f0..b1c03a0b219fd3eddd93c1deaeb18ab79c85f168 100644
|
|
||||||
--- a/browser/components/preferences/config/account-sync.mjs
|
|
||||||
+++ b/browser/components/preferences/config/account-sync.mjs
|
|
||||||
@@ -42,6 +42,7 @@ Preferences.addAll([
|
|
||||||
{ id: "services.sync.engine.creditcards", type: "bool" },
|
|
||||||
{ id: "services.sync.engine.addons", type: "bool" },
|
|
||||||
{ id: "services.sync.engine.prefs", type: "bool" },
|
|
||||||
+ { id: "services.sync.engine.spaces", type: "bool" },
|
|
||||||
]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
@@ -546,6 +547,7 @@ const SYNC_ENGINE_SETTINGS = [
|
|
||||||
type: "payments",
|
|
||||||
},
|
|
||||||
{ id: "syncAddons", pref: "services.sync.engine.addons", type: "addons" },
|
|
||||||
+ { id: "syncSpaces", pref: "services.sync.engine.spaces", type: "workspaces" },
|
|
||||||
{ id: "syncSettings", pref: "services.sync.engine.prefs", type: "settings" },
|
|
||||||
];
|
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ index 64aa0d98a0622c01f3dcfff1a04bfcda368354d2..2013e04b0881ad2295d6897b91e1573c
|
|||||||
{ id: "services.sync.engine.passwords", type: "bool" },
|
{ id: "services.sync.engine.passwords", type: "bool" },
|
||||||
{ id: "services.sync.engine.addresses", type: "bool" },
|
{ id: "services.sync.engine.addresses", type: "bool" },
|
||||||
{ id: "services.sync.engine.creditcards", type: "bool" },
|
{ id: "services.sync.engine.creditcards", type: "bool" },
|
||||||
+ { id: "services.sync.engine.spaces", type: "bool" },
|
+ { id: "services.sync.engine.workspaces", type: "bool" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let gSyncChooseWhatToSync = {
|
let gSyncChooseWhatToSync = {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ index a893c5ec3d007820d98f5d92dd039640faa2c181..9cbd00102e44ccf98b37845474d92d57
|
|||||||
+ <html:div class="sync-engine-workspaces">
|
+ <html:div class="sync-engine-workspaces">
|
||||||
+ <checkbox
|
+ <checkbox
|
||||||
+ data-l10n-id="sync-engine-workspaces"
|
+ data-l10n-id="sync-engine-workspaces"
|
||||||
+ preference="services.sync.engine.spaces"
|
+ preference="services.sync.engine.workspaces"
|
||||||
+ />
|
+ />
|
||||||
+ </html:div>
|
+ </html:div>
|
||||||
</html:div>
|
</html:div>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ index c379e1a5f82692406a92d9fcd3bca2769dfac5b2..af037dd3d995813d966524ac44a3795d
|
|||||||
<image class="sync-engine-image sync-engine-prefs" alt=""/>
|
<image class="sync-engine-image sync-engine-prefs" alt=""/>
|
||||||
<label data-l10n-id="sync-currently-syncing-settings"/>
|
<label data-l10n-id="sync-currently-syncing-settings"/>
|
||||||
</html:div>
|
</html:div>
|
||||||
+ <html:div engine_preference="services.sync.engine.spaces">
|
+ <html:div engine_preference="services.sync.engine.workspaces">
|
||||||
+ <image class="sync-engine-image sync-engine-workspaces" alt=""/>
|
+ <image class="sync-engine-image sync-engine-workspaces" alt=""/>
|
||||||
+ <label data-l10n-id="sync-currently-syncing-workspaces"/>
|
+ <label data-l10n-id="sync-currently-syncing-workspaces"/>
|
||||||
+ </html:div>
|
+ </html:div>
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ index c42b1a1a8df6b38886c17f71ea88e5aaa7eebc80..c741404ed973ee3ac246fe246c0f645e
|
|||||||
}
|
}
|
||||||
|
|
||||||
- if (replace) {
|
- if (replace) {
|
||||||
+ if (replace && !(!targetTab && this.selectedTab?.hasAttribute('zen-empty-tab'))) {
|
+ if (replace && !((targetTab || this.selectedTab)?.hasAttribute('zen-empty-tab'))) {
|
||||||
if (this.isTabGroupLabel(targetTab)) {
|
if (this.isTabGroupLabel(targetTab)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Replacing a tab group label with a tab is not supported"
|
"Replacing a tab group label with a tab is not supported"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp
|
diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp
|
||||||
index 328b7524810322561700347fa7f26403897635a8..58d33d68e050d11ea712413ad5c2170a112e404a 100644
|
index 73eb08f648d55121a0cd96cd808f932fb14bfee6..efa99861a5da12ad8d0daa2c0eb1473c57810397 100644
|
||||||
--- a/dom/base/Document.cpp
|
--- a/dom/base/Document.cpp
|
||||||
+++ b/dom/base/Document.cpp
|
+++ b/dom/base/Document.cpp
|
||||||
@@ -467,6 +467,7 @@
|
@@ -471,6 +471,7 @@
|
||||||
#include "prtime.h"
|
#include "prtime.h"
|
||||||
#include "prtypes.h"
|
#include "prtypes.h"
|
||||||
#include "xpcpublic.h"
|
#include "xpcpublic.h"
|
||||||
@@ -10,14 +10,15 @@ index 328b7524810322561700347fa7f26403897635a8..58d33d68e050d11ea712413ad5c2170a
|
|||||||
|
|
||||||
// clang-format off
|
// clang-format off
|
||||||
#include "mozilla/Encoding.h"
|
#include "mozilla/Encoding.h"
|
||||||
@@ -3198,6 +3199,10 @@ void Document::FillStyleSetUserAndUASheets() {
|
@@ -3164,6 +3165,11 @@ void Document::FillStyleSetUserAndUASheets() {
|
||||||
for (StyleSheet* sheet : *sheetService->UserStyleSheets()) {
|
}
|
||||||
styleSet.AppendStyleSheet(*sheet);
|
};
|
||||||
}
|
|
||||||
+ if (auto sheet = zen::ZenStyleSheetCache::Singleton()->GetModsSheet(); sheet && IsInChromeDocShell()) {
|
|
||||||
+ // The mods sheet is only used in the chrome docshell.
|
|
||||||
+ styleSet.AppendStyleSheet(*sheet);
|
|
||||||
+ }
|
|
||||||
|
|
||||||
StyleSheet* sheet = IsInChromeDocShell() ? cache->GetUserChromeSheet()
|
+ if (IsInChromeDocShell()) {
|
||||||
: cache->GetUserContentSheet();
|
+ // The mods sheet is only used in the chrome docshell.
|
||||||
|
+ MaybeAppend(zen::ZenStyleSheetCache::Singleton()->GetModsSheet());
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
MaybeAppend(IsInChromeDocShell() ? cache->GetUserChromeSheet()
|
||||||
|
: cache->GetUserContentSheet());
|
||||||
|
MaybeAppend(cache->GetUASheet());
|
||||||
|
|||||||
@@ -1,167 +0,0 @@
|
|||||||
diff --git a/gfx/webrender_bindings/WebRenderAPI.cpp b/gfx/webrender_bindings/WebRenderAPI.cpp
|
|
||||||
--- a/gfx/webrender_bindings/WebRenderAPI.cpp
|
|
||||||
+++ b/gfx/webrender_bindings/WebRenderAPI.cpp
|
|
||||||
@@ -298,11 +298,13 @@
|
|
||||||
panic_on_gl_error, picTileWidth, picTileHeight,
|
|
||||||
gfx::gfxVars::WebRenderRequiresHardwareDriver(),
|
|
||||||
StaticPrefs::gfx_webrender_low_quality_pinch_zoom_AtStartup(),
|
|
||||||
StaticPrefs::gfx_webrender_max_shared_surface_size_AtStartup(),
|
|
||||||
StaticPrefs::gfx_webrender_enable_subpixel_aa_AtStartup(),
|
|
||||||
- compositor->ShouldUseLayerCompositor())) {
|
|
||||||
+ compositor->ShouldUseLayerCompositor(),
|
|
||||||
+ StaticPrefs::
|
|
||||||
+ gfx_webrender_opaque_backdrop_fallback_AtStartup())) {
|
|
||||||
// wr_window_new puts a message into gfxCriticalNote if it returns
|
|
||||||
// false
|
|
||||||
MOZ_ASSERT(errorMessage);
|
|
||||||
error.AssignASCII(errorMessage);
|
|
||||||
wr_api_free_error_msg(errorMessage);
|
|
||||||
diff --git a/gfx/webrender_bindings/src/bindings.rs b/gfx/webrender_bindings/src/bindings.rs
|
|
||||||
--- a/gfx/webrender_bindings/src/bindings.rs
|
|
||||||
+++ b/gfx/webrender_bindings/src/bindings.rs
|
|
||||||
@@ -1998,10 +1998,11 @@
|
|
||||||
reject_software_rasterizer: bool,
|
|
||||||
low_quality_pinch_zoom: bool,
|
|
||||||
max_shared_surface_size: i32,
|
|
||||||
enable_subpixel_aa: bool,
|
|
||||||
use_layer_compositor: bool,
|
|
||||||
+ opaque_backdrop_fallback: bool,
|
|
||||||
) -> bool {
|
|
||||||
assert!(unsafe { is_in_render_thread() });
|
|
||||||
|
|
||||||
// Ensure the WR profiler callbacks are hooked up to the Gecko profiler.
|
|
||||||
set_profiler_hooks(Some(&PROFILER_HOOKS));
|
|
||||||
@@ -2164,10 +2165,11 @@
|
|
||||||
texture_cache_config,
|
|
||||||
reject_software_rasterizer,
|
|
||||||
low_quality_pinch_zoom,
|
|
||||||
max_shared_surface_size,
|
|
||||||
enable_dithering,
|
|
||||||
+ opaque_backdrop_fallback,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let window_size = DeviceIntSize::new(window_width, window_height);
|
|
||||||
let notifier = Box::new(CppNotifier { window_id });
|
|
||||||
diff --git a/gfx/wr/webrender/src/device/gl.rs b/gfx/wr/webrender/src/device/gl.rs
|
|
||||||
--- a/gfx/wr/webrender/src/device/gl.rs
|
|
||||||
+++ b/gfx/wr/webrender/src/device/gl.rs
|
|
||||||
@@ -3982,10 +3982,14 @@
|
|
||||||
|
|
||||||
pub fn disable_color_write(&self) {
|
|
||||||
self.gl.color_mask(false, false, false, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
+ pub fn set_color_mask(&self, r: bool, g: bool, b: bool, a: bool) {
|
|
||||||
+ self.gl.color_mask(r, g, b, a);
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
pub fn set_blend(&mut self, enable: bool) {
|
|
||||||
if enable {
|
|
||||||
self.gl.enable(gl::BLEND);
|
|
||||||
} else {
|
|
||||||
self.gl.disable(gl::BLEND);
|
|
||||||
diff --git a/gfx/wr/webrender/src/renderer/init.rs b/gfx/wr/webrender/src/renderer/init.rs
|
|
||||||
--- a/gfx/wr/webrender/src/renderer/init.rs
|
|
||||||
+++ b/gfx/wr/webrender/src/renderer/init.rs
|
|
||||||
@@ -204,6 +204,8 @@
|
|
||||||
pub low_quality_pinch_zoom: bool,
|
|
||||||
pub max_shared_surface_size: i32,
|
|
||||||
/// If true, open a debug socket to listen for remote debugger.
|
|
||||||
/// Relies on `debugger` cargo feature being enabled.
|
|
||||||
pub enable_debugger: bool,
|
|
||||||
+ /// See explanation of `gfx.webrender.opaque-backdrop-fallback`.
|
|
||||||
+ pub opaque_backdrop_fallback: bool,
|
|
||||||
}
|
|
||||||
@@ -277,9 +279,10 @@
|
|
||||||
enable_instancing: true,
|
|
||||||
reject_software_rasterizer: false,
|
|
||||||
low_quality_pinch_zoom: false,
|
|
||||||
max_shared_surface_size: 2048,
|
|
||||||
enable_debugger: true,
|
|
||||||
+ opaque_backdrop_fallback: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -802,10 +805,11 @@
|
|
||||||
allocated_native_surfaces: FastHashSet::default(),
|
|
||||||
debug_overlay_state: DebugOverlayState::new(),
|
|
||||||
buffer_damage_tracker: BufferDamageTracker::default(),
|
|
||||||
max_primitive_instance_count,
|
|
||||||
enable_instancing: options.enable_instancing,
|
|
||||||
+ opaque_backdrop_fallback: options.opaque_backdrop_fallback,
|
|
||||||
consecutive_oom_frames: 0,
|
|
||||||
target_frame_publish_id: None,
|
|
||||||
pending_result_msg: None,
|
|
||||||
layer_compositor_frame_state_in_prev_frame: None,
|
|
||||||
external_composite_debug_items: Vec::new(),
|
|
||||||
diff --git a/gfx/wr/webrender/src/renderer/mod.rs b/gfx/wr/webrender/src/renderer/mod.rs
|
|
||||||
--- a/gfx/wr/webrender/src/renderer/mod.rs
|
|
||||||
+++ b/gfx/wr/webrender/src/renderer/mod.rs
|
|
||||||
@@ -867,10 +867,12 @@
|
|
||||||
buffer_damage_tracker: BufferDamageTracker,
|
|
||||||
|
|
||||||
max_primitive_instance_count: usize,
|
|
||||||
enable_instancing: bool,
|
|
||||||
|
|
||||||
+ opaque_backdrop_fallback: bool,
|
|
||||||
+
|
|
||||||
/// Count consecutive oom frames to detectif we are stuck unable to render
|
|
||||||
/// in a loop.
|
|
||||||
consecutive_oom_frames: u32,
|
|
||||||
|
|
||||||
/// update() defers processing of ResultMsg, if frame_publish_id of
|
|
||||||
@@ -2787,18 +2789,29 @@
|
|
||||||
let read_target = ReadTarget::from_texture(cache_texture);
|
|
||||||
|
|
||||||
// Should always be drawing to picture cache tiles or off-screen surface!
|
|
||||||
debug_assert!(!draw_target.is_default());
|
|
||||||
let device_to_framebuffer = Scale::new(1i32);
|
|
||||||
+ let dest_fb_rect = dest * device_to_framebuffer;
|
|
||||||
|
|
||||||
self.device.blit_render_target(
|
|
||||||
read_target,
|
|
||||||
src * device_to_framebuffer,
|
|
||||||
draw_target,
|
|
||||||
- dest * device_to_framebuffer,
|
|
||||||
+ dest_fb_rect,
|
|
||||||
TextureFilter::Linear,
|
|
||||||
);
|
|
||||||
+
|
|
||||||
+ if self.opaque_backdrop_fallback {
|
|
||||||
+ self.device.set_color_mask(false, false, false, true);
|
|
||||||
+ self.device.clear_target(
|
|
||||||
+ Some([0.0, 0.0, 0.0, 1.0]),
|
|
||||||
+ None,
|
|
||||||
+ Some(dest_fb_rect),
|
|
||||||
+ );
|
|
||||||
+ self.device.set_color_mask(true, true, true, true);
|
|
||||||
+ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_picture_cache_target(
|
|
||||||
diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml
|
|
||||||
--- a/modules/libpref/init/StaticPrefList.yaml
|
|
||||||
+++ b/modules/libpref/init/StaticPrefList.yaml
|
|
||||||
@@ -8439,10 +8439,17 @@
|
|
||||||
#else
|
|
||||||
value: false
|
|
||||||
#endif
|
|
||||||
mirror: once
|
|
||||||
|
|
||||||
+# Make backdrop-filter treat its captured backdrop as if it had been
|
|
||||||
+# composited over an opaque-black background. (See bug 2036640)
|
|
||||||
+- name: gfx.webrender.opaque-backdrop-fallback
|
|
||||||
+ type: bool
|
|
||||||
+ value: true
|
|
||||||
+ mirror: once
|
|
||||||
+
|
|
||||||
# Disable wait of GPU execution completion
|
|
||||||
- name: gfx.webrender.wait-gpu-finished.disabled
|
|
||||||
type: bool
|
|
||||||
value: false
|
|
||||||
mirror: once
|
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
diff --git a/services/sync/modules/service.sys.mjs b/services/sync/modules/service.sys.mjs
|
|
||||||
index 4f4efe572909441abdd81909282936702c225621..81ab06a9414ed656b644a21b40b683f06a4f8dbf 100644
|
|
||||||
--- a/services/sync/modules/service.sys.mjs
|
|
||||||
+++ b/services/sync/modules/service.sys.mjs
|
|
||||||
@@ -99,6 +99,10 @@ function getEngineModules() {
|
|
||||||
whenTrue: "ExtensionStorageEngineKinto",
|
|
||||||
whenFalse: "ExtensionStorageEngineBridge",
|
|
||||||
};
|
|
||||||
+ result.Spaces = {
|
|
||||||
+ module: "resource:///modules/zen/ZenWorkspacesSync.sys.mjs",
|
|
||||||
+ symbol: "ZenWorkspacesEngine",
|
|
||||||
+ };
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
diff --git a/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs b/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs
|
|
||||||
index 5702ff28cc22206f5ce16584dac8a78d816562ce..3d08ecc97ce5995b30d8a4af0c33df329b428008 100644
|
|
||||||
--- a/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs
|
|
||||||
+++ b/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs
|
|
||||||
@@ -270,11 +270,29 @@ _ContextualIdentityService.prototype = {
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
- create(name, icon, color) {
|
|
||||||
+ create(name, icon, color, id = null) {
|
|
||||||
this.ensureDataReady();
|
|
||||||
|
|
||||||
- // Retrieve the next userContextId available.
|
|
||||||
- let userContextId = ++this._lastUserContextId;
|
|
||||||
+ if (id !== null) {
|
|
||||||
+ id = typeof id == "string" ? Number(id) : id;
|
|
||||||
+ if (!Number.isSafeInteger(id) || id <= 0) {
|
|
||||||
+ throw new Error(`Invalid userContextId '${id}'`);
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ let userContextId;
|
|
||||||
+ if (
|
|
||||||
+ id !== null &&
|
|
||||||
+ !this._identities.some(identity => identity.userContextId === id)
|
|
||||||
+ ) {
|
|
||||||
+ userContextId = id;
|
|
||||||
+ this._lastUserContextId = Math.max(
|
|
||||||
+ this._lastUserContextId,
|
|
||||||
+ userContextId
|
|
||||||
+ );
|
|
||||||
+ } else {
|
|
||||||
+ userContextId = ++this._lastUserContextId;
|
|
||||||
+ }
|
|
||||||
|
|
||||||
// Throw an error if the next userContextId available is invalid (the one associated to
|
|
||||||
// MAX_USER_CONTEXT_ID is already reserved to "userContextIdInternal.webextStorageLocal", which
|
|
||||||
22
src/toolkit/mozapps/extensions/Blocklist-sys-mjs.patch
Normal file
22
src/toolkit/mozapps/extensions/Blocklist-sys-mjs.patch
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
diff --git a/toolkit/mozapps/extensions/Blocklist.sys.mjs b/toolkit/mozapps/extensions/Blocklist.sys.mjs
|
||||||
|
index 4e1e0aac91612c846bb664efe3dbb50ecf63597d..93eb9a05520198eb3f9af35aa58d789719169434 100644
|
||||||
|
--- a/toolkit/mozapps/extensions/Blocklist.sys.mjs
|
||||||
|
+++ b/toolkit/mozapps/extensions/Blocklist.sys.mjs
|
||||||
|
@@ -411,7 +411,7 @@ class TargetAppFilter {
|
||||||
|
if (!Array.isArray(versionRange)) {
|
||||||
|
const { maxVersion = "*" } = versionRange;
|
||||||
|
const matchesRange =
|
||||||
|
- Services.vc.compare(lazy.gApp.version, maxVersion) <= 0;
|
||||||
|
+ Services.vc.compare(AppConstants.ZEN_FIREFOX_VERSION, maxVersion) <= 0;
|
||||||
|
return matchesRange ? entry : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -433,7 +433,7 @@ class TargetAppFilter {
|
||||||
|
const { maxVersion = "*" } = ta;
|
||||||
|
if (
|
||||||
|
guid == lazy.gAppID &&
|
||||||
|
- Services.vc.compare(lazy.gApp.version, maxVersion) <= 0
|
||||||
|
+ Services.vc.compare(AppConstants.ZEN_FIREFOX_VERSION, maxVersion) <= 0
|
||||||
|
) {
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
@@ -64,11 +64,11 @@ export class nsZenBoostEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the ZenBoosts JSWindowActor child for the currently selected tab.
|
* Returns the parent Boost JSActor for the currently selected tab.
|
||||||
*
|
*
|
||||||
* @returns {ZenBoostsChild} zenBoostsChild Boost JSActor child
|
* @returns {ZenBoostsParent} Boost JSActor parent
|
||||||
*/
|
*/
|
||||||
get zenBoostsChild() {
|
get zenBoostsParent() {
|
||||||
const linkedBrowser = this.openerWindow.gBrowser.selectedTab.linkedBrowser;
|
const linkedBrowser = this.openerWindow.gBrowser.selectedTab.linkedBrowser;
|
||||||
const actor =
|
const actor =
|
||||||
linkedBrowser.browsingContext.currentWindowGlobal.getActor("ZenBoosts");
|
linkedBrowser.browsingContext.currentWindowGlobal.getActor("ZenBoosts");
|
||||||
@@ -466,13 +466,13 @@ export class nsZenBoostEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onZapButtonPressed() {
|
async onZapButtonPressed() {
|
||||||
this.zenBoostsChild.sendQuery("ZenBoost:ToggleZapMode");
|
this.zenBoostsParent.sendQuery("ZenBoost:ToggleZapMode");
|
||||||
// Focus the parent browser window
|
// Focus the parent browser window
|
||||||
this.openerWindow.focus();
|
this.openerWindow.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
async onPickerButtonPressed() {
|
async onPickerButtonPressed() {
|
||||||
this.zenBoostsChild.sendQuery("ZenBoost:TogglePickerMode");
|
this.zenBoostsParent.sendQuery("ZenBoost:TogglePickerMode");
|
||||||
this.openerWindow.focus();
|
this.openerWindow.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,11 +497,11 @@ ${cssSelector} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onInspectorButtonPressed() {
|
onInspectorButtonPressed() {
|
||||||
this.zenBoostsChild.sendQuery("ZenBoost:OpenInspector");
|
this.zenBoostsParent.sendQuery("ZenBoost:OpenInspector");
|
||||||
}
|
}
|
||||||
|
|
||||||
async onUpdateZapButtonVisual() {
|
async onUpdateZapButtonVisual() {
|
||||||
const actor = this.zenBoostsChild;
|
const actor = this.zenBoostsParent;
|
||||||
const zapButton = this.doc.getElementById("zen-boost-zap");
|
const zapButton = this.doc.getElementById("zen-boost-zap");
|
||||||
|
|
||||||
const zapEnabled = await actor.sendQuery("ZenBoost:ZapModeEnabled");
|
const zapEnabled = await actor.sendQuery("ZenBoost:ZapModeEnabled");
|
||||||
@@ -513,7 +513,7 @@ ${cssSelector} {
|
|||||||
|
|
||||||
async onUpdatePickerButtonVisual() {
|
async onUpdatePickerButtonVisual() {
|
||||||
const pickerButton = this.doc.getElementById("zen-boost-css-picker");
|
const pickerButton = this.doc.getElementById("zen-boost-css-picker");
|
||||||
const selectEnabled = await this.zenBoostsChild.sendQuery(
|
const selectEnabled = await this.zenBoostsParent.sendQuery(
|
||||||
"ZenBoost:SelectorPickerModeEnabled"
|
"ZenBoost:SelectorPickerModeEnabled"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -660,7 +660,7 @@ ${cssSelector} {
|
|||||||
/**
|
/**
|
||||||
* Handles the size toggle button press, cycling through size override options
|
* Handles the size toggle button press, cycling through size override options
|
||||||
*/
|
*/
|
||||||
async onBoostSizePressed() {
|
onBoostSizePressed() {
|
||||||
if (this.currentBoostData.sizeOverride == 1) {
|
if (this.currentBoostData.sizeOverride == 1) {
|
||||||
this.currentBoostData.sizeOverride = 1.1;
|
this.currentBoostData.sizeOverride = 1.1;
|
||||||
} else if (this.currentBoostData.sizeOverride == 1.1) {
|
} else if (this.currentBoostData.sizeOverride == 1.1) {
|
||||||
@@ -671,7 +671,7 @@ ${cssSelector} {
|
|||||||
this.currentBoostData.sizeOverride = 0.9;
|
this.currentBoostData.sizeOverride = 0.9;
|
||||||
} else if (this.currentBoostData.sizeOverride == 0.9) {
|
} else if (this.currentBoostData.sizeOverride == 0.9) {
|
||||||
this.currentBoostData.sizeOverride = 1;
|
this.currentBoostData.sizeOverride = 1;
|
||||||
await this.zenBoostsChild.updateBoostSizeOverride(
|
this.zenBoostsParent.updateBoostSizeOverride(
|
||||||
this.currentBoostData.sizeOverride,
|
this.currentBoostData.sizeOverride,
|
||||||
/* disable: */ true
|
/* disable: */ true
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export class ZenBoostsParent extends JSWindowActorParent {
|
|||||||
(sizeOverride !== 1 || disable)
|
(sizeOverride !== 1 || disable)
|
||||||
) {
|
) {
|
||||||
const fullZoom = this.browsingContext.topChromeWindow.FullZoom;
|
const fullZoom = this.browsingContext.topChromeWindow.FullZoom;
|
||||||
fullZoom.setZoom(sizeOverride);
|
fullZoom.setZoom(sizeOverride, this.browsingContext.top.embedderElement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -353,3 +353,11 @@ body,
|
|||||||
:root[zen-right-side="true"] #zen-appcontent-navbar-wrapper #PanelUI-button {
|
:root[zen-right-side="true"] #zen-appcontent-navbar-wrapper #PanelUI-button {
|
||||||
order: 2;
|
order: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* See #14409: firefox 150's body paint containment prevents the right side compact
|
||||||
|
sidebar from remaining active at the screen edge in macOS fullscreen */
|
||||||
|
@media (-moz-platform: macos) {
|
||||||
|
:root[macOSNativeFullscreen][zen-compact-mode][zen-right-side] body {
|
||||||
|
contain: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -712,7 +712,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Hide the results body when there are no results, to avoid showing an empty box */
|
/* Hide the results body when there are no results, to avoid showing an empty box */
|
||||||
.urlbarView[noresults] > .urlbarView-body-outer > .urlbarView-body-inner,
|
.urlbar[noresults] .urlbarView > .urlbarView-body-outer > .urlbarView-body-inner,
|
||||||
#urlbar-search-mode-indicator-close {
|
#urlbar-search-mode-indicator-close {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,7 +190,7 @@
|
|||||||
--urlbar-box-active-bgcolor: var(--toolbarbutton-hover-background) !important;
|
--urlbar-box-active-bgcolor: var(--toolbarbutton-hover-background) !important;
|
||||||
--zen-input-border-color: light-dark(rgb(204, 204, 204), rgb(66, 65, 77));
|
--zen-input-border-color: light-dark(rgb(204, 204, 204), rgb(66, 65, 77));
|
||||||
--urlbar-box-hover-bgcolor: var(--toolbarbutton-hover-background) !important;
|
--urlbar-box-hover-bgcolor: var(--toolbarbutton-hover-background) !important;
|
||||||
--input-bgcolor: light-dark(rgba(255, 255, 255, 0.2), rgba(0, 0, 0, 0.2)) !important;
|
--input-text-background-color: light-dark(rgba(255, 255, 255, 0.2), rgba(0, 0, 0, 0.2)) !important;
|
||||||
--toolbar-field-focus-background-color: light-dark(
|
--toolbar-field-focus-background-color: light-dark(
|
||||||
rgba(255, 255, 255, 0.25),
|
rgba(255, 255, 255, 0.25),
|
||||||
rgba(0, 0, 0, 0.3)
|
rgba(0, 0, 0, 0.3)
|
||||||
|
|||||||
@@ -977,6 +977,14 @@ window.gZenCompactModeManager = {
|
|||||||
!gZenGlanceManager._animating &&
|
!gZenGlanceManager._animating &&
|
||||||
!this._nextTimeWillBeActive
|
!this._nextTimeWillBeActive
|
||||||
) {
|
) {
|
||||||
|
const isTabRoutedToCurrentSpace =
|
||||||
|
beforeRouteResult.targetRoute === gZenWorkspaces.activeWorkspace;
|
||||||
|
// Do not show the toast if the sidebar is not hidden and
|
||||||
|
// the tab is being routed to the current space
|
||||||
|
if (!isSidebarHidden && isTabRoutedToCurrentSpace) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let messageId = "zen-background-tab-opened-toast";
|
let messageId = "zen-background-tab-opened-toast";
|
||||||
let toastOptions = {
|
let toastOptions = {
|
||||||
button: {
|
button: {
|
||||||
@@ -988,7 +996,7 @@ window.gZenCompactModeManager = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (beforeRouteResult.isRouteFound) {
|
if (beforeRouteResult.isRouteFound && !isTabRoutedToCurrentSpace) {
|
||||||
messageId = "zen-space-routing-tab-routed-toast";
|
messageId = "zen-space-routing-tab-routed-toast";
|
||||||
toastOptions = {
|
toastOptions = {
|
||||||
l10nArgs: { targetWorkspace: beforeRouteResult.targetWorkspaceName },
|
l10nArgs: { targetWorkspace: beforeRouteResult.targetWorkspaceName },
|
||||||
|
|||||||
@@ -654,6 +654,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#shouldSwitchSpace(event) {
|
#shouldSwitchSpace(event) {
|
||||||
|
if (document.documentElement.hasAttribute("customizing")) {
|
||||||
|
return { isNearLeftEdge: false, isNearRightEdge: false };
|
||||||
|
}
|
||||||
|
|
||||||
const padding = Services.prefs.getIntPref(
|
const padding = Services.prefs.getIntPref(
|
||||||
"zen.workspaces.dnd-switch-padding"
|
"zen.workspaces.dnd-switch-padding"
|
||||||
);
|
);
|
||||||
@@ -722,10 +726,11 @@
|
|||||||
|
|
||||||
#handle_sidebarDragOver(event) {
|
#handle_sidebarDragOver(event) {
|
||||||
const dt = event.dataTransfer;
|
const dt = event.dataTransfer;
|
||||||
|
const isTabDrag = dt.mozTypesAt(0)[0] === TAB_DROP_TYPE;
|
||||||
|
|
||||||
const { isNearLeftEdge, isNearRightEdge } =
|
const { isNearLeftEdge, isNearRightEdge } =
|
||||||
this.#shouldSwitchSpace(event);
|
this.#shouldSwitchSpace(event);
|
||||||
if (isNearLeftEdge || isNearRightEdge) {
|
if (isTabDrag && (isNearLeftEdge || isNearRightEdge)) {
|
||||||
if (!this.#changeSpaceTimer && !this.#isOutOfWindow) {
|
if (!this.#changeSpaceTimer && !this.#isOutOfWindow) {
|
||||||
this.#changeSpaceTimer = setTimeout(() => {
|
this.#changeSpaceTimer = setTimeout(() => {
|
||||||
this.clearDragOverVisuals();
|
this.clearDragOverVisuals();
|
||||||
@@ -749,6 +754,10 @@
|
|||||||
|
|
||||||
handle_spaceIconDragOver(event) {
|
handle_spaceIconDragOver(event) {
|
||||||
const dt = event.dataTransfer;
|
const dt = event.dataTransfer;
|
||||||
|
if (dt.mozTypesAt(0)[0] !== TAB_DROP_TYPE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
|
const draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
|
||||||
if (draggedTab.hasAttribute("zen-essential")) {
|
if (draggedTab.hasAttribute("zen-essential")) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -20,5 +20,4 @@ DIRS += [
|
|||||||
"share",
|
"share",
|
||||||
"spaces",
|
"spaces",
|
||||||
"space-routing",
|
"space-routing",
|
||||||
"sync",
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ const lazy = {};
|
|||||||
ChromeUtils.defineESModuleGetters(lazy, {
|
ChromeUtils.defineESModuleGetters(lazy, {
|
||||||
ZenLiveFoldersManager:
|
ZenLiveFoldersManager:
|
||||||
"resource:///modules/zen/ZenLiveFoldersManager.sys.mjs",
|
"resource:///modules/zen/ZenLiveFoldersManager.sys.mjs",
|
||||||
ZenSyncStore: "resource:///modules/zen/ZenSyncManager.sys.mjs",
|
|
||||||
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
||||||
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
|
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
|
||||||
SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs",
|
SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs",
|
||||||
@@ -610,7 +609,6 @@ export class nsZenSessionManager {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.#collectWindowData(windows);
|
this.#collectWindowData(windows);
|
||||||
lazy.ZenSyncStore.notifyAboutChanges();
|
|
||||||
// This would save the data to disk asynchronously or when quitting the app.
|
// This would save the data to disk asynchronously or when quitting the app.
|
||||||
let sidebar = this.#sidebarWithoutCloning;
|
let sidebar = this.#sidebarWithoutCloning;
|
||||||
this.#file.data = sidebar;
|
this.#file.data = sidebar;
|
||||||
@@ -624,15 +622,6 @@ export class nsZenSessionManager {
|
|||||||
this.log(`Saving Zen session data with ${sidebar.tabs?.length || 0} tabs`);
|
this.log(`Saving Zen session data with ${sidebar.tabs?.length || 0} tabs`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the sidebar data object.
|
|
||||||
*
|
|
||||||
* @returns {object}
|
|
||||||
*/
|
|
||||||
getSidebarData() {
|
|
||||||
return this.#sidebarWithoutCloning;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the last known backup should be deleted and a new one
|
* Called when the last known backup should be deleted and a new one
|
||||||
* created. This uses the #deferredBackupTask to debounce clusters of
|
* created. This uses the #deferredBackupTask to debounce clusters of
|
||||||
|
|||||||
@@ -769,8 +769,8 @@ class nsZenWindowSync {
|
|||||||
const otherBrowser = aTab.linkedBrowser;
|
const otherBrowser = aTab.linkedBrowser;
|
||||||
|
|
||||||
// We aren't closing the other tab so, we also need to swap its tablisteners.
|
// We aren't closing the other tab so, we also need to swap its tablisteners.
|
||||||
let filter = otherTabBrowser._tabFilters.get(aTab);
|
let filter = otherTabBrowser._getTabProgressFilter(aTab);
|
||||||
let tabListener = otherTabBrowser._tabListeners.get(aTab);
|
let tabListener = otherTabBrowser._getTabProgressListener(aTab);
|
||||||
try {
|
try {
|
||||||
otherBrowser.webProgress.removeProgressListener(filter);
|
otherBrowser.webProgress.removeProgressListener(filter);
|
||||||
filter.removeProgressListener(tabListener);
|
filter.removeProgressListener(tabListener);
|
||||||
@@ -792,7 +792,7 @@ class nsZenWindowSync {
|
|||||||
true,
|
true,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
otherTabBrowser._tabListeners.set(aTab, tabListener);
|
otherTabBrowser._setTabProgressListener(aTab, tabListener);
|
||||||
|
|
||||||
const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL;
|
const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL;
|
||||||
filter.addProgressListener(tabListener, notifyAll);
|
filter.addProgressListener(tabListener, notifyAll);
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* 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/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
const lazy = {};
|
||||||
|
|
||||||
|
ChromeUtils.defineLazyGetter(lazy, "l10n", () => {
|
||||||
|
return new Localization(["browser/zen-workspaces.ftl"], true);
|
||||||
|
});
|
||||||
|
|
||||||
class nsZenWorkspaceCreation extends MozXULElement {
|
class nsZenWorkspaceCreation extends MozXULElement {
|
||||||
#wasInCollapsedMode = false;
|
#wasInCollapsedMode = false;
|
||||||
|
|
||||||
@@ -192,7 +198,7 @@ class nsZenWorkspaceCreation extends MozXULElement {
|
|||||||
|
|
||||||
this.currentProfile = {
|
this.currentProfile = {
|
||||||
id: 0,
|
id: 0,
|
||||||
name: ContextualIdentityService.formatContextLabel("user-context-none"),
|
name: lazy.l10n.formatValueSync("zen-workspace-default-profile"),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
this.inputProfile.parentNode.hidden = true;
|
this.inputProfile.parentNode.hidden = true;
|
||||||
@@ -297,10 +303,19 @@ class nsZenWorkspaceCreation extends MozXULElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onProfilePopupShowing(event) {
|
onProfilePopupShowing(event) {
|
||||||
return window.createUserContextMenu(event, {
|
window.createUserContextMenu(event, {
|
||||||
isContextMenu: true,
|
isContextMenu: true,
|
||||||
showDefaultTab: true,
|
showDefaultTab: true,
|
||||||
|
showManageContainers: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const defaultItem = event.target.querySelector('[data-usercontextid="0"]');
|
||||||
|
if (defaultItem) {
|
||||||
|
defaultItem.removeAttribute("data-l10n-id");
|
||||||
|
defaultItem.label = lazy.l10n.formatValueSync(
|
||||||
|
"zen-workspace-default-profile"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onProfilePopupCommand(event) {
|
onProfilePopupCommand(event) {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ const lazy = {};
|
|||||||
|
|
||||||
ChromeUtils.defineESModuleGetters(lazy, {
|
ChromeUtils.defineESModuleGetters(lazy, {
|
||||||
ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
|
ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
|
||||||
ZenSyncStore: "resource:///modules/zen/ZenSyncManager.sys.mjs",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ChromeUtils.defineLazyGetter(lazy, "browserBackgroundElement", () => {
|
ChromeUtils.defineLazyGetter(lazy, "browserBackgroundElement", () => {
|
||||||
@@ -22,6 +21,10 @@ ChromeUtils.defineLazyGetter(lazy, "toolbarBackgroundElement", () => {
|
|||||||
return document.getElementById("zen-toolbar-background");
|
return document.getElementById("zen-toolbar-background");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ChromeUtils.defineLazyGetter(lazy, "l10n", () => {
|
||||||
|
return new Localization(["browser/zen-workspaces.ftl"], true);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zen Spaces manager. This class is mainly responsible for the UI
|
* Zen Spaces manager. This class is mainly responsible for the UI
|
||||||
* and user interactions but it also contains some logic to manage
|
* and user interactions but it also contains some logic to manage
|
||||||
@@ -149,7 +152,7 @@ class nsZenWorkspaces {
|
|||||||
if (!this.privateWindowOrDisabled) {
|
if (!this.privateWindowOrDisabled) {
|
||||||
const observerFunction = async () => {
|
const observerFunction = async () => {
|
||||||
delete this._workspaceBookmarksCache;
|
delete this._workspaceBookmarksCache;
|
||||||
await this.#initializeWorkspaceBookmarks();
|
await this.workspaceBookmarks();
|
||||||
this._invalidateBookmarkContainers();
|
this._invalidateBookmarkContainers();
|
||||||
};
|
};
|
||||||
Services.obs.addObserver(observerFunction, "workspace-bookmarks-updated");
|
Services.obs.addObserver(observerFunction, "workspace-bookmarks-updated");
|
||||||
@@ -169,63 +172,6 @@ class nsZenWorkspaces {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies live sync changes: updates workspace cache, removes deleted items,
|
|
||||||
* then creates/updates pulled items.
|
|
||||||
*
|
|
||||||
* @param {{ spaces: Array}} pulled Reconcile-pulled items.
|
|
||||||
* @param {{ spaces: Array}} removals Items to remove.
|
|
||||||
*/
|
|
||||||
async _applySyncChanges(pulled, removals = {}) {
|
|
||||||
if (!this.shouldHaveWorkspaces || this.privateWindowOrDisabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.promiseInitialized;
|
|
||||||
|
|
||||||
// 1. Update workspace cache (remove deleted, merge pulled)
|
|
||||||
const removedSpaceIds = new Set((removals.spaces || []).map(s => s.uuid));
|
|
||||||
if (removedSpaceIds.size || pulled.spaces?.length) {
|
|
||||||
const localMap = new Map(
|
|
||||||
this.getWorkspaces()
|
|
||||||
.filter(w => !removedSpaceIds.has(w.uuid))
|
|
||||||
.map(w => [w.uuid, w])
|
|
||||||
);
|
|
||||||
for (const space of pulled.spaces || []) {
|
|
||||||
if (!space?.uuid) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const existing = localMap.get(space.uuid);
|
|
||||||
localMap.set(space.uuid, existing ? { ...existing, ...space } : space);
|
|
||||||
}
|
|
||||||
await this.propagateWorkspaces(
|
|
||||||
this.#getOrderedWorkspacesByPosition(Array.from(localMap.values()))
|
|
||||||
);
|
|
||||||
this.#propagateWorkspaceData();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#getOrderedWorkspacesByPosition(workspaces) {
|
|
||||||
return [...workspaces]
|
|
||||||
.map((workspace, index) => ({ workspace, index }))
|
|
||||||
.sort((a, b) => {
|
|
||||||
const aPosition =
|
|
||||||
typeof a.workspace.position === "number"
|
|
||||||
? a.workspace.position
|
|
||||||
: a.index;
|
|
||||||
const bPosition =
|
|
||||||
typeof b.workspace.position === "number"
|
|
||||||
? b.workspace.position
|
|
||||||
: b.index;
|
|
||||||
return aPosition - bPosition || a.index - b.index;
|
|
||||||
})
|
|
||||||
.map(({ workspace }) => {
|
|
||||||
// strip the position property that comes from pulled workspaces
|
|
||||||
const rest = { ...workspace };
|
|
||||||
delete rest.position;
|
|
||||||
return rest;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#afterLoadInit() {
|
#afterLoadInit() {
|
||||||
const onResize = (...args) => {
|
const onResize = (...args) => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
@@ -752,17 +698,17 @@ class nsZenWorkspaces {
|
|||||||
return spacesForSS;
|
return spacesForSS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async #initializeWorkspaceBookmarks() {
|
async workspaceBookmarks() {
|
||||||
if (this.privateWindowOrDisabled) {
|
if (this.privateWindowOrDisabled) {
|
||||||
this._workspaceBookmarksCache = {
|
this._workspaceBookmarksCache = {
|
||||||
bookmarks: [],
|
bookmarks: [],
|
||||||
lastChangeTimestamp: 0,
|
lastChangeTimestamp: 0,
|
||||||
};
|
};
|
||||||
return;
|
return this._workspaceBookmarksCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._workspaceBookmarksCache) {
|
if (this._workspaceBookmarksCache) {
|
||||||
return;
|
return this._workspaceBookmarksCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [bookmarks, lastChangeTimestamp] = await Promise.all([
|
const [bookmarks, lastChangeTimestamp] = await Promise.all([
|
||||||
@@ -771,6 +717,8 @@ class nsZenWorkspaces {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
this._workspaceBookmarksCache = { bookmarks, lastChangeTimestamp };
|
this._workspaceBookmarksCache = { bookmarks, lastChangeTimestamp };
|
||||||
|
|
||||||
|
return this._workspaceCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreWorkspacesFromSessionStore(aWinData = {}) {
|
restoreWorkspacesFromSessionStore(aWinData = {}) {
|
||||||
@@ -830,7 +778,7 @@ class nsZenWorkspaces {
|
|||||||
return (async () => {
|
return (async () => {
|
||||||
await this.#waitForPromises();
|
await this.#waitForPromises();
|
||||||
this.#afterLoadInit();
|
this.#afterLoadInit();
|
||||||
await this.#initializeWorkspaceBookmarks();
|
await this.workspaceBookmarks();
|
||||||
await this.changeWorkspace(activeWorkspace, { onInit: true });
|
await this.changeWorkspace(activeWorkspace, { onInit: true });
|
||||||
this.#fixTabPositions();
|
this.#fixTabPositions();
|
||||||
this.onWindowResize();
|
this.onWindowResize();
|
||||||
@@ -856,13 +804,6 @@ class nsZenWorkspaces {
|
|||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
#markWorkspaceChanged(workspaceId) {
|
|
||||||
lazy.ZenSyncStore.markItemChanged({
|
|
||||||
type: "space",
|
|
||||||
id: workspaceId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async selectStartPage() {
|
async selectStartPage() {
|
||||||
if (!this.workspaceEnabled || gZenUIManager.testingEnabled) {
|
if (!this.workspaceEnabled || gZenUIManager.testingEnabled) {
|
||||||
return;
|
return;
|
||||||
@@ -1263,11 +1204,19 @@ class nsZenWorkspaces {
|
|||||||
workspace = this.getActiveWorkspaceFromCache();
|
workspace = this.getActiveWorkspaceFromCache();
|
||||||
}
|
}
|
||||||
let containerTabId = workspace.containerTabId;
|
let containerTabId = workspace.containerTabId;
|
||||||
return window.createUserContextMenu(event, {
|
window.createUserContextMenu(event, {
|
||||||
isContextMenu: true,
|
isContextMenu: true,
|
||||||
excludeUserContextId: containerTabId,
|
excludeUserContextId: containerTabId,
|
||||||
showDefaultTab: true,
|
showDefaultTab: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const defaultItem = event.target.querySelector('[data-usercontextid="0"]');
|
||||||
|
if (defaultItem) {
|
||||||
|
defaultItem.removeAttribute("data-l10n-id");
|
||||||
|
defaultItem.label = lazy.l10n.formatValueSync(
|
||||||
|
"zen-workspace-default-profile"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveWorkspace(workspaceData) {
|
saveWorkspace(workspaceData) {
|
||||||
@@ -1283,19 +1232,12 @@ class nsZenWorkspaces {
|
|||||||
} else {
|
} else {
|
||||||
workspacesData.push(workspaceData);
|
workspacesData.push(workspaceData);
|
||||||
}
|
}
|
||||||
// mark item as changed for sync
|
|
||||||
this.#markWorkspaceChanged(workspaceData.uuid);
|
|
||||||
|
|
||||||
this.#propagateWorkspaceData();
|
this.#propagateWorkspaceData();
|
||||||
}
|
}
|
||||||
|
|
||||||
removeWorkspace(windowID) {
|
removeWorkspace(windowID) {
|
||||||
let { promise, resolve } = Promise.withResolvers();
|
let { promise, resolve } = Promise.withResolvers();
|
||||||
this.#deleteWorkspaceOwnedTabs(windowID);
|
this.#deleteWorkspaceOwnedTabs(windowID);
|
||||||
|
|
||||||
// mark item as changed for sync
|
|
||||||
this.#markWorkspaceChanged(windowID);
|
|
||||||
|
|
||||||
let workspacesData = this.getWorkspaces();
|
let workspacesData = this.getWorkspaces();
|
||||||
// Remove the workspace from the cache
|
// Remove the workspace from the cache
|
||||||
workspacesData = workspacesData.filter(
|
workspacesData = workspacesData.filter(
|
||||||
@@ -1427,50 +1369,30 @@ class nsZenWorkspaces {
|
|||||||
if (this.privateWindowOrDisabled) {
|
if (this.privateWindowOrDisabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const workspaces = this.getWorkspaces();
|
const workspaces = this._workspaceCache;
|
||||||
// Track previous positions so we only notify observers for workspaces whose
|
|
||||||
// position changed during the reorder.
|
|
||||||
const previousPositions = new Map(
|
|
||||||
workspaces.map((workspace, index) => [workspace.uuid, index])
|
|
||||||
);
|
|
||||||
|
|
||||||
const workspace = workspaces.find(w => w.uuid === id);
|
const workspace = workspaces.find(w => w.uuid === id);
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
console.warn(`Workspace with ID ${id} not found for reordering.`);
|
console.warn(`Workspace with ID ${id} not found for reordering.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the workspace from its current position
|
// Remove the workspace from its current position
|
||||||
const currentIndex = workspaces.indexOf(workspace);
|
const currentIndex = workspaces.indexOf(workspace);
|
||||||
if (currentIndex === -1) {
|
if (currentIndex === -1) {
|
||||||
console.warn(`Workspace with ID ${id} not found in the list.`);
|
console.warn(`Workspace with ID ${id} not found in the list.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
workspaces.splice(currentIndex, 1);
|
||||||
// Insert the workspace at the new position
|
// Insert the workspace at the new position
|
||||||
if (newPosition < 0 || newPosition >= workspaces.length) {
|
if (newPosition < 0 || newPosition > workspaces.length) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Invalid position ${newPosition} for reordering workspace with ID ${id}.`
|
`Invalid position ${newPosition} for reordering workspace with ID ${id}.`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
workspaces.splice(currentIndex, 1);
|
|
||||||
workspaces.splice(newPosition, 0, workspace);
|
workspaces.splice(newPosition, 0, workspace);
|
||||||
|
|
||||||
// Propagate the changes if the order has changed
|
// Propagate the changes if the order has changed
|
||||||
if (currentIndex !== newPosition) {
|
if (currentIndex !== newPosition) {
|
||||||
this._workspaceCache = workspaces;
|
this.#propagateWorkspaceData();
|
||||||
|
|
||||||
for (const [i, ws] of workspaces.entries()) {
|
|
||||||
if (previousPositions.get(ws.uuid) === i) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// mark item as changed for sync
|
|
||||||
this.#markWorkspaceChanged(ws.uuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#propagateWorkspaceData(workspaces);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2443,7 +2365,7 @@ class nsZenWorkspaces {
|
|||||||
for (const tab of gBrowser.tabs) {
|
for (const tab of gBrowser.tabs) {
|
||||||
if (
|
if (
|
||||||
!tab.hasAttribute("zen-workspace-id") &&
|
!tab.hasAttribute("zen-workspace-id") &&
|
||||||
!tab.hasAttribute("zen-essential")
|
!tab.hasAttribute("zen-workspace-id")
|
||||||
) {
|
) {
|
||||||
tab.setAttribute("zen-workspace-id", workspace.uuid);
|
tab.setAttribute("zen-workspace-id", workspace.uuid);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ zen-workspace-creation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
& form {
|
& form {
|
||||||
--input-bgcolor: light-dark(rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.05));
|
--input-text-background-color: light-dark(rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.05));
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -53,7 +53,7 @@ zen-workspace-creation {
|
|||||||
padding: 9px 6px;
|
padding: 9px 6px;
|
||||||
border-radius: 8px !important;
|
border-radius: 8px !important;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: var(--input-bgcolor);
|
background-color: var(--input-text-background-color);
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
@@ -108,7 +108,7 @@ zen-workspace-creation {
|
|||||||
|
|
||||||
& .zen-workspace-creation-name {
|
& .zen-workspace-creation-name {
|
||||||
--input-border-color: transparent;
|
--input-border-color: transparent;
|
||||||
--input-bgcolor: transparent;
|
--input-text-background-color: transparent;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
outline: none;
|
outline: none;
|
||||||
@@ -120,7 +120,7 @@ zen-workspace-creation {
|
|||||||
padding: 4px;
|
padding: 4px;
|
||||||
border-radius: 8px !important;
|
border-radius: 8px !important;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: var(--input-bgcolor);
|
background-color: var(--input-text-background-color);
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -149,7 +149,7 @@ zen-workspace-creation {
|
|||||||
& .zen-workspace-creation-edit-theme-button {
|
& .zen-workspace-creation-edit-theme-button {
|
||||||
border-radius: 8px !important;
|
border-radius: 8px !important;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: var(--input-bgcolor);
|
background-color: var(--input-text-background-color);
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
|||||||
@@ -1,160 +0,0 @@
|
|||||||
/* 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/. */
|
|
||||||
|
|
||||||
const lazy = {};
|
|
||||||
|
|
||||||
ChromeUtils.defineESModuleGetters(lazy, {
|
|
||||||
ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
|
|
||||||
ContextualIdentityService:
|
|
||||||
"resource://gre/modules/ContextualIdentityService.sys.mjs",
|
|
||||||
ZenWindowSync: "resource:///modules/zen/ZenWindowSync.sys.mjs",
|
|
||||||
});
|
|
||||||
|
|
||||||
function normalizeUserContextId(value) {
|
|
||||||
const normalized = typeof value === "string" ? Number(value) : value;
|
|
||||||
if (!Number.isSafeInteger(normalized) || normalized <= 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ZenSyncManager {
|
|
||||||
getSidebarData() {
|
|
||||||
return lazy.ZenSessionStore.getSidebarData();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to ignore changes to items. This is used to prevent
|
|
||||||
* infinite loops when applying incoming sync changes.
|
|
||||||
*
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
#ignoreChanges = false;
|
|
||||||
|
|
||||||
#changedItems = new Map();
|
|
||||||
|
|
||||||
markItemChanged(item) {
|
|
||||||
if (item.type && item.id && !this.#ignoreChanges) {
|
|
||||||
const key = `${item.type}~${item.id}`;
|
|
||||||
this.#changedItems.set(key, { type: item.type, id: item.id });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#getChangedItems() {
|
|
||||||
return Array.from(this.#changedItems.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
#clearChangedItems() {
|
|
||||||
this.#changedItems.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyAboutChanges() {
|
|
||||||
const changedItems = this.#getChangedItems();
|
|
||||||
|
|
||||||
for (const item of changedItems) {
|
|
||||||
Services.obs.notifyObservers(
|
|
||||||
{ wrappedJSObject: item },
|
|
||||||
"zen-workspace-item-changed"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.#clearChangedItems();
|
|
||||||
}
|
|
||||||
|
|
||||||
async applyIncomingBatch(pulled, removals) {
|
|
||||||
try {
|
|
||||||
this.#ignoreChanges = true;
|
|
||||||
this.#applyIncomingContainers(
|
|
||||||
pulled.containers || [],
|
|
||||||
removals.containers || []
|
|
||||||
);
|
|
||||||
|
|
||||||
const win = lazy.ZenWindowSync.firstSyncedWindow;
|
|
||||||
if (win?.gZenWorkspaces) {
|
|
||||||
await win.gZenWorkspaces._applySyncChanges(pulled, removals);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("ZenSyncManager: Failed to apply incoming sync data:", e);
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
this.#ignoreChanges = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#applyIncomingContainers(pulledContainers, removedContainers) {
|
|
||||||
const localContainersById = new Map(
|
|
||||||
lazy.ContextualIdentityService.getPublicIdentities().map(container => [
|
|
||||||
container.userContextId,
|
|
||||||
container,
|
|
||||||
])
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const container of pulledContainers) {
|
|
||||||
if (!container.name) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userContextId = normalizeUserContextId(container.userContextId);
|
|
||||||
if (userContextId === null) {
|
|
||||||
console.warn(
|
|
||||||
"ZenSyncManager: Ignoring incoming container with invalid userContextId",
|
|
||||||
{ container }
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const existsLocally = localContainersById.has(userContextId);
|
|
||||||
|
|
||||||
if (existsLocally) {
|
|
||||||
lazy.ContextualIdentityService.update(
|
|
||||||
userContextId,
|
|
||||||
container.name,
|
|
||||||
container.icon,
|
|
||||||
container.color
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const createdIdentity = lazy.ContextualIdentityService.create(
|
|
||||||
container.name,
|
|
||||||
container.icon,
|
|
||||||
container.color,
|
|
||||||
userContextId
|
|
||||||
);
|
|
||||||
if (createdIdentity) {
|
|
||||||
localContainersById.set(createdIdentity.userContextId, createdIdentity);
|
|
||||||
}
|
|
||||||
if (createdIdentity && createdIdentity.userContextId !== userContextId) {
|
|
||||||
console.warn("ZenSyncManager: Container sync created unexpected ID", {
|
|
||||||
requestedId: userContextId,
|
|
||||||
createdId: createdIdentity.userContextId,
|
|
||||||
name: container.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const container of removedContainers) {
|
|
||||||
const userContextId = normalizeUserContextId(container.userContextId);
|
|
||||||
if (userContextId === null) {
|
|
||||||
console.warn(
|
|
||||||
"ZenSyncManager: Ignoring container removal with invalid userContextId",
|
|
||||||
{ container }
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!localContainersById.has(userContextId)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
lazy.ContextualIdentityService.remove(userContextId);
|
|
||||||
localContainersById.delete(userContextId);
|
|
||||||
} catch {
|
|
||||||
// Container may already be gone locally.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ZenSyncStore = new ZenSyncManager();
|
|
||||||
@@ -1,404 +0,0 @@
|
|||||||
/* 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 {
|
|
||||||
Store,
|
|
||||||
SyncEngine,
|
|
||||||
Tracker,
|
|
||||||
} from "resource://services-sync/engines.sys.mjs";
|
|
||||||
import { CryptoWrapper } from "resource://services-sync/record.sys.mjs";
|
|
||||||
import { SCORE_INCREMENT_XLARGE } from "resource://services-sync/constants.sys.mjs";
|
|
||||||
|
|
||||||
const lazy = {};
|
|
||||||
|
|
||||||
ChromeUtils.defineESModuleGetters(lazy, {
|
|
||||||
ZenSyncStore: "resource:///modules/zen/ZenSyncManager.sys.mjs",
|
|
||||||
ContextualIdentityService:
|
|
||||||
"resource://gre/modules/ContextualIdentityService.sys.mjs",
|
|
||||||
});
|
|
||||||
|
|
||||||
const RECORD_ID_PREFIX_BY_TYPE = Object.freeze({
|
|
||||||
space: "s",
|
|
||||||
container: "c",
|
|
||||||
});
|
|
||||||
|
|
||||||
const RECORD_TYPE_BY_PREFIX = Object.freeze({
|
|
||||||
s: "space",
|
|
||||||
c: "container",
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sync record wrapper for workspace and container items stored in the
|
|
||||||
* Workspaces engine collection.
|
|
||||||
*/
|
|
||||||
export class ZenWorkspacesRecord extends CryptoWrapper {
|
|
||||||
_logName = "Sync.Record.ZenSpaces";
|
|
||||||
}
|
|
||||||
|
|
||||||
ZenWorkspacesRecord.prototype.type = "spaces";
|
|
||||||
|
|
||||||
function parseRecordId(id) {
|
|
||||||
const sep = id.indexOf("~");
|
|
||||||
if (sep === -1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const prefix = id.slice(0, sep);
|
|
||||||
const key = id.slice(sep + 1);
|
|
||||||
return { type: RECORD_TYPE_BY_PREFIX[prefix] || prefix, key };
|
|
||||||
}
|
|
||||||
|
|
||||||
function createRecordId(type, id) {
|
|
||||||
const prefix = RECORD_ID_PREFIX_BY_TYPE[type];
|
|
||||||
if (!prefix) {
|
|
||||||
throw new Error(`Unknown Spaces Sync record type: ${type}`);
|
|
||||||
}
|
|
||||||
return `${prefix}~${id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeUserContextId(value) {
|
|
||||||
const normalized = typeof value === "string" ? Number(value) : value;
|
|
||||||
if (!Number.isSafeInteger(normalized) || normalized <= 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Strips the sync-envelope fields (`id` and `type`) from incoming record data
|
|
||||||
* and restores the item's real identity key where needed
|
|
||||||
*
|
|
||||||
* @param {object} data
|
|
||||||
*/
|
|
||||||
function stripSyncFields(data) {
|
|
||||||
const rest = { ...data };
|
|
||||||
delete rest.id;
|
|
||||||
delete rest.type;
|
|
||||||
return rest;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sync store implementation that serializes local workspace and container
|
|
||||||
* state into records and applies incoming remote changes.
|
|
||||||
*/
|
|
||||||
class ZenWorkspacesStore extends Store {
|
|
||||||
constructor(name, engine) {
|
|
||||||
super(name, engine);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAllIDs() {
|
|
||||||
const ids = {};
|
|
||||||
const sidebar = lazy.ZenSyncStore.getSidebarData();
|
|
||||||
|
|
||||||
for (const space of sidebar.spaces || []) {
|
|
||||||
if (space.uuid) {
|
|
||||||
ids[createRecordId("space", space.uuid)] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const c of lazy.ContextualIdentityService.getPublicIdentities()) {
|
|
||||||
ids[createRecordId("container", c.userContextId)] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ids;
|
|
||||||
}
|
|
||||||
|
|
||||||
async itemExists(id) {
|
|
||||||
const parsed = parseRecordId(id);
|
|
||||||
if (!parsed) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const sidebar = lazy.ZenSyncStore.getSidebarData();
|
|
||||||
|
|
||||||
switch (parsed.type) {
|
|
||||||
case "space":
|
|
||||||
return (sidebar.spaces || []).some(s => s.uuid === parsed.key);
|
|
||||||
case "container":
|
|
||||||
return lazy.ContextualIdentityService.getPublicIdentities().some(
|
|
||||||
c => String(c.userContextId) === parsed.key
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createRecord(id, collection) {
|
|
||||||
const record = new ZenWorkspacesRecord(collection, id);
|
|
||||||
const parsed = parseRecordId(id);
|
|
||||||
if (!parsed) {
|
|
||||||
record.deleted = true;
|
|
||||||
return record;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sidebar = lazy.ZenSyncStore.getSidebarData();
|
|
||||||
|
|
||||||
switch (parsed.type) {
|
|
||||||
case "space": {
|
|
||||||
const spaces = sidebar.spaces || [];
|
|
||||||
const idx = spaces.findIndex(s => s.uuid === parsed.key);
|
|
||||||
if (idx === -1) {
|
|
||||||
record.deleted = true;
|
|
||||||
return record;
|
|
||||||
}
|
|
||||||
const rest = { ...spaces[idx] };
|
|
||||||
delete rest.syncStatus;
|
|
||||||
record.cleartext = { id, type: "space", ...rest, position: idx };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "container": {
|
|
||||||
const container =
|
|
||||||
lazy.ContextualIdentityService.getPublicIdentities().find(
|
|
||||||
c => String(c.userContextId) === parsed.key
|
|
||||||
);
|
|
||||||
if (!container) {
|
|
||||||
record.deleted = true;
|
|
||||||
return record;
|
|
||||||
}
|
|
||||||
record.cleartext = {
|
|
||||||
id,
|
|
||||||
type: "container",
|
|
||||||
userContextId: container.userContextId,
|
|
||||||
name: container.name,
|
|
||||||
icon: container.icon,
|
|
||||||
color: container.color,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
record.deleted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return record;
|
|
||||||
}
|
|
||||||
|
|
||||||
async applyIncomingBatch(records, _countTelemetry) {
|
|
||||||
const pulled = { spaces: [], containers: [] };
|
|
||||||
const removals = { spaces: [], containers: [] };
|
|
||||||
|
|
||||||
for (const record of records) {
|
|
||||||
if (record.deleted) {
|
|
||||||
this._collectRemoval(record.id, removals);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const data = record.cleartext;
|
|
||||||
if (!data?.type) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const clean = stripSyncFields(data);
|
|
||||||
switch (data.type) {
|
|
||||||
case "space":
|
|
||||||
pulled.spaces.push(clean);
|
|
||||||
break;
|
|
||||||
case "container":
|
|
||||||
pulled.containers.push(clean);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suppress change tracking while applying incoming data to prevent
|
|
||||||
// feedback loops where applied items get re-uploaded immediately.
|
|
||||||
this.engine._tracker.ignoreAll = true;
|
|
||||||
try {
|
|
||||||
await lazy.ZenSyncStore.applyIncomingBatch(pulled, removals);
|
|
||||||
} finally {
|
|
||||||
this.engine._tracker.ignoreAll = false;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
_collectRemoval(id, removals) {
|
|
||||||
const parsed = parseRecordId(id);
|
|
||||||
if (!parsed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (parsed.type) {
|
|
||||||
case "space":
|
|
||||||
removals.spaces.push({ uuid: parsed.key });
|
|
||||||
break;
|
|
||||||
case "container": {
|
|
||||||
const userContextId = normalizeUserContextId(parsed.key);
|
|
||||||
if (userContextId === null) {
|
|
||||||
console.warn(
|
|
||||||
"ZenWorkspacesStore: Ignoring container removal with invalid userContextId",
|
|
||||||
{ id }
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
removals.containers.push({ userContextId });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async create(record) {
|
|
||||||
await this._applySingle(record);
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(record) {
|
|
||||||
await this._applySingle(record);
|
|
||||||
}
|
|
||||||
|
|
||||||
async _applySingle(record) {
|
|
||||||
this.engine._tracker.ignoreAll = true;
|
|
||||||
try {
|
|
||||||
if (record.deleted) {
|
|
||||||
const removals = { spaces: [], containers: [] };
|
|
||||||
this._collectRemoval(record.id, removals);
|
|
||||||
await lazy.ZenSyncStore.applyIncomingBatch(
|
|
||||||
{ spaces: [], containers: [] },
|
|
||||||
removals
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = record.cleartext;
|
|
||||||
if (!data?.type) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const clean = stripSyncFields(data);
|
|
||||||
const pulled = { spaces: [], containers: [] };
|
|
||||||
switch (data.type) {
|
|
||||||
case "space":
|
|
||||||
pulled.spaces.push(clean);
|
|
||||||
break;
|
|
||||||
case "container":
|
|
||||||
pulled.containers.push(clean);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await lazy.ZenSyncStore.applyIncomingBatch(pulled, {
|
|
||||||
spaces: [],
|
|
||||||
containers: [],
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
this.engine._tracker.ignoreAll = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async remove() {
|
|
||||||
// No-op: never delete user data on wipe
|
|
||||||
}
|
|
||||||
|
|
||||||
async wipe() {
|
|
||||||
// No-op: never delete user data on wipe
|
|
||||||
}
|
|
||||||
|
|
||||||
changeItemID() {
|
|
||||||
// No-op
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sync tracker that watches workspace and contextual identity observers and
|
|
||||||
* marks the corresponding record IDs as changed.
|
|
||||||
*/
|
|
||||||
class ZenWorkspacesTracker extends Tracker {
|
|
||||||
#changedIDs = {};
|
|
||||||
#ignoreAll = false;
|
|
||||||
|
|
||||||
get ignoreAll() {
|
|
||||||
return this.#ignoreAll;
|
|
||||||
}
|
|
||||||
|
|
||||||
set ignoreAll(value) {
|
|
||||||
this.#ignoreAll = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
onStart() {
|
|
||||||
Services.obs.addObserver(this, "zen-workspace-item-changed");
|
|
||||||
Services.obs.addObserver(this, "contextual-identity-created");
|
|
||||||
Services.obs.addObserver(this, "contextual-identity-updated");
|
|
||||||
Services.obs.addObserver(this, "contextual-identity-deleted");
|
|
||||||
}
|
|
||||||
|
|
||||||
onStop() {
|
|
||||||
Services.obs.removeObserver(this, "zen-workspace-item-changed");
|
|
||||||
Services.obs.removeObserver(this, "contextual-identity-created");
|
|
||||||
Services.obs.removeObserver(this, "contextual-identity-updated");
|
|
||||||
Services.obs.removeObserver(this, "contextual-identity-deleted");
|
|
||||||
}
|
|
||||||
|
|
||||||
observe(subject, topic, _data) {
|
|
||||||
if (this.#ignoreAll) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (topic === "zen-workspace-item-changed") {
|
|
||||||
const type = subject?.wrappedJSObject?.type;
|
|
||||||
const id = subject?.wrappedJSObject?.id;
|
|
||||||
if (type && id) {
|
|
||||||
this._trackChange({ type, id });
|
|
||||||
}
|
|
||||||
} else if (topic.startsWith("contextual-identity-")) {
|
|
||||||
const id = subject?.wrappedJSObject?.userContextId;
|
|
||||||
if (id && normalizeUserContextId(id) !== null) {
|
|
||||||
this._trackChange({ type: "container", id });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_trackChange(data) {
|
|
||||||
if (data.type && data.id) {
|
|
||||||
const id = createRecordId(data.type, data.id);
|
|
||||||
this.#changedIDs[id] = Date.now() / 1000;
|
|
||||||
this.score += SCORE_INCREMENT_XLARGE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getChangedIDs() {
|
|
||||||
return { ...this.#changedIDs };
|
|
||||||
}
|
|
||||||
|
|
||||||
async addChangedID(id, when) {
|
|
||||||
this.#changedIDs[id] = when;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeChangedID(...ids) {
|
|
||||||
for (const id of ids) {
|
|
||||||
delete this.#changedIDs[id];
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearChangedIDs() {
|
|
||||||
this.#changedIDs = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sync engine entrypoint that wires the Workspaces record, store, and tracker
|
|
||||||
* implementations into Firefox Sync.
|
|
||||||
*/
|
|
||||||
export class ZenWorkspacesEngine extends SyncEngine {
|
|
||||||
static get name() {
|
|
||||||
return "Spaces";
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(service) {
|
|
||||||
super("Spaces", service);
|
|
||||||
}
|
|
||||||
|
|
||||||
get _storeObj() {
|
|
||||||
return ZenWorkspacesStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
get _trackerObj() {
|
|
||||||
return ZenWorkspacesTracker;
|
|
||||||
}
|
|
||||||
|
|
||||||
get _recordObj() {
|
|
||||||
return ZenWorkspacesRecord;
|
|
||||||
}
|
|
||||||
|
|
||||||
get version() {
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
get syncPriority() {
|
|
||||||
return 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
get allowSkippedRecord() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# 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/.
|
|
||||||
|
|
||||||
EXTRA_JS_MODULES.zen += [
|
|
||||||
"ZenSyncManager.sys.mjs",
|
|
||||||
"ZenWorkspacesSync.sys.mjs",
|
|
||||||
]
|
|
||||||
@@ -1019,12 +1019,15 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
|
|||||||
}
|
}
|
||||||
|
|
||||||
canEssentialBeAdded(tab) {
|
canEssentialBeAdded(tab) {
|
||||||
|
const isExistingEssentialTab = tab.hasAttribute("zen-essential");
|
||||||
return (
|
return (
|
||||||
!(
|
!(
|
||||||
(tab.getAttribute("usercontextid") || 0) !=
|
(tab.getAttribute("usercontextid") || 0) !=
|
||||||
(gZenWorkspaces.getActiveWorkspaceFromCache().containerTabId || 0) &&
|
(gZenWorkspaces.getActiveWorkspaceFromCache().containerTabId || 0) &&
|
||||||
gZenWorkspaces.containerSpecificEssentials
|
gZenWorkspaces.containerSpecificEssentials
|
||||||
) && gBrowser._numZenEssentials < this.maxEssentialTabs
|
) &&
|
||||||
|
(isExistingEssentialTab ||
|
||||||
|
gBrowser._numZenEssentials < this.maxEssentialTabs)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -118,7 +118,19 @@ run-if = [
|
|||||||
]
|
]
|
||||||
tags = "os_integration"
|
tags = "os_integration"
|
||||||
|
|
||||||
|
["browser_setDefaultProtocolHandler.js"]
|
||||||
|
run-if = [
|
||||||
|
"os == 'win'",
|
||||||
|
]
|
||||||
|
tags = "os_integration"
|
||||||
|
|
||||||
["browser_setDesktopBackgroundPreview.js"]
|
["browser_setDesktopBackgroundPreview.js"]
|
||||||
disabled="Disabled by import_external_tests.py"
|
disabled="Disabled by import_external_tests.py"
|
||||||
support-files = ["large.png"]
|
support-files = ["large.png", "canvas.html"]
|
||||||
|
tags = "os_integration"
|
||||||
|
|
||||||
|
["browser_windowsSetDefaultAppCmdHandler.js"]
|
||||||
|
run-if = [
|
||||||
|
"os == 'win'",
|
||||||
|
]
|
||||||
tags = "os_integration"
|
tags = "os_integration"
|
||||||
|
|||||||
@@ -71,4 +71,10 @@ add_task(async function () {
|
|||||||
],
|
],
|
||||||
screenshotPath
|
screenshotPath
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Regression test for bug 2003265, likely to become flaky if it regresses.
|
||||||
|
await testFileCreationPositive(
|
||||||
|
["-url", "about:blank", "-screenshot", screenshotPath],
|
||||||
|
screenshotPath
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ ChromeUtils.defineESModuleGetters(this, {
|
|||||||
sinon: "resource://testing-common/Sinon.sys.mjs",
|
sinon: "resource://testing-common/Sinon.sys.mjs",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const confusedFoxPath = ShellService.getBundledPdfFile("confused_fox.pdf").path;
|
||||||
|
const SET_HANDLER_WIN11 = Ci.nsIWindowsShellService.OPEN_WITH_SET_HANDLER;
|
||||||
|
|
||||||
const setDefaultBrowserUserChoiceStub = sinon.stub();
|
const setDefaultBrowserUserChoiceStub = sinon.stub();
|
||||||
const setDefaultExtensionHandlersUserChoiceStub = sinon
|
const setDefaultExtensionHandlersUserChoiceStub = sinon
|
||||||
.stub()
|
.stub()
|
||||||
@@ -34,17 +37,15 @@ const _userChoiceImpossibleTelemetryResultStub = sinon
|
|||||||
const setDefaultStub = sinon.stub();
|
const setDefaultStub = sinon.stub();
|
||||||
// We'll dynamically update this as needed during the tests.
|
// We'll dynamically update this as needed during the tests.
|
||||||
const queryCurrentDefaultHandlerForStub = sinon.stub();
|
const queryCurrentDefaultHandlerForStub = sinon.stub();
|
||||||
const launchOpenWithDefaultPickerForFileTypeStub = sinon.stub();
|
const launchSetDefaultAppPickerStub = sinon.stub();
|
||||||
const launchModernSettingsDialogDefaultAppsStub = sinon.stub();
|
const launchModernSettingsDialogDefaultAppsStub = sinon.stub();
|
||||||
const shellStub = sinon.stub(ShellService, "shellService").value({
|
const shellStub = sinon.stub(ShellService, "shellService").value({
|
||||||
setDefaultBrowser: setDefaultStub,
|
setDefaultBrowser: setDefaultStub,
|
||||||
queryCurrentDefaultHandlerFor: queryCurrentDefaultHandlerForStub,
|
queryCurrentDefaultHandlerFor: queryCurrentDefaultHandlerForStub,
|
||||||
QueryInterface: () => ({
|
launchSetDefaultAppPicker: launchSetDefaultAppPickerStub,
|
||||||
launchOpenWithDefaultPickerForFileType:
|
launchModernSettingsDialogDefaultApps:
|
||||||
launchOpenWithDefaultPickerForFileTypeStub,
|
launchModernSettingsDialogDefaultAppsStub,
|
||||||
launchModernSettingsDialogDefaultApps:
|
QueryInterface: ChromeUtils.generateQI([]),
|
||||||
launchModernSettingsDialogDefaultAppsStub,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
registerCleanupFunction(() => {
|
registerCleanupFunction(() => {
|
||||||
@@ -228,7 +229,7 @@ add_task(async function test_setAsDefaultPDFHandler_knownBrowser() {
|
|||||||
const expectedArguments = [aumi, [".pdf", "FirefoxPDF"]];
|
const expectedArguments = [aumi, [".pdf", "FirefoxPDF"]];
|
||||||
const resetStubs = () => {
|
const resetStubs = () => {
|
||||||
setDefaultExtensionHandlersUserChoiceStub.resetHistory();
|
setDefaultExtensionHandlersUserChoiceStub.resetHistory();
|
||||||
launchOpenWithDefaultPickerForFileTypeStub.resetHistory();
|
launchSetDefaultAppPickerStub.resetHistory();
|
||||||
launchModernSettingsDialogDefaultAppsStub.resetHistory();
|
launchModernSettingsDialogDefaultAppsStub.resetHistory();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -250,7 +251,7 @@ add_task(async function test_setAsDefaultPDFHandler_knownBrowser() {
|
|||||||
"Called default browser agent with expected arguments"
|
"Called default browser agent with expected arguments"
|
||||||
);
|
);
|
||||||
Assert.ok(
|
Assert.ok(
|
||||||
launchOpenWithDefaultPickerForFileTypeStub.notCalled,
|
launchSetDefaultAppPickerStub.notCalled,
|
||||||
"Did not fall back to open-with picker"
|
"Did not fall back to open-with picker"
|
||||||
);
|
);
|
||||||
Assert.ok(
|
Assert.ok(
|
||||||
@@ -271,7 +272,7 @@ add_task(async function test_setAsDefaultPDFHandler_knownBrowser() {
|
|||||||
"Called default browser agent with expected arguments"
|
"Called default browser agent with expected arguments"
|
||||||
);
|
);
|
||||||
Assert.ok(
|
Assert.ok(
|
||||||
launchOpenWithDefaultPickerForFileTypeStub.notCalled,
|
launchSetDefaultAppPickerStub.notCalled,
|
||||||
"Did not fall back to open-with picker"
|
"Did not fall back to open-with picker"
|
||||||
);
|
);
|
||||||
Assert.ok(
|
Assert.ok(
|
||||||
@@ -289,7 +290,7 @@ add_task(async function test_setAsDefaultPDFHandler_knownBrowser() {
|
|||||||
"Did not use userChoice"
|
"Did not use userChoice"
|
||||||
);
|
);
|
||||||
Assert.ok(
|
Assert.ok(
|
||||||
launchOpenWithDefaultPickerForFileTypeStub.notCalled,
|
launchSetDefaultAppPickerStub.notCalled,
|
||||||
"Did not fall back to open-with picker"
|
"Did not fall back to open-with picker"
|
||||||
);
|
);
|
||||||
Assert.ok(
|
Assert.ok(
|
||||||
@@ -310,7 +311,7 @@ add_task(async function test_setAsDefaultPDFHandler_knownBrowser() {
|
|||||||
"Called default browser agent with expected arguments"
|
"Called default browser agent with expected arguments"
|
||||||
);
|
);
|
||||||
Assert.ok(
|
Assert.ok(
|
||||||
launchOpenWithDefaultPickerForFileTypeStub.notCalled,
|
launchSetDefaultAppPickerStub.notCalled,
|
||||||
"Did not fall back to open-with picker"
|
"Did not fall back to open-with picker"
|
||||||
);
|
);
|
||||||
Assert.ok(
|
Assert.ok(
|
||||||
@@ -365,8 +366,11 @@ add_task(async function test_setAsDefaultPDFHandler_fallback() {
|
|||||||
|
|
||||||
Assert.ok(userChoiceStub.called, "Attempted userChoice");
|
Assert.ok(userChoiceStub.called, "Attempted userChoice");
|
||||||
Assert.ok(
|
Assert.ok(
|
||||||
launchOpenWithDefaultPickerForFileTypeStub.calledWith(".pdf"),
|
launchSetDefaultAppPickerStub.calledWith(
|
||||||
"Fell back to open-with picker for .pdf"
|
confusedFoxPath,
|
||||||
|
SET_HANDLER_WIN11
|
||||||
|
),
|
||||||
|
"Fell back to open-with picker with bundled PDF path and Win11 flag"
|
||||||
);
|
);
|
||||||
Assert.ok(
|
Assert.ok(
|
||||||
launchModernSettingsDialogDefaultAppsStub.notCalled,
|
launchModernSettingsDialogDefaultAppsStub.notCalled,
|
||||||
@@ -392,7 +396,7 @@ add_task(async function test_setAsDefaultPDFHandler_fallback() {
|
|||||||
);
|
);
|
||||||
userChoiceStub.resetHistory();
|
userChoiceStub.resetHistory();
|
||||||
isDefaultHandlerForStub.resetHistory();
|
isDefaultHandlerForStub.resetHistory();
|
||||||
launchOpenWithDefaultPickerForFileTypeStub.resetHistory();
|
launchSetDefaultAppPickerStub.resetHistory();
|
||||||
launchModernSettingsDialogDefaultAppsStub.resetHistory();
|
launchModernSettingsDialogDefaultAppsStub.resetHistory();
|
||||||
|
|
||||||
info(
|
info(
|
||||||
@@ -413,22 +417,25 @@ add_task(async function test_setAsDefaultPDFHandler_fallback() {
|
|||||||
isDefaultHandlerForStub.returns(true);
|
isDefaultHandlerForStub.returns(true);
|
||||||
userChoiceStub.resetHistory();
|
userChoiceStub.resetHistory();
|
||||||
isDefaultHandlerForStub.resetHistory();
|
isDefaultHandlerForStub.resetHistory();
|
||||||
launchOpenWithDefaultPickerForFileTypeStub.resetHistory();
|
launchSetDefaultAppPickerStub.resetHistory();
|
||||||
launchModernSettingsDialogDefaultAppsStub.resetHistory();
|
launchModernSettingsDialogDefaultAppsStub.resetHistory();
|
||||||
|
|
||||||
info(
|
info(
|
||||||
"When userChoice fails and open-with picker fails, should fall back to settings dialog"
|
"When userChoice fails and open-with picker fails, should fall back to settings dialog"
|
||||||
);
|
);
|
||||||
Services.fog.testResetFOG();
|
Services.fog.testResetFOG();
|
||||||
launchOpenWithDefaultPickerForFileTypeStub.throws(
|
launchSetDefaultAppPickerStub.throws(
|
||||||
new Error("mock IOpenWithLauncher failure")
|
new Error("mock IOpenWithLauncher failure")
|
||||||
);
|
);
|
||||||
await ShellService.setAsDefaultPDFHandler(false);
|
await ShellService.setAsDefaultPDFHandler(false);
|
||||||
|
|
||||||
Assert.ok(userChoiceStub.called, "Attempted userChoice");
|
Assert.ok(userChoiceStub.called, "Attempted userChoice");
|
||||||
Assert.ok(
|
Assert.ok(
|
||||||
launchOpenWithDefaultPickerForFileTypeStub.calledWith(".pdf"),
|
launchSetDefaultAppPickerStub.calledWith(
|
||||||
"Attempted open-with picker for .pdf"
|
confusedFoxPath,
|
||||||
|
SET_HANDLER_WIN11
|
||||||
|
),
|
||||||
|
"Attempted open-with picker with bundled PDF path and Win11 flag"
|
||||||
);
|
);
|
||||||
Assert.ok(
|
Assert.ok(
|
||||||
launchModernSettingsDialogDefaultAppsStub.called,
|
launchModernSettingsDialogDefaultAppsStub.called,
|
||||||
@@ -463,7 +470,7 @@ add_task(async function test_setAsDefaultPDFHandler_fallback() {
|
|||||||
);
|
);
|
||||||
userChoiceStub.resetHistory();
|
userChoiceStub.resetHistory();
|
||||||
isDefaultHandlerForStub.resetHistory();
|
isDefaultHandlerForStub.resetHistory();
|
||||||
launchOpenWithDefaultPickerForFileTypeStub.resetHistory();
|
launchSetDefaultAppPickerStub.resetHistory();
|
||||||
launchModernSettingsDialogDefaultAppsStub.resetHistory();
|
launchModernSettingsDialogDefaultAppsStub.resetHistory();
|
||||||
|
|
||||||
info(
|
info(
|
||||||
@@ -499,7 +506,7 @@ add_task(async function test_setAsDefaultPDFHandler_fallback() {
|
|||||||
"Event result_is_default is false when no method set the default"
|
"Event result_is_default is false when no method set the default"
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
launchOpenWithDefaultPickerForFileTypeStub.reset();
|
launchSetDefaultAppPickerStub.reset();
|
||||||
launchModernSettingsDialogDefaultAppsStub.reset();
|
launchModernSettingsDialogDefaultAppsStub.reset();
|
||||||
sandbox.restore();
|
sandbox.restore();
|
||||||
await SpecialPowers.popPrefEnv();
|
await SpecialPowers.popPrefEnv();
|
||||||
@@ -529,7 +536,7 @@ add_task(async function test_setAsDefaultPDFHandler_useOpenWithDisabled() {
|
|||||||
await ShellService.setAsDefaultPDFHandler(false);
|
await ShellService.setAsDefaultPDFHandler(false);
|
||||||
|
|
||||||
Assert.ok(
|
Assert.ok(
|
||||||
launchOpenWithDefaultPickerForFileTypeStub.notCalled,
|
launchSetDefaultAppPickerStub.notCalled,
|
||||||
"Did not invoke open-with picker when pref is disabled"
|
"Did not invoke open-with picker when pref is disabled"
|
||||||
);
|
);
|
||||||
Assert.ok(
|
Assert.ok(
|
||||||
@@ -550,7 +557,7 @@ add_task(async function test_setAsDefaultPDFHandler_useOpenWithDisabled() {
|
|||||||
"Event result_is_default reflects isDefaultHandlerFor"
|
"Event result_is_default reflects isDefaultHandlerFor"
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
launchOpenWithDefaultPickerForFileTypeStub.reset();
|
launchSetDefaultAppPickerStub.reset();
|
||||||
launchModernSettingsDialogDefaultAppsStub.reset();
|
launchModernSettingsDialogDefaultAppsStub.reset();
|
||||||
sandbox.restore();
|
sandbox.restore();
|
||||||
await SpecialPowers.popPrefEnv();
|
await SpecialPowers.popPrefEnv();
|
||||||
|
|||||||
@@ -0,0 +1,321 @@
|
|||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
ChromeUtils.defineESModuleGetters(this, {
|
||||||
|
DEFAULT_PROTOCOL_URLS:
|
||||||
|
"moz-src:///browser/components/shell/ShellService.sys.mjs",
|
||||||
|
SET_DEFAULT_REDIRECT_PREF:
|
||||||
|
"moz-src:///browser/components/shell/WindowsSetDefaultRedirect.sys.mjs",
|
||||||
|
WindowsSetDefaultRedirect:
|
||||||
|
"moz-src:///browser/components/shell/WindowsSetDefaultRedirect.sys.mjs",
|
||||||
|
sinon: "resource://testing-common/Sinon.sys.mjs",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Everything here is Windows.
|
||||||
|
Assert.equal(AppConstants.platform, "win", "Platform is Windows");
|
||||||
|
|
||||||
|
// Protocol defaults carry OPEN_WITH_PROTOCOL_MESSAGING on top of the
|
||||||
|
// set-handler flag, unlike the file-type flags in browser_setDefaultPDFHandler.
|
||||||
|
const SET_PROTOCOL_WIN11 =
|
||||||
|
Ci.nsIWindowsShellService.OPEN_WITH_SET_HANDLER |
|
||||||
|
Ci.nsIWindowsShellService.OPEN_WITH_PROTOCOL_MESSAGING;
|
||||||
|
const SET_PROTOCOL_WIN10 =
|
||||||
|
Ci.nsIWindowsShellService.OPEN_WITH_SET_HANDLER_WIN10 |
|
||||||
|
Ci.nsIWindowsShellService.OPEN_WITH_PROTOCOL_MESSAGING;
|
||||||
|
|
||||||
|
const launchSetDefaultAppPickerStub = sinon.stub();
|
||||||
|
const launchModernSettingsDialogDefaultAppsStub = sinon.stub();
|
||||||
|
const shellStub = sinon.stub(ShellService, "shellService").value({
|
||||||
|
launchSetDefaultAppPicker: launchSetDefaultAppPickerStub,
|
||||||
|
launchModernSettingsDialogDefaultApps:
|
||||||
|
launchModernSettingsDialogDefaultAppsStub,
|
||||||
|
QueryInterface: ChromeUtils.generateQI([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
registerCleanupFunction(() => {
|
||||||
|
shellStub.restore();
|
||||||
|
Services.prefs.clearUserPref(SET_DEFAULT_REDIRECT_PREF);
|
||||||
|
});
|
||||||
|
|
||||||
|
function resetState() {
|
||||||
|
launchSetDefaultAppPickerStub.reset();
|
||||||
|
launchModernSettingsDialogDefaultAppsStub.reset();
|
||||||
|
Services.prefs.clearUserPref(SET_DEFAULT_REDIRECT_PREF);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readPrefObject() {
|
||||||
|
return JSON.parse(
|
||||||
|
Services.prefs.getStringPref(SET_DEFAULT_REDIRECT_PREF, "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
add_task(async function test_default_https_uses_url_and_win11_flag() {
|
||||||
|
resetState();
|
||||||
|
const sandbox = sinon.createSandbox();
|
||||||
|
sandbox.stub(ShellService, "_isWindows11").returns(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ShellService.setAsDefaultProtocolHandler("https");
|
||||||
|
|
||||||
|
Assert.ok(
|
||||||
|
launchSetDefaultAppPickerStub.calledOnce,
|
||||||
|
"Picker invoked once for the https default"
|
||||||
|
);
|
||||||
|
Assert.deepEqual(
|
||||||
|
launchSetDefaultAppPickerStub.firstCall.args,
|
||||||
|
[DEFAULT_PROTOCOL_URLS.https, SET_PROTOCOL_WIN11],
|
||||||
|
"Picker called with https URL and SET_HANDLER | PROTOCOL_MESSAGING"
|
||||||
|
);
|
||||||
|
Assert.ok(
|
||||||
|
launchModernSettingsDialogDefaultAppsStub.notCalled,
|
||||||
|
"Modern settings not invoked when launcher succeeds"
|
||||||
|
);
|
||||||
|
Assert.deepEqual(
|
||||||
|
readPrefObject(),
|
||||||
|
{
|
||||||
|
openWithArg: DEFAULT_PROTOCOL_URLS.https,
|
||||||
|
overrideUri: null,
|
||||||
|
type: WindowsSetDefaultRedirect.TYPE.PROTOCOL,
|
||||||
|
},
|
||||||
|
"Pref records the URL; overrideUri null so the round-trip is suppressed"
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
sandbox.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_custom_url_overrides_protocol_default() {
|
||||||
|
resetState();
|
||||||
|
const sandbox = sinon.createSandbox();
|
||||||
|
sandbox.stub(ShellService, "_isWindows11").returns(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const customUrl = "https://custom.example.com/handler-check";
|
||||||
|
await ShellService.setAsDefaultProtocolHandler("https", customUrl, true);
|
||||||
|
|
||||||
|
Assert.deepEqual(
|
||||||
|
launchSetDefaultAppPickerStub.firstCall.args,
|
||||||
|
[customUrl, SET_PROTOCOL_WIN11],
|
||||||
|
"Picker called with caller-provided URL, not the protocol default"
|
||||||
|
);
|
||||||
|
Assert.deepEqual(
|
||||||
|
readPrefObject(),
|
||||||
|
{
|
||||||
|
openWithArg: customUrl,
|
||||||
|
overrideUri: DEFAULT_PROTOCOL_URLS.https,
|
||||||
|
type: WindowsSetDefaultRedirect.TYPE.PROTOCOL,
|
||||||
|
},
|
||||||
|
"Pref stashes the custom URL + protocol-default overrideUri for the round-trip"
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
sandbox.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_win10_uses_set_handler_win10_flag() {
|
||||||
|
resetState();
|
||||||
|
const sandbox = sinon.createSandbox();
|
||||||
|
sandbox.stub(ShellService, "_isWindows11").returns(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ShellService.setAsDefaultProtocolHandler("https");
|
||||||
|
Assert.deepEqual(
|
||||||
|
launchSetDefaultAppPickerStub.firstCall.args,
|
||||||
|
[DEFAULT_PROTOCOL_URLS.https, SET_PROTOCOL_WIN10],
|
||||||
|
"Picker called with SET_HANDLER_WIN10 | PROTOCOL_MESSAGING when not on Win11"
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
sandbox.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_falls_back_to_settings_when_picker_throws() {
|
||||||
|
resetState();
|
||||||
|
const sandbox = sinon.createSandbox();
|
||||||
|
sandbox.stub(ShellService, "_isWindows11").returns(true);
|
||||||
|
launchSetDefaultAppPickerStub.throws(new Error("mock launcher failure"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ShellService.setAsDefaultProtocolHandler("https", undefined, true);
|
||||||
|
|
||||||
|
Assert.ok(launchSetDefaultAppPickerStub.called, "Tried picker first");
|
||||||
|
Assert.ok(
|
||||||
|
launchModernSettingsDialogDefaultAppsStub.called,
|
||||||
|
"Fell through to modern settings when launcher threw"
|
||||||
|
);
|
||||||
|
Assert.ok(
|
||||||
|
!Services.prefs.prefHasUserValue(SET_DEFAULT_REDIRECT_PREF),
|
||||||
|
"Pending redirect cleared on launcher failure so a later round-trip can't pick up stale intent"
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
sandbox.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_unknown_protocol_with_no_url_throws() {
|
||||||
|
resetState();
|
||||||
|
|
||||||
|
await Assert.rejects(
|
||||||
|
ShellService.setAsDefaultProtocolHandler("notaprotocol"),
|
||||||
|
/No URL provided and no DEFAULT_PROTOCOL_URLS fallback/,
|
||||||
|
"Throws when neither URL nor protocol fallback resolves to a URL"
|
||||||
|
);
|
||||||
|
Assert.ok(
|
||||||
|
launchSetDefaultAppPickerStub.notCalled,
|
||||||
|
"Picker not invoked when args don't resolve to a URL"
|
||||||
|
);
|
||||||
|
Assert.ok(
|
||||||
|
!Services.prefs.prefHasUserValue(SET_DEFAULT_REDIRECT_PREF),
|
||||||
|
"Pref not touched when validation fails before any side effects"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for the deferred set_default_protocol_handler_attempt event to be
|
||||||
|
// recorded, then return the single event emitted by the most recent call.
|
||||||
|
async function awaitAttemptEvent() {
|
||||||
|
await TestUtils.waitForCondition(() => {
|
||||||
|
const events =
|
||||||
|
Glean.browser.setDefaultProtocolHandlerAttempt.testGetValue();
|
||||||
|
return events && events.length;
|
||||||
|
}, "Recorded set_default_protocol_handler_attempt event");
|
||||||
|
const events = Glean.browser.setDefaultProtocolHandlerAttempt.testGetValue();
|
||||||
|
Assert.equal(events.length, 1, "Recorded exactly one attempt event");
|
||||||
|
return events[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
add_task(async function test_telemetry_records_open_with_success() {
|
||||||
|
resetState();
|
||||||
|
const sandbox = sinon.createSandbox();
|
||||||
|
sandbox.stub(ShellService, "_isWindows11").returns(true);
|
||||||
|
const isDefaultHandlerForStub = sandbox
|
||||||
|
.stub(ShellService, "isDefaultHandlerFor")
|
||||||
|
.returns(true);
|
||||||
|
await SpecialPowers.pushPrefEnv({
|
||||||
|
set: [["browser.shell.setDefaultProtocolHandler.attemptWaitTimeMs", 0]],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
Services.fog.testResetFOG();
|
||||||
|
await ShellService.setAsDefaultProtocolHandler("https");
|
||||||
|
|
||||||
|
const event = await awaitAttemptEvent();
|
||||||
|
Assert.equal(event.extra.method, "open_with", "Event method is open_with");
|
||||||
|
Assert.equal(event.extra.success, "true", "Event success is true");
|
||||||
|
Assert.equal(event.extra.protocol, "https", "Event protocol is https");
|
||||||
|
Assert.equal(
|
||||||
|
event.extra.result_is_default,
|
||||||
|
"true",
|
||||||
|
"Event result_is_default reflects isDefaultHandlerFor"
|
||||||
|
);
|
||||||
|
Assert.ok(
|
||||||
|
isDefaultHandlerForStub.calledWith("https"),
|
||||||
|
"Sampled isDefaultHandlerFor with the protocol"
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
sandbox.restore();
|
||||||
|
await SpecialPowers.popPrefEnv();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_telemetry_records_settings_fallback() {
|
||||||
|
resetState();
|
||||||
|
const sandbox = sinon.createSandbox();
|
||||||
|
sandbox.stub(ShellService, "_isWindows11").returns(true);
|
||||||
|
sandbox.stub(ShellService, "isDefaultHandlerFor").returns(true);
|
||||||
|
launchSetDefaultAppPickerStub.throws(new Error("mock launcher failure"));
|
||||||
|
await SpecialPowers.pushPrefEnv({
|
||||||
|
set: [["browser.shell.setDefaultProtocolHandler.attemptWaitTimeMs", 0]],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
Services.fog.testResetFOG();
|
||||||
|
await ShellService.setAsDefaultProtocolHandler("mailto");
|
||||||
|
|
||||||
|
Assert.equal(
|
||||||
|
Glean.browser.setDefaultProtocolHandlerModernSettingsResult.Success.testGetValue(),
|
||||||
|
1,
|
||||||
|
"Recorded modern settings success"
|
||||||
|
);
|
||||||
|
|
||||||
|
const event = await awaitAttemptEvent();
|
||||||
|
Assert.equal(
|
||||||
|
event.extra.method,
|
||||||
|
"settings",
|
||||||
|
"Event method is settings (last attempted)"
|
||||||
|
);
|
||||||
|
Assert.equal(
|
||||||
|
event.extra.success,
|
||||||
|
"true",
|
||||||
|
"Event success reflects modern settings launch"
|
||||||
|
);
|
||||||
|
Assert.equal(event.extra.protocol, "mailto", "Event protocol is mailto");
|
||||||
|
} finally {
|
||||||
|
sandbox.restore();
|
||||||
|
await SpecialPowers.popPrefEnv();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_telemetry_records_complete_failure() {
|
||||||
|
resetState();
|
||||||
|
const sandbox = sinon.createSandbox();
|
||||||
|
sandbox.stub(ShellService, "_isWindows11").returns(true);
|
||||||
|
sandbox.stub(ShellService, "isDefaultHandlerFor").returns(false);
|
||||||
|
launchSetDefaultAppPickerStub.throws(new Error("mock launcher failure"));
|
||||||
|
launchModernSettingsDialogDefaultAppsStub.throws(
|
||||||
|
new Error("mock modern settings failure")
|
||||||
|
);
|
||||||
|
await SpecialPowers.pushPrefEnv({
|
||||||
|
set: [["browser.shell.setDefaultProtocolHandler.attemptWaitTimeMs", 0]],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
Services.fog.testResetFOG();
|
||||||
|
await ShellService.setAsDefaultProtocolHandler("https");
|
||||||
|
|
||||||
|
Assert.equal(
|
||||||
|
Glean.browser.setDefaultProtocolHandlerModernSettingsResult.Failure.testGetValue(),
|
||||||
|
1,
|
||||||
|
"Recorded modern settings failure"
|
||||||
|
);
|
||||||
|
|
||||||
|
const event = await awaitAttemptEvent();
|
||||||
|
Assert.equal(event.extra.method, "settings", "Event method is settings");
|
||||||
|
Assert.equal(
|
||||||
|
event.extra.success,
|
||||||
|
"false",
|
||||||
|
"Event success is false when every method failed"
|
||||||
|
);
|
||||||
|
Assert.equal(
|
||||||
|
event.extra.result_is_default,
|
||||||
|
"false",
|
||||||
|
"Event result_is_default is false when no method set the default"
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
sandbox.restore();
|
||||||
|
await SpecialPowers.popPrefEnv();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_overwrites_existing_pending_redirect() {
|
||||||
|
resetState();
|
||||||
|
const sandbox = sinon.createSandbox();
|
||||||
|
sandbox.stub(ShellService, "_isWindows11").returns(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// A differently-typed leftover value on the pref must not trip the
|
||||||
|
// setStringPref in WindowsSetDefaultRedirect.arm (it clears first).
|
||||||
|
Services.prefs.setBoolPref(SET_DEFAULT_REDIRECT_PREF, true);
|
||||||
|
await ShellService.setAsDefaultProtocolHandler("https", undefined, false);
|
||||||
|
Assert.deepEqual(
|
||||||
|
readPrefObject(),
|
||||||
|
{
|
||||||
|
openWithArg: DEFAULT_PROTOCOL_URLS.https,
|
||||||
|
overrideUri: null,
|
||||||
|
type: WindowsSetDefaultRedirect.TYPE.PROTOCOL,
|
||||||
|
},
|
||||||
|
"Stale value replaced with the structured redirect on the next call"
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
sandbox.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether the preview image for setDesktopBackground is rendered
|
* Check whether the preview image for setDesktopBackground is rendered
|
||||||
* correctly, without stretching
|
* correctly, without stretching, for both <img> and <canvas> targets.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
add_setup(async function () {
|
add_setup(async function () {
|
||||||
@@ -12,82 +12,85 @@ add_setup(async function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
add_task(async function () {
|
async function testPreview(url, targetSelector) {
|
||||||
await BrowserTestUtils.withNewTab(
|
await BrowserTestUtils.withNewTab({ gBrowser, url }, async browser => {
|
||||||
{
|
const dialogLoad = BrowserTestUtils.domWindowOpened(null, async win => {
|
||||||
gBrowser,
|
await BrowserTestUtils.waitForEvent(win, "load");
|
||||||
url: getRootDirectory(gTestPath) + "large.png",
|
Assert.equal(
|
||||||
},
|
win.document.documentElement.getAttribute("windowtype"),
|
||||||
async () => {
|
"Shell:SetDesktopBackground",
|
||||||
const dialogLoad = BrowserTestUtils.domWindowOpened(null, async win => {
|
"Opened correct window"
|
||||||
await BrowserTestUtils.waitForEvent(win, "load");
|
);
|
||||||
Assert.equal(
|
return true;
|
||||||
win.document.documentElement.getAttribute("windowtype"),
|
});
|
||||||
"Shell:SetDesktopBackground",
|
|
||||||
"Opened correct window"
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
const image = content.document.images[0];
|
await BrowserTestUtils.synthesizeMouseAtCenter(
|
||||||
EventUtils.synthesizeMouseAtCenter(image, { type: "contextmenu" });
|
targetSelector,
|
||||||
|
{ type: "contextmenu" },
|
||||||
|
browser
|
||||||
|
);
|
||||||
|
|
||||||
const menu = document.getElementById("contentAreaContextMenu");
|
const menu = document.getElementById("contentAreaContextMenu");
|
||||||
await BrowserTestUtils.waitForPopupEvent(menu, "shown");
|
await BrowserTestUtils.waitForPopupEvent(menu, "shown");
|
||||||
const menuClosed = BrowserTestUtils.waitForPopupEvent(menu, "hidden");
|
const menuClosed = BrowserTestUtils.waitForPopupEvent(menu, "hidden");
|
||||||
|
|
||||||
const menuItem = document.getElementById("context-setDesktopBackground");
|
const menuItem = document.getElementById("context-setDesktopBackground");
|
||||||
try {
|
try {
|
||||||
menu.activateItem(menuItem);
|
menu.activateItem(menuItem);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
ok(
|
ok(
|
||||||
menuItem.hidden,
|
menuItem.hidden,
|
||||||
"should only fail to activate when menu item is hidden"
|
"should only fail to activate when menu item is hidden"
|
||||||
);
|
);
|
||||||
ok(
|
ok(
|
||||||
!ShellService.canSetDesktopBackground,
|
!ShellService.canSetDesktopBackground,
|
||||||
"Should only hide when not able to set the desktop background"
|
"Should only hide when not able to set the desktop background"
|
||||||
);
|
);
|
||||||
is(
|
is(
|
||||||
AppConstants.platform,
|
AppConstants.platform,
|
||||||
"linux",
|
"linux",
|
||||||
"Should always be able to set desktop background on non-linux platforms"
|
"Should always be able to set desktop background on non-linux platforms"
|
||||||
);
|
);
|
||||||
todo(false, "Skipping test on this configuration");
|
todo(false, "Skipping test on this configuration");
|
||||||
|
|
||||||
menu.hidePopup();
|
|
||||||
await menuClosed;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
menu.hidePopup();
|
||||||
await menuClosed;
|
await menuClosed;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const win = await dialogLoad;
|
await menuClosed;
|
||||||
|
|
||||||
/* setDesktopBackground.js does a setTimeout to wait for correct
|
const win = await dialogLoad;
|
||||||
|
|
||||||
|
/* setDesktopBackground.js does a setTimeout to wait for correct
|
||||||
dimensions. If we don't wait here we could read the preview dimensions
|
dimensions. If we don't wait here we could read the preview dimensions
|
||||||
before they're changed to match the screen */
|
before they're changed to match the screen */
|
||||||
await TestUtils.waitForTick();
|
await TestUtils.waitForTick();
|
||||||
|
|
||||||
const canvas = win.document.getElementById("screen");
|
const canvas = win.document.getElementById("screen");
|
||||||
const screenRatio = screen.width / screen.height;
|
const screenRatio = screen.width / screen.height;
|
||||||
const previewRatio = canvas.clientWidth / canvas.clientHeight;
|
const previewRatio = canvas.clientWidth / canvas.clientHeight;
|
||||||
|
|
||||||
info(`Screen dimensions are ${screen.width}x${screen.height}`);
|
info(`Screen dimensions are ${screen.width}x${screen.height}`);
|
||||||
info(`Screen's raw ratio is ${screenRatio}`);
|
info(`Screen's raw ratio is ${screenRatio}`);
|
||||||
info(
|
info(`Preview dimensions are ${canvas.clientWidth}x${canvas.clientHeight}`);
|
||||||
`Preview dimensions are ${canvas.clientWidth}x${canvas.clientHeight}`
|
info(`Preview's raw ratio is ${previewRatio}`);
|
||||||
);
|
|
||||||
info(`Preview's raw ratio is ${previewRatio}`);
|
|
||||||
|
|
||||||
Assert.ok(
|
Assert.ok(
|
||||||
previewRatio < screenRatio + 0.01 && previewRatio > screenRatio - 0.01,
|
previewRatio < screenRatio + 0.01 && previewRatio > screenRatio - 0.01,
|
||||||
"Preview's aspect ratio is within ±.01 of screen's"
|
"Preview's aspect ratio is within ±.01 of screen's"
|
||||||
);
|
);
|
||||||
|
|
||||||
win.close();
|
win.close();
|
||||||
|
|
||||||
await menuClosed;
|
await menuClosed;
|
||||||
}
|
});
|
||||||
);
|
}
|
||||||
|
|
||||||
|
add_task(async function test_image() {
|
||||||
|
await testPreview(getRootDirectory(gTestPath) + "large.png", "img");
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_canvas() {
|
||||||
|
await testPreview(getRootDirectory(gTestPath) + "canvas.html", "canvas");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,293 @@
|
|||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
const { CommandLineHandler } = ChromeUtils.importESModule(
|
||||||
|
"moz-src:///browser/components/shell/WindowsSetDefaultAppCmdHandler.sys.mjs"
|
||||||
|
);
|
||||||
|
|
||||||
|
ChromeUtils.defineESModuleGetters(this, {
|
||||||
|
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
|
||||||
|
SET_DEFAULT_REDIRECT_PREF:
|
||||||
|
"moz-src:///browser/components/shell/WindowsSetDefaultRedirect.sys.mjs",
|
||||||
|
ShellService: "moz-src:///browser/components/shell/ShellService.sys.mjs",
|
||||||
|
WindowsSetDefaultRedirect:
|
||||||
|
"moz-src:///browser/components/shell/WindowsSetDefaultRedirect.sys.mjs",
|
||||||
|
sinon: "resource://testing-common/Sinon.sys.mjs",
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.equal(AppConstants.platform, "win", "Platform is Windows");
|
||||||
|
|
||||||
|
const confusedFoxPath = ShellService.getBundledPdfFile("confused_fox.pdf").path;
|
||||||
|
const blankURISpec = Services.io.newFileURI(
|
||||||
|
ShellService.getBundledPdfFile("blank.pdf")
|
||||||
|
).spec;
|
||||||
|
|
||||||
|
const workingDir = Services.dirsvc.get("GreD", Ci.nsIFile);
|
||||||
|
|
||||||
|
// Build a real nsICommandLine from an args array, so we exercise the same
|
||||||
|
// flag-parsing the OS-initiated launch goes through
|
||||||
|
function makeCmdLine(args, state = Ci.nsICommandLine.STATE_INITIAL_LAUNCH) {
|
||||||
|
return Cu.createCommandLine(args, workingDir, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arm the one-shot redirect via the real ShellService helper, the way
|
||||||
|
// setAsDefault{*}Handler does right before launching the OS picker. These
|
||||||
|
// tests all use file openWithArgs, so default the type.
|
||||||
|
function armRedirect(
|
||||||
|
openWithArg,
|
||||||
|
overrideUri,
|
||||||
|
type = WindowsSetDefaultRedirect.TYPE.FILE
|
||||||
|
) {
|
||||||
|
WindowsSetDefaultRedirect.arm(openWithArg, overrideUri, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
let fakeWin;
|
||||||
|
let getTopWindowStub;
|
||||||
|
let openWindowStub;
|
||||||
|
|
||||||
|
add_setup(function () {
|
||||||
|
fakeWin = { openTrustedLinkIn: sinon.stub() };
|
||||||
|
// Stubbed rather than spied: getTopWindow must return our fake window (and
|
||||||
|
// null on demand), and openWindow must not actually open a window in the
|
||||||
|
// test harness. We only inspect the calls.
|
||||||
|
getTopWindowStub = sinon.stub(BrowserWindowTracker, "getTopWindow");
|
||||||
|
openWindowStub = sinon.stub(BrowserWindowTracker, "openWindow");
|
||||||
|
});
|
||||||
|
|
||||||
|
registerCleanupFunction(() => {
|
||||||
|
sinon.restore();
|
||||||
|
Services.prefs.clearUserPref(SET_DEFAULT_REDIRECT_PREF);
|
||||||
|
});
|
||||||
|
|
||||||
|
function resetState() {
|
||||||
|
fakeWin.openTrustedLinkIn.resetHistory();
|
||||||
|
getTopWindowStub.reset();
|
||||||
|
getTopWindowStub.returns(fakeWin);
|
||||||
|
openWindowStub.reset();
|
||||||
|
Services.prefs.clearUserPref(SET_DEFAULT_REDIRECT_PREF);
|
||||||
|
}
|
||||||
|
|
||||||
|
add_task(async function test_no_osint_returns_early() {
|
||||||
|
resetState();
|
||||||
|
armRedirect(confusedFoxPath, blankURISpec);
|
||||||
|
const cmdLine = makeCmdLine(["-url", confusedFoxPath]);
|
||||||
|
new CommandLineHandler().handle(cmdLine);
|
||||||
|
|
||||||
|
Assert.equal(
|
||||||
|
cmdLine.preventDefault,
|
||||||
|
false,
|
||||||
|
"preventDefault left untouched without -osint"
|
||||||
|
);
|
||||||
|
Assert.greaterOrEqual(
|
||||||
|
cmdLine.findFlag("url", false),
|
||||||
|
0,
|
||||||
|
"-url remains for the next handler"
|
||||||
|
);
|
||||||
|
Assert.ok(
|
||||||
|
fakeWin.openTrustedLinkIn.notCalled,
|
||||||
|
"No tab opened without -osint"
|
||||||
|
);
|
||||||
|
Assert.ok(openWindowStub.notCalled, "No window opened without -osint");
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_osint_without_url_returns_early() {
|
||||||
|
resetState();
|
||||||
|
armRedirect(confusedFoxPath, blankURISpec);
|
||||||
|
const cmdLine = makeCmdLine(["-osint"]);
|
||||||
|
new CommandLineHandler().handle(cmdLine);
|
||||||
|
|
||||||
|
Assert.equal(
|
||||||
|
cmdLine.preventDefault,
|
||||||
|
false,
|
||||||
|
"preventDefault left untouched without -url"
|
||||||
|
);
|
||||||
|
Assert.ok(fakeWin.openTrustedLinkIn.notCalled, "No tab opened without -url");
|
||||||
|
Assert.ok(openWindowStub.notCalled, "No window opened without -url");
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_no_pending_redirect_leaves_arg() {
|
||||||
|
// No armed redirect: even a -url that looks like our stub PDF is a real,
|
||||||
|
// user-initiated open (e.g. they double-clicked the file), so we must leave
|
||||||
|
// it for BrowserContentHandler.
|
||||||
|
resetState();
|
||||||
|
|
||||||
|
const cmdLine = makeCmdLine(["-osint", "-url", confusedFoxPath]);
|
||||||
|
new CommandLineHandler().handle(cmdLine);
|
||||||
|
|
||||||
|
Assert.equal(
|
||||||
|
cmdLine.preventDefault,
|
||||||
|
false,
|
||||||
|
"preventDefault not set when no redirect is pending"
|
||||||
|
);
|
||||||
|
Assert.greaterOrEqual(
|
||||||
|
cmdLine.findFlag("url", false),
|
||||||
|
0,
|
||||||
|
"-url preserved for BrowserContentHandler"
|
||||||
|
);
|
||||||
|
Assert.ok(
|
||||||
|
fakeWin.openTrustedLinkIn.notCalled,
|
||||||
|
"No tab opened when no redirect is pending"
|
||||||
|
);
|
||||||
|
Assert.ok(
|
||||||
|
openWindowStub.notCalled,
|
||||||
|
"No window opened when no redirect is pending"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_unrelated_url_arg_is_ignored() {
|
||||||
|
resetState();
|
||||||
|
armRedirect(confusedFoxPath, blankURISpec);
|
||||||
|
|
||||||
|
const cmdLine = makeCmdLine([
|
||||||
|
"-osint",
|
||||||
|
"-url",
|
||||||
|
"https://example.com/some-page",
|
||||||
|
]);
|
||||||
|
new CommandLineHandler().handle(cmdLine);
|
||||||
|
|
||||||
|
Assert.equal(
|
||||||
|
cmdLine.preventDefault,
|
||||||
|
false,
|
||||||
|
"preventDefault not set for a -url that isn't the pending openWithArg"
|
||||||
|
);
|
||||||
|
Assert.greaterOrEqual(
|
||||||
|
cmdLine.findFlag("url", false),
|
||||||
|
0,
|
||||||
|
"-url preserved for subsequent handler"
|
||||||
|
);
|
||||||
|
Assert.ok(
|
||||||
|
fakeWin.openTrustedLinkIn.notCalled,
|
||||||
|
"No tab opened for an unrelated -url"
|
||||||
|
);
|
||||||
|
Assert.ok(
|
||||||
|
Services.prefs.prefHasUserValue(SET_DEFAULT_REDIRECT_PREF),
|
||||||
|
"Pending redirect untouched when the openWithArg doesn't match"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_suppress_only_when_target_null() {
|
||||||
|
resetState();
|
||||||
|
armRedirect(confusedFoxPath, null);
|
||||||
|
|
||||||
|
const cmdLine = makeCmdLine(["-osint", "-url", confusedFoxPath]);
|
||||||
|
new CommandLineHandler().handle(cmdLine);
|
||||||
|
|
||||||
|
Assert.equal(
|
||||||
|
cmdLine.preventDefault,
|
||||||
|
true,
|
||||||
|
"openWithArg suppressed so BrowserContentHandler skips it"
|
||||||
|
);
|
||||||
|
Assert.equal(
|
||||||
|
cmdLine.findFlag("url", false),
|
||||||
|
-1,
|
||||||
|
"-url consumed even with no redirect target"
|
||||||
|
);
|
||||||
|
Assert.ok(
|
||||||
|
fakeWin.openTrustedLinkIn.notCalled,
|
||||||
|
"No redirect when target is null"
|
||||||
|
);
|
||||||
|
Assert.ok(openWindowStub.notCalled, "No fallback window when target is null");
|
||||||
|
Assert.ok(
|
||||||
|
!Services.prefs.prefHasUserValue(SET_DEFAULT_REDIRECT_PREF),
|
||||||
|
"Redirect intent consumed"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_redirects_to_top_window() {
|
||||||
|
resetState();
|
||||||
|
armRedirect(confusedFoxPath, blankURISpec);
|
||||||
|
|
||||||
|
const cmdLine = makeCmdLine(["-osint", "-url", confusedFoxPath]);
|
||||||
|
new CommandLineHandler().handle(cmdLine);
|
||||||
|
|
||||||
|
Assert.equal(
|
||||||
|
cmdLine.preventDefault,
|
||||||
|
true,
|
||||||
|
"Default open suppressed for the pending openWithArg"
|
||||||
|
);
|
||||||
|
Assert.equal(cmdLine.findFlag("url", false), -1, "-url consumed");
|
||||||
|
Assert.ok(
|
||||||
|
fakeWin.openTrustedLinkIn.calledOnce,
|
||||||
|
"Redirected into the top window"
|
||||||
|
);
|
||||||
|
Assert.deepEqual(fakeWin.openTrustedLinkIn.firstCall.args, [
|
||||||
|
blankURISpec,
|
||||||
|
"tab",
|
||||||
|
]);
|
||||||
|
Assert.ok(openWindowStub.notCalled, "No new window when one exists");
|
||||||
|
Assert.ok(
|
||||||
|
!Services.prefs.prefHasUserValue(SET_DEFAULT_REDIRECT_PREF),
|
||||||
|
"Redirect intent is one-shot and cleared after use"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_opens_new_window_when_no_top() {
|
||||||
|
resetState();
|
||||||
|
getTopWindowStub.returns(null);
|
||||||
|
armRedirect(confusedFoxPath, blankURISpec);
|
||||||
|
|
||||||
|
const cmdLine = makeCmdLine(["-osint", "-url", confusedFoxPath]);
|
||||||
|
new CommandLineHandler().handle(cmdLine);
|
||||||
|
|
||||||
|
Assert.equal(
|
||||||
|
cmdLine.preventDefault,
|
||||||
|
true,
|
||||||
|
"Default open suppressed even when no top window exists"
|
||||||
|
);
|
||||||
|
Assert.ok(
|
||||||
|
fakeWin.openTrustedLinkIn.notCalled,
|
||||||
|
"Top-window path skipped when getTopWindow returns null"
|
||||||
|
);
|
||||||
|
Assert.ok(openWindowStub.calledOnce, "Falls back to openWindow");
|
||||||
|
|
||||||
|
const opts = openWindowStub.firstCall.args[0];
|
||||||
|
Assert.ok(
|
||||||
|
opts && opts.args,
|
||||||
|
"openWindow called with a {args} options object"
|
||||||
|
);
|
||||||
|
Assert.ok(
|
||||||
|
opts.args instanceof Ci.nsISupportsString,
|
||||||
|
"args is an nsISupportsString"
|
||||||
|
);
|
||||||
|
Assert.equal(
|
||||||
|
opts.args.data,
|
||||||
|
blankURISpec,
|
||||||
|
"nsISupportsString carries the redirect URI"
|
||||||
|
);
|
||||||
|
Assert.ok(
|
||||||
|
!Services.prefs.prefHasUserValue(SET_DEFAULT_REDIRECT_PREF),
|
||||||
|
"Redirect intent is one-shot and cleared after use"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_intent_is_one_shot() {
|
||||||
|
resetState();
|
||||||
|
armRedirect(confusedFoxPath, blankURISpec);
|
||||||
|
|
||||||
|
new CommandLineHandler().handle(
|
||||||
|
makeCmdLine(["-osint", "-url", confusedFoxPath])
|
||||||
|
);
|
||||||
|
Assert.ok(
|
||||||
|
fakeWin.openTrustedLinkIn.calledOnce,
|
||||||
|
"First call honors the pending redirect"
|
||||||
|
);
|
||||||
|
|
||||||
|
fakeWin.openTrustedLinkIn.resetHistory();
|
||||||
|
const second = makeCmdLine(["-osint", "-url", confusedFoxPath]);
|
||||||
|
new CommandLineHandler().handle(second);
|
||||||
|
|
||||||
|
Assert.equal(
|
||||||
|
second.preventDefault,
|
||||||
|
false,
|
||||||
|
"Second call leaves the openWithArg alone because the intent was consumed"
|
||||||
|
);
|
||||||
|
Assert.greaterOrEqual(
|
||||||
|
second.findFlag("url", false),
|
||||||
|
0,
|
||||||
|
"-url preserved on the second call (no pending redirect)"
|
||||||
|
);
|
||||||
|
Assert.ok(
|
||||||
|
fakeWin.openTrustedLinkIn.notCalled,
|
||||||
|
"Second call does not redirect"
|
||||||
|
);
|
||||||
|
});
|
||||||
11
src/zen/tests/mochitests/shell/canvas.html
Normal file
11
src/zen/tests/mochitests/shell/canvas.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<canvas id="c" width="200" height="100"></canvas>
|
||||||
|
<script>
|
||||||
|
const ctx = document.getElementById("c").getContext("2d");
|
||||||
|
ctx.fillStyle = "red";
|
||||||
|
ctx.fillRect(0, 0, 200, 100);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
657
src/zen/tests/mochitests/shell/unit/test_dynamicLauncher.js
Normal file
657
src/zen/tests/mochitests/shell/unit/test_dynamicLauncher.js
Normal file
@@ -0,0 +1,657 @@
|
|||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
ChromeUtils.defineESModuleGetters(this, {
|
||||||
|
ShellService: "moz-src:///browser/components/shell/ShellService.sys.mjs",
|
||||||
|
Subprocess: "resource://gre/modules/Subprocess.sys.mjs",
|
||||||
|
TestUtils: "resource://testing-common/TestUtils.sys.mjs",
|
||||||
|
XPCOMUtils: "resource://gre/modules/XPCOMUtils.sys.mjs",
|
||||||
|
});
|
||||||
|
|
||||||
|
// This tests the browser's interaction with the Dynamic Launcher portal, which
|
||||||
|
// is documented at
|
||||||
|
//
|
||||||
|
// https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.DynamicLauncher
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyServiceGetter(
|
||||||
|
this,
|
||||||
|
"IniParserFactory",
|
||||||
|
"@mozilla.org/xpcom/ini-parser-factory;1",
|
||||||
|
Ci.nsIINIParserFactory
|
||||||
|
);
|
||||||
|
|
||||||
|
function desktopEntryFromObject(object) {
|
||||||
|
let ini = IniParserFactory.createINIParser().QueryInterface(
|
||||||
|
Ci.nsIINIParserWriter
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const property of Object.getOwnPropertyNames(object)) {
|
||||||
|
ini.setString("Desktop Entry", property, "" + object[property]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ini;
|
||||||
|
}
|
||||||
|
|
||||||
|
function objectFromDesktopEntry(aINI) {
|
||||||
|
let ini = IniParserFactory.createINIParser();
|
||||||
|
ini.initFromString(aINI);
|
||||||
|
|
||||||
|
let sections = ini.getSections();
|
||||||
|
ok(sections.hasMore(), "There is at least one section");
|
||||||
|
Assert.equal(sections.getNext(), "Desktop Entry", "First key is correct");
|
||||||
|
Assert.equal(sections.hasMore(), false, "...and there aren't others");
|
||||||
|
|
||||||
|
let keys = ini.getKeys("Desktop Entry");
|
||||||
|
let result = Object.create(null);
|
||||||
|
while (keys.hasMore()) {
|
||||||
|
let key = keys.getNext();
|
||||||
|
result[key] = ini.getString("Desktop Entry", key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const portalBusName = "org.freedesktop.portal.Desktop";
|
||||||
|
const portalObjectPath = "/org/freedesktop/portal/desktop";
|
||||||
|
const portalInterfaceName = "org.freedesktop.portal.DynamicLauncher";
|
||||||
|
const dbusMockInterface = "org.freedesktop.DBus.Mock";
|
||||||
|
const addObjectMethod = `${dbusMockInterface}.AddObject`;
|
||||||
|
const addMethodMethod = `${dbusMockInterface}.AddMethod`;
|
||||||
|
const emitSignalDetailedMethod = `${dbusMockInterface}.EmitSignalDetailed`;
|
||||||
|
const getCallsMethod = `${dbusMockInterface}.GetCalls`;
|
||||||
|
const clearCallsMethod = `${dbusMockInterface}.ClearCalls`;
|
||||||
|
const resetMethod = `${dbusMockInterface}.Reset`;
|
||||||
|
const mockRequestObjectPath = "/org/freedesktop/portal/desktop/request";
|
||||||
|
|
||||||
|
var DBUS_SESSION_BUS_ADDRESS = "";
|
||||||
|
var DBUS_SESSION_BUS_PID = 0; // eslint-disable-line no-unused-vars
|
||||||
|
var DBUS_MOCK = null;
|
||||||
|
var gdbusCmd = null;
|
||||||
|
|
||||||
|
async function callDbusMethod(objectPath, methodName, args) {
|
||||||
|
let mockProcess = await Subprocess.call({
|
||||||
|
command: gdbusCmd,
|
||||||
|
arguments: [
|
||||||
|
"call",
|
||||||
|
"--session",
|
||||||
|
"-d",
|
||||||
|
portalBusName,
|
||||||
|
"-o",
|
||||||
|
objectPath,
|
||||||
|
"-m",
|
||||||
|
methodName,
|
||||||
|
...args,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return mockProcess.wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launches a D-Bus daemon that the tests will run under, and sets it (via
|
||||||
|
// environment variables) to be used as the D-Bus connection. Additionally, sets
|
||||||
|
// up dbusmock to allow the test to create fake D-Bus objects; this avoids
|
||||||
|
// needing to display UI to the user, or depending on the details of the
|
||||||
|
// specific portal they have installed.
|
||||||
|
add_setup(async function setup() {
|
||||||
|
// Start and use a separate message bus for the tests, to not interfere with
|
||||||
|
// the current's session message bus.
|
||||||
|
let dbus = await Subprocess.call({
|
||||||
|
command: await Subprocess.pathSearch("dbus-launch"),
|
||||||
|
});
|
||||||
|
await dbus.wait();
|
||||||
|
let stdout = await dbus.stdout.readString();
|
||||||
|
let lines = stdout.split("\n");
|
||||||
|
for (let i in lines) {
|
||||||
|
let tokens = lines[i].split("=");
|
||||||
|
switch (tokens.shift()) {
|
||||||
|
case "DBUS_SESSION_BUS_ADDRESS":
|
||||||
|
DBUS_SESSION_BUS_ADDRESS = tokens.join("=");
|
||||||
|
break;
|
||||||
|
case "DBUS_SESSION_BUS_PID":
|
||||||
|
DBUS_SESSION_BUS_PID = tokens.join();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gdbusCmd = await Subprocess.pathSearch("gdbus");
|
||||||
|
|
||||||
|
Services.env.set("DBUS_SESSION_BUS_ADDRESS", DBUS_SESSION_BUS_ADDRESS);
|
||||||
|
Services.env.set("GTK_USE_PORTAL", "1");
|
||||||
|
|
||||||
|
// dbusmock is used to mock the native messaging portal's D-Bus API.
|
||||||
|
DBUS_MOCK = await Subprocess.call({
|
||||||
|
command: await Subprocess.pathSearch("python3"),
|
||||||
|
arguments: [
|
||||||
|
"-m",
|
||||||
|
"dbusmock",
|
||||||
|
portalBusName,
|
||||||
|
portalObjectPath,
|
||||||
|
portalInterfaceName,
|
||||||
|
// vvvvvvvvvvvvv why is this needed?!
|
||||||
|
"-l",
|
||||||
|
"/dev/null",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait until dbusmock is ready
|
||||||
|
await TestUtils.waitForCondition(async () => {
|
||||||
|
let res = await callDbusMethod(
|
||||||
|
portalObjectPath,
|
||||||
|
"org.freedesktop.DBus.Mock.GetCalls",
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
return res.exitCode == 0;
|
||||||
|
}, "waiting for dbusmock");
|
||||||
|
|
||||||
|
registerCleanupFunction(async function () {
|
||||||
|
await DBUS_MOCK.kill();
|
||||||
|
// XXX: While this works locally, it consistently fails when tests are run
|
||||||
|
// in CI, with "xpcshell return code: -15". This needs to be investigated
|
||||||
|
// further. This leaves a stray dbus-daemon process behind,
|
||||||
|
// which isn't ideal, but is harmless.
|
||||||
|
/*await lazy.Subprocess.call({
|
||||||
|
command: await lazy.Subprocess.pathSearch("kill"),
|
||||||
|
arguments: ["-SIGQUIT", DBUS_SESSION_BUS_PID],
|
||||||
|
});*/
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines who called the corresponding D-Bus method. dbusmock doesn't
|
||||||
|
* provide this information, but we need it to create a portal Request path, so
|
||||||
|
* this uses a one-off dbus-monitor instead.
|
||||||
|
*
|
||||||
|
* This function has two phases, determined by the promises in its return value:
|
||||||
|
*
|
||||||
|
* - monitorReady will resolve when dbus-monitor appears to be awake. At this
|
||||||
|
* point, the method call should happen.
|
||||||
|
*
|
||||||
|
* - senderPromise will resolve when the call is detected. This will resolve
|
||||||
|
* to the sender of the method call.
|
||||||
|
*
|
||||||
|
* @param {string} method
|
||||||
|
* The method call to look for. (Currently you can't key based on the object
|
||||||
|
* path.)
|
||||||
|
* @returns {{monitorReady:Promise<undefined>,senderPromise:Promise<string>}}
|
||||||
|
* A set of promises corresponding to the current 'phase' of the check.
|
||||||
|
*/
|
||||||
|
function sniffDbusMethodCaller(method) {
|
||||||
|
let readyResolvers = Promise.withResolvers();
|
||||||
|
let doneResolvers = Promise.withResolvers();
|
||||||
|
|
||||||
|
// Use .then here so this can be a synchronous function, so you don't
|
||||||
|
// additionally have to worry about the promise returned.
|
||||||
|
Subprocess.pathSearch("dbus-monitor")
|
||||||
|
.then(command =>
|
||||||
|
Subprocess.call({
|
||||||
|
command,
|
||||||
|
arguments: ["--session"],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then(async process => {
|
||||||
|
while (true) {
|
||||||
|
for (const line of (await process.stdout.readString()).split("\n")) {
|
||||||
|
readyResolvers.resolve();
|
||||||
|
|
||||||
|
// Extract the sender name from the dbus-monitor process's output.
|
||||||
|
if (
|
||||||
|
line.startsWith("method call") &&
|
||||||
|
line.includes(`member=${method}`)
|
||||||
|
) {
|
||||||
|
let match = line.match(/sender=(\S*)/);
|
||||||
|
ok(match, "dbus-monitor output informs us of the sender");
|
||||||
|
doneResolvers.resolve(match[1]);
|
||||||
|
await process.kill();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(err => {
|
||||||
|
readyResolvers.reject(err);
|
||||||
|
doneResolvers.reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
monitorReady: readyResolvers.promise,
|
||||||
|
senderPromise: doneResolvers.promise,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits for a D-Bus method call with the given object path, method, and
|
||||||
|
* position in time (in case multiple calls happened).
|
||||||
|
*
|
||||||
|
* @param {string} objectPath
|
||||||
|
* The object path the call should be for. This must be a dbusmock-based mock
|
||||||
|
* object.
|
||||||
|
* @param {string} method
|
||||||
|
* The method to get information for.
|
||||||
|
* @param {number} offset
|
||||||
|
* Indicates that information should be returned for the nth (zero-based)
|
||||||
|
* call.
|
||||||
|
* @returns {{offset:number,params:string}}
|
||||||
|
* The offset after this call as a number (zero if it wasn't called), and the
|
||||||
|
* string representation of the arguments; e.g. "'abc', 'def'".
|
||||||
|
*/
|
||||||
|
async function expectDbusMockCall(objectPath, method, offset) {
|
||||||
|
let getCalls = await Subprocess.call({
|
||||||
|
command: gdbusCmd,
|
||||||
|
arguments: [
|
||||||
|
"call",
|
||||||
|
"--session",
|
||||||
|
"-d",
|
||||||
|
portalBusName,
|
||||||
|
"-o",
|
||||||
|
objectPath,
|
||||||
|
"-m",
|
||||||
|
getCallsMethod,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
let out = "";
|
||||||
|
while (!out.endsWith("\n")) {
|
||||||
|
out += await getCalls.stdout.readString();
|
||||||
|
}
|
||||||
|
// The other regexes are fragile, so remove the byte arrays that break them.
|
||||||
|
out = out.replaceAll(/<\('bytes', <\[byte 0x[0-9a-fx, ]+\]>\)>/g, "<bytes>");
|
||||||
|
out = out.match(/\((@a\(tsav\) )?\[(.*)\],\)/)[2];
|
||||||
|
let calls = out.matchAll(/\(.*?\),?/g);
|
||||||
|
let methodCalled = false;
|
||||||
|
let params = {};
|
||||||
|
let i = 0;
|
||||||
|
for (let call of calls) {
|
||||||
|
if (i++ < offset) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let matches = call[0].match(
|
||||||
|
/\((uint64 )?(?<timestamp>\d+), '(?<method>\w+)', (@av )?\[(?<params>.*)\]\),?/
|
||||||
|
);
|
||||||
|
ok(parseFloat(matches.groups.timestamp), "timestamp is valid");
|
||||||
|
if (matches.groups.method == method) {
|
||||||
|
methodCalled = true;
|
||||||
|
params = matches.groups.params;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (method) {
|
||||||
|
ok(methodCalled, `The ${method} mock was called`);
|
||||||
|
} else {
|
||||||
|
equal(i, 0, "No method mock was called");
|
||||||
|
}
|
||||||
|
|
||||||
|
await getCalls.wait();
|
||||||
|
return { offset: i, params };
|
||||||
|
}
|
||||||
|
|
||||||
|
add_task(async function test_successful_creation() {
|
||||||
|
await callDbusMethod(portalObjectPath, resetMethod, []);
|
||||||
|
await callDbusMethod(portalObjectPath, clearCallsMethod, []);
|
||||||
|
|
||||||
|
await callDbusMethod(portalObjectPath, addMethodMethod, [
|
||||||
|
portalInterfaceName,
|
||||||
|
"PrepareInstall",
|
||||||
|
"ssva{sv}",
|
||||||
|
"o",
|
||||||
|
`ret = "/should/not/be/used"`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
let object = {
|
||||||
|
Version: "1.5",
|
||||||
|
Type: "Application",
|
||||||
|
Name: "Example Launcher Name",
|
||||||
|
Icon:
|
||||||
|
Services.dirsvc.get("CurWorkD", Ci.nsIFile).path +
|
||||||
|
"/favicon-normal16.png",
|
||||||
|
Exec: "browser command",
|
||||||
|
};
|
||||||
|
let ini = desktopEntryFromObject(object);
|
||||||
|
|
||||||
|
let { monitorReady, senderPromise } = sniffDbusMethodCaller("PrepareInstall");
|
||||||
|
await monitorReady;
|
||||||
|
|
||||||
|
let promise = ShellService.requestInstallDynamicLauncher("a.b.c", ini, null);
|
||||||
|
|
||||||
|
let senderName = await senderPromise;
|
||||||
|
let result = await expectDbusMockCall(portalObjectPath, "PrepareInstall", 0);
|
||||||
|
let [parentWindow, name, icon] = result.params.split(", ");
|
||||||
|
Assert.equal(parentWindow, "<''>", "No parent window was provided");
|
||||||
|
Assert.equal(name, "<'Example Launcher Name'>", "Launcher name was provided");
|
||||||
|
Assert.equal(icon, "<bytes>", "Icon was some sequence of bytes");
|
||||||
|
|
||||||
|
let match = result.params.match(/'handle_token': <'(?<token>.*)'>/);
|
||||||
|
ok(match, "Start arguments contain a handle token");
|
||||||
|
let handleToken = match.groups.token;
|
||||||
|
|
||||||
|
await callDbusMethod(portalObjectPath, addMethodMethod, [
|
||||||
|
portalInterfaceName,
|
||||||
|
"Install",
|
||||||
|
"sssa{sv}",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Mock the Request object that is expected to be created in response to
|
||||||
|
// calling the Start method on the native messaging portal, wait for it to be
|
||||||
|
// available, and emit its Response signal.
|
||||||
|
let requestPath = `${mockRequestObjectPath}/${senderName
|
||||||
|
.slice(1)
|
||||||
|
.replace(".", "_")}/${handleToken}`;
|
||||||
|
await callDbusMethod(portalObjectPath, addObjectMethod, [
|
||||||
|
requestPath,
|
||||||
|
"org.freedesktop.portal.Request",
|
||||||
|
"@a{sv} {}",
|
||||||
|
"@a(ssss) []",
|
||||||
|
]);
|
||||||
|
await callDbusMethod(requestPath, emitSignalDetailedMethod, [
|
||||||
|
"org.freedesktop.portal.Request",
|
||||||
|
"Response",
|
||||||
|
"ua{sv}",
|
||||||
|
`[<uint32 0>, <@a{sv} {'token': <'qwerty'>}>]`,
|
||||||
|
`{'destination': <'${senderName}'>}`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verify that the Install method was called as expected after the Start
|
||||||
|
// request completed.
|
||||||
|
result = await expectDbusMockCall(portalObjectPath, "Install", result.offset);
|
||||||
|
let [token, desktopFileId, desktopEntry] = result.params.split(", ");
|
||||||
|
Assert.equal(token, "<'qwerty'>", "Correct token is provided");
|
||||||
|
Assert.equal(
|
||||||
|
desktopFileId,
|
||||||
|
"<'a.b.c.desktop'>",
|
||||||
|
"Correct desktop file ID is provided"
|
||||||
|
);
|
||||||
|
|
||||||
|
desktopEntry = desktopEntry
|
||||||
|
.replace(/^<'/, "")
|
||||||
|
.replace(/'>$/, "")
|
||||||
|
.replaceAll(/\\n/g, "\n");
|
||||||
|
let entry = objectFromDesktopEntry(desktopEntry);
|
||||||
|
Assert.deepEqual(entry, object, "Desktop entry contains expected values");
|
||||||
|
|
||||||
|
// Check for exceptions etc.
|
||||||
|
await promise;
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_prepareinstall_error() {
|
||||||
|
await callDbusMethod(portalObjectPath, resetMethod, []);
|
||||||
|
await callDbusMethod(portalObjectPath, clearCallsMethod, []);
|
||||||
|
|
||||||
|
await callDbusMethod(portalObjectPath, addMethodMethod, [
|
||||||
|
portalInterfaceName,
|
||||||
|
"PrepareInstall",
|
||||||
|
"ssva{sv}",
|
||||||
|
"o",
|
||||||
|
`raise dbus.exceptions.DBusException("prepareinstall failed", name="org.mozilla.Error.Mocked")`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
let object = {
|
||||||
|
Version: "1.5",
|
||||||
|
Type: "Application",
|
||||||
|
Name: "Example Launcher Name",
|
||||||
|
Icon:
|
||||||
|
Services.dirsvc.get("CurWorkD", Ci.nsIFile).path +
|
||||||
|
"/favicon-normal16.png",
|
||||||
|
Exec: "browser command",
|
||||||
|
};
|
||||||
|
let ini = desktopEntryFromObject(object);
|
||||||
|
|
||||||
|
await Assert.rejects(
|
||||||
|
(async () =>
|
||||||
|
ShellService.requestInstallDynamicLauncher("a.b.c", ini, null))(),
|
||||||
|
/.*prepareinstall failed.*/,
|
||||||
|
"Failure was propagated from D-Bus"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_install_error() {
|
||||||
|
await callDbusMethod(portalObjectPath, resetMethod, []);
|
||||||
|
await callDbusMethod(portalObjectPath, clearCallsMethod, []);
|
||||||
|
|
||||||
|
await callDbusMethod(portalObjectPath, addMethodMethod, [
|
||||||
|
portalInterfaceName,
|
||||||
|
"PrepareInstall",
|
||||||
|
"ssva{sv}",
|
||||||
|
"o",
|
||||||
|
`ret = "/should/not/be/used"`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
let object = {
|
||||||
|
Version: "1.5",
|
||||||
|
Type: "Application",
|
||||||
|
Name: "Example Launcher Name",
|
||||||
|
Icon:
|
||||||
|
Services.dirsvc.get("CurWorkD", Ci.nsIFile).path +
|
||||||
|
"/favicon-normal16.png",
|
||||||
|
Exec: "browser command",
|
||||||
|
};
|
||||||
|
let ini = desktopEntryFromObject(object);
|
||||||
|
|
||||||
|
let { monitorReady, senderPromise } = sniffDbusMethodCaller("PrepareInstall");
|
||||||
|
await monitorReady;
|
||||||
|
|
||||||
|
let promise = ShellService.requestInstallDynamicLauncher("a.b.c", ini, null);
|
||||||
|
|
||||||
|
let senderName = await senderPromise;
|
||||||
|
let result = await expectDbusMockCall(portalObjectPath, "PrepareInstall", 0);
|
||||||
|
|
||||||
|
let match = result.params.match(/'handle_token': <'(?<token>.*)'>/);
|
||||||
|
ok(match, "Start arguments contain a handle token");
|
||||||
|
let handleToken = match.groups.token;
|
||||||
|
|
||||||
|
await callDbusMethod(portalObjectPath, addMethodMethod, [
|
||||||
|
portalInterfaceName,
|
||||||
|
"Install",
|
||||||
|
"sssa{sv}",
|
||||||
|
"",
|
||||||
|
`raise dbus.exceptions.DBusException("plain install failed", name="org.mozilla.Error.Mocked")`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Mock the Request object that is expected to be created in response to
|
||||||
|
// calling the Start method on the native messaging portal, wait for it to be
|
||||||
|
// available, and emit its Response signal.
|
||||||
|
let requestPath = `${mockRequestObjectPath}/${senderName
|
||||||
|
.slice(1)
|
||||||
|
.replace(".", "_")}/${handleToken}`;
|
||||||
|
await callDbusMethod(portalObjectPath, addObjectMethod, [
|
||||||
|
requestPath,
|
||||||
|
"org.freedesktop.portal.Request",
|
||||||
|
"@a{sv} {}",
|
||||||
|
"@a(ssss) []",
|
||||||
|
]);
|
||||||
|
await callDbusMethod(requestPath, emitSignalDetailedMethod, [
|
||||||
|
"org.freedesktop.portal.Request",
|
||||||
|
"Response",
|
||||||
|
"ua{sv}",
|
||||||
|
`[<uint32 0>, <@a{sv} {'token': <'qwerty'>}>]`,
|
||||||
|
`{'destination': <'${senderName}'>}`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
result = await expectDbusMockCall(portalObjectPath, "Install", result.offset);
|
||||||
|
await Assert.rejects(
|
||||||
|
promise,
|
||||||
|
/.*plain install failed.*/,
|
||||||
|
"Promise rejected with expected content."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_negativeResponseFromPrepareInstall() {
|
||||||
|
await callDbusMethod(portalObjectPath, resetMethod, []);
|
||||||
|
await callDbusMethod(portalObjectPath, clearCallsMethod, []);
|
||||||
|
|
||||||
|
await callDbusMethod(portalObjectPath, addMethodMethod, [
|
||||||
|
portalInterfaceName,
|
||||||
|
"PrepareInstall",
|
||||||
|
"ssva{sv}",
|
||||||
|
"o",
|
||||||
|
`ret = "/should/not/be/used"`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
let object = {
|
||||||
|
Version: "1.5",
|
||||||
|
Type: "Application",
|
||||||
|
Name: "Example Launcher Name",
|
||||||
|
Icon:
|
||||||
|
Services.dirsvc.get("CurWorkD", Ci.nsIFile).path +
|
||||||
|
"/favicon-normal16.png",
|
||||||
|
Exec: "browser command",
|
||||||
|
};
|
||||||
|
let ini = desktopEntryFromObject(object);
|
||||||
|
|
||||||
|
let { monitorReady, senderPromise } = sniffDbusMethodCaller("PrepareInstall");
|
||||||
|
await monitorReady;
|
||||||
|
|
||||||
|
let promise = ShellService.requestInstallDynamicLauncher("a.b.c", ini, null);
|
||||||
|
|
||||||
|
let senderName = await senderPromise;
|
||||||
|
let result = await expectDbusMockCall(portalObjectPath, "PrepareInstall", 0);
|
||||||
|
|
||||||
|
let match = result.params.match(/'handle_token': <'(?<token>.*)'>/);
|
||||||
|
ok(match, "Start arguments contain a handle token");
|
||||||
|
let handleToken = match.groups.token;
|
||||||
|
|
||||||
|
let requestPath = `${mockRequestObjectPath}/${senderName
|
||||||
|
.slice(1)
|
||||||
|
.replace(".", "_")}/${handleToken}`;
|
||||||
|
await callDbusMethod(portalObjectPath, addObjectMethod, [
|
||||||
|
requestPath,
|
||||||
|
"org.freedesktop.portal.Request",
|
||||||
|
"@a{sv} {}",
|
||||||
|
"@a(ssss) []",
|
||||||
|
]);
|
||||||
|
await callDbusMethod(requestPath, emitSignalDetailedMethod, [
|
||||||
|
"org.freedesktop.portal.Request",
|
||||||
|
"Response",
|
||||||
|
"ua{sv}",
|
||||||
|
`[<uint32 2>, <@a{sv} {}>]`,
|
||||||
|
`{'destination': <'${senderName}'>}`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Assert.rejects(
|
||||||
|
promise,
|
||||||
|
/.*Response was non-zero.*/,
|
||||||
|
"Promise rejected with expected content."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_missingTokenFromPrepareInstall() {
|
||||||
|
await callDbusMethod(portalObjectPath, resetMethod, []);
|
||||||
|
await callDbusMethod(portalObjectPath, clearCallsMethod, []);
|
||||||
|
|
||||||
|
await callDbusMethod(portalObjectPath, addMethodMethod, [
|
||||||
|
portalInterfaceName,
|
||||||
|
"PrepareInstall",
|
||||||
|
"ssva{sv}",
|
||||||
|
"o",
|
||||||
|
`ret = "/should/not/be/used"`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
let object = {
|
||||||
|
Version: "1.5",
|
||||||
|
Type: "Application",
|
||||||
|
Name: "Example Launcher Name",
|
||||||
|
Icon:
|
||||||
|
Services.dirsvc.get("CurWorkD", Ci.nsIFile).path +
|
||||||
|
"/favicon-normal16.png",
|
||||||
|
Exec: "browser command",
|
||||||
|
};
|
||||||
|
let ini = desktopEntryFromObject(object);
|
||||||
|
|
||||||
|
let { monitorReady, senderPromise } = sniffDbusMethodCaller("PrepareInstall");
|
||||||
|
await monitorReady;
|
||||||
|
|
||||||
|
let promise = ShellService.requestInstallDynamicLauncher("a.b.c", ini, null);
|
||||||
|
|
||||||
|
let senderName = await senderPromise;
|
||||||
|
let result = await expectDbusMockCall(portalObjectPath, "PrepareInstall", 0);
|
||||||
|
|
||||||
|
let match = result.params.match(/'handle_token': <'(?<token>.*)'>/);
|
||||||
|
ok(match, "Start arguments contain a handle token");
|
||||||
|
let handleToken = match.groups.token;
|
||||||
|
|
||||||
|
let requestPath = `${mockRequestObjectPath}/${senderName
|
||||||
|
.slice(1)
|
||||||
|
.replace(".", "_")}/${handleToken}`;
|
||||||
|
await callDbusMethod(portalObjectPath, addObjectMethod, [
|
||||||
|
requestPath,
|
||||||
|
"org.freedesktop.portal.Request",
|
||||||
|
"@a{sv} {}",
|
||||||
|
"@a(ssss) []",
|
||||||
|
]);
|
||||||
|
await callDbusMethod(requestPath, emitSignalDetailedMethod, [
|
||||||
|
"org.freedesktop.portal.Request",
|
||||||
|
"Response",
|
||||||
|
"ua{sv}",
|
||||||
|
`[<uint32 0>, <@a{sv} {}>]`,
|
||||||
|
`{'destination': <'${senderName}'>}`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Assert.rejects(
|
||||||
|
promise,
|
||||||
|
/.*No token was provided from the portal.*/,
|
||||||
|
"Promise rejected with expected content."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_bad_icon() {
|
||||||
|
await callDbusMethod(portalObjectPath, resetMethod, []);
|
||||||
|
await callDbusMethod(portalObjectPath, clearCallsMethod, []);
|
||||||
|
|
||||||
|
let object = {
|
||||||
|
Version: "1.5",
|
||||||
|
Type: "Application",
|
||||||
|
Name: "Example Launcher Name",
|
||||||
|
Icon:
|
||||||
|
Services.dirsvc.get("CurWorkD", Ci.nsIFile).path + "/does-not-exist.png",
|
||||||
|
Exec: "browser command",
|
||||||
|
};
|
||||||
|
let ini = desktopEntryFromObject(object);
|
||||||
|
|
||||||
|
await Assert.rejects(
|
||||||
|
(async () =>
|
||||||
|
ShellService.requestInstallDynamicLauncher("a.b.c", ini, null))(),
|
||||||
|
/.*Failed to open .*png.*/,
|
||||||
|
"The resulting error should indicate that the file wasn't found."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_uninstall_success() {
|
||||||
|
await callDbusMethod(portalObjectPath, resetMethod, []);
|
||||||
|
await callDbusMethod(portalObjectPath, clearCallsMethod, []);
|
||||||
|
|
||||||
|
await callDbusMethod(portalObjectPath, addMethodMethod, [
|
||||||
|
portalInterfaceName,
|
||||||
|
"Uninstall",
|
||||||
|
"sa{sv}",
|
||||||
|
"",
|
||||||
|
``,
|
||||||
|
]);
|
||||||
|
|
||||||
|
await ShellService.requestUninstallDynamicLauncher("a.b.c");
|
||||||
|
let result = await expectDbusMockCall(portalObjectPath, "Uninstall", 0);
|
||||||
|
|
||||||
|
Assert.equal(
|
||||||
|
result.params,
|
||||||
|
"<'a.b.c.desktop'>, <@a{sv} {}>",
|
||||||
|
"Correct desktop entry ID was provided"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_uninstall_failure() {
|
||||||
|
await callDbusMethod(portalObjectPath, resetMethod, []);
|
||||||
|
await callDbusMethod(portalObjectPath, clearCallsMethod, []);
|
||||||
|
|
||||||
|
await callDbusMethod(portalObjectPath, addMethodMethod, [
|
||||||
|
portalInterfaceName,
|
||||||
|
"Uninstall",
|
||||||
|
"sa{sv}",
|
||||||
|
"",
|
||||||
|
`raise dbus.exceptions.DBusException("uninstall dbus failed", name="org.mozilla.Error.Mocked")`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Assert.rejects(
|
||||||
|
(async () => ShellService.requestUninstallDynamicLauncher("a.b.c"))(),
|
||||||
|
/.*uninstall dbus failed.*/,
|
||||||
|
"The uninstall request should fail."
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
/* Any copyright is dedicated to the Public Domain.
|
|
||||||
* https://creativecommons.org/publicdomain/zero/1.0/ */
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
ChromeUtils.defineESModuleGetters(this, {
|
|
||||||
StartupOSIntegration:
|
|
||||||
"moz-src:///browser/components/shell/StartupOSIntegration.sys.mjs",
|
|
||||||
WindowsLaunchOnLogin: "resource://gre/modules/WindowsLaunchOnLogin.sys.mjs",
|
|
||||||
sinon: "resource://testing-common/Sinon.sys.mjs",
|
|
||||||
});
|
|
||||||
|
|
||||||
const PREF = "browser.startup.windowsLaunchOnLogin.defaultEnabled";
|
|
||||||
|
|
||||||
async function runWith({ isFirstRun, prefValue, approved }) {
|
|
||||||
let sandbox = sinon.createSandbox();
|
|
||||||
let approvedStub = sandbox
|
|
||||||
.stub(WindowsLaunchOnLogin, "getLaunchOnLoginApproved")
|
|
||||||
.resolves(approved);
|
|
||||||
let createStub = sandbox
|
|
||||||
.stub(WindowsLaunchOnLogin, "createLaunchOnLogin")
|
|
||||||
.resolves();
|
|
||||||
|
|
||||||
if (prefValue === null) {
|
|
||||||
Services.prefs.clearUserPref(PREF);
|
|
||||||
} else {
|
|
||||||
Services.prefs.setBoolPref(PREF, prefValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await StartupOSIntegration.maybeCreateLaunchOnLoginOnFirstRun(isFirstRun);
|
|
||||||
return { approvedStub, createStub };
|
|
||||||
} finally {
|
|
||||||
sandbox.restore();
|
|
||||||
Services.prefs.clearUserPref(PREF);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
add_task(async function test_creates_when_all_conditions_true() {
|
|
||||||
let { createStub } = await runWith({
|
|
||||||
isFirstRun: true,
|
|
||||||
prefValue: true,
|
|
||||||
approved: true,
|
|
||||||
});
|
|
||||||
Assert.ok(
|
|
||||||
createStub.calledOnce,
|
|
||||||
"createLaunchOnLogin should be called when isFirstRun, pref, and approval are all true"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
add_task(async function test_skips_when_not_first_run() {
|
|
||||||
let { createStub, approvedStub } = await runWith({
|
|
||||||
isFirstRun: false,
|
|
||||||
prefValue: true,
|
|
||||||
approved: true,
|
|
||||||
});
|
|
||||||
Assert.ok(
|
|
||||||
!createStub.called,
|
|
||||||
"createLaunchOnLogin should not be called when isFirstRun is false"
|
|
||||||
);
|
|
||||||
Assert.ok(
|
|
||||||
!approvedStub.called,
|
|
||||||
"getLaunchOnLoginApproved should be short-circuited when isFirstRun is false"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
add_task(async function test_skips_when_pref_disabled() {
|
|
||||||
let { createStub, approvedStub } = await runWith({
|
|
||||||
isFirstRun: true,
|
|
||||||
prefValue: false,
|
|
||||||
approved: true,
|
|
||||||
});
|
|
||||||
Assert.ok(
|
|
||||||
!createStub.called,
|
|
||||||
"createLaunchOnLogin should not be called when pref is false"
|
|
||||||
);
|
|
||||||
Assert.ok(
|
|
||||||
!approvedStub.called,
|
|
||||||
"getLaunchOnLoginApproved should be short-circuited when pref is false"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
add_task(async function test_skips_when_windows_policy_denies() {
|
|
||||||
let { createStub, approvedStub } = await runWith({
|
|
||||||
isFirstRun: true,
|
|
||||||
prefValue: true,
|
|
||||||
approved: false,
|
|
||||||
});
|
|
||||||
Assert.ok(
|
|
||||||
approvedStub.calledOnce,
|
|
||||||
"getLaunchOnLoginApproved should be consulted when pref and isFirstRun are true"
|
|
||||||
);
|
|
||||||
Assert.ok(
|
|
||||||
!createStub.called,
|
|
||||||
"createLaunchOnLogin should not be called when Windows policy denies"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
add_task(async function test_uses_pref_default_when_unset() {
|
|
||||||
let { createStub } = await runWith({
|
|
||||||
isFirstRun: true,
|
|
||||||
prefValue: null,
|
|
||||||
approved: true,
|
|
||||||
});
|
|
||||||
Assert.ok(
|
|
||||||
createStub.calledOnce,
|
|
||||||
"createLaunchOnLogin should be called when pref is at its built-in default of true"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -10,6 +10,21 @@ run-if = [
|
|||||||
"os == 'linux'",
|
"os == 'linux'",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
["test_dynamicLauncher.js"]
|
||||||
|
environment = [
|
||||||
|
"XPCSHELL_TEST=1",
|
||||||
|
# The following prevents gsettings from initializing dbus before our mock is
|
||||||
|
# ready.
|
||||||
|
"GSETTINGS_BACKEND=memory",
|
||||||
|
]
|
||||||
|
run-if = [
|
||||||
|
"os == 'linux'",
|
||||||
|
]
|
||||||
|
support-files = [
|
||||||
|
"../../../places/tests/browser/favicon-normal16.png",
|
||||||
|
]
|
||||||
|
tags = ["portal"]
|
||||||
|
|
||||||
["test_linuxDesktopEntry.js"]
|
["test_linuxDesktopEntry.js"]
|
||||||
run-if = [
|
run-if = [
|
||||||
"os == 'linux'",
|
"os == 'linux'",
|
||||||
@@ -20,11 +35,6 @@ run-if = [
|
|||||||
"os == 'mac'",
|
"os == 'mac'",
|
||||||
]
|
]
|
||||||
|
|
||||||
["test_maybeCreateLaunchOnLoginOnFirstRun.js"]
|
|
||||||
run-if = [
|
|
||||||
"os == 'win'"
|
|
||||||
]
|
|
||||||
|
|
||||||
["test_secondaryTileJs.js"]
|
["test_secondaryTileJs.js"]
|
||||||
run-if = [
|
run-if = [
|
||||||
"os == 'win'"
|
"os == 'win'"
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
"binaryName": "zen",
|
"binaryName": "zen",
|
||||||
"version": {
|
"version": {
|
||||||
"product": "firefox",
|
"product": "firefox",
|
||||||
"version": "153.0",
|
"version": "153.0.1",
|
||||||
"candidate": "153.0",
|
"candidate": "153.0.1",
|
||||||
"candidateBuild": 1
|
"candidateBuild": 1
|
||||||
},
|
},
|
||||||
"buildOptions": {
|
"buildOptions": {
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
"brandShortName": "Zen",
|
"brandShortName": "Zen",
|
||||||
"brandFullName": "Zen Browser",
|
"brandFullName": "Zen Browser",
|
||||||
"release": {
|
"release": {
|
||||||
"displayVersion": "1.21.9b",
|
"displayVersion": "1.21.10b",
|
||||||
"github": {
|
"github": {
|
||||||
"repo": "zen-browser/desktop"
|
"repo": "zen-browser/desktop"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -346,17 +346,6 @@ fn expand_pref_values(prefs: &mut [Preference]) {
|
|||||||
}
|
}
|
||||||
pref.value = new_value.clone();
|
pref.value = new_value.clone();
|
||||||
}
|
}
|
||||||
// Also change the condition if it contains placeholders
|
|
||||||
if let Some(condition) = &pref.condition {
|
|
||||||
let mut new_condition = condition.clone();
|
|
||||||
for (key, value) in &env_values {
|
|
||||||
let placeholder = format!("@{}@", key);
|
|
||||||
if new_condition.contains(&placeholder) {
|
|
||||||
new_condition = new_condition.replace(&placeholder, if *value { "1" } else { "0" });
|
|
||||||
}
|
|
||||||
pref.condition = Some(new_condition.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user