Compare commits

..

85 Commits

Author SHA1 Message Date
mr. m
f044433bd1 feat: New drag and drop system, b=no-bug, c=tabs, common, folders 2025-12-16 01:50:41 +01:00
mr. m
6367a5aba0 feat: Implement workspace sync store into the session file, b=closes #10857, c=common, tabs, tests, workspaces 2025-12-15 19:19:52 +01:00
mr. m
74face2299 feat: Always make sure to save the last closed window to the sidebar object, b=no-bug, c=no-component 2025-12-15 15:34:08 +01:00
mr. m
8b33fe91eb chore: Finished basic support for split views, b=no-bug, c=folders, split-view 2025-12-15 00:54:21 +01:00
mr. m
fa1693d16b feat: Added partial support for split views, b=no-bug, c=split-view 2025-12-14 12:03:37 +01:00
mr. m
cc7780e078 test: Fixed tests for the pinned tabs manager, b=no-bug, c=tabs, folders, tests, welcome 2025-12-14 11:16:29 +01:00
mr. m
f094b6239d feat: Dont mix remoteness when changing browser views and restore window spaces, b=no-bug, c=common, folders, workspaces 2025-12-14 00:59:05 +01:00
mr. m
1207d2163d feat: Make sure we wait long enough before we initialize workspaces, b=no-bug, c=workspaces 2025-12-13 22:16:09 +01:00
mr. m
a3c6d412bb feat: Unsynced windows should always be allowed to change labels, b=no-bug, c=welcome 2025-12-13 21:11:23 +01:00
mr. m
cc31282ce2 feat: Start implementing old pinned tab behaviour we used to have, b=no-bug, c=common, tabs 2025-12-13 17:40:34 +01:00
mr. m
f5ae793ff0 Merge branch 'dev' into window-sync 2025-12-12 23:25:52 +01:00
mr. m
132fc0e28b chore: Turn off debug flags by default, b=no-bug, c=no-component 2025-12-12 19:16:13 +01:00
mr. m
38aad02d07 feat: Fixed closing windows on macos not returning to the original views, b=no-bug, c=no-component 2025-12-12 16:12:18 +01:00
mr. m
097c7ed17b fix: Fixed tab labels not changing on unfocused windows, b=no-bug, c=no-component 2025-12-12 15:10:07 +01:00
mr. m
669a54c5a2 chore: Small fixes and QA checks, b=no-bug, c=tabs, workspaces 2025-12-12 12:21:23 +01:00
mr. m
3285eb9dd5 feat: Make sure to always initialize an empty state with the sidebar object, b=no-bug, c=workspaces 2025-12-12 11:43:28 +01:00
mr. m
5ce257de83 feat: Allow unload to run instantly and fix closing windows on mac, b=no-bug, c=no-component 2025-12-10 18:09:07 +01:00
mr. m
d97404d03b Merge branch 'dev' into window-sync 2025-12-08 12:10:44 +01:00
Mr. M
60c0afc5ac Merge branch 'window-sync' of https://github.com/zen-browser/desktop into window-sync 2025-12-07 18:58:09 +01:00
Mr. M
307e8fc0ef feat: Make sure to not lose any tabs when opening from a private window, b=no-bug, c=workspaces 2025-12-07 18:58:04 +01:00
mr. m
cd8f8fdb6f Merge branch 'dev' into window-sync 2025-12-07 18:36:06 +01:00
Mr. M
f720dce237 feat: Make sure to properly flush all windows when making a new one and fix removing progress listeners, b=no-bug, c=workspaces 2025-12-06 18:49:26 +01:00
Mr. M
cde32ca1e0 feat: Implemented 'Move To...' Button for unsynced windows, b=no-bug, c=workspaces, common 2025-12-05 14:02:09 +01:00
Mr. M
aecea1f565 feat: Improved support for unsynced windows support, b=no-bug, c=workspaces, folders 2025-12-05 09:35:22 +01:00
Mr. M
73f408c9e1 feat: Stop rendering sub-layers when swaping browsers, b=no-bug, c=common 2025-12-04 17:05:39 +01:00
Mr. M
899d590329 fix: Move back active views when closing a window, b=no-bug, c=no-component 2025-12-03 23:43:23 +01:00
Mr. M
22f2b1f29b feat: Early support for unsynced windoiws, b=no-bug, c=workspaces 2025-12-03 17:57:46 +01:00
Mr. M
5d964511aa chore: Update patches to ff 146, b=no-bug, c=no-component 2025-12-03 16:49:37 +01:00
mr. m
a0268ace63 Discard changes to src/browser/components/tabbrowser/content/tab-js.patch 2025-12-03 16:47:45 +01:00
Mr. M
8be603ac88 Merge branch 'dev' of https://github.com/zen-browser/desktop into window-sync 2025-12-03 16:45:04 +01:00
Mr. M
8bf790df4c feat: Start syncing folders as well, b=no-bug, c=folders 2025-12-02 17:33:39 +01:00
Mr. M
a84bd5b0ce fix: Fixed going back to a different window not allowing to type on inputs, b=no-bug, c=no-component 2025-12-02 14:32:16 +01:00
Mr. M
ae62f5c41d feat: Run session saves right before writing and quiting, b=no-bug, c=common 2025-11-28 17:39:54 +01:00
Mr. M
7dd3cf5817 feat: Leave a screenshot of the page behind when switching windows or tabs, b=no-bug, c=common 2025-11-28 13:55:23 +01:00
Mr. M
722ffa5e97 chore: Remove extra patch, b=no-bug, c=no-component 2025-11-23 19:09:45 +01:00
Mr. M
57233a18f6 feat: Start moving browser views to the selected windows, b=no-bug, c=no-component 2025-11-23 19:08:39 +01:00
Mr. M
04ab9a4b4e feat: Listen for more tab events and properly sync them, b=no-bug, c=common, folders, tabs 2025-11-22 18:15:18 +01:00
Mr. M
1fcef12d0a feat: Listen to new tab opens for new sync system, b=no-bug, c=common, folders, tabs 2025-11-22 13:58:15 +01:00
Mr. M
c6cd912262 feat: Start making use of IDs instead of sync identifiers, b=no-bug, c=folders 2025-11-21 14:29:58 +01:00
Mr. M
a765d3642f Merge branch 'window-sync' of https://github.com/zen-browser/desktop into window-sync 2025-11-21 13:19:30 +01:00
Mr. M
40bc51f904 feat: Run session saver before opening a new window, b=no-bug, c=no-component 2025-11-21 13:16:57 +01:00
Mr. M
8a963a9257 feat: Move window sync to its own JS module, b=no-bug, c=workspaces 2025-11-21 13:16:43 +01:00
Mr. M
9c5bf19ae3 feat: Clone the previous state, b=no-bug, c=no-component 2025-11-21 13:12:22 +01:00
Mr. M
6fbba04dab feat: Run session saver before opening a new winodw, b=no-bug, c=tabs 2025-11-21 13:12:22 +01:00
mr. m
c5df63711b Discard changes to src/zen/tabs/ZenPinnedTabsStorage.mjs 2025-11-21 13:11:56 +01:00
mr. m
a61846bf9a Discard changes to src/browser/components/tabbrowser/content/tabbrowser-js.patch 2025-11-21 13:11:56 +01:00
mr. m
c2a5066181 Discard changes to src/browser/components/tabbrowser/content/tab-js.patch 2025-11-21 13:11:47 +01:00
mr. m
00417582fb Discard changes to src/browser/components/sessionstore/SessionStore-sys-mjs.patch 2025-11-21 13:11:47 +01:00
Mr. M
32d603ced9 chore: Update patches to ff 145, b=no-bug, c=no-component 2025-11-21 13:11:46 +01:00
Mr. M
2706e8761d feat: Dont restore windows that are already initialized, b=no-bug, c=no-component 2025-11-21 13:11:39 +01:00
Mr. M
61659c63d9 feat: Stop using pinned manager and use zen session sidebar, b=no-bug, c=common, folders, tabs, workspaces 2025-11-21 13:11:39 +01:00
Mr. M
34c725aad0 feat: Start doing out own session restore, b=no-bug, c=folders, tabs 2025-11-21 13:08:38 +01:00
mr. m
435762c682 Discard changes to prefs/browser.yaml 2025-11-21 13:06:46 +01:00
Mr. M
c60990c283 feat: Properly handle tab moves, b=no-bug, c=workspaces 2025-11-21 13:06:46 +01:00
Mr. M
1d26040fc6 feat: Start on new session restore, b=no-bug, c=no-component 2025-11-21 13:06:46 +01:00
Mr. M
acb3708936 feat: Dont session duplicate the tabs, b=no-bug, c=workspaces 2025-11-21 13:06:46 +01:00
mr. m
c3b9c3c526 feat: Also change icons and labels if the tab is pending, b=no-bug, c=tabs, workspaces 2025-11-21 13:06:45 +01:00
Mr. M
0adf260ddc feat: Full cross-window workspace syncing, b=no-bug, c=workspaces 2025-11-21 13:03:46 +01:00
Mr. M
ce986beb2f feat: Run session saver before opening a new window, b=no-bug, c=no-component 2025-11-17 18:51:14 +01:00
Mr. M
c86875b7b0 feat: Move window sync to its own JS module, b=no-bug, c=workspaces 2025-11-17 13:36:57 +01:00
Mr. M
eefc8cb20c feat: Clone the previous state, b=no-bug, c=no-component 2025-11-13 17:03:27 +01:00
Mr. M
12c921fd87 feat: Run session saver before opening a new winodw, b=no-bug, c=tabs 2025-11-13 14:40:52 +01:00
mr. m
e419c4dc9f Merge branch 'dev' into window-sync
Signed-off-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
2025-11-13 13:58:38 +01:00
mr. m
68b37ac736 Discard changes to src/zen/tabs/ZenPinnedTabsStorage.mjs 2025-11-13 13:58:18 +01:00
mr. m
7f225ac3ee Discard changes to src/browser/components/tabbrowser/content/tabbrowser-js.patch 2025-11-13 13:57:55 +01:00
mr. m
3e39ef2538 Discard changes to src/browser/components/tabbrowser/content/tab-js.patch 2025-11-13 13:57:42 +01:00
mr. m
e5517eb164 Discard changes to src/browser/components/sessionstore/SessionStore-sys-mjs.patch 2025-11-13 13:57:32 +01:00
Mr. M
c4dd470864 chore: Update patches to ff 145, b=no-bug, c=no-component 2025-11-13 13:56:31 +01:00
Mr. M
bf1b0dcd48 feat: Dont restore windows that are already initialized, b=no-bug, c=no-component 2025-11-01 13:40:14 +01:00
Mr. M
76acc8b0e4 feat: Stop using pinned manager and use zen session sidebar, b=no-bug, c=common, folders, tabs, workspaces 2025-10-31 23:07:38 +01:00
mr. m
1b83b77cad Merge branch 'dev' into window-sync
Signed-off-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
2025-10-31 21:56:19 +01:00
Mr. M
79ff574978 feat: Start doing out own session restore, b=no-bug, c=folders, tabs 2025-10-29 23:57:20 +01:00
mr. m
af20a65fa1 Merge branch 'dev' into window-sync 2025-10-19 23:11:11 +02:00
mr. m
4a7f8fc9c0 Merge branch 'dev' into window-sync 2025-10-19 12:53:06 +02:00
mr. m
a738a829de Merge branch 'dev' into window-sync 2025-10-18 19:53:04 +02:00
mr. m
240a031e38 Discard changes to prefs/browser.yaml 2025-10-18 19:52:53 +02:00
Mr. M
9bc7b9ce4e Merge branch 'window-sync' of https://github.com/zen-browser/desktop into window-sync 2025-09-28 23:45:17 +02:00
Mr. M
86006c8891 feat: Start on new session restore, b=no-bug, c=no-component 2025-09-28 23:45:13 +02:00
Mr. M
a55b1c7495 feat: Properly handle tab moves, b=no-bug, c=workspaces 2025-09-27 17:54:28 +02:00
mr. m
6e6337a95b Merge branch 'dev' into window-sync 2025-09-27 16:58:29 +02:00
mr. m
6b12153c8a Merge branch 'dev' into window-sync 2025-09-27 09:35:44 +02:00
mr. m
f6922ef2ba Merge branch 'dev' into window-sync 2025-09-03 13:06:44 +02:00
Mr. M
91f5d58fbc feat: Dont session duplicate the tabs, b=no-bug, c=workspaces 2025-09-02 16:21:04 +02:00
mr. m
7a4cdaa45c feat: Also change icons and labels if the tab is pending, b=no-bug, c=tabs, workspaces 2025-09-01 16:09:59 +02:00
Mr. M
81e854a89f feat: Full cross-window workspace syncing, b=no-bug, c=workspaces 2025-08-27 23:25:07 +02:00
165 changed files with 3138 additions and 10514 deletions

View File

@@ -95,10 +95,6 @@ jobs:
echo "Checking if patches apply cleanly..."
npm run import
- name: Import external tests
if: steps.git-check.outputs.files_changed == 'true'
run: python3 scripts/import_external_tests.py
- name: Create pull request
uses: peter-evans/create-pull-request@v7
if: steps.git-check.outputs.files_changed == 'true'

View File

@@ -17,8 +17,6 @@ engine/
surfer.json
src/zen/tests/mochitests/*
src/browser/app/profile/*.js
pnpm-lock.yaml

View File

@@ -15,5 +15,6 @@
"ebay",
"ebay-*"
]
}
},
"timestamp": 1765455207275
}

View File

@@ -4,7 +4,7 @@
import js from '@eslint/js';
import globals from 'globals';
import { defineConfig, globalIgnores } from 'eslint/config';
import { defineConfig } from 'eslint/config';
import zenGlobals from './src/zen/zen.globals.js';
export default defineConfig([
@@ -23,5 +23,4 @@ export default defineConfig([
},
ignores: ['**/vendor/**', '**/tests/**'],
},
globalIgnores(['**/mochitests/**']),
]);

View File

@@ -191,9 +191,6 @@ category-zen-CKS =
.tooltiptext = { pane-zen-CKS-title }
pane-settings-CKS-title = { -brand-short-name } Keyboard Shortcuts
category-zen-marketplace =
.tooltiptext = Zen Mods
zen-settings-CKS-header = Customize your keyboard shortcuts
zen-settings-CKS-description = Change the default keyboard shortcuts to your liking and improve your browsing experience

View File

@@ -66,6 +66,10 @@ zen-panel-ui-gradient-click-to-add = Click to add a color
zen-workspace-creation-name =
.placeholder = Space Name
zen-move-tab-to-workspace-button =
.label = Move To...
.tooltiptext = Move all tabs in this window to a Space
zen-workspaces-panel-context-reorder =
.label = Reorder Spaces

View File

@@ -8,6 +8,9 @@
- name: browser.sessionstore.restore_pinned_tabs_on_demand
value: true
- name: browser.tabs.loadBookmarksInTabs
value: false
- name: browser.tabs.hoverPreview.enabled
value: false
@@ -20,6 +23,18 @@
- name: browser.toolbars.bookmarks.visibility
value: 'never'
- name: browser.bookmarks.openInTabClosesMenu
value: false
- name: browser.menu.showViewImageInfo
value: true
- name: findbar.highlightAll
value: true
- name: layout.word_select.eat_space_to_next_word
value: false
- name: widget.non-native-theme.scrollbar.style
value: 2
@@ -43,6 +58,9 @@
- name: browser.download.manager.addToRecentDocs
value: false
- name: browser.download.open_pdf_attachments_inline
value: true
- name: browser.download.alwaysOpenPanel
value: false

View File

@@ -8,12 +8,3 @@
- name: network.predictor.enable-hover-on-ssl
value: true
# See https://github.com/zen-browser/desktop/issues/11599, this pref seems to
# have disabled itself on macos for some unknown reason.
# Make sure its in sync with:
# https://searchfox.org/firefox-main/rev/1477feb9706f4ccc5bd571c1c215832a6fbb7464/modules/libpref/init/StaticPrefList.yaml#7741-7748
- name: gfx.webrender.compositor
condition: 'defined(XP_WIN) || defined(XP_DARWIN)'
value: '@cond'
mirror: once

View File

@@ -26,6 +26,7 @@
value: true
locked: true
# Enable private suggestions
- name: browser.search.suggest.enabled
value: false

View File

@@ -7,9 +7,3 @@
- name: zen.downloads.download-animation-duration
value: 1000 # ms
- name: zen.downloads.icon-popup-position
# 0: Follow tab's position
# 1: Left side always
# 2: Right side always
value: 0

View File

@@ -0,0 +1,9 @@
# 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: zen.session-store.backup-file
value: true
- name: zen.session-store.log
value: false

View File

@@ -40,12 +40,6 @@
- name: zen.view.window.scheme
value: 2
- name: zen.view.drag-and-drop.move-over-threshold
value: 70
- name: zen.view.drag-and-drop.edge-zone-threshold
value: 25
- name: zen.view.context-menu.refresh
value: false

View File

@@ -0,0 +1,9 @@
# 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: zen.window-sync.enabled
value: true
- name: zen.window-sync.log
value: false

View File

@@ -1,124 +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 os
import tomllib
import shutil
BASE_PATH = os.path.join("src", "zen", "tests")
EXTERNAL_TESTS_MANIFEST = os.path.join(BASE_PATH, "manifest.toml")
EXTERNAL_TESTS_OUTPUT = os.path.join(BASE_PATH, "mochitests")
FILE_PREFIX = """
# 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/.
# This file is autogenerated by scripts/import_external_tests.py
# Do not edit manually.
BROWSER_CHROME_MANIFESTS += [
"""
FILE_SUFFIX = "]"
def get_tests_manifest():
with open(EXTERNAL_TESTS_MANIFEST, "rb") as f:
return tomllib.load(f)
def die_with_error(message):
print(f"ERROR: {message}")
exit(1)
def validate_tests_path(path, files, ignore_list):
for ignore in ignore_list:
if ignore not in files:
die_with_error(f"Ignore file '{ignore}' not found in tests folder '{path}'")
if "browser.toml" not in files or "browser.js" in ignore_list:
die_with_error(f"'browser.toml' not found in tests folder '{path}'")
def disable_and_replace_manifest(manifest, output_path):
toml_file = os.path.join(output_path, "browser.toml")
disabled_tests = manifest.get("disable", [])
with open(toml_file, "r") as f:
data = f.read()
for test in disabled_tests:
segment = f'["{test}"]'
if segment not in data:
die_with_error(f"Could not disable test '{test}' as it was not found in '{toml_file}'")
replace_with = f'["{test}"]\ndisabled="Disabled by import_external_tests.py"'
data = data.replace(segment, replace_with)
for replacement in manifest.get("replace-manifest", {}).keys():
if replacement not in data:
die_with_error(f"Could not replace manifest entry '{replacement}' as it was not found in '{toml_file}'")
data = data.replace(replacement, manifest["replace-manifest"][replacement])
with open(toml_file, "w") as f:
f.write(data)
def import_test_suite(test_suite, source_path, output_path, ignore_list, manifest, is_direct_path=False):
print(f"Importing test suite '{test_suite}' from '{source_path}'")
tests_folder = os.path.join("engine", source_path)
if not is_direct_path:
tests_folder = os.path.join(tests_folder, "tests")
if not os.path.exists(tests_folder):
die_with_error(f"Tests folder not found: {tests_folder}")
files = os.listdir(tests_folder)
validate_tests_path(tests_folder, files, ignore_list)
if os.path.exists(output_path):
shutil.rmtree(output_path)
os.makedirs(output_path, exist_ok=True)
for item in files:
if item in ignore_list:
continue
s = os.path.join(tests_folder, item)
d = os.path.join(output_path, item)
if os.path.isdir(s):
shutil.copytree(s, d)
else:
shutil.copy2(s, d)
disable_and_replace_manifest(manifest[test_suite], output_path)
def write_moz_build_file(manifest):
moz_build_path = os.path.join(EXTERNAL_TESTS_OUTPUT, "moz.build")
print(f"Writing moz.build file to '{moz_build_path}'")
with open(moz_build_path, "w") as f:
f.write(FILE_PREFIX)
for test_suite in manifest.keys():
f.write(f'\t"{test_suite}/browser.toml",\n')
f.write(FILE_SUFFIX)
def make_sure_ordered_tests(manifest):
ordered_tests = sorted(manifest.keys())
if list(manifest.keys()) != ordered_tests:
die_with_error("Test suites in manifest.toml are not in alphabetical order.")
def main():
manifest = get_tests_manifest()
if os.path.exists(EXTERNAL_TESTS_OUTPUT):
shutil.rmtree(EXTERNAL_TESTS_OUTPUT)
os.makedirs(EXTERNAL_TESTS_OUTPUT, exist_ok=True)
make_sure_ordered_tests(manifest)
for test_suite, config in manifest.items():
import_test_suite(
test_suite=test_suite,
source_path=config["source"],
output_path=os.path.join(EXTERNAL_TESTS_OUTPUT, test_suite),
ignore_list=config.get("ignore", []),
is_direct_path=config.get("is_direct_path", False),
manifest=manifest
)
write_moz_build_file(manifest)
if __name__ == "__main__":
main()

View File

@@ -15,8 +15,6 @@ IGNORE_PREFS_FILE_OUT = os.path.join(
'engine', 'testing', 'mochitest', 'ignorePrefs.json'
)
MOCHITEST_NAME = "mochitests"
def copy_ignore_prefs():
print("Copying ignorePrefs.json from src/zen/tests to engine/testing/mochitest...")
@@ -61,9 +59,7 @@ def main():
os.execvp(command[0], command)
if path in ("", "all"):
test_dirs = [p for p in Path("zen/tests").iterdir() if p.is_dir() and p.name != MOCHITEST_NAME]
mochitest_dirs = [p for p in Path(f"zen/tests/{MOCHITEST_NAME}").iterdir() if p.is_dir()]
test_dirs.extend(mochitest_dirs)
test_dirs = [p for p in Path("zen/tests").iterdir() if p.is_dir()]
test_paths = [str(p) for p in test_dirs]
run_mach_with_paths(test_paths)
else:

View File

@@ -1,5 +1,5 @@
diff --git a/browser/base/content/browser-addons.js b/browser/base/content/browser-addons.js
index fadcbfca95ee28140579430c0371baad0e2f216a..22a92ca450ef5087610169b15d7f3344c9accddf 100644
index fadcbfca95ee28140579430c0371baad0e2f216a..7454b801b4ad892d6ad122277eb7c7736e976f9f 100644
--- a/browser/base/content/browser-addons.js
+++ b/browser/base/content/browser-addons.js
@@ -1069,7 +1069,7 @@ var gXPInstallObserver = {
@@ -38,131 +38,37 @@ index fadcbfca95ee28140579430c0371baad0e2f216a..22a92ca450ef5087610169b15d7f3344
}
return anchorID;
@@ -2547,7 +2547,7 @@ var gUnifiedExtensions = {
requestAnimationFrame(() => this.updateAttention());
},
- onToolbarVisibilityChange(toolbarId, isVisible) {
+ onToolbarVisibilityChange(toolbarId, isVisible, panel = this.panel) {
// A list of extension widget IDs (possibly empty).
let widgetIDs;
@@ -2561,7 +2561,7 @@ var gUnifiedExtensions = {
}
// The list of overflowed extensions in the extensions panel.
- const overflowedExtensionsList = this.panel.querySelector(
+ const overflowedExtensionsList = panel.querySelector(
"#overflowed-extensions-list"
);
@@ -2662,37 +2662,41 @@ var gUnifiedExtensions = {
@@ -2657,11 +2657,7 @@ var gUnifiedExtensions = {
// Lazy load the unified-extensions-panel panel the first time we need to
// display it.
if (!this._panel) {
- let template = document.getElementById(
- "unified-extensions-panel-template"
- );
- template.replaceWith(template.content);
- this._panel = document.getElementById("unified-extensions-panel");
+ this._panel = document.getElementById("zen-unified-site-data-panel");
let customizationArea = this._panel.querySelector(
"#unified-extensions-area"
);
template.replaceWith(template.content);
this._panel = document.getElementById("unified-extensions-panel");
- let customizationArea = this._panel.querySelector(
- "#unified-extensions-area"
- );
- CustomizableUI.registerPanelNode(
- customizationArea,
- CustomizableUI.AREA_ADDONS
- );
- CustomizableUI.addPanelCloseListeners(this._panel);
-
- this._panel
- .querySelector("#unified-extensions-manage-extensions")
- .addEventListener("command", () => {
- BrowserAddonUI.openAddonsMgr("addons://list/extension");
- });
-
- // Lazy-load the l10n strings. Those strings are used for the CUI and
- // non-CUI extensions in the unified extensions panel.
- document
- .getElementById("unified-extensions-context-menu")
- .querySelectorAll("[data-lazy-l10n-id]")
- .forEach(el => {
- el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id"));
- el.removeAttribute("data-lazy-l10n-id");
- });
+ this.initializePanel(this._panel);
}
return this._panel;
},
+ initializePanel(panel) {
+ let customizationArea = panel.querySelector(
+ "#unified-extensions-area"
+ );
+ CustomizableUI.registerPanelNode(
+ customizationArea,
+ CustomizableUI.AREA_ADDONS
+ );
+ CustomizableUI.addPanelCloseListeners(panel);
+
+ panel
+ .querySelector("#unified-extensions-manage-extensions")
+ .addEventListener("command", () => {
+ BrowserAddonUI.openAddonsMgr("addons://list/extension");
+ });
+
+ // Lazy-load the l10n strings. Those strings are used for the CUI and
+ // non-CUI extensions in the unified extensions panel.
+ document
+ .getElementById("unified-extensions-context-menu")
+ .querySelectorAll("[data-lazy-l10n-id]")
+ .forEach(el => {
+ el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id"));
+ el.removeAttribute("data-lazy-l10n-id");
+ });
+ },
+
// `aEvent` and `reason` are optional. If `reason` is specified, it should be
// a valid argument to gUnifiedExtensions.recordButtonTelemetry().
- async togglePanel(aEvent, reason) {
+ async togglePanel(aEvent, reason, panel = this._panel, view = "unified-extensions-view", button = this._button) {
if (!CustomizationHandler.isCustomizing()) {
if (aEvent) {
@@ -2714,6 +2710,7 @@ var gUnifiedExtensions = {
// and no alternative content is available for display in the panel.
const policies = this.getActivePolicies();
if (
@@ -2729,32 +2733,30 @@ var gUnifiedExtensions = {
this.blocklistAttentionInfo =
await AddonManager.getBlocklistAttentionInfo();
- let panel = this.panel;
-
if (!this._listView) {
this._listView = PanelMultiView.getViewNode(
document,
- "unified-extensions-view"
+ view,
);
this._listView.addEventListener("ViewShowing", this);
this._listView.addEventListener("ViewHiding", this);
}
- if (this._button.open) {
+ if (button.open) {
PanelMultiView.hidePopup(panel);
- this._button.open = false;
+ button.open = false;
} else {
// Overflow extensions placed in collapsed toolbars, if any.
for (const toolbarId of CustomizableUI.getCollapsedToolbarIds(window)) {
// We pass `false` because all these toolbars are collapsed.
- this.onToolbarVisibilityChange(toolbarId, /* isVisible */ false);
+ this.onToolbarVisibilityChange(toolbarId, /* isVisible */ false, panel);
}
panel.hidden = false;
+ false &&
policies.length &&
!this.hasExtensionsInPanel(policies) &&
!this.isPrivateWindowMissingExtensionsWithoutPBMAccess() &&
@@ -2754,7 +2751,7 @@ var gUnifiedExtensions = {
this.recordButtonTelemetry(reason || "extensions_panel_showing");
this.ensureButtonShownBeforeAttachingPanel(panel);
- PanelMultiView.openPopup(panel, this._button, {
PanelMultiView.openPopup(panel, this._button, {
- position: "bottomright topright",
+ PanelMultiView.openPopup(panel, button, {
+ position: gZenUIManager.panelUIPosition(panel, button),
+ position: gZenUIManager.panelUIPosition(panel, this._button),
triggerEvent: aEvent,
});
}
@@ -2941,18 +2943,20 @@ var gUnifiedExtensions = {
@@ -2941,18 +2938,20 @@ var gUnifiedExtensions = {
this._maybeMoveWidgetNodeBack(widgetId);
}

View File

@@ -1,5 +1,5 @@
diff --git a/browser/base/content/navigator-toolbox.js b/browser/base/content/navigator-toolbox.js
index 7b776b15d52367a008ce6bf53dcfcbbe007b7453..da23f716c753f5a43f17bb5ed7a3d335891168c2 100644
index 7b776b15d52367a008ce6bf53dcfcbbe007b7453..d9a3404905b73db7c8f202ab166d5f3c625351f6 100644
--- a/browser/base/content/navigator-toolbox.js
+++ b/browser/base/content/navigator-toolbox.js
@@ -6,7 +6,7 @@
@@ -27,28 +27,21 @@ index 7b776b15d52367a008ce6bf53dcfcbbe007b7453..da23f716c753f5a43f17bb5ed7a3d335
gBrowser.handleNewTabMiddleClick(element, event);
break;
@@ -316,6 +318,7 @@ document.addEventListener(
@@ -315,7 +317,7 @@ document.addEventListener(
#pageActionButton,
#downloads-button,
#fxa-toolbar-menu-button,
#unified-extensions-button,
- #unified-extensions-button,
+ #zen-site-data-icon-button,
#library-button
`);
if (!element) {
@@ -398,6 +401,16 @@ document.addEventListener(
@@ -394,7 +396,7 @@ document.addEventListener(
gSync.toggleAccountPanel(element, event);
break;
- case "unified-extensions-button":
+ case "zen-site-data-icon-button":
gUnifiedExtensions.togglePanel(event);
break;
+ case "zen-site-data-icon-button":
+ gUnifiedExtensions.togglePanel(
+ event,
+ null,
+ window.gZenSiteDataPanel.unifiedPanel,
+ window.gZenSiteDataPanel.unifiedPanelView,
+ window.gZenSiteDataPanel.anchor,
+ );
+ break;
+
case "library-button":
PanelUI.showSubView("appMenu-libraryView", element, event);
break;

View File

@@ -45,7 +45,6 @@
# Scripts used all over the browser
<script type="module" src="chrome://browser/content/zen-components/ZenFolder.mjs"></script>
<script type="module" src="chrome://browser/content/zen-components/ZenPinnedTabsStorage.mjs"></script>
<script type="module" src="chrome://browser/content/zen-components/ZenWorkspacesStorage.mjs"></script>
<script type="module" src="chrome://browser/content/zen-components/ZenMediaController.mjs"></script>

View File

@@ -59,3 +59,8 @@
<menuseparator />
<menuitem id="context_zenOpenSiteSettings" data-l10n-id="zen-site-data-site-settings"/>
</menupopup>
<menupopup id="zenMoveTabsToSyncedWorkspacePopup">
# Popup to move tabs to a synced workspace.
# This would be automatically populated with the list of available synced workspaces.
</menupopup>

View File

@@ -32,7 +32,7 @@
command="Browser:AddBookmarkAs"
flex="1" />
</hbox>
<vbox id="zen-site-data-section-addons" class="zen-site-data-section">
<vbox class="zen-site-data-section">
<hbox class="zen-site-data-section-header">
<label data-l10n-id="unified-extensions-header-title" flex="1" />
<label data-l10n-id="zen-generic-manage" id="zen-site-data-manage-addons" />
@@ -72,7 +72,7 @@
# for this specific button / id
<toolbarbutton id="unified-extensions-manage-extensions"
class="subviewbutton panel-subview-footer-button unified-extensions-manage-extensions"
data-l10n-id="unified-extensions-manage-extensions"
data-l10n-id="unified-extensions-manage-extensions"
hidden="true" />
</vbox>
<vbox class="zen-site-data-section">
@@ -85,7 +85,7 @@
</vbox>
</vbox>
<hbox id="zen-site-data-footer">
<toolbarbutton id="zen-site-data-security-info"
<toolbarbutton id="zen-site-data-security-info"
class="subviewbutton zen-interactive-button" />
<toolbarbutton id="zen-site-data-actions"
class="subviewbutton zen-interactive-button"

View File

@@ -6,7 +6,6 @@
# the window is fully loaded.
# Make sure they are loaded before the global-scripts.inc file.
<script type="text/javascript" src="chrome://browser/content/zen-sets.js"></script>
<script type="text/javascript" src="chrome://browser/content/zen-components/ZenWorkspacesSync.mjs"></script>
<script type="module" src="chrome://browser/content/zen-components/ZenHasPolyfill.mjs"></script>
<script type="module" src="chrome://browser/content/zen-components/ZenWorkspaces.mjs"></script>

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/places/content/editBookmark.js b/browser/components/places/content/editBookmark.js
index f562f19741d882d92365da531b55e2810a0e79ea..9339e1158b074c41fc19bf91cbfde3c4016594b9 100644
index f562f19741d882d92365da531b55e2810a0e79ea..a68ce8191314845c589f3a9f14b56028e0532628 100644
--- a/browser/components/places/content/editBookmark.js
+++ b/browser/components/places/content/editBookmark.js
@@ -387,6 +387,10 @@ var gEditItemOverlay = {
@@ -31,34 +31,11 @@ index f562f19741d882d92365da531b55e2810a0e79ea..9339e1158b074c41fc19bf91cbfde3c4
}
break;
}
@@ -1280,6 +1288,148 @@ var gEditItemOverlay = {
@@ -1280,6 +1288,128 @@ var gEditItemOverlay = {
get bookmarkState() {
return this._bookmarkState;
},
+
+ async _initWorkspaceSelector() {
+ if(document.documentElement.getAttribute("windowtype") === "Places:Organizer") {
+ return;
+ }
+ this._workspaces = await ZenWorkspacesStorage.getWorkspaces();
+
+ const selectElement = this._workspaceSelect;
+
+ // Clear any existing options
+ while (selectElement.firstChild) {
+ selectElement.removeChild(selectElement.firstChild);
+ }
+
+ // For each workspace, create an option element
+ for (let workspace of this._workspaces) {
+ const option = document.createElementNS("http://www.w3.org/1999/xhtml", "option");
+ option.textContent = workspace.name;
+ option.value = workspace.uuid;
+ selectElement.appendChild(option);
+ }
+
+ selectElement.disabled = this.readOnly;
+ },
+ async onWorkspaceSelectionChange(event) {
+ if(document.documentElement.getAttribute("windowtype") === "Places:Organizer") {
+ return;
@@ -129,7 +106,10 @@ index f562f19741d882d92365da531b55e2810a0e79ea..9339e1158b074c41fc19bf91cbfde3c4
+ if(document.documentElement.getAttribute("windowtype") === "Places:Organizer") {
+ return;
+ }
+ this._workspaces = await ZenWorkspacesStorage.getWorkspaces();
+ const { ZenSessionStore } = ChromeUtils.importESModule(
+ "resource:///modules/zen/ZenSessionManager.sys.mjs"
+ );
+ this._workspaces = ZenSessionStore.getClonedSpaces();
+ const workspaceList = this._workspaceList;
+ if(aInfo.node?.bookmarkGuid) {
+ this._selectedWorkspaces = await ZenWorkspaceBookmarksStorage.getBookmarkWorkspaces(aInfo.node.bookmarkGuid);
@@ -180,7 +160,7 @@ index f562f19741d882d92365da531b55e2810a0e79ea..9339e1158b074c41fc19bf91cbfde3c4
};
ChromeUtils.defineLazyGetter(gEditItemOverlay, "_folderTree", () => {
@@ -1318,6 +1468,9 @@ for (let elt of [
@@ -1318,6 +1448,9 @@ for (let elt of [
"locationField",
"keywordField",
"tagsField",

View File

@@ -0,0 +1,21 @@
diff --git a/browser/components/sessionstore/SessionFile.sys.mjs b/browser/components/sessionstore/SessionFile.sys.mjs
index 31140cb8be3b529a0952ca8dc55165690b0e2120..605c9e0aa84da0a2d3171a0573e8cd95e27bd0c4 100644
--- a/browser/components/sessionstore/SessionFile.sys.mjs
+++ b/browser/components/sessionstore/SessionFile.sys.mjs
@@ -22,6 +22,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
RunState: "resource:///modules/sessionstore/RunState.sys.mjs",
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
SessionWriter: "resource:///modules/sessionstore/SessionWriter.sys.mjs",
+ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
});
const PREF_UPGRADE_BACKUP = "browser.sessionstore.upgradeBackup.latestBuildID";
@@ -380,7 +381,7 @@ var SessionFileInternal = {
this._readOrigin = result.origin;
result.noFilesFound = noFilesFound;
-
+ await lazy.ZenSessionStore.readFile();
return result;
},

View File

@@ -0,0 +1,20 @@
diff --git a/browser/components/sessionstore/SessionSaver.sys.mjs b/browser/components/sessionstore/SessionSaver.sys.mjs
index 9141793550f7c7ff6aa63d4c85bf571b4499e2d0..f00314ebf75ac826e1c9cca8af264ff8aae106c0 100644
--- a/browser/components/sessionstore/SessionSaver.sys.mjs
+++ b/browser/components/sessionstore/SessionSaver.sys.mjs
@@ -20,6 +20,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
SessionFile: "resource:///modules/sessionstore/SessionFile.sys.mjs",
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs",
+ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
});
/*
@@ -305,6 +306,7 @@ var SessionSaverInternal = {
this._maybeClearCookiesAndStorage(state);
Glean.sessionRestore.collectData.stopAndAccumulate(timerId);
+ lazy.ZenSessionStore.saveState(state);
return this._writeState(state);
},

View File

@@ -0,0 +1,21 @@
diff --git a/browser/components/sessionstore/SessionStartup.sys.mjs b/browser/components/sessionstore/SessionStartup.sys.mjs
index be23213ae9ec7e59358a17276c6c3764d38d9996..ca5a8ccc916ceeab5140f1278d15233cefbe5815 100644
--- a/browser/components/sessionstore/SessionStartup.sys.mjs
+++ b/browser/components/sessionstore/SessionStartup.sys.mjs
@@ -40,6 +40,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
StartupPerformance:
"resource:///modules/sessionstore/StartupPerformance.sys.mjs",
sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs",
+ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
});
const STATE_RUNNING_STR = "running";
@@ -179,6 +180,8 @@ export var SessionStartup = {
this._initialState = parsed;
}
+ lazy.ZenSessionStore.onFileRead(this._initialState);
+
if (this._initialState == null) {
// No valid session found.
this._sessionType = this.NO_SESSION;

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/sessionstore/SessionStore.sys.mjs b/browser/components/sessionstore/SessionStore.sys.mjs
index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb22176c60c4e 100644
index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..5ce4eb0b21cf4ed2b8e7c6ad0c57e77416a2ab48 100644
--- a/browser/components/sessionstore/SessionStore.sys.mjs
+++ b/browser/components/sessionstore/SessionStore.sys.mjs
@@ -127,6 +127,8 @@ const TAB_EVENTS = [
@@ -11,7 +11,15 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
];
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
@@ -1911,6 +1913,8 @@ var SessionStoreInternal = {
@@ -196,6 +198,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs",
TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
+ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "blankURI", () => {
@@ -1911,6 +1914,8 @@ var SessionStoreInternal = {
case "TabPinned":
case "TabUnpinned":
case "SwapDocShells":
@@ -20,19 +28,68 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
this.saveStateDelayed(win);
break;
case "TabGroupCreate":
@@ -2384,11 +2388,9 @@ var SessionStoreInternal = {
tabbrowser.selectedTab.label;
@@ -2020,6 +2025,10 @@ var SessionStoreInternal = {
this._windows[aWindow.__SSi].isTaskbarTab = true;
}
+ if (aWindow.document.documentElement.hasAttribute("zen-unsynced-window")) {
+ this._windows[aWindow.__SSi].isZenUnsynced = true;
+ }
+
let tabbrowser = aWindow.gBrowser;
// add tab change listeners to all already existing tabs
@@ -2151,7 +2160,6 @@ var SessionStoreInternal = {
if (closedWindowState) {
let newWindowState;
if (
- AppConstants.platform == "macosx" ||
!lazy.SessionStartup.willRestore()
) {
// We want to split the window up into pinned tabs and unpinned tabs.
@@ -2215,6 +2223,15 @@ var SessionStoreInternal = {
});
this._shouldRestoreLastSession = false;
}
+ else if (!aInitialState && isRegularWindow) {
+ let windowPromises = [];
+ for (let window of this._browserWindows) {
+ windowPromises.push(lazy.TabStateFlusher.flushWindow(window));
+ }
+ Promise.all(windowPromises).finally(() => {
+ lazy.ZenSessionStore.restoreNewWindow(aWindow, this);
+ });
+ }
if (this._restoreLastWindow && aWindow.toolbar.visible) {
// always reset (if not a popup window)
@@ -2465,7 +2482,7 @@ var SessionStoreInternal = {
// 2) Flush the window.
// 3) When the flush is complete, revisit our decision to store the window
// in _closedWindows, and add/remove as necessary.
- if (!winData.isPrivate && !winData.isTaskbarTab) {
+ if (!winData.isPrivate && !winData.isTaskbarTab && !winData.isZenUnsynced) {
this.maybeSaveClosedWindow(winData, isLastWindow);
}
- if (AppConstants.platform != "macosx") {
// Until we decide otherwise elsewhere, this window is part of a series
// of closing windows to quit.
winData._shouldRestore = true;
- }
@@ -2486,7 +2503,7 @@ var SessionStoreInternal = {
// Store the window's close date to figure out when each individual tab
// was closed. This timestamp should allow re-arranging data based on how
@@ -3373,7 +3375,7 @@ var SessionStoreInternal = {
// Save non-private windows if they have at
// least one saveable tab or are the last window.
- if (!winData.isPrivate && !winData.isTaskbarTab) {
+ if (!winData.isPrivate && !winData.isTaskbarTab && !winData.isZenUnsynced) {
this.maybeSaveClosedWindow(winData, isLastWindow);
if (!isLastWindow && winData.closedId > -1) {
@@ -2582,6 +2599,7 @@ var SessionStoreInternal = {
let alreadyStored = winIndex != -1;
// If sidebar command is truthy, i.e. sidebar is open, store sidebar settings
let shouldStore = hasSaveableTabs || isLastWindow;
+ lazy.ZenSessionStore.maybeSaveClosedWindow(winData, isLastWindow);
if (shouldStore && !alreadyStored) {
let index = this._closedWindows.findIndex(win => {
@@ -3373,7 +3391,7 @@ var SessionStoreInternal = {
if (!isPrivateWindow && tabState.isPrivate) {
return;
}
@@ -41,12 +98,12 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
return;
}
@@ -4089,6 +4091,12 @@ var SessionStoreInternal = {
@@ -4089,6 +4107,12 @@ var SessionStoreInternal = {
Math.min(tabState.index, tabState.entries.length)
);
tabState.pinned = false;
+ tabState.zenEssential = false;
+ tabState.zenPinnedId = null;
+ tabState.zenSyncId = null;
+ tabState.zenIsGlance = false;
+ tabState.zenGlanceId = null;
+ tabState.zenHasStaticLabel = false;
@@ -54,7 +111,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
if (inBackground === false) {
aWindow.gBrowser.selectedTab = newTab;
@@ -4525,6 +4533,7 @@ var SessionStoreInternal = {
@@ -4525,6 +4549,7 @@ var SessionStoreInternal = {
// Append the tab if we're opening into a different window,
tabIndex: aSource == aTargetWindow ? pos : Infinity,
pinned: state.pinned,
@@ -62,7 +119,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
userContextId: state.userContextId,
skipLoad: true,
preferredRemoteType,
@@ -5374,7 +5383,7 @@ var SessionStoreInternal = {
@@ -5374,7 +5399,7 @@ var SessionStoreInternal = {
for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) {
let tab = tabbrowser.tabs[i];
@@ -71,16 +128,16 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
removableTabs.push(tab);
}
}
@@ -5434,7 +5443,7 @@ var SessionStoreInternal = {
}
@@ -5483,7 +5508,7 @@ var SessionStoreInternal = {
let workspaceID = aWindow.getWorkspaceID();
- if (workspaceID) {
+ if (workspaceID && !(this.isLastRestorableWindow() && AppConstants.platform == "macosx")) {
winData.workspaceID = workspaceID;
}
},
@@ -5625,11 +5634,12 @@ var SessionStoreInternal = {
// collect the data for all windows
for (ix in this._windows) {
- if (this._windows[ix]._restoring || this._windows[ix].isTaskbarTab) {
+ if (this._windows[ix]._restoring || this._windows[ix].isTaskbarTab || this._windows[ix].isZenUnsynced) {
// window data is still in _statesToRestore
continue;
}
@@ -5625,11 +5650,16 @@ var SessionStoreInternal = {
}
let tabbrowser = aWindow.gBrowser;
@@ -90,19 +147,15 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
let winData = this._windows[aWindow.__SSi];
let tabsData = (winData.tabs = []);
+ winData.activeZenSpace = aWindow.gZenWorkspaces?.activeWorkspace || null;
+ winData.splitViewData = aWindow.gZenViewSplitter?.storeDataForSessionStore();
+ winData.folders = aWindow.gZenFolders?.storeDataForSessionStore() || [];
+ winData.spaces = aWindow.gZenWorkspaces?.getWorkspaces();
+
// update the internal state data for this window
for (let tab of tabs) {
if (tab == aWindow.FirefoxViewHandler.tab) {
@@ -5640,6 +5650,7 @@ var SessionStoreInternal = {
tabsData.push(tabData);
}
+ winData.folders = aWindow.gZenFolders?.storeDataForSessionStore() || [];
// update tab group state for this window
winData.groups = [];
for (let tabGroup of aWindow.gBrowser.tabGroups) {
@@ -5652,7 +5663,7 @@ var SessionStoreInternal = {
@@ -5652,7 +5682,7 @@ var SessionStoreInternal = {
// a window is closed, point to the first item in the tab strip instead (it will never be the Firefox View tab,
// since it's only inserted into the tab strip after it's selected).
if (aWindow.FirefoxViewHandler.tab?.selected) {
@@ -111,7 +164,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
winData.title = tabbrowser.tabs[0].label;
}
winData.selected = selectedIndex;
@@ -5765,8 +5776,8 @@ var SessionStoreInternal = {
@@ -5765,8 +5795,8 @@ var SessionStoreInternal = {
// selectTab represents.
let selectTab = 0;
if (overwriteTabs) {
@@ -122,16 +175,17 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
selectTab = Math.min(selectTab, winData.tabs.length);
}
@@ -5809,6 +5820,8 @@ var SessionStoreInternal = {
@@ -5809,6 +5839,9 @@ var SessionStoreInternal = {
winData.tabs,
winData.groups ?? []
);
+ aWindow.gZenFolders?.restoreDataFromSessionStore(winData.folders);
+ aWindow.gZenViewSplitter?.restoreDataFromSessionStore(winData.splitViewData);
+ aWindow.gZenWorkspaces?.restoreWorkspacesFromSessionStore(winData);
this._log.debug(
`restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs`
);
@@ -6372,6 +6385,25 @@ var SessionStoreInternal = {
@@ -6372,6 +6405,25 @@ var SessionStoreInternal = {
// Most of tabData has been restored, now continue with restoring
// attributes that may trigger external events.
@@ -145,8 +199,8 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
+ if (tabData.zenHasStaticLabel) {
+ tab.setAttribute("zen-has-static-label", "true");
+ }
+ if (tabData.zenPinnedId) {
+ tab.setAttribute("zen-pin-id", tabData.zenPinnedId);
+ if (tabData.zenSyncId) {
+ tab.setAttribute("id", tabData.zenSyncId);
+ }
+ if (tabData.zenDefaultUserContextId) {
+ tab.setAttribute("zenDefaultUserContextId", true);
@@ -157,7 +211,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
if (tabData.pinned) {
tabbrowser.pinTab(tab);
@@ -7290,7 +7322,7 @@ var SessionStoreInternal = {
@@ -7290,7 +7342,7 @@ var SessionStoreInternal = {
let groupsToSave = new Map();
for (let tIndex = 0; tIndex < window.tabs.length; ) {
@@ -166,7 +220,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
// Adjust window.selected
if (tIndex + 1 < window.selected) {
window.selected -= 1;
@@ -7305,7 +7337,7 @@ var SessionStoreInternal = {
@@ -7305,7 +7357,7 @@ var SessionStoreInternal = {
);
// We don't want to increment tIndex here.
continue;

View File

@@ -1,13 +1,13 @@
diff --git a/browser/components/sessionstore/TabState.sys.mjs b/browser/components/sessionstore/TabState.sys.mjs
index 82721356d191055bec0d4b0ca49e481221988801..1ea5c394c704da295149443d7794961a12f2060b 100644
index 82721356d191055bec0d4b0ca49e481221988801..80547ec951f881bef134b637730954eb1525c623 100644
--- a/browser/components/sessionstore/TabState.sys.mjs
+++ b/browser/components/sessionstore/TabState.sys.mjs
@@ -85,7 +85,22 @@ class _TabState {
@@ -85,7 +85,24 @@ class _TabState {
tabData.groupId = tab.group.id;
}
+ tabData.zenWorkspace = tab.getAttribute("zen-workspace-id");
+ tabData.zenPinnedId = tab.getAttribute("zen-pin-id");
+ tabData.zenSyncId = tab.getAttribute("id");
+ tabData.zenEssential = tab.getAttribute("zen-essential");
+ tabData.pinned = tabData.pinned || tabData.zenEssential;
+ tabData.zenDefaultUserContextId = tab.getAttribute("zenDefaultUserContextId");
@@ -17,6 +17,8 @@ index 82721356d191055bec0d4b0ca49e481221988801..1ea5c394c704da295149443d7794961a
+ tabData.zenHasStaticLabel = tab.hasAttribute("zen-has-static-label");
+ tabData.zenGlanceId = tab.getAttribute("glance-id");
+ tabData.zenIsGlance = tab.hasAttribute("zen-glance-tab");
+ tabData._zenPinnedInitialState = tab._zenPinnedInitialState;
+ tabData._zenIsActiveTab = tab._zenContentsVisible;
+
tabData.searchMode = tab.ownerGlobal.gURLBar.getSearchMode(browser, true);
+ if (tabData.searchMode?.source === tab.ownerGlobal.UrlbarUtils.RESULT_SOURCE.ZEN_ACTIONS) {
@@ -25,3 +27,12 @@ index 82721356d191055bec0d4b0ca49e481221988801..1ea5c394c704da295149443d7794961a
tabData.userContextId = tab.userContextId || 0;
@@ -98,7 +115,7 @@ class _TabState {
// Copy data from the tab state cache only if the tab has fully finished
// restoring. We don't want to overwrite data contained in __SS_data.
- this.copyFromCache(browser.permanentKey, tabData, options);
+ this.copyFromCache(tab.permanentKey, tabData, options);
// After copyFromCache() was called we check for properties that are kept
// in the cache only while the tab is pending or restoring. Once that

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/drag-and-drop.js b/browser/components/tabbrowser/content/drag-and-drop.js
index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9548dce83 100644
index 97b931c3c7385a52d20204369fcf6d6999053687..13e872a6c11061b5d7e669476072075d0e685eb9 100644
--- a/browser/components/tabbrowser/content/drag-and-drop.js
+++ b/browser/components/tabbrowser/content/drag-and-drop.js
@@ -32,6 +32,9 @@
@@ -12,18 +12,17 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
if (isTab(element)) {
return element;
}
@@ -112,6 +115,10 @@
@@ -112,6 +115,9 @@
}
let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
+ if (draggedTab && dropEffect === "move") {
+ gZenPinnedTabManager.applyDragoverClass(event, draggedTab);
+ gZenViewSplitter.onBrowserDragEndToSplit(event);
+ }
if (
(dropEffect == "move" || dropEffect == "copy") &&
document == draggedTab.ownerDocument &&
@@ -266,6 +273,18 @@
@@ -266,6 +272,18 @@
this._tabDropIndicator.hidden = true;
event.stopPropagation();
@@ -42,7 +41,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
if (draggedTab && dropEffect == "copy") {
let duplicatedDraggedTab;
let duplicatedTabs = [];
@@ -291,8 +310,9 @@
@@ -291,8 +309,9 @@
let translateOffsetY = oldTranslateY % tabHeight;
let newTranslateX = oldTranslateX - translateOffsetX;
let newTranslateY = oldTranslateY - translateOffsetY;
@@ -54,7 +53,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
if (this._isContainerVerticalPinnedGrid(draggedTab)) {
// Update both translate axis for pinned vertical expanded tabs
@@ -308,8 +328,8 @@
@@ -308,8 +327,8 @@
}
} else {
let tabs = this._tabbrowserTabs.ariaFocusableItems.slice(
@@ -65,7 +64,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
);
let size = this._tabbrowserTabs.verticalMode ? "height" : "width";
let screenAxis = this._tabbrowserTabs.verticalMode
@@ -362,11 +382,13 @@
@@ -362,11 +381,13 @@
this._dragToPinPromoCard,
];
let shouldPin =
@@ -79,7 +78,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
isTab(draggedTab) &&
draggedTab.pinned &&
this._tabbrowserTabs.arrowScrollbox.contains(event.target);
@@ -384,6 +406,7 @@
@@ -384,6 +405,7 @@
(oldTranslateY && oldTranslateY != newTranslateY);
} else if (this._tabbrowserTabs.verticalMode) {
shouldTranslate &&= oldTranslateY && oldTranslateY != newTranslateY;
@@ -87,7 +86,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
} else {
shouldTranslate &&= oldTranslateX && oldTranslateX != newTranslateX;
}
@@ -440,7 +463,7 @@
@@ -440,7 +462,7 @@
item.removeAttribute("tabdrop-samewindow");
resolve();
};
@@ -96,7 +95,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
postTransitionCleanup();
} else {
let onTransitionEnd = transitionendEvent => {
@@ -581,6 +604,7 @@
@@ -581,6 +603,7 @@
let nextItem = this._tabbrowserTabs.ariaFocusableItems[newIndex];
let tabGroup = isTab(nextItem) && nextItem.group;
@@ -104,7 +103,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
gBrowser.loadTabs(urls, {
inBackground,
replace,
@@ -618,7 +642,16 @@
@@ -618,7 +641,16 @@
this._expandGroupOnDrop(draggedTab);
}
this._resetTabsAfterDrop(draggedTab.ownerDocument);
@@ -122,7 +121,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
if (
dt.mozUserCancelled ||
dt.dropEffect != "none" ||
@@ -822,7 +855,10 @@
@@ -822,7 +854,10 @@
_getDragTarget(event, { ignoreSides = false } = {}) {
let { target } = event;
while (target) {
@@ -134,7 +133,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
break;
}
target = target.parentNode;
@@ -839,14 +875,17 @@
@@ -839,14 +874,17 @@
return null;
}
}
@@ -154,7 +153,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
!this._tabbrowserTabs.expandOnHover
);
}
@@ -877,7 +916,8 @@
@@ -877,7 +915,8 @@
isTabGroupLabel(draggedTab) &&
draggedTab._dragData?.expandGroupOnDrop
) {
@@ -164,7 +163,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
}
}
@@ -942,10 +982,7 @@
@@ -942,10 +981,7 @@
if (this._isContainerVerticalPinnedGrid(tab)) {
// In expanded vertical mode, the max number of pinned tabs per row is dynamic
// Set this before adjusting dragged tab's position
@@ -176,7 +175,16 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
let tabsPerRow = 0;
let position = RTL_UI
? window.windowUtils.getBoundsWithoutFlushing(
@@ -1112,7 +1149,7 @@
@@ -1055,7 +1091,7 @@
// using updateDragImage. On Linux, we can use a panel.
if (platform == "win" || platform == "macosx") {
captureListener = function () {
- dt.updateDragImage(canvas, dragImageOffset, dragImageOffset);
+ dt.updateDragImage(tab, dragImageOffset, dragImageOffset);
};
} else {
// Create a panel to use it in setDragImage
@@ -1112,7 +1148,7 @@
let dropEffect = this.getDropEffectForTabDrag(event);
let isMovingInTabStrip = !fromTabList && dropEffect == "move";
let collapseTabGroupDuringDrag =
@@ -185,7 +193,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
tab._dragData = {
offsetX: this._tabbrowserTabs.verticalMode
@@ -1122,7 +1159,7 @@
@@ -1122,7 +1158,7 @@
? event.screenY - window.screenY - tabOffset
: event.screenY - window.screenY,
scrollPos:
@@ -194,7 +202,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
? this._tabbrowserTabs.pinnedTabsContainer.scrollPosition
: this._tabbrowserTabs.arrowScrollbox.scrollPosition,
screenX: event.screenX,
@@ -1149,6 +1186,7 @@
@@ -1149,6 +1185,7 @@
if (collapseTabGroupDuringDrag) {
tab.group.collapsed = true;
@@ -202,7 +210,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
}
}
}
@@ -1173,6 +1211,16 @@
@@ -1173,6 +1210,16 @@
if (tabStripItemElement.hasAttribute("dragtarget")) {
return;
}
@@ -219,7 +227,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
let isPinned = tab.pinned;
let numPinned = gBrowser.pinnedTabCount;
let allTabs = this._tabbrowserTabs.ariaFocusableItems;
@@ -1624,10 +1672,7 @@
@@ -1624,10 +1671,7 @@
return;
}
@@ -231,7 +239,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
let directionX = screenX > dragData.animLastScreenX;
let directionY = screenY > dragData.animLastScreenY;
@@ -1636,6 +1681,8 @@
@@ -1636,6 +1680,8 @@
let { width: tabWidth, height: tabHeight } =
draggedTab.getBoundingClientRect();
@@ -240,7 +248,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
let shiftSizeX = tabWidth * movingTabs.length;
let shiftSizeY = tabHeight;
dragData.tabWidth = tabWidth;
@@ -1672,8 +1719,8 @@
@@ -1672,8 +1718,8 @@
let lastBoundX =
lastTabInRow.screenX +
lastTabInRow.getBoundingClientRect().width -
@@ -251,161 +259,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
translateX = Math.min(Math.max(translateX, firstBoundX), lastBoundX);
translateY = Math.min(Math.max(translateY, firstBoundY), lastBoundY);
@@ -1833,13 +1880,18 @@
this._clearDragOverGroupingTimer();
this.#clearPinnedDropIndicatorTimer();
- let isPinned = draggedTab.pinned;
- let numPinned = gBrowser.pinnedTabCount;
+ let isPinned = draggedTab?.group ? draggedTab.group.pinned : draggedTab.pinned;
+ let numPinned = gBrowser._numVisiblePinTabsWithoutCollapsed;
+ let essential = draggedTab.hasAttribute("zen-essential");
+ const isDraggingFolder = isTabGroupLabel(draggedTab) && draggedTab.group?.isZenFolder;
let allTabs = this._tabbrowserTabs.ariaFocusableItems;
let tabs = allTabs.slice(
- isPinned ? 0 : numPinned,
- isPinned ? numPinned : undefined
+ (isPinned && essential) ? 0 : gBrowser._numZenEssentials,
+ isPinned ? (essential ? gBrowser._numZenEssentials : (isDraggingFolder ? numPinned : undefined)) : undefined
);
+ if (draggedTab.group?.hasAttribute("split-view-group")) {
+ draggedTab = draggedTab.group.labelElement;
+ }
if (this._rtlMode) {
tabs.reverse();
@@ -1854,7 +1906,7 @@
let translateAxis = this._tabbrowserTabs.verticalMode
? "translateY"
: "translateX";
- let { width: tabWidth, height: tabHeight } = bounds(draggedTab);
+ let { width: tabWidth, height: tabHeight } = bounds(draggedTab.group?.hasAttribute("split-view-group") ? draggedTab.group : draggedTab);
let tabSize = this._tabbrowserTabs.verticalMode ? tabHeight : tabWidth;
let translateX = event.screenX - dragData.screenX;
let translateY = event.screenY - dragData.screenY;
@@ -1870,6 +1922,12 @@
);
let lastMovingTab = movingTabs.at(-1);
let firstMovingTab = movingTabs[0];
+ if (lastMovingTab.group?.hasAttribute("split-view-group")) {
+ lastMovingTab = lastMovingTab.group;
+ }
+ if (firstMovingTab.group?.hasAttribute("split-view-group")) {
+ firstMovingTab = firstMovingTab.group;
+ }
let endEdge = ele => ele[screenAxis] + bounds(ele)[size];
let lastMovingTabScreen = endEdge(lastMovingTab);
let firstMovingTabScreen = firstMovingTab[screenAxis];
@@ -1884,6 +1942,13 @@
let endBound = this._rtlMode
? endEdge(this._tabbrowserTabs) - lastMovingTabScreen
: periphery[screenAxis] - 1 - lastMovingTabScreen;
+ {
+ let firstTab = tabs.at(this._rtlMode ? -1 : 0);
+ let lastTab = tabs.at(this._rtlMode ? 0 : -1);
+ startBound = firstTab[screenAxis] - firstMovingTabScreen;
+ endBound = endEdge(lastTab) - lastMovingTabScreen;
+ endBound = gZenPinnedTabManager.getLastTabBound(endBound, lastTab, isDraggingFolder);
+ }
translate = Math.min(Math.max(translate, startBound), endBound);
// Center the tab under the cursor if the tab is not under the cursor while dragging
@@ -2075,6 +2140,8 @@
};
let dropElement = getOverlappedElement();
+ if (dropElement?.hasAttribute("split-view-group")) dropElement = dropElement.labelElement;
+ gZenPinnedTabManager.animateSeparatorMove(movingTabs, dropElement, isPinned, event);
let newDropElementIndex;
if (dropElement) {
@@ -2157,7 +2224,7 @@
? Services.prefs.getIntPref(
"browser.tabs.dragDrop.moveOverThresholdPercent"
) / 100
- : 0.5;
+ : Services.prefs.getIntPref('zen.view.drag-and-drop.move-over-threshold') / 100;
moveOverThreshold = Math.min(1, Math.max(0, moveOverThreshold));
let shouldMoveOver = overlapPercent > moveOverThreshold;
if (logicalForward && shouldMoveOver) {
@@ -2190,6 +2257,7 @@
// If dragging a group over another group, don't make it look like it is
// possible to drop the dragged group inside the other group.
if (
+ false &&
isTabGroupLabel(draggedTab) &&
dropElement?.group &&
(!dropElement.group.collapsed ||
@@ -2216,20 +2284,13 @@
let isOutOfBounds = isPinned
? dropElement.elementIndex >= numPinned
: dropElement.elementIndex < numPinned;
- if (isOutOfBounds) {
- // Drop after last pinned tab
- dropElement = this._tabbrowserTabs.ariaFocusableItems[numPinned - 1];
- dropBefore = false;
- }
}
- if (
- gBrowser._tabGroupsEnabled &&
- isTab(draggedTab) &&
- !isPinned &&
- (!numPinned || newDropElementIndex >= numPinned)
- ) {
+ if (isTab(draggedTab) || isTabGroupLabel(draggedTab)) {
let dragOverGroupingThreshold = 1 - moveOverThreshold;
+ if (draggedTab && !dropElement?.group) {
+ gZenFolders.highlightGroupOnDragOver(null);
+ }
let groupingDelay = Services.prefs.getIntPref(
"browser.tabs.dragDrop.createGroup.delayMS"
);
@@ -2237,6 +2298,7 @@
// When dragging tab(s) over an ungrouped tab, signal to the user
// that dropping the tab(s) will create a new tab group.
let shouldCreateGroupOnDrop =
+ false &&
!movingTabsSet.has(dropElement) &&
isTab(dropElement) &&
!dropElement?.group &&
@@ -2245,6 +2307,7 @@
// When dragging tab(s) over a collapsed tab group label, signal to the
// user that dropping the tab(s) will add them to the group.
let shouldDropIntoCollapsedTabGroup =
+ false &&
isTabGroupLabel(dropElement) &&
dropElement.group.collapsed &&
overlapPercent > dragOverGroupingThreshold;
@@ -2302,6 +2365,14 @@
dropElement = dropElementGroup.tabs[0];
dropBefore = true;
}
+ ({ dropElement, colorCode, dropBefore } = gZenFolders.handleDragOverTabGroupLabel(
+ dropElement,
+ draggedTab,
+ overlapPercent,
+ movingTabs,
+ dropBefore,
+ colorCode
+ ));
}
this._setDragOverGroupColor(colorCode);
this._tabbrowserTabs.toggleAttribute(
@@ -2324,10 +2395,11 @@
dragData.dropBefore = dropBefore;
dragData.animDropElementIndex = newDropElementIndex;
+ gZenFolders.setFolderIndentation(movingTabs, dropElement);
// Shift background tabs to leave a gap where the dragged tab
// would currently be dropped.
for (let item of tabs) {
- if (item == draggedTab) {
+ if (item == draggedTab || (item.group?.hasAttribute("split-view-group") && item.group == draggedTab.group)) {
continue;
}
@@ -2417,11 +2489,13 @@
@@ -2417,6 +2463,7 @@
}
finishAnimateTabMove() {
@@ -413,13 +267,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
if (!this.#isMovingTab()) {
return;
}
this.#setMovingTabMode(false);
+ gZenFolders.highlightGroupOnDragOver(null);
for (let item of this._tabbrowserTabs.ariaFocusableItems) {
this._resetGroupTarget(item);
@@ -2457,7 +2531,7 @@
@@ -2457,7 +2504,7 @@
tab.style.left = "";
tab.style.top = "";
tab.style.maxWidth = "";
@@ -428,7 +276,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
}
for (let label of draggedTabDocument.getElementsByClassName(
"tab-group-label-container"
@@ -2467,7 +2541,7 @@
@@ -2467,7 +2514,7 @@
label.style.left = "";
label.style.top = "";
label.style.maxWidth = "";

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tab.js b/browser/components/tabbrowser/content/tab.js
index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..02d70e9b0261f92917d274759838cfbfd6214f77 100644
index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..23b134a8ba674978182415a8bac926f7600f564a 100644
--- a/browser/components/tabbrowser/content/tab.js
+++ b/browser/components/tabbrowser/content/tab.js
@@ -21,6 +21,7 @@
@@ -121,15 +121,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..02d70e9b0261f92917d274759838cfbf
on_click(event) {
if (event.button != 0) {
return;
@@ -575,6 +599,7 @@
)
);
} else {
+ gZenPinnedTabManager._removePinnedAttributes(this, true);
gBrowser.removeTab(this, {
animate: true,
triggeringEvent: event,
@@ -587,6 +612,14 @@
@@ -587,6 +611,14 @@
// (see tabbrowser-tabs 'click' handler).
gBrowser.tabContainer._blockDblClick = true;
}
@@ -144,7 +136,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..02d70e9b0261f92917d274759838cfbf
}
on_dblclick(event) {
@@ -610,6 +643,8 @@
@@ -610,6 +642,8 @@
animate: true,
triggeringEvent: event,
});

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js
index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f53bc059b 100644
index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18e829ca07 100644
--- a/browser/components/tabbrowser/content/tabbrowser.js
+++ b/browser/components/tabbrowser/content/tabbrowser.js
@@ -386,6 +386,7 @@
@@ -10,7 +10,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
const browsers = [];
if (this.#activeSplitView) {
for (const tab of this.#activeSplitView.tabs) {
@@ -450,15 +451,64 @@
@@ -450,15 +451,66 @@
return this.tabContainer.visibleTabs;
}
@@ -18,6 +18,8 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
+ return this.#handleTabMove(...args);
+ }
+
+ get zenTabProgressListener() { return TabProgressListener; }
+
+ get _numVisiblePinTabsWithoutCollapsed() {
+ let i = 0;
+ for (let item of this.tabContainer.ariaFocusableItems) {
@@ -77,7 +79,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
set selectedTab(val) {
if (
gSharedTabWarning.willShowSharedTabWarning(val) ||
@@ -613,6 +663,7 @@
@@ -613,6 +665,7 @@
this.tabpanels.appendChild(panel);
let tab = this.tabs[0];
@@ -85,7 +87,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
tab.linkedPanel = uniqueId;
this._selectedTab = tab;
this._selectedBrowser = browser;
@@ -898,13 +949,17 @@
@@ -898,13 +951,17 @@
}
this.showTab(aTab);
@@ -104,7 +106,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
aTab.setAttribute("pinned", "true");
this._updateTabBarForPinnedTabs();
@@ -917,11 +972,15 @@
@@ -917,11 +974,15 @@
}
this.#handleTabMove(aTab, () => {
@@ -121,7 +123,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
});
aTab.style.marginInlineStart = "";
@@ -1098,6 +1157,8 @@
@@ -1098,6 +1159,8 @@
let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"];
@@ -130,7 +132,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (
aIconURL &&
!LOCAL_PROTOCOLS.some(protocol => aIconURL.startsWith(protocol))
@@ -1107,6 +1168,9 @@
@@ -1107,6 +1170,9 @@
);
return;
}
@@ -140,7 +142,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
let browser = this.getBrowserForTab(aTab);
browser.mIconURL = aIconURL;
@@ -1379,7 +1443,6 @@
@@ -1379,7 +1445,6 @@
// Preview mode should not reset the owner
if (!this._previewMode && !oldTab.selected) {
@@ -148,7 +150,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
}
let lastRelatedTab = this._lastRelatedTabMap.get(oldTab);
@@ -1470,6 +1533,7 @@
@@ -1470,6 +1535,7 @@
if (!this._previewMode) {
newTab.recordTimeFromUnloadToReload();
newTab.updateLastAccessed();
@@ -156,7 +158,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
oldTab.updateLastAccessed();
// if this is the foreground window, update the last-seen timestamps.
if (this.ownerGlobal == BrowserWindowTracker.getTopWindow()) {
@@ -1622,6 +1686,9 @@
@@ -1622,6 +1688,9 @@
}
let activeEl = document.activeElement;
@@ -166,17 +168,20 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// If focus is on the old tab, move it to the new tab.
if (activeEl == oldTab) {
newTab.focus();
@@ -1945,7 +2012,8 @@
@@ -1945,7 +2014,11 @@
}
_setTabLabel(aTab, aLabel, { beforeTabOpen, isContentTitle, isURL } = {}) {
- if (!aLabel || aLabel.includes("about:reader?")) {
+ if (!aTab._zenContentsVisible && !aTab._zenChangeLabelFlag && !aTab._labelIsInitialTitle && !gZenWorkspaces.privateWindowOrDisabled) {
+ return false;
+ }
+ gZenPinnedTabManager.onTabLabelChanged(aTab);
+ if (!aLabel || aLabel.includes("about:reader?") || aTab.hasAttribute("zen-has-static-label")) {
+ if (!aLabel || aLabel.includes("about:reader?") || (aTab.hasAttribute("zen-has-static-label") && !aTab._zenChangeLabelFlag)) {
return false;
}
@@ -2053,7 +2121,7 @@
@@ -2053,7 +2126,7 @@
newIndex = this.selectedTab._tPos + 1;
}
@@ -185,7 +190,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (this.isTabGroupLabel(targetTab)) {
throw new Error(
"Replacing a tab group label with a tab is not supported"
@@ -2328,6 +2396,7 @@
@@ -2328,6 +2401,7 @@
uriIsAboutBlank,
userContextId,
skipLoad,
@@ -193,7 +198,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
} = {}) {
let b = document.createXULElement("browser");
// Use the JSM global to create the permanentKey, so that if the
@@ -2401,8 +2470,7 @@
@@ -2401,8 +2475,7 @@
// we use a different attribute name for this?
b.setAttribute("name", name);
}
@@ -203,7 +208,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
b.setAttribute("transparent", "true");
}
@@ -2567,7 +2635,7 @@
@@ -2567,7 +2640,7 @@
let panel = this.getPanel(browser);
let uniqueId = this._generateUniquePanelID();
@@ -212,7 +217,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
aTab.linkedPanel = uniqueId;
// Inject the <browser> into the DOM if necessary.
@@ -2626,8 +2694,8 @@
@@ -2626,8 +2699,8 @@
// If we transitioned from one browser to two browsers, we need to set
// hasSiblings=false on both the existing browser and the new browser.
if (this.tabs.length == 2) {
@@ -223,7 +228,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
} else {
aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1;
}
@@ -2814,7 +2882,6 @@
@@ -2814,7 +2887,6 @@
this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, {
tabIndex: tab._tPos + 1,
userContextId: tab.userContextId,
@@ -231,16 +236,17 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
focusUrlBar: true,
});
resolve(this.selectedBrowser);
@@ -2923,6 +2990,8 @@
@@ -2923,6 +2995,9 @@
schemelessInput,
hasValidUserGestureActivation = false,
textDirectiveUserActivation = false,
+ _forZenEmptyTab,
+ essential,
+ zenWorkspaceId,
} = {}
) {
// all callers of addTab that pass a params object need to pass
@@ -2933,10 +3002,17 @@
@@ -2933,10 +3008,17 @@
);
}
@@ -258,7 +264,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// If we're opening a foreground tab, set the owner by default.
ownerTab ??= inBackground ? null : this.selectedTab;
@@ -2944,6 +3020,7 @@
@@ -2944,6 +3026,7 @@
if (this.selectedTab.owner) {
this.selectedTab.owner = null;
}
@@ -266,14 +272,16 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// Find the tab that opened this one, if any. This is used for
// determining positioning, and inherited attributes such as the
@@ -2996,6 +3073,19 @@
@@ -2996,6 +3079,21 @@
noInitialLabel,
skipBackgroundNotify,
});
+ if (hasZenDefaultUserContextId) {
+ t.setAttribute("zenDefaultUserContextId", "true");
+ }
+ if (zenForcedWorkspaceId !== undefined) {
+ if (zenWorkspaceId) {
+ t.setAttribute("zen-workspace-id", zenWorkspaceId);
+ } else if (zenForcedWorkspaceId !== undefined) {
+ t.setAttribute("zen-workspace-id", zenForcedWorkspaceId);
+ t.setAttribute("change-workspace", "")
+ }
@@ -286,7 +294,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (insertTab) {
// Insert the tab into the tab container in the correct position.
this.#insertTabAtIndex(t, {
@@ -3004,6 +3094,7 @@
@@ -3004,6 +3102,7 @@
ownerTab,
openerTab,
pinned,
@@ -294,7 +302,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
bulkOrderedOpen,
tabGroup: tabGroup ?? openerTab?.group,
});
@@ -3022,6 +3113,7 @@
@@ -3022,6 +3121,7 @@
openWindowInfo,
skipLoad,
triggeringRemoteType,
@@ -302,7 +310,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
}));
if (focusUrlBar) {
@@ -3146,6 +3238,12 @@
@@ -3146,6 +3246,12 @@
}
}
@@ -315,7 +323,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// Additionally send pinned tab events
if (pinned) {
this.#notifyPinnedStatus(t);
@@ -3349,10 +3447,10 @@
@@ -3349,10 +3455,10 @@
isAdoptingGroup = false,
isUserTriggered = false,
telemetryUserCreateSource = "unknown",
@@ -327,7 +335,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
}
if (!color) {
@@ -3373,9 +3471,14 @@
@@ -3373,9 +3479,14 @@
label,
isAdoptingGroup
);
@@ -344,7 +352,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
);
group.addTabs(tabs);
@@ -3496,7 +3599,7 @@
@@ -3496,7 +3607,7 @@
}
this.#handleTabMove(tab, () =>
@@ -353,7 +361,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
);
}
@@ -3698,6 +3801,7 @@
@@ -3698,6 +3809,7 @@
openWindowInfo,
skipLoad,
triggeringRemoteType,
@@ -361,7 +369,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
}
) {
// If we don't have a preferred remote type (or it is `NOT_REMOTE`), and
@@ -3767,6 +3871,7 @@
@@ -3767,6 +3879,7 @@
openWindowInfo,
name,
skipLoad,
@@ -369,7 +377,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
});
}
@@ -3955,7 +4060,7 @@
@@ -3955,7 +4068,7 @@
// Add a new tab if needed.
if (!tab) {
let createLazyBrowser =
@@ -378,7 +386,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
let url = "about:blank";
if (tabData.entries?.length) {
@@ -3992,8 +4097,10 @@
@@ -3992,8 +4105,10 @@
insertTab: false,
skipLoad: true,
preferredRemoteType,
@@ -390,7 +398,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (select) {
tabToSelect = tab;
}
@@ -4005,7 +4112,8 @@
@@ -4005,7 +4120,8 @@
this.pinTab(tab);
// Then ensure all the tab open/pinning information is sent.
this._fireTabOpen(tab, {});
@@ -400,7 +408,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
let { groupId } = tabData;
const tabGroup = tabGroupWorkingData.get(groupId);
// if a tab refers to a tab group we don't know, skip any group
@@ -4019,7 +4127,10 @@
@@ -4019,7 +4135,10 @@
tabGroup.stateData.id,
tabGroup.stateData.color,
tabGroup.stateData.collapsed,
@@ -412,7 +420,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
);
tabsFragment.appendChild(tabGroup.node);
}
@@ -4064,9 +4175,23 @@
@@ -4064,9 +4183,23 @@
// to remove the old selected tab.
if (tabToSelect) {
let leftoverTab = this.selectedTab;
@@ -428,15 +436,15 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
+ gZenWorkspaces._initialTab._shouldRemove = true;
+ }
+ }
+ }
}
+ else {
+ gZenWorkspaces._tabToRemoveForEmpty = this.selectedTab;
}
+ }
+ this._hasAlreadyInitializedZenSessionStore = true;
if (tabs.length > 1 || !tabs[0].selected) {
this._updateTabsAfterInsert();
@@ -4257,11 +4382,14 @@
@@ -4257,11 +4390,14 @@
if (ownerTab) {
tab.owner = ownerTab;
}
@@ -452,7 +460,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (
!bulkOrderedOpen &&
((openerTab &&
@@ -4273,7 +4401,7 @@
@@ -4273,7 +4409,7 @@
let lastRelatedTab =
openerTab && this._lastRelatedTabMap.get(openerTab);
let previousTab = lastRelatedTab || openerTab || this.selectedTab;
@@ -461,7 +469,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
tabGroup = previousTab.group;
}
if (
@@ -4284,7 +4412,7 @@
@@ -4284,7 +4420,7 @@
) {
elementIndex = Infinity;
} else if (previousTab.visible) {
@@ -470,7 +478,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
} else if (previousTab == FirefoxViewHandler.tab) {
elementIndex = 0;
}
@@ -4312,14 +4440,14 @@
@@ -4312,14 +4448,14 @@
}
// Ensure index is within bounds.
if (tab.pinned) {
@@ -489,7 +497,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (pinned && !itemAfter?.pinned) {
itemAfter = null;
@@ -4330,7 +4458,7 @@
@@ -4330,7 +4466,7 @@
this.tabContainer._invalidateCachedTabs();
@@ -498,7 +506,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (this.isTab(itemAfter) && itemAfter.group == tabGroup) {
// Place at the front of, or between tabs in, the same tab group
this.tabContainer.insertBefore(tab, itemAfter);
@@ -4358,7 +4486,11 @@
@@ -4358,7 +4494,11 @@
const tabContainer = pinned
? this.tabContainer.pinnedTabsContainer
: this.tabContainer;
@@ -510,7 +518,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
}
this._updateTabsAfterInsert();
@@ -4366,6 +4498,7 @@
@@ -4366,6 +4506,7 @@
if (pinned) {
this._updateTabBarForPinnedTabs();
}
@@ -518,17 +526,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
TabBarVisibility.update();
}
@@ -4655,6 +4788,9 @@
return;
}
+ for (let tab of selectedTabs) {
+ gZenPinnedTabManager._removePinnedAttributes(tab, true);
+ }
this.removeTabs(selectedTabs, { isUserTriggered, telemetrySource });
}
@@ -4916,6 +5052,7 @@
@@ -4916,6 +5057,7 @@
telemetrySource,
} = {}
) {
@@ -536,7 +534,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// When 'closeWindowWithLastTab' pref is enabled, closing all tabs
// can be considered equivalent to closing the window.
if (
@@ -5005,6 +5142,7 @@
@@ -5005,6 +5147,7 @@
if (lastToClose) {
this.removeTab(lastToClose, aParams);
}
@@ -544,7 +542,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
} catch (e) {
console.error(e);
}
@@ -5043,6 +5181,12 @@
@@ -5043,6 +5186,12 @@
aTab._closeTimeNoAnimTimerId = Glean.browserTabclose.timeNoAnim.start();
}
@@ -557,7 +555,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// Handle requests for synchronously removing an already
// asynchronously closing tab.
if (!animate && aTab.closing) {
@@ -5057,6 +5201,9 @@
@@ -5057,6 +5206,9 @@
// state).
let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width;
let isLastTab = this.#isLastTabInWindow(aTab);
@@ -567,7 +565,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (
!this._beginRemoveTab(aTab, {
closeWindowFastpath: true,
@@ -5105,7 +5252,13 @@
@@ -5105,7 +5257,13 @@
// We're not animating, so we can cancel the animation stopwatch.
Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId);
aTab._closeTimeAnimTimerId = null;
@@ -582,7 +580,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
return;
}
@@ -5239,7 +5392,7 @@
@@ -5239,7 +5397,7 @@
closeWindowWithLastTab != null
? closeWindowWithLastTab
: !window.toolbar.visible ||
@@ -591,7 +589,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (closeWindow) {
// We've already called beforeunload on all the relevant tabs if we get here,
@@ -5263,6 +5416,7 @@
@@ -5263,6 +5421,7 @@
newTab = true;
}
@@ -599,7 +597,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
aTab._endRemoveArgs = [closeWindow, newTab];
// swapBrowsersAndCloseOther will take care of closing the window without animation.
@@ -5303,13 +5457,7 @@
@@ -5303,13 +5462,7 @@
aTab._mouseleave();
if (newTab) {
@@ -614,7 +612,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
} else {
TabBarVisibility.update();
}
@@ -5442,6 +5590,7 @@
@@ -5442,6 +5595,7 @@
this.tabs[i]._tPos = i;
}
@@ -622,7 +620,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (!this._windowIsClosing) {
// update tab close buttons state
this.tabContainer._updateCloseButtons();
@@ -5663,6 +5812,7 @@
@@ -5663,6 +5817,7 @@
}
let excludeTabs = new Set(aExcludeTabs);
@@ -630,7 +628,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// If this tab has a successor, it should be selectable, since
// hiding or closing a tab removes that tab as a successor.
@@ -5675,13 +5825,13 @@
@@ -5675,13 +5830,13 @@
!excludeTabs.has(aTab.owner) &&
Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")
) {
@@ -646,7 +644,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
);
let tab = this.tabContainer.findNextTab(aTab, {
@@ -5697,7 +5847,7 @@
@@ -5697,7 +5852,7 @@
}
if (tab) {
@@ -655,7 +653,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
}
// If no qualifying visible tab was found, see if there is a tab in
@@ -5718,7 +5868,7 @@
@@ -5718,7 +5873,7 @@
});
}
@@ -664,7 +662,47 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
}
_blurTab(aTab) {
@@ -6124,10 +6274,10 @@
@@ -5729,7 +5884,7 @@
* @returns {boolean}
* False if swapping isn't permitted, true otherwise.
*/
- swapBrowsersAndCloseOther(aOurTab, aOtherTab) {
+ swapBrowsersAndCloseOther(aOurTab, aOtherTab, zenCloseOther = true) {
// Do not allow transfering a private tab to a non-private window
// and vice versa.
if (
@@ -5783,6 +5938,7 @@
// fire the beforeunload event in the process. Close the other
// window if this was its last tab.
if (
+ zenCloseOther &&
!remoteBrowser._beginRemoveTab(aOtherTab, {
adoptedByTab: aOurTab,
closeWindowWithLastTab: true,
@@ -5794,7 +5950,7 @@
// If this is the last tab of the window, hide the window
// immediately without animation before the docshell swap, to avoid
// about:blank being painted.
- let [closeWindow] = aOtherTab._endRemoveArgs;
+ let [closeWindow] = !zenCloseOther ? [false] : aOtherTab._endRemoveArgs;
if (closeWindow) {
let win = aOtherTab.ownerGlobal;
win.windowUtils.suppressAnimation(true);
@@ -5918,11 +6074,13 @@
}
// Finish tearing down the tab that's going away.
+ if (zenCloseOther) {
if (closeWindow) {
aOtherTab.ownerGlobal.close();
} else {
remoteBrowser._endRemoveTab(aOtherTab);
}
+ }
this.setTabTitle(aOurTab);
@@ -6124,10 +6282,10 @@
SessionStore.deleteCustomTabValue(aTab, "hiddenBy");
}
@@ -677,15 +715,33 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
aTab.selected ||
aTab.closing ||
// Tabs that are sharing the screen, microphone or camera cannot be hidden.
@@ -6186,6 +6336,7 @@
@@ -6185,7 +6343,8 @@
*
* @param {MozTabbrowserTab|MozTabbrowserTabGroup|MozTabbrowserTabGroup.labelElement} aTab
*/
replaceTabWithWindow(aTab, aOptions) {
- replaceTabWithWindow(aTab, aOptions) {
+ replaceTabWithWindow(aTab, aOptions, zenForceSync = false) {
+ if (!this.isTab(aTab)) return; // TODO: Handle tab groups
if (this.tabs.length == 1) {
return null;
}
@@ -6319,7 +6470,7 @@
@@ -6209,12 +6368,14 @@
}
// tell a new window to take the "dropped" tab
- return window.openDialog(
+ let win = window.openDialog(
AppConstants.BROWSER_CHROME_URL,
"_blank",
options,
aTab
);
+ win._zenStartupSyncFlag = zenForceSync ? 'synced' : 'unsynced';
+ return win;
}
/**
@@ -6319,7 +6480,7 @@
* `true` if element is a `<tab-group>`
*/
isTabGroup(element) {
@@ -694,7 +750,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
}
/**
@@ -6404,8 +6555,8 @@
@@ -6404,8 +6565,8 @@
}
// Don't allow mixing pinned and unpinned tabs.
@@ -705,7 +761,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
} else {
tabIndex = Math.max(tabIndex, this.pinnedTabCount);
}
@@ -6431,10 +6582,16 @@
@@ -6431,10 +6592,16 @@
this.#handleTabMove(
element,
() => {
@@ -724,7 +780,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (neighbor && this.isTab(element) && tabIndex > element._tPos) {
neighbor.after(element);
} else {
@@ -6492,23 +6649,28 @@
@@ -6492,23 +6659,28 @@
#moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) {
if (this.isTabGroupLabel(targetElement)) {
targetElement = targetElement.group;
@@ -759,7 +815,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
} else if (!element.pinned && targetElement && targetElement.pinned) {
// If the caller asks to move an unpinned element next to a pinned
// tab, move the unpinned element to be the first unpinned element
@@ -6521,14 +6683,34 @@
@@ -6521,14 +6693,34 @@
// move the tab group right before the first unpinned tab.
// 4. Moving a tab group and the first unpinned tab is grouped:
// move the tab group right before the first unpinned tab's tab group.
@@ -795,7 +851,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
element.pinned
? this.tabContainer.pinnedTabsContainer
: this.tabContainer;
@@ -6537,7 +6719,7 @@
@@ -6537,7 +6729,7 @@
element,
() => {
if (moveBefore) {
@@ -804,7 +860,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
} else if (targetElement) {
targetElement.after(element);
} else {
@@ -6607,10 +6789,10 @@
@@ -6607,10 +6799,10 @@
* @param {TabMetricsContext} [metricsContext]
*/
moveTabToGroup(aTab, aGroup, metricsContext) {
@@ -817,7 +873,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
return;
}
if (aTab.group && aTab.group.id === aGroup.id) {
@@ -6656,6 +6838,7 @@
@@ -6656,6 +6848,7 @@
let state = {
tabIndex: tab._tPos,
@@ -825,7 +881,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
};
if (tab.visible) {
state.elementIndex = tab.elementIndex;
@@ -6682,7 +6865,7 @@
@@ -6682,7 +6875,7 @@
let changedTabGroup =
previousTabState.tabGroupId != currentTabState.tabGroupId;
@@ -834,7 +890,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
tab.dispatchEvent(
new CustomEvent("TabMove", {
bubbles: true,
@@ -6723,6 +6906,10 @@
@@ -6723,6 +6916,10 @@
moveActionCallback();
@@ -845,7 +901,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// Clear tabs cache after moving nodes because the order of tabs may have
// changed.
this.tabContainer._invalidateCachedTabs();
@@ -7623,7 +7810,7 @@
@@ -7623,7 +7820,7 @@
// preventDefault(). It will still raise the window if appropriate.
break;
}
@@ -854,7 +910,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
window.focus();
aEvent.preventDefault();
break;
@@ -7640,7 +7827,6 @@
@@ -7640,7 +7837,6 @@
}
case "TabGroupCollapse":
aEvent.target.tabs.forEach(tab => {
@@ -862,7 +918,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
});
break;
case "TabGroupCreateByUser":
@@ -8589,6 +8775,7 @@
@@ -8589,6 +8785,7 @@
aWebProgress.isTopLevel
) {
this.mTab.setAttribute("busy", "true");
@@ -870,7 +926,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
gBrowser._tabAttrModified(this.mTab, ["busy"]);
this.mTab._notselectedsinceload = !this.mTab.selected;
}
@@ -9623,7 +9810,7 @@ var TabContextMenu = {
@@ -9623,7 +9820,7 @@ var TabContextMenu = {
);
contextUnpinSelectedTabs.hidden =
!this.contextTab.pinned || !this.multiselected;
@@ -879,11 +935,3 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// Build Ask Chat items
TabContextMenu.GenAI.buildTabMenu(
document.getElementById("context_askChat"),
@@ -9943,6 +10130,7 @@ var TabContextMenu = {
)
);
} else {
+ gZenPinnedTabManager._removePinnedAttributes(this.contextTab, true);
gBrowser.removeTab(this.contextTab, {
animate: true,
...gBrowser.TabMetrics.userTriggeredContext(

View File

@@ -1,7 +1,16 @@
diff --git a/browser/components/tabbrowser/content/tabs.js b/browser/components/tabbrowser/content/tabs.js
index 6b6c04599fe80983d13d2069ca62b99d8ad70271..a765f2decc3a565226ac8793422474052f476573 100644
index 6b6c04599fe80983d13d2069ca62b99d8ad70271..009a9c398e2434b8b6704ed2c75b0f09ecc22ca1 100644
--- a/browser/components/tabbrowser/content/tabs.js
+++ b/browser/components/tabbrowser/content/tabs.js
@@ -235,7 +235,7 @@
true
)
? new window.TabStacking(this)
- : new window.TabDragAndDrop(this);
+ : new window.ZenDragAndDrop(this);
this.tabDragAndDrop.init();
}
@@ -436,7 +436,7 @@
// and we're not hitting the scroll buttons.
if (

View File

@@ -1,44 +0,0 @@
diff --git a/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs b/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs
index 4db61038e5e476bad3a61dbdb707e5222c1f08f8..9eca13d9cfac3b762917aaaa942267effb743cf7 100644
--- a/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs
@@ -45,11 +45,13 @@ function defaultQuery(conditions = "") {
let query = `
SELECT h.url, h.title, ${SQL_BOOKMARK_TAGS_FRAGMENT}, h.id, t.open_count,
${lazy.PAGES_FRECENCY_FIELD} AS frecency, t.userContextId,
- h.last_visit_date, NULLIF(t.groupId, '') groupId
+ h.last_visit_date, NULLIF(t.groupId, '') groupId, zp.url AS pinned_url, zp.title AS pinned_title
FROM moz_places h
LEFT JOIN moz_openpages_temp t
ON t.url = h.url
AND (t.userContextId = :userContextId OR (t.userContextId <> -1 AND :userContextId IS NULL))
+ LEFT JOIN zen_pins zp
+ ON zp.url = h.url
WHERE (
(:switchTabsEnabled AND t.open_count > 0) OR
${lazy.PAGES_FRECENCY_FIELD} <> 0
@@ -63,7 +65,7 @@ function defaultQuery(conditions = "") {
:matchBehavior, :searchBehavior, NULL)
ELSE
AUTOCOMPLETE_MATCH(:searchString, h.url,
- h.title, '',
+ IFNULL(zp.title, h.title), '',
h.visit_count, h.typed,
0, t.open_count,
:matchBehavior, :searchBehavior, NULL)
@@ -1176,11 +1178,13 @@ class Search {
? lazy.PlacesUtils.toDate(lastVisitPRTime).getTime()
: undefined;
let tabGroup = row.getResultByName("groupId");
+ let pinnedTitle = row.getResultByIndex(12);
+ let pinnedUrl = row.getResultByIndex("pinned_url");
let match = {
placeId,
- value: url,
- comment: bookmarkTitle || historyTitle,
+ value: pinnedUrl || url,
+ comment: pinnedTitle || bookmarkTitle || historyTitle,
icon: UrlbarUtils.getIconForUrl(url),
frecency: frecency || FRECENCY_DEFAULT,
userContextId,

View File

@@ -13,3 +13,4 @@
category app-startup nsBrowserGlue @mozilla.org/browser/browserglue;1 application={ec8030f7-c20a-464f-9b0e-13a3a9e97384}
#include common/Components.manifest
#include sessionstore/SessionComponents.manifest

View File

@@ -0,0 +1,455 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
// Wrap in a block to prevent leaking to window scope.
{
const isTab = (element) => gBrowser.isTab(element);
const isTabGroupLabel = (element) => gBrowser.isTabGroupLabel(element);
/**
* The elements in the tab strip from `this.ariaFocusableItems` that contain
* logical information are:
*
* - <tab> (.tabbrowser-tab)
* - <tab-group> label element (.tab-group-label)
*
* The elements in the tab strip that contain the space inside of the <tabs>
* element are:
*
* - <tab> (.tabbrowser-tab)
* - <tab-group> label element wrapper (.tab-group-label-container)
*
* When working with tab strip items, if you need logical information, you
* can get it directly, e.g. `element.elementIndex` or `element._tPos`. If
* you need spatial information like position or dimensions, then you should
* call this function. For example, `elementToMove(element).getBoundingClientRect()`
* or `elementToMove(element).style.top`.
*
* @param {MozTabbrowserTab|typeof MozTabbrowserTabGroup.labelElement} element
* @returns {MozTabbrowserTab|vbox}
*/
const elementToMove = (element) => {
if (element.group?.hasAttribute('split-view-group')) {
return element.group;
}
if (isTab(element)) {
return element;
}
if (isTabGroupLabel(element)) {
return element.closest('.tab-group-label-container');
}
throw new Error(`Element "${element.tagName}" is not expected to move`);
};
window.ZenDragAndDrop = class extends window.TabDragAndDrop {
constructor(tabbrowserTabs) {
super(tabbrowserTabs);
}
startTabDrag(event, tab, ...args) {
super.startTabDrag(event, tab, ...args);
let dt = event.dataTransfer;
const { offsetX, offsetY } = this.#getDragImageOffset(tab);
dt.updateDragImage(tab, offsetX, offsetY);
}
_animateTabMove(event) {
let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
let dragData = draggedTab._dragData;
let movingTabs = dragData.movingTabs;
let movingTabsSet = dragData.movingTabsSet;
dragData.animLastScreenPos ??= this._tabbrowserTabs.verticalMode
? dragData.screenY
: dragData.screenX;
let allTabs = this._tabbrowserTabs.ariaFocusableItems;
let numEssentials = gBrowser._numZenEssentials;
let isEssential = draggedTab.hasAttribute('zen-essential');
let tabs = allTabs.slice(
isEssential ? 0 : numEssentials,
isEssential ? numEssentials : undefined
);
let screen = this._tabbrowserTabs.verticalMode ? event.screenY : event.screenX;
if (screen == dragData.animLastScreenPos) {
return;
}
let screenForward = screen > dragData.animLastScreenPos;
dragData.animLastScreenPos = screen;
this._clearDragOverGroupingTimer();
if (this._rtlMode) {
tabs.reverse();
}
let bounds = (ele) => window.windowUtils.getBoundsWithoutFlushing(ele);
let logicalForward = screenForward != this._rtlMode;
let screenAxis = this._tabbrowserTabs.verticalMode ? 'screenY' : 'screenX';
let size = this._tabbrowserTabs.verticalMode ? 'height' : 'width';
let { width: tabWidth, height: tabHeight } = bounds(draggedTab);
let tabSize = this._tabbrowserTabs.verticalMode ? tabHeight : tabWidth;
let translateX = event.screenX - dragData.screenX;
let translateY = event.screenY - dragData.screenY;
dragData.tabWidth = tabWidth;
dragData.tabHeight = tabHeight;
dragData.translateX = translateX;
dragData.translateY = translateY;
// Move the dragged tab based on the mouse position.
let periphery = document.getElementById('tabbrowser-arrowscrollbox-periphery');
let lastMovingTab = movingTabs.at(-1);
let firstMovingTab = movingTabs[0];
let endEdge = (ele) => ele[screenAxis] + bounds(ele)[size];
let lastMovingTabScreen = endEdge(lastMovingTab);
let firstMovingTabScreen = firstMovingTab[screenAxis];
let shiftSize = lastMovingTabScreen - firstMovingTabScreen;
let translate = screen - dragData[screenAxis];
// Constrain the range over which the moving tabs can move between the edge of the tabstrip and periphery.
// Add 1 to periphery so we don't overlap it.
let startBound = this._rtlMode
? endEdge(periphery) + 1 - firstMovingTabScreen
: this._tabbrowserTabs[screenAxis] - firstMovingTabScreen;
let endBound = this._rtlMode
? endEdge(this._tabbrowserTabs) - lastMovingTabScreen
: periphery[screenAxis] - 1 - lastMovingTabScreen;
let firstTab = tabs.at(this._rtlMode ? -1 : 0);
let lastTab = tabs.at(this._rtlMode ? 0 : -1);
startBound = firstTab[screenAxis] - firstMovingTabScreen;
endBound = endEdge(lastTab) - lastMovingTabScreen;
translate = Math.min(Math.max(translate, startBound), endBound);
// Center the tab under the cursor if the tab is not under the cursor while dragging
let draggedTabScreenAxis = draggedTab[screenAxis] + translate;
if (
(screen < draggedTabScreenAxis || screen > draggedTabScreenAxis + tabSize) &&
draggedTabScreenAxis + tabSize < endBound &&
draggedTabScreenAxis > startBound
) {
translate = screen - draggedTab[screenAxis] - tabSize / 2;
// Ensure, after the above calculation, we are still within bounds
translate = Math.min(Math.max(translate, startBound), endBound);
}
if (!gBrowser.pinnedTabCount && !this._dragToPinPromoCard.shouldRender) {
let pinnedDropIndicatorMargin = parseFloat(
window.getComputedStyle(this._pinnedDropIndicator).marginInline
);
this._checkWithinPinnedContainerBounds({
firstMovingTabScreen,
lastMovingTabScreen,
pinnedTabsStartEdge: this._rtlMode
? endEdge(this._tabbrowserTabs.arrowScrollbox) + pinnedDropIndicatorMargin
: this[screenAxis],
pinnedTabsEndEdge: this._rtlMode
? endEdge(this._tabbrowserTabs)
: this._tabbrowserTabs.arrowScrollbox[screenAxis] - pinnedDropIndicatorMargin,
translate,
draggedTab,
});
}
dragData.translatePos = translate;
tabs = tabs.filter((t) => !movingTabsSet.has(t) || t == draggedTab);
/**
* When the `draggedTab` is just starting to move, the `draggedTab` is in
* its original location and the `dropElementIndex == draggedTab.elementIndex`.
* Any tabs or tab group labels passed in as `item` will result in a 0 shift
* because all of those items should also continue to appear in their original
* locations.
*
* Once the `draggedTab` is more "backward" in the tab strip than its original
* position, any tabs or tab group labels between the `draggedTab`'s original
* `elementIndex` and the current `dropElementIndex` should shift "forward"
* out of the way of the dragging tabs.
*
* When the `draggedTab` is more "forward" in the tab strip than its original
* position, any tabs or tab group labels between the `draggedTab`'s original
* `elementIndex` and the current `dropElementIndex` should shift "backward"
* out of the way of the dragging tabs.
*
* @param {MozTabbrowserTab|MozTabbrowserTabGroup.label} item
* @param {number} dropElementIndex
* @returns {number}
*/
let getTabShift = (item, dropElementIndex) => {
if (item.elementIndex < draggedTab.elementIndex && item.elementIndex >= dropElementIndex) {
return this._rtlMode ? -shiftSize : shiftSize;
}
if (item.elementIndex > draggedTab.elementIndex && item.elementIndex < dropElementIndex) {
return this._rtlMode ? shiftSize : -shiftSize;
}
return 0;
};
let oldDropElementIndex = dragData.animDropElementIndex ?? movingTabs[0].elementIndex;
/**
* Returns the higher % by which one element overlaps another
* in the tab strip.
*
* When element 1 is further forward in the tab strip:
*
* p1 p2 p1+s1 p2+s2
* | | | |
* ---------------------------------
* ========================
* s1
* ===================
* s2
* ==========
* overlap
*
* When element 2 is further forward in the tab strip:
*
* p2 p1 p2+s2 p1+s1
* | | | |
* ---------------------------------
* ========================
* s2
* ===================
* s1
* ==========
* overlap
*
* @param {number} p1
* Position (x or y value in screen coordinates) of element 1.
* @param {number} s1
* Size (width or height) of element 1.
* @param {number} p2
* Position (x or y value in screen coordinates) of element 2.
* @param {number} s2
* Size (width or height) of element 1.
* @returns {number}
* Percent between 0.0 and 1.0 (inclusive) of element 1 or element 2
* that is overlapped by the other element. If the elements have
* different sizes, then this returns the larger overlap percentage.
*/
function greatestOverlap(p1, s1, p2, s2) {
let overlapSize;
if (p1 < p2) {
// element 1 starts first
overlapSize = p1 + s1 - p2;
} else {
// element 2 starts first
overlapSize = p2 + s2 - p1;
}
// No overlap if size is <= 0
if (overlapSize <= 0) {
return 0;
}
// Calculate the overlap fraction from each element's perspective.
let overlapPercent = Math.max(overlapSize / s1, overlapSize / s2);
return Math.min(overlapPercent, 1);
}
/**
* Determine what tab/tab group label we're dragging over.
*
* When dragging right or downwards, the reference point for overlap is
* the right or bottom edge of the most forward moving tab.
*
* When dragging left or upwards, the reference point for overlap is the
* left or top edge of the most backward moving tab.
*
* @returns {Element|null}
* The tab or tab group label that should be used to visually shift tab
* strip elements out of the way of the dragged tab(s) during a drag
* operation. Note: this is not used to determine where the dragged
* tab(s) will be dropped, it is only used for visual animation at this
* time.
*/
let getOverlappedElement = () => {
let point = (screenForward ? lastMovingTabScreen : firstMovingTabScreen) + translate;
let low = 0;
let high = tabs.length - 1;
while (low <= high) {
let mid = Math.floor((low + high) / 2);
if (tabs[mid] == draggedTab && ++mid > high) {
break;
}
let element = tabs[mid];
let elementForSize = elementToMove(element);
screen = elementForSize[screenAxis] + getTabShift(element, oldDropElementIndex);
if (screen > point) {
high = mid - 1;
} else if (screen + bounds(elementForSize)[size] < point) {
low = mid + 1;
} else {
return element;
}
}
return null;
};
let dropElement = getOverlappedElement();
let newDropElementIndex;
if (dropElement) {
newDropElementIndex = dropElement.elementIndex;
} else {
// When the dragged element(s) moves past a tab strip item, the dragged
// element's leading edge starts dragging over empty space, resulting in
// no overlapping `dropElement`. In these cases, try to fall back to the
// previous animation drop element index to avoid unstable animations
// (tab strip items snapping back and forth to shift out of the way of
// the dragged element(s)).
newDropElementIndex = oldDropElementIndex;
// We always want to have a `dropElement` so that we can determine where to
// logically drop the dragged element(s).
//
// It's tempting to set `dropElement` to
// `this.ariaFocusableItems.at(oldDropElementIndex)`, and that is correct
// for most cases, but there are edge cases:
//
// 1) the drop element index range needs to be one larger than the number of
// items that can move in the tab strip. The simplest example is when all
// tabs are ungrouped and unpinned: for 5 tabs, the drop element index needs
// to be able to go from 0 (become the first tab) to 5 (become the last tab).
// `this.ariaFocusableItems.at(5)` would be `undefined` when dragging to the
// end of the tab strip. In this specific case, it works to fall back to
// setting the drop element to the last tab.
//
// 2) the `elementIndex` values of the tab strip items do not change during
// the drag operation. When dragging the last tab or multiple tabs at the end
// of the tab strip, having `dropElement` fall back to the last tab makes the
// drop element one of the moving tabs. This can have some unexpected behavior
// if not careful. Falling back to the last tab that's not moving (instead of
// just the last tab) helps ensure that `dropElement` is always a stable target
// to drop next to.
//
// 3) all of the elements in the tab strip are moving, in which case there can't
// be a drop element and it should stay `undefined`.
//
// 4) we just started dragging and the `oldDropElementIndex` has its default
// valuë of `movingTabs[0].elementIndex`. In this case, the drop element
// shouldn't be a moving tab, so keep it `undefined`.
let lastPossibleDropElement = this._rtlMode
? tabs.find((t) => t != draggedTab)
: tabs.findLast((t) => t != draggedTab);
let maxElementIndexForDropElement = lastPossibleDropElement?.elementIndex;
if (Number.isInteger(maxElementIndexForDropElement)) {
let index = Math.min(oldDropElementIndex, maxElementIndexForDropElement);
let oldDropElementCandidate = this._tabbrowserTabs.ariaFocusableItems.at(index);
if (!movingTabsSet.has(oldDropElementCandidate)) {
dropElement = oldDropElementCandidate;
}
}
}
let moveOverThreshold;
let overlapPercent;
let dropBefore;
if (dropElement) {
let dropElementForOverlap = elementToMove(dropElement);
let dropElementScreen = dropElementForOverlap[screenAxis];
let dropElementPos = dropElementScreen + getTabShift(dropElement, oldDropElementIndex);
let dropElementSize = bounds(dropElementForOverlap)[size];
let firstMovingTabPos = firstMovingTabScreen + translate;
overlapPercent = greatestOverlap(
firstMovingTabPos,
shiftSize,
dropElementPos,
dropElementSize
);
moveOverThreshold = gBrowser._tabGroupsEnabled
? Services.prefs.getIntPref('browser.tabs.dragDrop.moveOverThresholdPercent') / 100
: 0.5;
moveOverThreshold = Math.min(1, Math.max(0, moveOverThreshold));
let shouldMoveOver = overlapPercent > moveOverThreshold;
if (logicalForward && shouldMoveOver) {
newDropElementIndex++;
} else if (!logicalForward && !shouldMoveOver) {
newDropElementIndex++;
if (newDropElementIndex > oldDropElementIndex) {
// FIXME: Not quite sure what's going on here, but this check
// prevents jittery back-and-forth movement of background tabs
// in certain cases.
newDropElementIndex = oldDropElementIndex;
}
}
// Recalculate the overlap with the updated drop index for when the
// drop element moves over.
dropElementPos = dropElementScreen + getTabShift(dropElement, newDropElementIndex);
overlapPercent = greatestOverlap(
firstMovingTabPos,
shiftSize,
dropElementPos,
dropElementSize
);
dropBefore = firstMovingTabPos < dropElementPos;
if (this._rtlMode) {
dropBefore = !dropBefore;
}
}
this.#applyDragoverIndicator(translate, dropElement, draggedTab);
if (
newDropElementIndex == oldDropElementIndex &&
dropBefore == dragData.dropBefore &&
dropElement == dragData.dropElement
) {
return;
}
dragData.dropElement = dropElement;
dragData.dropBefore = dropBefore;
dragData.animDropElementIndex = newDropElementIndex;
}
#applyDragoverIndicator(translate, dropElement, draggedTab) {
const separation = 8;
let shouldPlayHapticFeedback = false;
if (!dropElement) {
return;
}
translate += draggedTab._dragData.screenY;
let rect = elementToMove(dropElement).getBoundingClientRect();
const indicator = gZenPinnedTabManager.dragIndicator;
const halfSize = rect.height / 2;
let top = 0;
if (translate >= rect.top + halfSize) {
top = Math.round(rect.top + rect.height) + 'px';
} else {
top = Math.round(rect.top) + 'px';
}
if (indicator.style.top !== top) {
shouldPlayHapticFeedback = true;
}
indicator.setAttribute('orientation', 'horizontal');
indicator.style.setProperty('--indicator-left', rect.left + separation / 2 + 'px');
indicator.style.setProperty('--indicator-width', rect.width - separation + 'px');
indicator.style.top = top;
indicator.style.removeProperty('left');
if (shouldPlayHapticFeedback) {
Services.zen.playHapticFeedback();
}
}
#getDragImageOffset(tab) {
const { offsetX, offsetY } = tab._dragData;
const rect = tab.getBoundingClientRect();
return {
offsetX: offsetX - rect.left,
offsetY: offsetY - rect.top,
};
}
};
}

View File

@@ -11,4 +11,6 @@
ChromeUtils.importESModule("chrome://browser/content/zen-components/ZenMods.mjs", { global: "current" });
ChromeUtils.importESModule("chrome://browser/content/zen-components/ZenKeyboardShortcuts.mjs", { global: "current" });
ChromeUtils.importESModule("chrome://browser/content/zen-components/ZenSessionStore.mjs", { global: "current" });
Services.scriptloader.loadSubScript("chrome://browser/content/zen-components/ZenDragAndDrop.js", this);
}

View File

@@ -13,6 +13,7 @@
content/browser/zen-components/ZenSessionStore.mjs (../../zen/common/modules/ZenSessionStore.mjs)
content/browser/zen-components/ZenHasPolyfill.mjs (../../zen/common/modules/ZenHasPolyfill.mjs)
content/browser/zen-components/ZenSidebarNotification.mjs (../../zen/common/modules/ZenSidebarNotification.mjs)
content/browser/zen-components/ZenDragAndDrop.js (../../zen/common/ZenDragAndDrop.js)
content/browser/zen-components/ZenEmojisData.min.mjs (../../zen/common/emojis/ZenEmojisData.min.mjs)
content/browser/zen-components/ZenEmojiPicker.mjs (../../zen/common/emojis/ZenEmojiPicker.mjs)

View File

@@ -38,6 +38,10 @@ export class nsZenMultiWindowFeature {
if (!nsZenMultiWindowFeature.isActiveWindow) {
return;
}
return this.forEachWindow(callback);
}
async forEachWindow(callback) {
for (const browser of nsZenMultiWindowFeature.browsers) {
try {
if (browser.closed) continue;

View File

@@ -17,8 +17,9 @@ class ZenSessionStore extends nsZenPreloadedFeature {
if (tabData.zenWorkspace) {
tab.setAttribute('zen-workspace-id', tabData.zenWorkspace);
}
if (tabData.zenPinnedId) {
tab.setAttribute('zen-pin-id', tabData.zenPinnedId);
// Keep for now, for backward compatibility for window sync to work.
if (tabData.zenSyncId || tabData.zenPinnedId) {
tab.setAttribute('id', tabData.zenSyncId || tabData.zenPinnedId);
}
if (tabData.zenHasStaticLabel) {
tab.setAttribute('zen-has-static-label', 'true');
@@ -32,6 +33,9 @@ class ZenSessionStore extends nsZenPreloadedFeature {
if (tabData.zenPinnedEntry) {
tab.setAttribute('zen-pinned-entry', tabData.zenPinnedEntry);
}
if (tabData._zenPinnedInitialState) {
tab._zenPinnedInitialState = tabData._zenPinnedInitialState;
}
}
async #waitAndCleanup() {

View File

@@ -12,12 +12,7 @@ class ZenStartup {
isReady = false;
async init() {
// important: We do this to ensure that some firefox components
// are initialized before we start our own initialization.
// please, do not remove this line and if you do, make sure to
// test the startup process.
await new Promise((resolve) => setTimeout(resolve, 0));
init() {
this.openWatermark();
this.#initBrowserBackground();
this.#changeSidebarLocation();
@@ -97,6 +92,7 @@ class ZenStartup {
// Just in case we didn't get the right size.
gZenUIManager.updateTabsToolbar();
this.closeWatermark();
document.getElementById('tabbrowser-arrowscrollbox').setAttribute('orient', 'vertical');
this.isReady = true;
});
}

View File

@@ -669,9 +669,7 @@ window.gZenUIManager = {
}
if (
(gZenVerticalTabsManager._hasSetSingleToolbar && gZenVerticalTabsManager._prefsRightSide) ||
(panel?.id === 'zen-unified-site-data-panel' &&
!gZenVerticalTabsManager._hasSetSingleToolbar) ||
(panel?.id === 'unified-extensions-panel' && gZenVerticalTabsManager._hasSetSingleToolbar)
(panel?.id === 'zen-unified-site-data-panel' && !gZenVerticalTabsManager._hasSetSingleToolbar)
) {
block = 'bottomright';
inline = 'topright';
@@ -813,7 +811,6 @@ window.gZenVerticalTabsManager = {
!aItem.isConnected ||
gZenUIManager.testingEnabled ||
!gZenStartup.isReady ||
!gZenPinnedTabManager.hasInitializedPins ||
aItem.group?.hasAttribute('split-view-group')
) {
return;
@@ -1312,14 +1309,6 @@ window.gZenVerticalTabsManager = {
} else {
gBrowser.setTabTitle(this._tabEdited);
}
if (this._tabEdited.getAttribute('zen-pin-id')) {
// Update pin title in storage
await gZenPinnedTabManager.updatePinTitle(
this._tabEdited,
this._tabEdited.label,
!!newName
);
}
// Maybe add some confetti here?!?
gZenUIManager.motion.animate(

View File

@@ -60,3 +60,15 @@
}
}
}
.zen-pseudo-browser-image {
position: absolute;
inset: 0;
opacity: 0.4;
pointer-events: none;
z-index: 2;
}
browser[zen-pseudo-hidden='true'] {
-moz-subtree-hidden-only-visually: 1 !important;
}

View File

@@ -495,22 +495,20 @@ body > #confetti {
}
@media -moz-pref('zen.theme.hide-unified-extensions-button') {
#unified-extensions-button:not([showing]) {
#unified-extensions-button {
display: none !important;
}
}
@media not -moz-pref('zen.theme.hide-unified-extensions-button') {
#zen-site-data-section-addons {
display: none;
}
#unified-extensions-button:not([showing]) {
display: none !important;
}
#zen-site-data-header {
gap: 8px;
align-items: center;
padding: 10px 9px;
padding-bottom: 8px;
padding-bottom: 0;
:root[zen-single-toolbar='true']:not([zen-right-side='true']) & {
flex-direction: row-reverse;
@@ -522,7 +520,7 @@ body > #confetti {
-moz-context-properties: fill;
fill: currentColor;
color: light-dark(rgba(0, 0, 0, 0.8), rgba(255, 255, 255, 0.8));
width: 48px;
max-width: 48px;
height: 32px;
position: relative;

View File

@@ -231,6 +231,13 @@
--toolbox-textcolor: currentColor !important;
}
&[zen-unsynced-window='true'] {
--zen-main-browser-background: linear-gradient(130deg, light-dark(rgb(240, 230, 200), rgb(30, 25, 20)) 0%, light-dark(rgb(220, 200, 150), rgb(50, 45, 40)) 100%);
--zen-main-browser-background-toolbar: var(--zen-main-browser-background);
--zen-primary-color: light-dark(rgb(200, 100, 20), rgb(220, 120, 30)) !important;
--toolbox-textcolor: currentColor !important;
}
--toolbar-field-background-color: var(--zen-colors-input-bg) !important;
--arrowpanel-background: var(--zen-dialog-background) !important;

View File

@@ -116,6 +116,9 @@ export const ZenCustomizableUI = new (class {
#initCreateNewButton(window) {
const button = window.document.getElementById('zen-create-new-button');
button.addEventListener('command', (event) => {
if (window.gZenWorkspaces.privateWindowOrDisabled) {
return window.document.getElementById('cmd_newNavigatorTab').doCommand();
}
if (button.hasAttribute('open')) {
return;
}

View File

@@ -6,138 +6,135 @@ document.addEventListener(
'MozBeforeInitialXULLayout',
() => {
// <commandset id="mainCommandSet"> defined in browser-sets.inc
document
.getElementById('zenCommandSet')
.addEventListener('command', (event) => {
switch (event.target.id) {
case 'cmd_zenCompactModeToggle':
gZenCompactModeManager.toggle();
break;
case 'cmd_zenCompactModeShowSidebar':
gZenCompactModeManager.toggleSidebar();
break;
case 'cmd_toggleCompactModeIgnoreHover':
gZenCompactModeManager.toggle(true);
break;
case 'cmd_zenWorkspaceForward':
gZenWorkspaces.changeWorkspaceShortcut();
break;
case 'cmd_zenWorkspaceBackward':
gZenWorkspaces.changeWorkspaceShortcut(-1);
break;
case 'cmd_zenSplitViewGrid':
gZenViewSplitter.toggleShortcut('grid');
break;
case 'cmd_zenSplitViewVertical':
gZenViewSplitter.toggleShortcut('vsep');
break;
case 'cmd_zenSplitViewHorizontal':
gZenViewSplitter.toggleShortcut('hsep');
break;
case 'cmd_zenSplitViewUnsplit':
gZenViewSplitter.toggleShortcut('unsplit');
break;
case 'cmd_zenSplitViewContextMenu':
gZenViewSplitter.contextSplitTabs();
break;
case 'cmd_zenCopyCurrentURLMarkdown':
gZenCommonActions.copyCurrentURLAsMarkdownToClipboard();
break;
case 'cmd_zenCopyCurrentURL':
gZenCommonActions.copyCurrentURLToClipboard();
break;
case 'cmd_zenPinnedTabReset':
gZenPinnedTabManager.resetPinnedTab(gBrowser.selectedTab);
break;
case 'cmd_zenPinnedTabResetNoTab':
gZenPinnedTabManager.resetPinnedTab();
break;
case 'cmd_zenToggleSidebar':
gZenVerticalTabsManager.toggleExpand();
break;
case 'cmd_zenOpenZenThemePicker':
gZenThemePicker.openThemePicker(event);
break;
case 'cmd_zenChangeWorkspaceTab':
gZenWorkspaces.changeTabWorkspace(
event.sourceEvent.target.getAttribute('zen-workspace-id')
);
break;
case 'cmd_zenToggleTabsOnRight':
gZenVerticalTabsManager.toggleTabsOnRight();
break;
case 'cmd_zenSplitViewLinkInNewTab':
gZenViewSplitter.splitLinkInNewTab();
break;
case 'cmd_zenNewEmptySplit':
setTimeout(() => {
gZenViewSplitter.createEmptySplit();
}, 0);
break;
case 'cmd_zenReplacePinnedUrlWithCurrent':
gZenPinnedTabManager.replacePinnedUrlWithCurrent();
break;
case 'cmd_contextZenAddToEssentials':
gZenPinnedTabManager.addToEssentials();
break;
case 'cmd_contextZenRemoveFromEssentials':
gZenPinnedTabManager.removeEssentials();
break;
case 'cmd_zenCtxDeleteWorkspace':
gZenWorkspaces.contextDeleteWorkspace(event);
break;
case 'cmd_zenChangeWorkspaceName':
gZenVerticalTabsManager.renameTabStart({
target: gZenWorkspaces.activeWorkspaceIndicator.querySelector(
'.zen-current-workspace-indicator-name'
),
});
break;
case 'cmd_zenChangeWorkspaceIcon':
gZenWorkspaces.changeWorkspaceIcon();
break;
case 'cmd_zenReorderWorkspaces':
gZenUIManager.showToast('zen-workspaces-how-to-reorder-title', {
timeout: 9000,
descriptionId: 'zen-workspaces-how-to-reorder-desc',
});
break;
case 'cmd_zenOpenWorkspaceCreation':
gZenWorkspaces.openWorkspaceCreation(event);
break;
case 'cmd_zenOpenFolderCreation':
gZenFolders.createFolder([], {
renameFolder: true,
});
break;
case 'cmd_zenTogglePinTab': {
const currentTab = gBrowser.selectedTab;
if (currentTab && !currentTab.hasAttribute('zen-empty-tab')) {
if (currentTab.pinned) {
gBrowser.unpinTab(currentTab);
} else {
gBrowser.pinTab(currentTab);
}
document.getElementById('zenCommandSet').addEventListener('command', (event) => {
switch (event.target.id) {
case 'cmd_zenCompactModeToggle':
gZenCompactModeManager.toggle();
break;
case 'cmd_zenCompactModeShowSidebar':
gZenCompactModeManager.toggleSidebar();
break;
case 'cmd_toggleCompactModeIgnoreHover':
gZenCompactModeManager.toggle(true);
break;
case 'cmd_zenWorkspaceForward':
gZenWorkspaces.changeWorkspaceShortcut();
break;
case 'cmd_zenWorkspaceBackward':
gZenWorkspaces.changeWorkspaceShortcut(-1);
break;
case 'cmd_zenSplitViewGrid':
gZenViewSplitter.toggleShortcut('grid');
break;
case 'cmd_zenSplitViewVertical':
gZenViewSplitter.toggleShortcut('vsep');
break;
case 'cmd_zenSplitViewHorizontal':
gZenViewSplitter.toggleShortcut('hsep');
break;
case 'cmd_zenSplitViewUnsplit':
gZenViewSplitter.toggleShortcut('unsplit');
break;
case 'cmd_zenSplitViewContextMenu':
gZenViewSplitter.contextSplitTabs();
break;
case 'cmd_zenCopyCurrentURLMarkdown':
gZenCommonActions.copyCurrentURLAsMarkdownToClipboard();
break;
case 'cmd_zenCopyCurrentURL':
gZenCommonActions.copyCurrentURLToClipboard();
break;
case 'cmd_zenPinnedTabReset':
gZenPinnedTabManager.resetPinnedTab(gBrowser.selectedTab);
break;
case 'cmd_zenPinnedTabResetNoTab':
gZenPinnedTabManager.resetPinnedTab();
break;
case 'cmd_zenToggleSidebar':
gZenVerticalTabsManager.toggleExpand();
break;
case 'cmd_zenOpenZenThemePicker':
gZenThemePicker.openThemePicker(event);
break;
case 'cmd_zenChangeWorkspaceTab':
gZenWorkspaces.changeTabWorkspace(
event.sourceEvent.target.getAttribute('zen-workspace-id')
);
break;
case 'cmd_zenToggleTabsOnRight':
gZenVerticalTabsManager.toggleTabsOnRight();
break;
case 'cmd_zenSplitViewLinkInNewTab':
gZenViewSplitter.splitLinkInNewTab();
break;
case 'cmd_zenNewEmptySplit':
setTimeout(() => {
gZenViewSplitter.createEmptySplit();
}, 0);
break;
case 'cmd_zenReplacePinnedUrlWithCurrent':
gZenPinnedTabManager.replacePinnedUrlWithCurrent();
break;
case 'cmd_contextZenAddToEssentials':
gZenPinnedTabManager.addToEssentials();
break;
case 'cmd_contextZenRemoveFromEssentials':
gZenPinnedTabManager.removeEssentials();
break;
case 'cmd_zenCtxDeleteWorkspace':
gZenWorkspaces.contextDeleteWorkspace(event);
break;
case 'cmd_zenChangeWorkspaceName':
gZenVerticalTabsManager.renameTabStart({
target: gZenWorkspaces.activeWorkspaceIndicator.querySelector(
'.zen-current-workspace-indicator-name'
),
});
break;
case 'cmd_zenChangeWorkspaceIcon':
gZenWorkspaces.changeWorkspaceIcon();
break;
case 'cmd_zenReorderWorkspaces':
gZenUIManager.showToast('zen-workspaces-how-to-reorder-title', {
timeout: 9000,
descriptionId: 'zen-workspaces-how-to-reorder-desc',
});
break;
case 'cmd_zenOpenWorkspaceCreation':
gZenWorkspaces.openWorkspaceCreation(event);
break;
case 'cmd_zenOpenFolderCreation':
gZenFolders.createFolder([], {
renameFolder: true,
});
break;
case 'cmd_zenTogglePinTab': {
const currentTab = gBrowser.selectedTab;
if (currentTab && !currentTab.hasAttribute('zen-empty-tab')) {
if (currentTab.pinned) {
gBrowser.unpinTab(currentTab);
} else {
gBrowser.pinTab(currentTab);
}
break;
}
case 'cmd_zenCloseUnpinnedTabs':
gZenWorkspaces.closeAllUnpinnedTabs();
break;
case 'cmd_zenUnloadWorkspace': {
gZenWorkspaces.unloadWorkspace();
break;
}
default:
gZenGlanceManager.handleMainCommandSet(event);
if (event.target.id.startsWith('cmd_zenWorkspaceSwitch')) {
const index = parseInt(event.target.id.replace('cmd_zenWorkspaceSwitch', ''), 10) - 1;
gZenWorkspaces.shortcutSwitchTo(index);
}
break;
break;
}
});
case 'cmd_zenCloseUnpinnedTabs':
gZenWorkspaces.closeAllUnpinnedTabs();
break;
case 'cmd_zenUnloadWorkspace': {
gZenWorkspaces.unloadWorkspace();
break;
}
default:
gZenGlanceManager.handleMainCommandSet(event);
if (event.target.id.startsWith('cmd_zenWorkspaceSwitch')) {
const index = parseInt(event.target.id.replace('cmd_zenWorkspaceSwitch', ''), 10) - 1;
gZenWorkspaces.shortcutSwitchTo(index);
}
break;
}
});
},
{ once: true }
);

View File

@@ -131,9 +131,6 @@ class nsZenDownloadAnimationElement extends HTMLElement {
}
#areTabsOnRightSide() {
const position = Services.prefs.getIntPref('zen.downloads.icon-popup-position', 0);
if (position === 1) return false;
if (position === 2) return true;
return Services.prefs.getBoolPref('zen.tabs.vertical.right-side');
}

View File

@@ -150,7 +150,6 @@ class ZenFolder extends MozTabbrowserTabGroup {
for (let tab of this.allItems.reverse()) {
tab = tab.group.hasAttribute('split-view-group') ? tab.group : tab;
if (tab.hasAttribute('zen-empty-tab')) {
await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id'));
gBrowser.removeTab(tab);
} else {
gBrowser.ungroupTab(tab);
@@ -160,7 +159,6 @@ class ZenFolder extends MozTabbrowserTabGroup {
async delete() {
for (const tab of this.allItemsRecursive) {
await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id'));
if (tab.hasAttribute('zen-empty-tab')) {
// Manually remove the empty tabs as removeTabs() inside removeTabGroup
// does ignore them.

View File

@@ -33,13 +33,10 @@ function formatRelativeTime(timestamp) {
class nsZenFolders extends nsZenDOMOperatedFeature {
#ZEN_MAX_SUBFOLDERS = Services.prefs.getIntPref('zen.folders.max-subfolders', 5);
#ZEN_EDGE_ZONE_THRESHOLD =
Services.prefs.getIntPref('zen.view.drag-and-drop.edge-zone-threshold', 25) / 100;
#popup = null;
#popupTimer = null;
#mouseTimer = null;
#lastHighlightedGroup = null;
#lastFolderContextMenu = null;
@@ -101,20 +98,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
.querySelector('menupopup');
changeFolderSpace.innerHTML = '';
for (const workspace of [...gZenWorkspaces._workspaceCache.workspaces].reverse()) {
const item = document.createXULElement('menuitem');
item.className = 'zen-workspace-context-menu-item';
item.setAttribute('zen-workspace-id', workspace.uuid);
item.setAttribute('disabled', workspace.uuid === gZenWorkspaces.activeWorkspace);
let name = workspace.name;
const iconIsSvg = workspace.icon && workspace.icon.endsWith('.svg');
if (workspace.icon && workspace.icon !== '' && !iconIsSvg) {
name = `${workspace.icon} ${name}`;
}
item.setAttribute('label', name);
if (iconIsSvg) {
item.setAttribute('image', workspace.icon);
item.classList.add('zen-workspace-context-icon');
}
const item = gZenWorkspaces.generateMenuItemForWorkspace(workspace);
item.addEventListener('command', (event) => {
if (!this.#lastFolderContextMenu) return;
this.changeFolderToSpace(
@@ -418,7 +402,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
if (selectedTab) {
selectedTab.setAttribute('zen-workspace-id', newWorkspace.uuid);
selectedTab.removeAttribute('folder-active');
gZenWorkspaces._lastSelectedWorkspaceTabs[newWorkspace.uuid] = selectedTab;
gZenWorkspaces.lastSelectedWorkspaceTabs[newWorkspace.uuid] = selectedTab;
}
resolve();
});
@@ -434,10 +418,10 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
tab.style.height = '';
}
gBrowser.TabStateFlusher.flush(tab.linkedBrowser);
if (gZenWorkspaces._lastSelectedWorkspaceTabs[currentWorkspace.uuid] === tab) {
if (gZenWorkspaces.lastSelectedWorkspaceTabs[currentWorkspace.uuid] === tab) {
// This tab is no longer the last selected tab in the previous workspace because it's being moved to
// the current workspace
delete gZenWorkspaces._lastSelectedWorkspaceTabs[currentWorkspace.uuid];
delete gZenWorkspaces.lastSelectedWorkspaceTabs[currentWorkspace.uuid];
}
}
}
@@ -456,9 +440,9 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
// we may encounter
tab.group.setAttribute('zen-workspace-id', workspaceId);
gBrowser.TabStateFlusher.flush(tab.linkedBrowser);
if (gZenWorkspaces._lastSelectedWorkspaceTabs[workspaceId] === tab) {
if (gZenWorkspaces.lastSelectedWorkspaceTabs[workspaceId] === tab) {
// This tab is no longer the last selected tab in the previous workspace because it's being moved to a new workspace
delete gZenWorkspaces._lastSelectedWorkspaceTabs[workspaceId];
delete gZenWorkspaces.lastSelectedWorkspaceTabs[workspaceId];
}
}
folder.dispatchEvent(new CustomEvent('ZenFolderChangedWorkspace', { bubbles: true }));
@@ -506,9 +490,6 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
tabs = [emptyTab, ...filteredTabs];
const folder = this._createFolderNode(options);
if (options.initialPinId) {
folder.setAttribute('zen-pin-id', options.initialPinId);
}
if (options.insertAfter) {
options.insertAfter.after(folder);
@@ -860,7 +841,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
.open(group.icon, { onlySvgIcons: true })
.then((icon) => {
this.setFolderUserIcon(group, icon);
group.dispatchEvent(new CustomEvent('ZenFolderIconChanged', { bubbles: true }));
group.dispatchEvent(new CustomEvent('TabGroupUpdate', { bubbles: true }));
})
.catch((err) => {
console.error(err);
@@ -938,7 +919,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
if (!parentFolder && folder.hasAttribute('split-view-group')) continue;
const emptyFolderTabs = folder.tabs
.filter((tab) => tab.hasAttribute('zen-empty-tab'))
.map((tab) => tab.getAttribute('zen-pin-id'));
.map((tab) => tab.getAttribute('id'));
let prevSiblingInfo = null;
const prevSibling = folder.previousElementSibling;
@@ -947,9 +928,8 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
if (prevSibling) {
if (gBrowser.isTabGroup(prevSibling)) {
prevSiblingInfo = { type: 'group', id: prevSibling.id };
} else if (gBrowser.isTab(prevSibling) && prevSibling.hasAttribute('zen-pin-id')) {
const zenPinId = prevSibling.getAttribute('zen-pin-id');
prevSiblingInfo = { type: 'tab', id: zenPinId };
} else if (gBrowser.isTab(prevSibling) && prevSibling.hasAttribute('id')) {
prevSiblingInfo = { type: 'tab', id: prevSibling.getAttribute('id') };
} else {
prevSiblingInfo = { type: 'start', id: null };
}
@@ -967,7 +947,6 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
prevSiblingInfo: prevSiblingInfo,
emptyTabIds: emptyFolderTabs,
userIcon: userIcon?.getAttribute('href'),
pinId: folder.getAttribute('zen-pin-id'),
// note: We shouldn't be using the workspace-id anywhere, we are just
// remembering it for the pinned tabs manager to use it later.
workspaceId: folder.getAttribute('zen-workspace-id'),
@@ -994,10 +973,8 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
tabFolderWorkingData.set(folderData.id, workingData);
const oldGroup = document.getElementById(folderData.id);
folderData.emptyTabIds.forEach((zenPinId) => {
oldGroup
?.querySelector(`tab[zen-pin-id="${zenPinId}"]`)
?.setAttribute('zen-empty-tab', true);
folderData.emptyTabIds.forEach((id) => {
oldGroup?.querySelector(`tab[id="${id}"]`)?.setAttribute('zen-empty-tab', true);
});
if (oldGroup) {
if (!folderData.splitViewGroup) {
@@ -1009,7 +986,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
saveOnWindowClose: folderData.saveOnWindowClose,
workspaceId: folderData.workspaceId,
});
folder.setAttribute('zen-pin-id', folderData.pinId);
folder.setAttribute('id', folderData.id);
workingData.node = folder;
oldGroup.before(folder);
} else {
@@ -1041,9 +1018,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
if (parentWorkingData && parentWorkingData.node) {
switch (stateData?.prevSiblingInfo?.type) {
case 'tab': {
const tab = parentWorkingData.node.querySelector(
`[zen-pin-id="${stateData.prevSiblingInfo.id}"]`
);
const tab = document.getElementById(stateData.prevSiblingInfo.id);
tab.after(node);
break;
}
@@ -1081,40 +1056,6 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
this._sessionRestoring = false;
}
/**
* Highlights the given tab group and removes highlight from any previously highlighted group.
* @param {MozTabbrowserTabGroup|undefined|null} folder The folder to highlight, or null to clear highlight.
* @param {Array<MozTabbrowserTab>|null} movingTabs The tabs being moved.
*/
highlightGroupOnDragOver(folder, movingTabs) {
if (folder === this.#lastHighlightedGroup) return;
const tab = movingTabs ? movingTabs[0] : null;
if (this.#lastHighlightedGroup && this.#lastHighlightedGroup !== folder) {
this.#lastHighlightedGroup.removeAttribute('selected');
if (this.#lastHighlightedGroup.collapsed) {
this.updateFolderIcon(this.#lastHighlightedGroup, 'close');
}
this.#lastHighlightedGroup = null;
}
if (
folder &&
(!folder.hasAttribute('split-view-group') || !folder.hasAttribute('selected')) &&
folder !== tab?.group &&
!(
folder.level >= this.#ZEN_MAX_SUBFOLDERS &&
movingTabs?.some((t) => gBrowser.isTabGroupLabel(t))
)
) {
folder.setAttribute('selected', 'true');
folder.style.transform = '';
if (folder.collapsed) {
this.updateFolderIcon(folder, 'open');
}
this.#lastHighlightedGroup = folder;
}
}
/**
* Ungroup a tab from all the active groups it belongs to.
* @param {MozTabbrowserTab[]} tabs The tab to ungroup.
@@ -1125,54 +1066,6 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
}
}
/**
* Handles the dragover logic when dragging a tab or tab group label over another tab group label.
* This function determines where the dragged item should be visually dropped (before/after the group, or inside it)
* and updates related styling and highlighting.
*
* @param {MozTabbrowserTabGroupLabel} currentDropElement The tab group label currently being dragged over.
* @param {MozTabbrowserTab|MozTabbrowserTabGroupLabel} draggedTab The tab or tab group label being dragged.
* @param {number} overlapPercent The percentage of overlap between the dragged item and the drop target.
* @param {Array<MozTabbrowserTab>} movingTabs An array of tabs that are currently being dragged together.
* @param {boolean} currentDropBefore Indicates if the current drop position is before the middle of the drop element.
* @param {string|undefined} currentColorCode The current color code for dragover highlighting.
* @returns {{dropElement: MozTabbrowserTabGroup|MozTabbrowserTab|MozTabbrowserTabGroupLabel, colorCode: string|undefined, dropBefore: boolean}}
* An object containing the updated drop element, color code for highlighting, and drop position.
*/
handleDragOverTabGroupLabel(
currentDropElement,
draggedTab,
overlapPercent,
movingTabs,
currentDropBefore,
currentColorCode
) {
let dropElement = currentDropElement;
let dropBefore = currentDropBefore;
let colorCode = currentColorCode;
const dropElementGroup = dropElement?.isZenFolder ? dropElement : dropElement?.group;
const isSplitGroup = dropElement?.group?.hasAttribute('split-view-group');
let firstGroupElem = dropElementGroup.querySelector('.zen-tab-group-start').nextElementSibling;
if (gBrowser.isTabGroup(firstGroupElem)) firstGroupElem = firstGroupElem.labelElement;
const isInMiddleZone =
overlapPercent >= this.#ZEN_EDGE_ZONE_THRESHOLD &&
overlapPercent <= 1 - this.#ZEN_EDGE_ZONE_THRESHOLD;
const shouldDropInside = isInMiddleZone && !isSplitGroup;
if (shouldDropInside) {
dropElement = firstGroupElem;
dropBefore = true;
this.highlightGroupOnDragOver(dropElementGroup, movingTabs);
} else {
colorCode = undefined;
this.highlightGroupOnDragOver(null);
}
return { dropElement, colorCode, dropBefore };
}
#normalizeGroupItems(items) {
return items
.filter((item) => !item.hasAttribute('zen-empty-tab'))

View File

@@ -13,4 +13,5 @@ DIRS += [
"tests",
"urlbar",
"toolkit",
"sessionstore",
]

View File

@@ -0,0 +1,10 @@
# 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/.
# Browser global components initializing before UI startup
category browser-before-ui-startup resource:///modules/zen/ZenSessionManager.sys.mjs ZenSessionStore.init
category browser-before-ui-startup resource:///modules/zen/ZenWindowSync.sys.mjs ZenWindowSync.init
# App shutdown consumers
category browser-quit-application-granted resource:///modules/zen/ZenWindowSync.sys.mjs ZenWindowSync.uninit

View File

@@ -0,0 +1,367 @@
/* 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 { JSONFile } from 'resource://gre/modules/JSONFile.sys.mjs';
import { XPCOMUtils } from 'resource://gre/modules/XPCOMUtils.sys.mjs';
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
PrivateBrowsingUtils: 'resource://gre/modules/PrivateBrowsingUtils.sys.mjs',
BrowserWindowTracker: 'resource:///modules/BrowserWindowTracker.sys.mjs',
TabGroupState: 'resource:///modules/sessionstore/TabGroupState.sys.mjs',
SessionStore: 'resource:///modules/sessionstore/SessionStore.sys.mjs',
SessionSaver: 'resource:///modules/sessionstore/SessionSaver.sys.mjs',
setTimeout: 'resource://gre/modules/Timer.sys.mjs',
});
XPCOMUtils.defineLazyPreferenceGetter(lazy, 'gShouldLog', 'zen.session-store.log', true);
// Note that changing this hidden pref will make the previous session file
// unused, causing a new session file to be created on next write.
const SHOULD_COMPRESS_FILE = Services.prefs.getBoolPref('zen.session-store.compress-file', true);
const SHOULD_BACKUP_FILE = Services.prefs.getBoolPref('zen.session-store.backup-file', true);
const FILE_NAME = SHOULD_COMPRESS_FILE ? 'zen-sessions.jsonlz4' : 'zen-sessions.json';
const MIGRATION_PREF = 'zen.ui.migration.session-manager-restore';
/**
* Class representing the sidebar object stored in the session file.
* This object holds all the data related to tabs, groups, folders
* and split view state.
*/
class nsZenSidebarObject {
#sidebar = {};
get data() {
return Cu.cloneInto(this.#sidebar, {});
}
set data(data) {
this.#sidebar = data;
}
}
export class nsZenSessionManager {
/**
* The JSON file instance used to read/write session data.
* @type {JSONFile}
*/
#file = null;
/**
* The sidebar object holding tabs, groups, folders and split view data.
* @type {nsZenSidebarObject}
*/
#sidebarObject = new nsZenSidebarObject();
// Called from SessionComponents.manifest on app-startup
init() {
let profileDir = Services.dirsvc.get('ProfD', Ci.nsIFile).path;
let backupFile = null;
if (SHOULD_BACKUP_FILE) {
backupFile = PathUtils.join(profileDir, 'zen-sessions-backup', FILE_NAME);
}
let filePath = PathUtils.join(profileDir, FILE_NAME);
this.#file = new JSONFile({
path: filePath,
compression: SHOULD_COMPRESS_FILE ? 'lz4' : undefined,
backupFile,
});
}
log(...args) {
if (lazy.gShouldLog) {
console.info('ZenSessionManager:', ...args);
}
}
/**
* Gets the spaces data from the Places database for migration.
* This is only called once during the first run after updating
* to a version that uses the new session manager.
*/
async #getSpacesFromDBForMigration() {
try {
const { PlacesUtils } = ChromeUtils.importESModule(
'resource://gre/modules/PlacesUtils.sys.mjs'
);
const db = await PlacesUtils.promiseDBConnection();
const rows = await db.executeCached('SELECT * FROM zen_workspaces ORDER BY created_at ASC');
this._migrationSpaceData = rows.map((row) => ({
uuid: row.getResultByName('uuid'),
name: row.getResultByName('name'),
icon: row.getResultByName('icon'),
containerTabId: row.getResultByName('container_id') ?? 0,
position: row.getResultByName('position'),
theme: row.getResultByName('theme_type')
? {
type: row.getResultByName('theme_type'),
gradientColors: JSON.parse(row.getResultByName('theme_colors')),
opacity: row.getResultByName('theme_opacity'),
rotation: row.getResultByName('theme_rotation'),
texture: row.getResultByName('theme_texture'),
}
: null,
}));
} catch {
/* ignore errors during migration */
}
}
/**
* Reads the session file and populates the sidebar object.
* This should be only called once at startup.
* @see SessionFileInternal.read
*/
async readFile() {
try {
let promises = [];
promises.push(this.#file.load());
if (!Services.prefs.getBoolPref(MIGRATION_PREF, false)) {
promises.push(this.#getSpacesFromDBForMigration());
}
await Promise.all(promises);
} catch (e) {
console.error('ZenSessionManager: Failed to read session file', e);
}
this.#sidebar = this.#file.data || {};
}
/**
* Called when the session file is read. Restores the sidebar data
* into all windows.
*
* @param initialState
* The initial session state read from the session file.
*/
onFileRead(initialState) {
// For the first time after migration, we restore the tabs
// That where going to be restored by SessionStore. The sidebar
// object will always be empty after migration because we haven't
// gotten the opportunity to save the session yet.
if (!Services.prefs.getBoolPref(MIGRATION_PREF, false)) {
Services.prefs.setBoolPref(MIGRATION_PREF, true);
for (const winData of initialState.windows || []) {
winData.spaces = this._migrationSpaceData || [];
}
delete this._migrationSpaceData;
return;
}
// If there's no initial state, nothing to restore. This would
// happen if the file is empty or corrupted.
if (!initialState) {
return;
}
// If there are no windows, we create an empty one. By default,
// firefox would create simply a new empty window, but we want
// to make sure that the sidebar object is properly initialized.
// This would happen on first run after having a single private window
// open when quitting the app, for example.
if (!initialState.windows?.length) {
initialState.windows = [{}];
}
// Restore all windows with the same sidebar object, this will
// guarantee that all tabs, groups, folders and split view data
// are properly synced across all windows.
this.log(`Restoring Zen session data into ${initialState.windows?.length || 0} windows`);
for (const winData of initialState.windows || []) {
this.#restoreWindowData(winData);
}
}
get #sidebar() {
return this.#sidebarObject.data;
}
set #sidebar(data) {
this.#sidebarObject.data = data;
}
/**
* Saves the current session state. Collects data and writes to disk.
*
* @param state
* The current session state.
*/
saveState(state) {
if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing || !state?.windows?.length) {
// Don't save (or even collect) anything in permanent private
// browsing mode. We also don't want to save if there are no windows.
return;
}
this.#collectWindowData(state);
// This would save the data to disk asynchronously or when
// quitting the app.
this.#file.data = this.#sidebar;
this.#file.saveSoon();
this.log(`Saving Zen session data with ${this.#sidebar.tabs?.length || 0} tabs`);
}
/**
* Saves the session data for a closed window if it meets the criteria.
* See SessionStoreInternal.maybeSaveClosedWindow for more details.
*
* @param aWinData - The window data object to save.
* @param isLastWindow - Whether this is the last saveable window.
*/
maybeSaveClosedWindow(aWinData, isLastWindow) {
// We only want to save the *last* normal window that is closed.
// If its not the last window, we can still update the sidebar object
// based on other open windows.
if (aWinData.isPopup || aWinData.isTaskbarTab || aWinData.isZenUnsynced || !isLastWindow) {
return;
}
this.log('Saving closed window session data into Zen session store');
this.saveState({ windows: [aWinData] });
}
/**
* Collects session data for a given window.
*
* @param state
* The current session state.
*/
#collectWindowData(state) {
let sidebarData = this.#sidebar;
if (!sidebarData) {
sidebarData = {};
}
sidebarData.lastCollected = Date.now();
this.#collectTabsData(sidebarData, state);
this.#sidebar = sidebarData;
}
#filterUnusedTabs(tabs) {
return tabs.filter((tab) => {
// We need to ignore empty tabs with no group association
// as they are not useful to restore.
return !(tab.zenIsEmpty && !tab.groupId);
});
}
/**
* Collects session data for all tabs in a given window.
*
* @param sidebarData
* The sidebar data object to populate.
* @param state
* The current session state.
*/
#collectTabsData(sidebarData, state) {
const tabIdRelationMap = new Map();
for (const window of state.windows) {
// Only accept the tabs with `_zenIsActiveTab` set to true from
// every window. We do this to avoid collecting tabs with invalid
// state when multiple windows are open. Note that if we a tab without
// this flag set in any other window, we just add it anyway.
for (const tabData of window.tabs) {
if (!tabIdRelationMap.has(tabData.zenSyncId) || tabData._zenIsActiveTab) {
tabIdRelationMap.set(tabData.zenSyncId, tabData);
}
}
}
sidebarData.tabs = this.#filterUnusedTabs(Array.from(tabIdRelationMap.values()));
sidebarData.folders = state.windows[0].folders;
sidebarData.splitViewData = state.windows[0].splitViewData;
sidebarData.groups = state.windows[0].groups;
sidebarData.spaces = state.windows[0].spaces;
}
/**
* Restores the sidebar data into a given window data object.
* We do this in order to make sure all new window objects
* have the same sidebar data.
*
* @param aWindowData
* The window data object to restore into.
*/
#restoreWindowData(aWindowData) {
const sidebar = this.#sidebar;
if (!sidebar) {
return;
}
aWindowData.tabs = sidebar.tabs || [];
aWindowData.splitViewData = sidebar.splitViewData;
aWindowData.folders = sidebar.folders;
aWindowData.groups = sidebar.groups;
aWindowData.spaces = sidebar.spaces;
}
/**
* Restores a new window with Zen session data. This should be called
* not at startup, but when a new window is opened by the user.
*
* @param aWindow
* The window to restore.
* @param SessionStoreInternal
* The SessionStore module instance.
* @param resolvePromise
* The promise resolver to call when done. We use a promise
* here because out workspace manager always waits for SessionStore
* to restore all the windows before initializing, but when opening
* a new window, that promise is always resolved, meaning it may run
* into a race condition if we try to restore the window synchronously
* here.
*/
restoreNewWindow(aWindow, SessionStoreInternal) {
if (aWindow.gZenWorkspaces?.privateWindowOrDisabled) {
return;
}
this.log('Restoring new window with Zen session data');
const state = lazy.SessionStore.getCurrentState(true);
const windows = (state.windows || []).filter(
(win) => !win.isPrivate && !win.isPopup && !win.isTaskbarTab && !win.isZenUnsynced
);
let windowToClone = windows[0] || {};
let newWindow = Cu.cloneInto(windowToClone, {});
if (windows.length < 2) {
// We only want to restore the sidebar object if we found
// only one normal window to clone from (which is the one
// we are opening).
this.log('Restoring sidebar data into new window');
this.#restoreWindowData(newWindow);
}
newWindow.tabs = this.#filterUnusedTabs(newWindow.tabs || []);
// These are window-specific from the previous window state that
// we don't want to restore into the new window. Otherwise, new
// windows would appear overlapping the previous one, or with
// the same size and position, which should be decided by the
// window manager.
delete newWindow.selected;
delete newWindow.screenX;
delete newWindow.screenY;
delete newWindow.width;
delete newWindow.height;
delete newWindow.sizemode;
delete newWindow.sizemodeBeforeMinimized;
delete newWindow.zIndex;
const newState = { windows: [newWindow] };
this.log(`Cloning window with ${newWindow.tabs.length} tabs`);
SessionStoreInternal._deferredInitialState = newState;
SessionStoreInternal.initializeWindow(aWindow, newState);
}
/**
* Gets the cloned spaces data from the sidebar object.
* This is used during migration to restore spaces into
* the initial session state.
*
* @returns {Array} The cloned spaces data.
*/
getClonedSpaces() {
const sidebar = this.#sidebar;
if (!sidebar || !sidebar.spaces) {
return [];
}
return Cu.cloneInto(sidebar.spaces, {});
}
}
export const ZenSessionStore = new nsZenSessionManager();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
# 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 += [
"ZenSessionManager.sys.mjs",
"ZenWindowSync.sys.mjs",
]

View File

@@ -202,6 +202,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
this.resetTabState(tab, forUnsplit);
if (tab.group && tab.group.hasAttribute('split-view-group')) {
gBrowser.ungroupTab(tab);
this.#dispatchItemEvent('ZenTabRemovedFromSplit', tab);
}
if (group.tabs.length < 2) {
// We need to remove all remaining tabs from the group when unsplitting
@@ -895,6 +896,21 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
}
}
/**
* Dispatches a custom event on a tab.
*
* @param {string} eventName - The name of the event to dispatch.
* @param {HTMLElement} item - The item on which to dispatch the event.
*/
#dispatchItemEvent(eventName, item) {
const event = new CustomEvent(eventName, {
detail: { item },
bubbles: true,
cancelable: false,
});
item.dispatchEvent(event);
}
/**
* Removes a group.
*
@@ -905,6 +921,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
for (const tab of group.tabs.reverse()) {
if (tab.group?.hasAttribute('split-view-group')) {
gBrowser.ungroupTab(tab);
this.#dispatchItemEvent('ZenTabRemovedFromSplit', tab);
}
}
if (this.currentView === groupIndex) {
@@ -1067,9 +1084,13 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
*
* @param {Tab[]} tabs - The tabs to split.
* @param {string|undefined} gridType - The type of grid layout.
* @param {number} initialIndex - The index of the initially active tab.
* use -1 to avoid selecting any tab.
* @return {object|undefined} The split view data or undefined if the split was not performed.
*/
splitTabs(tabs, gridType, initialIndex = 0) {
this.#withoutSplitViewTransition(() => {
const tabIndexToUse = Math.max(0, initialIndex);
return this.#withoutSplitViewTransition(() => {
// TODO: Add support for splitting essential tabs
tabs = tabs.filter((t) => !t.hidden && !t.hasAttribute('zen-empty-tab'));
if (tabs.length < 2 || tabs.length > this.MAX_TABS) {
@@ -1078,7 +1099,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
const existingSplitTab = tabs.find((tab) => tab.splitView);
if (existingSplitTab) {
this._moveTabsToContainer(tabs, tabs[initialIndex]);
this._moveTabsToContainer(tabs, tabs[tabIndexToUse]);
const groupIndex = this._data.findIndex((group) => group.tabs.includes(existingSplitTab));
const group = this._data[groupIndex];
const gridTypeChange = gridType && group.gridType !== gridType;
@@ -1105,7 +1126,8 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
return;
}
this.activateSplitView(group, true);
return;
this.#dispatchItemEvent('ZenSplitViewTabsSplit', group);
return group;
}
// We are here if none of the tabs have been previously split
@@ -1132,8 +1154,8 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
layoutTree: this.calculateLayoutTree(tabs, gridType),
};
this._data.push(splitData);
if (!this._sessionRestoring) {
window.gBrowser.selectedTab = tabs[initialIndex] ?? tabs[0];
if (!this._sessionRestoring && initialIndex >= 0) {
window.gBrowser.selectedTab = tabs[tabIndexToUse] ?? tabs[0];
}
// Add tabs to the split view group
@@ -1150,6 +1172,8 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
return;
}
this.activateSplitView(splitData);
this.#dispatchItemEvent('ZenSplitViewTabsSplit', splitGroup);
return splitData;
});
}
@@ -1855,9 +1879,10 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
}
// We can't create an empty group, so only create if we have tabs
let group = null;
if (tabs?.length) {
// Create a new group with the initial tabs
gBrowser.addTabGroup(tabs, {
group = gBrowser.addTabGroup(tabs, {
label: '',
showCreateUI: false,
insertBefore: tabs[0],
@@ -1865,7 +1890,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
});
}
return null;
return group;
}
storeDataForSessionStore() {
@@ -1936,7 +1961,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
#withoutSplitViewTransition(callback) {
this.tabBrowserPanel.classList.add('zen-split-view-no-transition');
try {
callback();
return callback();
} finally {
requestAnimationFrame(() => {
this.tabBrowserPanel.classList.remove('zen-split-view-no-transition');

View File

@@ -7,23 +7,7 @@ import { nsZenDOMOperatedFeature } from 'chrome://browser/content/zen-components
const lazy = {};
class ZenPinnedTabsObserver {
static ALL_EVENTS = [
'TabPinned',
'TabUnpinned',
'TabMove',
'TabGroupCreate',
'TabGroupRemoved',
'TabGroupMoved',
'ZenFolderRenamed',
'ZenFolderIconChanged',
'TabGroupCollapse',
'TabGroupExpand',
'TabGrouped',
'TabUngrouped',
'ZenFolderChangedWorkspace',
'TabAddedToEssentials',
'TabRemovedFromEssentials',
];
static ALL_EVENTS = ['TabPinned', 'TabUnpinned'];
#listeners = [];
@@ -76,12 +60,11 @@ class ZenPinnedTabsObserver {
}
class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
hasInitializedPins = false;
promiseInitializedPinned = new Promise((resolve) => {
this._resolvePinnedInitializedInternal = resolve;
});
async init() {
init() {
if (!this.enabled) {
return;
}
@@ -103,43 +86,16 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
onTabIconChanged(tab, url = null) {
tab.dispatchEvent(new CustomEvent('ZenTabIconChanged', { bubbles: true, detail: { tab } }));
const iconUrl = url ?? tab.iconImage.src;
if (!iconUrl && tab.hasAttribute('zen-pin-id')) {
try {
setTimeout(async () => {
const favicon = await this.getFaviconAsBase64(tab.linkedBrowser.currentURI);
if (favicon) {
gBrowser.setIcon(tab, favicon);
}
});
} catch {
// Handle error
}
} else {
if (tab.hasAttribute('zen-essential')) {
tab.style.setProperty('--zen-essential-tab-icon', `url(${iconUrl})`);
}
if (tab.hasAttribute('zen-essential')) {
tab.style.setProperty('--zen-essential-tab-icon', `url(${iconUrl})`);
}
}
_onTabResetPinButton(event, tab) {
event.stopPropagation();
const pin = this._pinsCache?.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
if (!pin) {
return;
}
let userContextId;
if (tab.hasAttribute('usercontextid')) {
userContextId = tab.getAttribute('usercontextid');
}
const pinnedUrl = Services.io.newURI(pin.url);
const browser = tab.linkedBrowser;
browser.loadURI(pinnedUrl, {
triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({
userContextId,
}),
});
this.resetPinChangedUrl(tab);
this._resetTabToStoredState(tab);
}
get enabled() {
@@ -150,260 +106,6 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
return lazy.zenTabsEssentialsMax;
}
async refreshPinnedTabs({ init = false } = {}) {
if (!this.enabled) {
return;
}
await ZenPinnedTabsStorage.promiseInitialized;
await this.#initializePinsCache();
setTimeout(async () => {
// Execute in a separate task to avoid blocking the main thread
await SessionStore.promiseAllWindowsRestored;
await gZenWorkspaces.promiseInitialized;
await this.#initializePinnedTabs(init);
if (init) {
this._hasFinishedLoading = true;
}
}, 100);
}
async #initializePinsCache() {
try {
// Get pin data
const pins = await ZenPinnedTabsStorage.getPins();
// Enhance pins with favicons
this._pinsCache = await Promise.all(
pins.map(async (pin) => {
try {
if (pin.isGroup) {
return pin; // Skip groups for now
}
const image = await this.getFaviconAsBase64(Services.io.newURI(pin.url));
return {
...pin,
iconUrl: image || null,
};
} catch {
// If favicon fetch fails, continue without icon
return {
...pin,
iconUrl: null,
};
}
})
);
} catch (ex) {
console.error('Failed to initialize pins cache:', ex);
this._pinsCache = [];
}
this.log(`Initialized pins cache with ${this._pinsCache.length} pins`);
return this._pinsCache;
}
#finishedInitializingPins() {
if (this.hasInitializedPins) {
return;
}
this._resolvePinnedInitializedInternal();
delete this._resolvePinnedInitializedInternal;
this.hasInitializedPins = true;
}
async #initializePinnedTabs(init = false) {
const pins = this._pinsCache;
if (!pins?.length || !init) {
this.#finishedInitializingPins();
return;
}
const pinnedTabsByUUID = new Map();
const pinsToCreate = new Set(pins.map((p) => p.uuid));
// First pass: identify existing tabs and remove those without pins
for (let tab of gZenWorkspaces.allStoredTabs) {
const pinId = tab.getAttribute('zen-pin-id');
if (!pinId) {
continue;
}
if (pinsToCreate.has(pinId)) {
// This is a valid pinned tab that matches a pin
pinnedTabsByUUID.set(pinId, tab);
pinsToCreate.delete(pinId);
if (lazy.zenPinnedTabRestorePinnedTabsToPinnedUrl && init) {
this._resetTabToStoredState(tab);
}
} else {
// This is a pinned tab that no longer has a corresponding pin
gBrowser.removeTab(tab);
}
}
for (const group of gZenWorkspaces.allTabGroups) {
const pinId = group.getAttribute('zen-pin-id');
if (!pinId) {
continue;
}
if (pinsToCreate.has(pinId)) {
// This is a valid pinned group that matches a pin
pinsToCreate.delete(pinId);
}
}
// Second pass: For every existing tab, update its label
// and set 'zen-has-static-label' attribute if it's been edited
for (let pin of pins) {
const tab = pinnedTabsByUUID.get(pin.uuid);
if (!tab) {
continue;
}
tab.removeAttribute('zen-has-static-label'); // So we can set it again
if (pin.title && pin.editedTitle) {
gBrowser._setTabLabel(tab, pin.title, { beforeTabOpen: true });
tab.setAttribute('zen-has-static-label', 'true');
}
}
const groups = new Map();
const pendingTabsInsideGroups = {};
// Third pass: create new tabs for pins that don't have tabs
for (let pin of pins) {
try {
if (!pinsToCreate.has(pin.uuid)) {
continue; // Skip pins that already have tabs
}
if (pin.isGroup) {
const tabs = [];
// If there's already existing tabs, let's use them
for (const [uuid, existingTab] of pinnedTabsByUUID) {
const pinObject = this._pinsCache.find((p) => p.uuid === uuid);
if (pinObject && pinObject.parentUuid === pin.uuid) {
tabs.push(existingTab);
}
}
// We still need to iterate through pending tabs since the database
// query doesn't guarantee the order of insertion
for (const [parentUuid, folderTabs] of Object.entries(pendingTabsInsideGroups)) {
if (parentUuid === pin.uuid) {
tabs.push(...folderTabs);
}
}
const group = gZenFolders.createFolder(tabs, {
label: pin.title,
collapsed: pin.isFolderCollapsed,
initialPinId: pin.uuid,
workspaceId: pin.workspaceUuid,
insertAfter:
groups.get(pin.parentUuid)?.querySelector('.tab-group-container')?.lastChild || null,
});
gZenFolders.setFolderUserIcon(group, pin.folderIcon);
groups.set(pin.uuid, group);
continue;
}
let params = {
skipAnimation: true,
allowInheritPrincipal: false,
skipBackgroundNotify: true,
userContextId: pin.containerTabId || 0,
createLazyBrowser: true,
skipLoad: true,
noInitialLabel: false,
};
// Create and initialize the tab
let newTab = gBrowser.addTrustedTab(pin.url, params);
newTab.setAttribute('zenDefaultUserContextId', true);
// Set initial label/title
if (pin.title) {
gBrowser.setInitialTabTitle(newTab, pin.title);
}
// Set the icon if we have it cached
if (pin.iconUrl) {
gBrowser.setIcon(newTab, pin.iconUrl);
}
newTab.setAttribute('zen-pin-id', pin.uuid);
if (pin.workspaceUuid) {
newTab.setAttribute('zen-workspace-id', pin.workspaceUuid);
}
if (pin.isEssential) {
newTab.setAttribute('zen-essential', 'true');
}
if (pin.editedTitle) {
newTab.setAttribute('zen-has-static-label', 'true');
}
// Initialize browser state if needed
if (!newTab.linkedBrowser._remoteAutoRemoved) {
let state = {
entries: [
{
url: pin.url,
title: pin.title,
triggeringPrincipal_base64: E10SUtils.SERIALIZED_SYSTEMPRINCIPAL,
},
],
userContextId: pin.containerTabId || 0,
image: pin.iconUrl,
};
SessionStore.setTabState(newTab, state);
}
this.log(`Created new pinned tab for pin ${pin.uuid} (isEssential: ${pin.isEssential})`);
gBrowser.pinTab(newTab);
if (pin.parentUuid) {
const parentGroup = groups.get(pin.parentUuid);
if (parentGroup) {
parentGroup.querySelector('.tab-group-container').appendChild(newTab);
} else {
if (pendingTabsInsideGroups[pin.parentUuid]) {
pendingTabsInsideGroups[pin.parentUuid].push(newTab);
} else {
pendingTabsInsideGroups[pin.parentUuid] = [newTab];
}
}
} else {
if (!pin.isEssential) {
const container = gZenWorkspaces.workspaceElement(
pin.workspaceUuid
)?.pinnedTabsContainer;
if (container) {
container.insertBefore(newTab, container.lastChild);
}
} else {
gZenWorkspaces.getEssentialsSection(pin.containerTabId).appendChild(newTab);
}
}
gBrowser.tabContainer._invalidateCachedTabs();
newTab.initialize();
} catch (ex) {
console.error('Failed to initialize pinned tabs:', ex);
}
}
setTimeout(() => {
this.#finishedInitializingPins();
}, 0);
gBrowser._updateTabBarForPinnedTabs();
gZenUIManager.updateTabsToolbar();
}
_onPinnedTabEvent(action, event) {
if (!this.enabled) return;
const tab = event.target;
@@ -413,236 +115,24 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
switch (action) {
case 'TabPinned':
case 'TabAddedToEssentials':
tab._zenClickEventListener = this._zenClickEventListener;
tab.addEventListener('click', tab._zenClickEventListener);
this._setPinnedAttributes(tab);
break;
case 'TabRemovedFromEssentials':
if (tab.pinned) {
this.#onTabMove(tab);
break;
}
// [Fall through]
case 'TabUnpinned':
this._removePinnedAttributes(tab);
if (tab._zenClickEventListener) {
tab.removeEventListener('click', tab._zenClickEventListener);
delete tab._zenClickEventListener;
}
break;
case 'TabMove':
this.#onTabMove(tab);
break;
case 'TabGroupCreate':
this.#onTabGroupCreate(event);
break;
case 'TabGroupRemoved':
this.#onTabGroupRemoved(event);
break;
case 'TabGroupMoved':
this.#onTabGroupMoved(event);
break;
case 'ZenFolderRenamed':
case 'ZenFolderIconChanged':
case 'TabGroupCollapse':
case 'TabGroupExpand':
case 'ZenFolderChangedWorkspace':
this.#updateGroupInfo(event.originalTarget, action);
break;
case 'TabGrouped':
this.#onTabGrouped(event);
break;
case 'TabUngrouped':
this.#onTabUngrouped(event);
break;
default:
console.warn('ZenPinnedTabManager: Unhandled tab event', action);
break;
}
}
async #onTabGroupCreate(event) {
const group = event.originalTarget;
if (!group.isZenFolder) {
return;
}
if (group.hasAttribute('zen-pin-id')) {
return; // Group already exists in storage
}
const workspaceId = group.getAttribute('zen-workspace-id');
let id = await ZenPinnedTabsStorage.createGroup(
group.name,
group.iconURL,
group.collapsed,
workspaceId,
group.getAttribute('zen-pin-id'),
group._pPos
);
group.setAttribute('zen-pin-id', id);
for (const tab of group.tabs) {
// Only add it if the tab is directly under the group
if (
tab.pinned &&
tab.hasAttribute('zen-pin-id') &&
tab.group === group &&
this.hasInitializedPins
) {
const tabPinId = tab.getAttribute('zen-pin-id');
await ZenPinnedTabsStorage.addTabToGroup(tabPinId, id, /* position */ tab._pPos);
}
}
await this.refreshPinnedTabs();
}
async #onTabGrouped(event) {
const tab = event.detail;
const group = tab.group;
if (!group.isZenFolder) {
return;
}
const pinId = group.getAttribute('zen-pin-id');
const tabPinId = tab.getAttribute('zen-pin-id');
const tabPin = this._pinsCache?.find((p) => p.uuid === tabPinId);
if (!tabPin || !tabPin.group) {
return;
}
ZenPinnedTabsStorage.addTabToGroup(tabPinId, pinId, /* position */ tab._pPos);
}
async #onTabUngrouped(event) {
const tab = event.detail;
const group = tab.group;
if (!group?.isZenFolder) {
return;
}
const tabPinId = tab.getAttribute('zen-pin-id');
const tabPin = this._pinsCache?.find((p) => p.uuid === tabPinId);
if (!tabPin) {
return;
}
ZenPinnedTabsStorage.removeTabFromGroup(tabPinId, /* position */ tab._pPos);
}
async #updateGroupInfo(group, action) {
if (!group?.isZenFolder) {
return;
}
const pinId = group.getAttribute('zen-pin-id');
const groupPin = this._pinsCache?.find((p) => p.uuid === pinId);
if (groupPin) {
groupPin.title = group.name;
groupPin.folderIcon = group.iconURL;
groupPin.isFolderCollapsed = group.collapsed;
groupPin.position = group._pPos;
groupPin.parentUuid = group.group?.getAttribute('zen-pin-id') || null;
groupPin.workspaceUuid = group.getAttribute('zen-workspace-id') || null;
await this.savePin(groupPin);
switch (action) {
case 'ZenFolderRenamed':
case 'ZenFolderIconChanged':
case 'TabGroupCollapse':
case 'TabGroupExpand':
break;
default:
for (const item of group.allItems) {
if (gBrowser.isTabGroup(item)) {
await this.#updateGroupInfo(item, action);
} else {
await this.#onTabMove(item);
}
}
}
}
}
async #onTabGroupRemoved(event) {
const group = event.originalTarget;
if (!group.isZenFolder) {
return;
}
await ZenPinnedTabsStorage.removePin(group.getAttribute('zen-pin-id'));
group.removeAttribute('zen-pin-id');
}
async #onTabGroupMoved(event) {
const group = event.originalTarget;
if (!group.isZenFolder) {
return;
}
const newIndex = group._pPos;
const pinId = group.getAttribute('zen-pin-id');
if (!pinId) {
return;
}
for (const tab of group.allItemsRecursive) {
if (tab.pinned && tab.getAttribute('zen-pin-id') === pinId) {
const pin = this._pinsCache.find((p) => p.uuid === pinId);
if (pin) {
pin.position = tab._pPos;
pin.parentUuid = tab.group?.getAttribute('zen-pin-id') || null;
pin.workspaceUuid = group.getAttribute('zen-workspace-id');
await this.savePin(pin, false);
}
break;
}
}
const groupPin = this._pinsCache?.find((p) => p.uuid === pinId);
if (groupPin) {
groupPin.position = newIndex;
groupPin.parentUuid = group.group?.getAttribute('zen-pin-id');
groupPin.workspaceUuid = group.getAttribute('zen-workspace-id');
await this.savePin(groupPin);
}
}
async #onTabMove(tab) {
if (!tab.pinned || !this._pinsCache) {
return;
}
const allTabs = [...gBrowser.tabs, ...gBrowser.tabGroups];
for (let i = 0; i < allTabs.length; i++) {
const otherTab = allTabs[i];
if (
otherTab.pinned &&
otherTab.getAttribute('zen-pin-id') !== tab.getAttribute('zen-pin-id')
) {
const actualPin = this._pinsCache.find(
(pin) => pin.uuid === otherTab.getAttribute('zen-pin-id')
);
if (!actualPin) {
continue;
}
actualPin.position = otherTab._pPos;
actualPin.workspaceUuid = otherTab.getAttribute('zen-workspace-id');
actualPin.parentUuid = otherTab.group?.getAttribute('zen-pin-id') || null;
await this.savePin(actualPin, false);
}
}
const actualPin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
if (!actualPin) {
return;
}
actualPin.position = tab._pPos;
actualPin.isEssential = tab.hasAttribute('zen-essential');
actualPin.parentUuid = tab.group?.getAttribute('zen-pin-id') || null;
actualPin.workspaceUuid = tab.getAttribute('zen-workspace-id') || null;
// There was a bug where the title and hasStaticLabel attribute were not being set
// This is a workaround to fix that
if (tab.hasAttribute('zen-has-static-label')) {
actualPin.editedTitle = true;
actualPin.title = tab.label;
}
await this.savePin(actualPin);
tab.dispatchEvent(
new CustomEvent('ZenPinnedTabMoved', {
detail: { tab },
})
);
#getTabState(tab) {
return JSON.parse(SessionStore.getTabState(tab));
}
async _onTabClick(e) {
@@ -668,110 +158,15 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
async replacePinnedUrlWithCurrent(tab = undefined) {
tab ??= TabContextMenu.contextTab;
if (!tab || !tab.pinned || !tab.getAttribute('zen-pin-id')) {
if (!tab || !tab.pinned) {
return;
}
const browser = tab.linkedBrowser;
const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
if (!pin) {
return;
}
const userContextId = tab.getAttribute('usercontextid');
pin.title = tab.label || browser.contentTitle;
pin.url = browser.currentURI.spec;
pin.workspaceUuid = tab.getAttribute('zen-workspace-id');
pin.userContextId = userContextId ? parseInt(userContextId, 10) : 0;
await this.savePin(pin);
window.gZenWindowSync.setPinnedTabState(tab);
this.resetPinChangedUrl(tab);
await this.refreshPinnedTabs();
gZenUIManager.showToast('zen-pinned-tab-replaced');
}
async _setPinnedAttributes(tab) {
if (
tab.hasAttribute('zen-pin-id') ||
!this._hasFinishedLoading ||
tab.hasAttribute('zen-empty-tab')
) {
return;
}
this.log(`Setting pinned attributes for tab ${tab.linkedBrowser.currentURI.spec}`);
const browser = tab.linkedBrowser;
const uuid = gZenUIManager.generateUuidv4();
const userContextId = tab.getAttribute('usercontextid');
let entry = null;
if (tab.getAttribute('zen-pinned-entry')) {
entry = JSON.parse(tab.getAttribute('zen-pinned-entry'));
}
await this.savePin({
uuid,
title: entry?.title || tab.label || browser.contentTitle,
url: entry?.url || browser.currentURI.spec,
containerTabId: userContextId ? parseInt(userContextId, 10) : 0,
workspaceUuid: tab.getAttribute('zen-workspace-id'),
isEssential: tab.getAttribute('zen-essential') === 'true',
parentUuid: tab.group?.getAttribute('zen-pin-id') || null,
position: tab._pPos,
});
tab.setAttribute('zen-pin-id', uuid);
tab.dispatchEvent(
new CustomEvent('ZenPinnedTabCreated', {
detail: { tab },
})
);
// This is used while migrating old pins to new system - we don't want to refresh when migrating
if (tab.getAttribute('zen-pinned-entry')) {
tab.removeAttribute('zen-pinned-entry');
return;
}
this.onLocationChange(browser);
await this.refreshPinnedTabs();
}
async _removePinnedAttributes(tab, isClosing = false) {
tab.removeAttribute('zen-has-static-label');
if (!tab.getAttribute('zen-pin-id') || this._temporarilyUnpiningEssential) {
return;
}
if (Services.startup.shuttingDown || window.skipNextCanClose) {
return;
}
this.log(`Removing pinned attributes for tab ${tab.getAttribute('zen-pin-id')}`);
await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id'));
this.resetPinChangedUrl(tab);
if (!isClosing) {
tab.removeAttribute('zen-pin-id');
tab.removeAttribute('zen-essential'); // Just in case
if (!tab.hasAttribute('zen-workspace-id') && gZenWorkspaces.workspaceEnabled) {
const workspace = await gZenWorkspaces.getActiveWorkspace();
tab.setAttribute('zen-workspace-id', workspace.uuid);
}
}
await this.refreshPinnedTabs();
tab.dispatchEvent(
new CustomEvent('ZenPinnedTabRemoved', {
detail: { tab },
})
);
}
_initClosePinnedTabShortcut() {
let cmdClose = document.getElementById('cmd_close');
@@ -780,21 +175,6 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
}
async savePin(pin, notifyObservers = true) {
if (!this.hasInitializedPins && !gZenUIManager.testingEnabled) {
return;
}
const existingPin = this._pinsCache.find((p) => p.uuid === pin.uuid);
if (existingPin) {
Object.assign(existingPin, pin);
} else {
// We shouldn't need it, but just in case there's
// a race condition while making new pinned tabs.
this._pinsCache.push(pin);
}
await ZenPinnedTabsStorage.savePin(pin, notifyObservers);
}
async onCloseTabShortcut(
event,
selectedTab = gBrowser.selectedTab,
@@ -841,7 +221,6 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
switch (behavior) {
case 'close': {
for (const tab of pinnedTabs) {
this._removePinnedAttributes(tab, true);
gBrowser.removeTab(tab, { animate: true });
}
break;
@@ -943,35 +322,14 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
_resetTabToStoredState(tab) {
const id = tab.getAttribute('zen-pin-id');
if (!id) {
return;
}
const state = this.#getTabState(tab);
const pin = this._pinsCache.find((pin) => pin.uuid === id);
if (!pin) {
return;
}
const initialState = tab._zenPinnedInitialState;
const tabState = SessionStore.getTabState(tab);
const state = JSON.parse(tabState);
// Remove everything except the entry we want to keep
state.entries = [initialState.entry];
const foundEntryIndex = state.entries?.findIndex((entry) => entry.url === pin.url);
if (foundEntryIndex === -1) {
state.entries = [
{
url: pin.url,
title: pin.title,
triggeringPrincipal_base64: lazy.E10SUtils.SERIALIZED_SYSTEMPRINCIPAL,
},
];
} else {
// Remove everything except the entry we want to keep
const existingEntry = state.entries[foundEntryIndex];
existingEntry.title = pin.title;
state.entries = [existingEntry];
}
state.image = pin.iconUrl || state.image;
state.image = initialState.image;
state.index = 0;
SessionStore.setTabState(tab, state);
@@ -1016,22 +374,15 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
if (tab.hasAttribute('zen-workspace-id')) {
tab.removeAttribute('zen-workspace-id');
}
if (tab.pinned && tab.hasAttribute('zen-pin-id')) {
const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
if (pin) {
pin.isEssential = true;
pin.workspaceUuid = null;
this.savePin(pin);
}
if (tab.pinned) {
gBrowser.zenHandleTabMove(tab, () => {
if (tab.ownerGlobal !== window) {
tab = gBrowser.adoptTab(tab, {
selectTab: tab.selected,
});
tab.setAttribute('zen-essential', 'true');
} else {
section.appendChild(tab);
}
section.appendChild(tab);
});
} else {
gBrowser.pinTab(tab);
@@ -1124,8 +475,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
const isVisible = contextTab.pinned && !contextTab.multiselected;
const zenAddEssential = document.getElementById('context_zen-add-essential');
document.getElementById('context_zen-reset-pinned-tab').hidden =
!isVisible || !contextTab.getAttribute('zen-pin-id');
document.getElementById('context_zen-reset-pinned-tab').hidden = !isVisible;
document.getElementById('context_zen-replace-pinned-url-with-current').hidden = !isVisible;
zenAddEssential.hidden = contextTab.getAttribute('zen-essential') || !!contextTab.group;
zenAddEssential.setAttribute(
@@ -1261,24 +611,25 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
}
async onLocationChange(browser) {
onLocationChange(browser) {
const tab = gBrowser.getTabForBrowser(browser);
if (!tab || !tab.pinned || tab.hasAttribute('zen-essential') || !this._pinsCache) {
return;
}
const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
if (!pin) {
if (
!tab ||
!tab.pinned ||
tab.hasAttribute('zen-essential') ||
!tab._zenPinnedInitialState?.entry
) {
return;
}
// Remove # and ? from the URL
const pinUrl = pin.url.split('#')[0];
const pinUrl = tab._zenPinnedInitialState.entry.url.split('#')[0];
const currentUrl = browser.currentURI.spec.split('#')[0];
// Add an indicator that the pin has been changed
if (pinUrl === currentUrl) {
this.resetPinChangedUrl(tab);
return;
}
this.pinHasChangedUrl(tab, pin);
this.pinHasChangedUrl(tab);
}
resetPinChangedUrl(tab) {
@@ -1290,7 +641,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
tab.style.removeProperty('--zen-original-tab-icon');
}
pinHasChangedUrl(tab, pin) {
pinHasChangedUrl(tab) {
if (tab.hasAttribute('zen-pinned-changed')) {
return;
}
@@ -1299,7 +650,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
} else {
tab.setAttribute('zen-pinned-changed', 'true');
}
tab.style.setProperty('--zen-original-tab-icon', `url(${pin.iconUrl?.spec})`);
tab.style.setProperty('--zen-original-tab-icon', `url(${tab._zenPinnedInitialState.image})`);
}
removeTabContainersDragoverClass(hideIndicator = true) {
@@ -1352,56 +703,6 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
: [separator];
}
animateSeparatorMove(movingTabs, dropElement, isPinned) {
let draggedTab = movingTabs[0];
if (gBrowser.isTabGroupLabel(draggedTab) && draggedTab.group.isZenFolder) {
this._isGoingToPinnedTabs = true;
return;
}
if (draggedTab?.group?.hasAttribute('split-view-group')) {
draggedTab = draggedTab.group;
}
const itemsToCheck = this.dragShiftableItems;
let translate = movingTabs[isPinned ? movingTabs.length - 1 : 0].getBoundingClientRect().top;
if (isPinned) {
const rect = draggedTab.getBoundingClientRect();
translate += rect.height;
}
const draggingTabHeight = movingTabs.reduce((acc, item) => {
return acc + window.windowUtils.getBoundsWithoutFlushing(item).height;
}, 0);
if (typeof this._topToNormalTabs === 'undefined') {
const rects = itemsToCheck.map((item) => window.windowUtils.getBoundsWithoutFlushing(item));
this._topToNormalTabs = rects[0].top + rects.at(-1).height / (isPinned ? 2 : 4);
}
let topToNormalTabs = this._topToNormalTabs;
const isGoingToPinnedTabs =
translate < topToNormalTabs && gBrowser.pinnedTabCount - gBrowser._numZenEssentials > 0;
const multiplier = isGoingToPinnedTabs !== isPinned ? (isGoingToPinnedTabs ? 1 : -1) : 0;
this._isGoingToPinnedTabs = isGoingToPinnedTabs;
if (!dropElement) {
itemsToCheck.forEach((item) => {
item.style.transform = `translateY(${draggingTabHeight * multiplier}px)`;
});
}
}
getLastTabBound(lastBound, lastTab, isDraggingFolder = false) {
if (!lastTab.pinned || isDraggingFolder) {
return lastBound;
}
const shiftedItems = this.dragShiftableItems;
let totalHeight = shiftedItems.reduce((acc, item) => {
return acc + window.windowUtils.getBoundsWithoutFlushing(item).height;
}, 0);
if (shiftedItems.length === 1) {
// Means the new tab button is not at the top or not visible
const lastTabRect = window.windowUtils.getBoundsWithoutFlushing(lastTab);
totalHeight += lastTabRect.height;
}
return lastBound + totalHeight + 6;
}
get dragIndicator() {
if (!this._dragIndicator) {
this._dragIndicator = document.createElement('div');
@@ -1415,39 +716,13 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
return document.documentElement.getAttribute('zen-sidebar-expanded') === 'true';
}
async updatePinTitle(tab, newTitle, isEdited = true, notifyObservers = true) {
const uuid = tab.getAttribute('zen-pin-id');
await ZenPinnedTabsStorage.updatePinTitle(uuid, newTitle, isEdited, notifyObservers);
await this.refreshPinnedTabs();
const browsers = Services.wm.getEnumerator('navigator:browser');
// update the label for the same pin across all windows
for (const browser of browsers) {
const tabs = browser.gBrowser.tabs;
// Fix pinned cache for the browser
const browserCache = browser.gZenPinnedTabManager?._pinsCache;
if (browserCache) {
const pin = browserCache.find((pin) => pin.uuid === uuid);
if (pin) {
pin.title = newTitle;
pin.editedTitle = isEdited;
}
}
for (let i = 0; i < tabs.length; i++) {
const tabToEdit = tabs[i];
if (tabToEdit.getAttribute('zen-pin-id') === uuid && tabToEdit !== tab) {
tabToEdit.removeAttribute('zen-has-static-label');
if (isEdited) {
gBrowser._setTabLabel(tabToEdit, newTitle);
tabToEdit.setAttribute('zen-has-static-label', 'true');
} else {
gBrowser.setTabTitle(tabToEdit);
}
break;
}
}
async updatePinTitle(tab, newTitle, isEdited = true) {
tab.removeAttribute('zen-has-static-label');
if (isEdited) {
gBrowser._setTabLabel(tab, newTitle);
tab.setAttribute('zen-has-static-label', 'true');
} else {
gBrowser.setTabTitle(tab);
}
}
@@ -1563,19 +838,8 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
}
async onTabLabelChanged(tab) {
if (!this._pinsCache) {
return;
}
// If our current pin in the cache point to about:blank, we need to update the entry
const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
if (!pin) {
return;
}
if (pin.url === 'about:blank' && tab.linkedBrowser.currentURI.spec !== 'about:blank') {
await this.replacePinnedUrlWithCurrent(tab);
}
onTabLabelChanged(tab) {
tab.dispatchEvent(new CustomEvent('ZenTabLabelChanged', { bubbles: true, detail: { tab } }));
}
}

View File

@@ -1,660 +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/.
window.ZenPinnedTabsStorage = {
_saveCache: [],
async init() {
await this._ensureTable();
},
async _ensureTable() {
await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage._ensureTable', async (db) => {
// Create the pins table if it doesn't exist
await db.execute(`
CREATE TABLE IF NOT EXISTS zen_pins (
id INTEGER PRIMARY KEY,
uuid TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
url TEXT,
container_id INTEGER,
workspace_uuid TEXT,
position INTEGER NOT NULL DEFAULT 0,
is_essential BOOLEAN NOT NULL DEFAULT 0,
is_group BOOLEAN NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
const columns = await db.execute(`PRAGMA table_info(zen_pins)`);
const columnNames = columns.map((row) => row.getResultByName('name'));
// Helper function to add column if it doesn't exist
const addColumnIfNotExists = async (columnName, definition) => {
if (!columnNames.includes(columnName)) {
await db.execute(`ALTER TABLE zen_pins ADD COLUMN ${columnName} ${definition}`);
}
};
await addColumnIfNotExists('edited_title', 'BOOLEAN NOT NULL DEFAULT 0');
await addColumnIfNotExists('is_folder_collapsed', 'BOOLEAN NOT NULL DEFAULT 0');
await addColumnIfNotExists('folder_icon', 'TEXT DEFAULT NULL');
await addColumnIfNotExists('folder_parent_uuid', 'TEXT DEFAULT NULL');
await db.execute(`
CREATE INDEX IF NOT EXISTS idx_zen_pins_uuid ON zen_pins(uuid)
`);
await db.execute(`
CREATE TABLE IF NOT EXISTS zen_pins_changes (
uuid TEXT PRIMARY KEY,
timestamp INTEGER NOT NULL
)
`);
await db.execute(`
CREATE INDEX IF NOT EXISTS idx_zen_pins_changes_uuid ON zen_pins_changes(uuid)
`);
this._resolveInitialized();
});
},
/**
* Private helper method to notify observers with a list of changed UUIDs.
* @param {string} event - The observer event name.
* @param {Array<string>} uuids - Array of changed workspace UUIDs.
*/
_notifyPinsChanged(event, uuids) {
if (uuids.length === 0) return; // No changes to notify
// Convert the array of UUIDs to a JSON string
const data = JSON.stringify(uuids);
Services.obs.notifyObservers(null, event, data);
},
async savePin(pin, notifyObservers = true) {
// If we find the exact same pin in the cache, skip saving
const existingIndex = this._saveCache.findIndex((cachedPin) => cachedPin.uuid === pin.uuid);
const copy = { ...pin };
if (existingIndex !== -1) {
const existingPin = this._saveCache[existingIndex];
const isSame = Object.keys(pin).every((key) => pin[key] === existingPin[key]);
if (isSame) {
return; // No changes, skip saving
} else {
// Update the cached pin
this._saveCache[existingIndex] = { ...copy };
}
} else {
// Add to cache
this._saveCache.push(copy);
}
const changedUUIDs = new Set();
await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.savePin', async (db) => {
await db.executeTransaction(async () => {
const now = Date.now();
let newPosition;
if ('position' in pin && Number.isFinite(pin.position)) {
newPosition = pin.position;
} else {
// Get the maximum position within the same parent group (or null for root level)
const maxPositionResult = await db.execute(
`
SELECT MAX("position") as max_position
FROM zen_pins
WHERE COALESCE(folder_parent_uuid, '') = COALESCE(:folder_parent_uuid, '')
`,
{ folder_parent_uuid: pin.parentUuid || null }
);
const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0;
newPosition = maxPosition + 1000;
}
// Insert or replace the pin
await db.executeCached(
`
INSERT OR REPLACE INTO zen_pins (
uuid, title, url, container_id, workspace_uuid, position,
is_essential, is_group, folder_parent_uuid, edited_title, created_at,
updated_at, is_folder_collapsed, folder_icon
) VALUES (
:uuid, :title, :url, :container_id, :workspace_uuid, :position,
:is_essential, :is_group, :folder_parent_uuid, :edited_title,
COALESCE((SELECT created_at FROM zen_pins WHERE uuid = :uuid), :now),
:now, :is_folder_collapsed, :folder_icon
)
`,
{
uuid: pin.uuid,
title: pin.title,
url: pin.isGroup ? '' : pin.url,
container_id: pin.containerTabId || null,
workspace_uuid: pin.workspaceUuid || null,
position: newPosition,
is_essential: pin.isEssential || false,
is_group: pin.isGroup || false,
folder_parent_uuid: pin.parentUuid || null,
edited_title: pin.editedTitle || false,
now,
folder_icon: pin.folderIcon || null,
is_folder_collapsed: pin.isFolderCollapsed || false,
}
);
await db.execute(
`
INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
VALUES (:uuid, :timestamp)
`,
{
uuid: pin.uuid,
timestamp: Math.floor(now / 1000),
}
);
changedUUIDs.add(pin.uuid);
await this.updateLastChangeTimestamp(db);
});
});
if (notifyObservers) {
this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs));
}
},
async getPins() {
const db = await PlacesUtils.promiseDBConnection();
const rows = await db.executeCached(`
SELECT * FROM zen_pins
ORDER BY position ASC
`);
return rows.map((row) => ({
uuid: row.getResultByName('uuid'),
title: row.getResultByName('title'),
url: row.getResultByName('url'),
containerTabId: row.getResultByName('container_id'),
workspaceUuid: row.getResultByName('workspace_uuid'),
position: row.getResultByName('position'),
isEssential: Boolean(row.getResultByName('is_essential')),
isGroup: Boolean(row.getResultByName('is_group')),
parentUuid: row.getResultByName('folder_parent_uuid'),
editedTitle: Boolean(row.getResultByName('edited_title')),
folderIcon: row.getResultByName('folder_icon'),
isFolderCollapsed: Boolean(row.getResultByName('is_folder_collapsed')),
}));
},
/**
* Create a new group
* @param {string} title - The title of the group
* @param {string} workspaceUuid - The workspace UUID (optional)
* @param {string} parentUuid - The parent group UUID (optional, null for root level)
* @param {number} position - The position of the group (optional, will auto-calculate if not provided)
* @param {boolean} notifyObservers - Whether to notify observers (default: true)
* @returns {Promise<string>} The UUID of the created group
*/
async createGroup(
title,
icon = null,
isCollapsed = false,
workspaceUuid = null,
parentUuid = null,
position = null,
notifyObservers = true
) {
if (!title || typeof title !== 'string') {
throw new Error('Group title is required and must be a string');
}
const groupUuid = gZenUIManager.generateUuidv4();
const groupPin = {
uuid: groupUuid,
title,
folderIcon: icon || null,
isFolderCollapsed: isCollapsed || false,
workspaceUuid,
parentUuid,
position,
isGroup: true,
isEssential: false,
editedTitle: true, // Group titles are always considered edited
};
await this.savePin(groupPin, notifyObservers);
return groupUuid;
},
/**
* Add an existing tab/pin to a group
* @param {string} tabUuid - The UUID of the tab to add to the group
* @param {string} groupUuid - The UUID of the target group
* @param {number} position - The position within the group (optional, will append if not provided)
* @param {boolean} notifyObservers - Whether to notify observers (default: true)
*/
async addTabToGroup(tabUuid, groupUuid, position = null, notifyObservers = true) {
if (!tabUuid || !groupUuid) {
throw new Error('Both tabUuid and groupUuid are required');
}
const changedUUIDs = new Set();
await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.addTabToGroup', async (db) => {
await db.executeTransaction(async () => {
// Verify the group exists and is actually a group
const groupCheck = await db.execute(
`SELECT is_group FROM zen_pins WHERE uuid = :groupUuid`,
{ groupUuid }
);
if (groupCheck.length === 0) {
throw new Error(`Group with UUID ${groupUuid} does not exist`);
}
if (!groupCheck[0].getResultByName('is_group')) {
throw new Error(`Pin with UUID ${groupUuid} is not a group`);
}
const tabCheck = await db.execute(`SELECT uuid FROM zen_pins WHERE uuid = :tabUuid`, {
tabUuid,
});
if (tabCheck.length === 0) {
throw new Error(`Tab with UUID ${tabUuid} does not exist`);
}
const now = Date.now();
let newPosition;
if (position !== null && Number.isFinite(position)) {
newPosition = position;
} else {
// Get the maximum position within the group
const maxPositionResult = await db.execute(
`SELECT MAX("position") as max_position FROM zen_pins WHERE folder_parent_uuid = :groupUuid`,
{ groupUuid }
);
const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0;
newPosition = maxPosition + 1000;
}
await db.execute(
`
UPDATE zen_pins
SET folder_parent_uuid = :groupUuid,
position = :newPosition,
updated_at = :now
WHERE uuid = :tabUuid
`,
{
tabUuid,
groupUuid,
newPosition,
now,
}
);
changedUUIDs.add(tabUuid);
await db.execute(
`
INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
VALUES (:uuid, :timestamp)
`,
{
uuid: tabUuid,
timestamp: Math.floor(now / 1000),
}
);
await this.updateLastChangeTimestamp(db);
});
});
if (notifyObservers) {
this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs));
}
},
/**
* Remove a tab from its group (move to root level)
* @param {string} tabUuid - The UUID of the tab to remove from its group
* @param {number} newPosition - The new position at root level (optional, will append if not provided)
* @param {boolean} notifyObservers - Whether to notify observers (default: true)
*/
async removeTabFromGroup(tabUuid, newPosition = null, notifyObservers = true) {
if (!tabUuid) {
throw new Error('tabUuid is required');
}
const changedUUIDs = new Set();
await PlacesUtils.withConnectionWrapper(
'ZenPinnedTabsStorage.removeTabFromGroup',
async (db) => {
await db.executeTransaction(async () => {
// Verify the tab exists and is in a group
const tabCheck = await db.execute(
`SELECT folder_parent_uuid FROM zen_pins WHERE uuid = :tabUuid`,
{ tabUuid }
);
if (tabCheck.length === 0) {
throw new Error(`Tab with UUID ${tabUuid} does not exist`);
}
if (!tabCheck[0].getResultByName('folder_parent_uuid')) {
return;
}
const now = Date.now();
let finalPosition;
if (newPosition !== null && Number.isFinite(newPosition)) {
finalPosition = newPosition;
} else {
// Get the maximum position at root level (where folder_parent_uuid is null)
const maxPositionResult = await db.execute(
`SELECT MAX("position") as max_position FROM zen_pins WHERE folder_parent_uuid IS NULL`
);
const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0;
finalPosition = maxPosition + 1000;
}
// Update the tab to be at root level
await db.execute(
`
UPDATE zen_pins
SET folder_parent_uuid = NULL,
position = :newPosition,
updated_at = :now
WHERE uuid = :tabUuid
`,
{
tabUuid,
newPosition: finalPosition,
now,
}
);
changedUUIDs.add(tabUuid);
// Record the change
await db.execute(
`
INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
VALUES (:uuid, :timestamp)
`,
{
uuid: tabUuid,
timestamp: Math.floor(now / 1000),
}
);
await this.updateLastChangeTimestamp(db);
});
}
);
if (notifyObservers) {
this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs));
}
},
async removePin(uuid, notifyObservers = true) {
const cachedIndex = this._saveCache.findIndex((cachedPin) => cachedPin.uuid === uuid);
if (cachedIndex !== -1) {
this._saveCache.splice(cachedIndex, 1);
}
const changedUUIDs = [uuid];
await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.removePin', async (db) => {
await db.executeTransaction(async () => {
// Get all child UUIDs first for change tracking
const children = await db.execute(
`SELECT uuid FROM zen_pins WHERE folder_parent_uuid = :uuid`,
{
uuid,
}
);
// Add child UUIDs to changedUUIDs array
for (const child of children) {
changedUUIDs.push(child.getResultByName('uuid'));
}
// Delete the pin/group itself
await db.execute(`DELETE FROM zen_pins WHERE uuid = :uuid`, { uuid });
// Record the changes
const now = Math.floor(Date.now() / 1000);
for (const changedUuid of changedUUIDs) {
await db.execute(
`
INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
VALUES (:uuid, :timestamp)
`,
{
uuid: changedUuid,
timestamp: now,
}
);
}
await this.updateLastChangeTimestamp(db);
});
});
if (notifyObservers) {
this._notifyPinsChanged('zen-pin-removed', changedUUIDs);
}
},
async wipeAllPins() {
await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.wipeAllPins', async (db) => {
await db.execute(`DELETE FROM zen_pins`);
await db.execute(`DELETE FROM zen_pins_changes`);
await this.updateLastChangeTimestamp(db);
});
},
async markChanged(uuid) {
await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.markChanged', async (db) => {
const now = Date.now();
await db.execute(
`
INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
VALUES (:uuid, :timestamp)
`,
{
uuid,
timestamp: Math.floor(now / 1000),
}
);
});
},
async getChangedIDs() {
const db = await PlacesUtils.promiseDBConnection();
const rows = await db.execute(`
SELECT uuid, timestamp FROM zen_pins_changes
`);
const changes = {};
for (const row of rows) {
changes[row.getResultByName('uuid')] = row.getResultByName('timestamp');
}
return changes;
},
async clearChangedIDs() {
await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.clearChangedIDs', async (db) => {
await db.execute(`DELETE FROM zen_pins_changes`);
});
},
shouldReorderPins(before, current, after) {
const minGap = 1; // Minimum allowed gap between positions
return (
(before !== null && current - before < minGap) || (after !== null && after - current < minGap)
);
},
async reorderAllPins(db, changedUUIDs) {
const pins = await db.execute(`
SELECT uuid
FROM zen_pins
ORDER BY position ASC
`);
for (let i = 0; i < pins.length; i++) {
const newPosition = (i + 1) * 1000; // Use large increments
await db.execute(
`
UPDATE zen_pins
SET position = :newPosition
WHERE uuid = :uuid
`,
{ newPosition, uuid: pins[i].getResultByName('uuid') }
);
changedUUIDs.add(pins[i].getResultByName('uuid'));
}
},
async updateLastChangeTimestamp(db) {
const now = Date.now();
await db.execute(
`
INSERT OR REPLACE INTO moz_meta (key, value)
VALUES ('zen_pins_last_change', :now)
`,
{ now }
);
},
async getLastChangeTimestamp() {
const db = await PlacesUtils.promiseDBConnection();
const result = await db.executeCached(`
SELECT value FROM moz_meta WHERE key = 'zen_pins_last_change'
`);
return result.length ? parseInt(result[0].getResultByName('value'), 10) : 0;
},
async updatePinPositions(pins) {
const changedUUIDs = new Set();
await PlacesUtils.withConnectionWrapper(
'ZenPinnedTabsStorage.updatePinPositions',
async (db) => {
await db.executeTransaction(async () => {
const now = Date.now();
for (let i = 0; i < pins.length; i++) {
const pin = pins[i];
const newPosition = (i + 1) * 1000;
await db.execute(
`
UPDATE zen_pins
SET position = :newPosition
WHERE uuid = :uuid
`,
{ newPosition, uuid: pin.uuid }
);
changedUUIDs.add(pin.uuid);
// Record the change
await db.execute(
`
INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
VALUES (:uuid, :timestamp)
`,
{
uuid: pin.uuid,
timestamp: Math.floor(now / 1000),
}
);
}
await this.updateLastChangeTimestamp(db);
});
}
);
this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs));
},
async updatePinTitle(uuid, newTitle, isEdited = true, notifyObservers = true) {
if (!uuid || typeof newTitle !== 'string') {
throw new Error('Invalid parameters: uuid and newTitle are required');
}
const changedUUIDs = new Set();
await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.updatePinTitle', async (db) => {
await db.executeTransaction(async () => {
const now = Date.now();
// Update the pin's title and edited_title flag
const result = await db.execute(
`
UPDATE zen_pins
SET title = :newTitle,
edited_title = :isEdited,
updated_at = :now
WHERE uuid = :uuid
`,
{
uuid,
newTitle,
isEdited,
now,
}
);
// Only proceed with change tracking if a row was actually updated
if (result.rowsAffected > 0) {
changedUUIDs.add(uuid);
// Record the change
await db.execute(
`
INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
VALUES (:uuid, :timestamp)
`,
{
uuid,
timestamp: Math.floor(now / 1000),
}
);
await this.updateLastChangeTimestamp(db);
}
});
});
if (notifyObservers && changedUUIDs.size > 0) {
this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs));
}
},
async __dropTables() {
await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.__dropTables', async (db) => {
await db.execute(`DROP TABLE IF EXISTS zen_pins`);
await db.execute(`DROP TABLE IF EXISTS zen_pins_changes`);
});
},
};
ZenPinnedTabsStorage.promiseInitialized = new Promise((resolve) => {
ZenPinnedTabsStorage._resolveInitialized = resolve;
ZenPinnedTabsStorage.init();
});

View File

@@ -2,7 +2,6 @@
# 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/.
content/browser/zen-components/ZenPinnedTabsStorage.mjs (../../zen/tabs/ZenPinnedTabsStorage.mjs)
content/browser/zen-components/ZenPinnedTabManager.mjs (../../zen/tabs/ZenPinnedTabManager.mjs)
* content/browser/zen-styles/zen-tabs.css (../../zen/tabs/zen-tabs.css)
content/browser/zen-styles/zen-tabs/vertical-tabs.css (../../zen/tabs/zen-tabs/vertical-tabs.css)

View File

@@ -6,7 +6,7 @@
add_task(async function test_Container_Essentials_Auto_Swithc() {
await gZenWorkspaces.createAndSaveWorkspace('Container Profile 1', undefined, false, 1);
const workspaces = await gZenWorkspaces._workspaces();
ok(workspaces.workspaces.length === 2, 'Two workspaces should exist.');
ok(workspaces.length === 2, 'Two workspaces should exist.');
let newTab = BrowserTestUtils.addTab(gBrowser, 'about:blank', {
skipAnimation: true,
@@ -27,11 +27,11 @@ add_task(async function test_Container_Essentials_Auto_Swithc() {
const newWorkspaceUUID = gZenWorkspaces.activeWorkspace;
Assert.equal(
gZenWorkspaces.activeWorkspace,
workspaces.workspaces[1].uuid,
workspaces[1].uuid,
'The new workspace should be active.'
);
// Change to the original workspace, there should be no essential tabs
await gZenWorkspaces.changeWorkspace(workspaces.workspaces[0]);
await gZenWorkspaces.changeWorkspace(workspaces[0]);
await gZenWorkspaces.removeWorkspace(newWorkspaceUUID);
});

View File

@@ -6,9 +6,9 @@
add_task(async function test_Check_Creation() {
await gZenWorkspaces.createAndSaveWorkspace('Container Profile 1', undefined, false, 1);
const workspaces = await gZenWorkspaces._workspaces();
ok(workspaces.workspaces.length === 2, 'Two workspaces should exist.');
ok(workspaces.length === 2, 'Two workspaces should exist.');
await gZenWorkspaces.changeWorkspace(workspaces.workspaces[1]);
await gZenWorkspaces.changeWorkspace(workspaces[1]);
let newTab = BrowserTestUtils.addTab(gBrowser, 'about:blank', {
skipAnimation: true,
userContextId: 1,
@@ -28,7 +28,7 @@ add_task(async function test_Check_Creation() {
const newWorkspaceUUID = gZenWorkspaces.activeWorkspace;
// Change to the original workspace, there should be no essential tabs
await gZenWorkspaces.changeWorkspace(workspaces.workspaces[0]);
await gZenWorkspaces.changeWorkspace(workspaces[0]);
ok(
!gBrowser.tabs.find(
(t) => t.hasAttribute('zen-essential') && t.getAttribute('usercontextid') == 1

View File

@@ -31,9 +31,6 @@ add_task(async function test_Duplicate_Tab_Inside_Folder() {
for (const t of folder.tabs) {
ok(t.pinned, 'All tabs in the folder should be pinned');
if (!t.hasAttribute('zen-empty-tab')) {
ok(t.hasAttribute('zen-pin-id'), 'All non-empty tabs should have a zen-pinned-id attribute');
}
}
gBrowser.selectedTab = selectedTab;

View File

@@ -10,9 +10,5 @@
"zen.mods.last-update",
"zen.view.compact.enable-at-startup",
"zen.urlbar.suggestions-learner",
"browser.newtabpage.activity-stream.trendingSearch.defaultSearchEngine",
// From the imported safebrowsing tests
"urlclassifier.phishTable",
"urlclassifier.malwareTable"
"browser.newtabpage.activity-stream.trendingSearch.defaultSearchEngine"
]

View File

@@ -1,30 +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/.
[reportbrokensite]
source = "browser/components/reportbrokensite/test/browser"
is_direct_path = true
disable = [
"browser_addon_data_sent.js"
]
[reportbrokensite.replace-manifest]
"../../../../../" = "../../../../"
[safebrowsing]
source = "browser/components/safebrowsing/content/test"
is_direct_path = true
[shell]
source = "browser/components/shell/test"
is_direct_path = true
disable = [
"browser_1119088.js",
"browser_setDesktopBackgroundPreview.js",
]
[tooltiptext]
source = "toolkit/components/tooltiptext"

View File

@@ -1,14 +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/.
# This file is autogenerated by scripts/import_external_tests.py
# Do not edit manually.
BROWSER_CHROME_MANIFESTS += [
"reportbrokensite/browser.toml",
"safebrowsing/browser.toml",
"shell/browser.toml",
"tooltiptext/browser.toml",
]

View File

@@ -1,57 +0,0 @@
[DEFAULT]
tags = "report-broken-site"
support-files = [
"example_report_page.html",
"head.js",
"sendMoreInfoTestEndpoint.html",
]
["browser_addon_data_sent.js"]
disabled="Disabled by import_external_tests.py"
support-files = [ "send_more_info.js" ]
skip-if = ["os == 'win' && os_version == '11.26100' && processor == 'x86_64' && opt"] # Bug 1955805
["browser_antitracking_data_sent.js"]
support-files = [ "send_more_info.js" ]
["browser_back_buttons.js"]
["browser_error_messages.js"]
["browser_experiment_data_sent.js"]
support-files = [ "send_more_info.js" ]
["browser_keyboard_navigation.js"]
skip-if = [
"os == 'linux' && os_version == '24.04' && processor == 'x86_64' && tsan", # Bug 1867132
"os == 'linux' && os_version == '24.04' && processor == 'x86_64' && asan", # Bug 1867132
"os == 'linux' && os_version == '24.04' && processor == 'x86_64' && debug", # Bug 1867132
"os == 'win' && os_version == '11.26100' && processor == 'x86_64' && asan", # Bug 1867132
]
["browser_learn_more_link.js"]
["browser_parent_menuitems.js"]
["browser_prefers_contrast.js"]
["browser_reason_dropdown.js"]
["browser_report_send.js"]
support-files = [ "send.js" ]
["browser_send_more_info.js"]
support-files = [
"send_more_info.js",
"../../../../toolkit/components/gfx/content/videotest.mp4",
]
["browser_tab_key_order.js"]
["browser_tab_switch_handling.js"]
["browser_webcompat.com_fallback.js"]
support-files = [
"send_more_info.js",
"../../../../toolkit/components/gfx/content/videotest.mp4",
]

View File

@@ -1,99 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* Tests to ensure that the right data is sent for
* private windows and when ETP blocks content.
*/
/* import-globals-from send.js */
/* import-globals-from send_more_info.js */
"use strict";
const { AddonTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/AddonTestUtils.sys.mjs"
);
AddonTestUtils.initMochitest(this);
Services.scriptloader.loadSubScript(
getRootDirectory(gTestPath) + "send_more_info.js",
this
);
add_common_setup();
const TEMP_ID = "testtempaddon@tests.mozilla.org";
const TEMP_NAME = "Temporary Addon";
const TEMP_VERSION = "0.1.0";
const PERM_ID = "testpermaddon@tests.mozilla.org";
const PERM_NAME = "Permanent Addon";
const PERM_VERSION = "0.2.0";
const DISABLED_ID = "testdisabledaddon@tests.mozilla.org";
const DISABLED_NAME = "Disabled Addon";
const DISABLED_VERSION = "0.3.0";
const EXPECTED_ADDONS = [
{ id: PERM_ID, name: PERM_NAME, temporary: false, version: PERM_VERSION },
{ id: TEMP_ID, name: TEMP_NAME, temporary: true, version: TEMP_VERSION },
];
function loadAddon(id, name, version, isTemp = false) {
return ExtensionTestUtils.loadExtension({
manifest: {
browser_specific_settings: { gecko: { id } },
name,
version,
},
useAddonManager: isTemp ? "temporary" : "permanent",
});
}
async function installAddons() {
const temp = await loadAddon(TEMP_ID, TEMP_NAME, TEMP_VERSION, true);
await temp.startup();
const perm = await loadAddon(PERM_ID, PERM_NAME, PERM_VERSION);
await perm.startup();
const dis = await loadAddon(DISABLED_ID, DISABLED_NAME, DISABLED_VERSION);
await dis.startup();
await (await AddonManager.getAddonByID(DISABLED_ID)).disable();
return async () => {
await temp.unload();
await perm.unload();
await dis.unload();
};
}
add_task(async function testSendButton() {
ensureReportBrokenSitePreffedOn();
ensureReasonOptional();
const addonCleanup = await installAddons();
const tab = await openTab(REPORTABLE_PAGE_URL);
await testSend(tab, AppMenu(), {
addons: EXPECTED_ADDONS,
});
closeTab(tab);
await addonCleanup();
});
add_task(async function testSendingMoreInfo() {
ensureReportBrokenSitePreffedOn();
ensureSendMoreInfoEnabled();
const addonCleanup = await installAddons();
const tab = await openTab(REPORTABLE_PAGE_URL);
await testSendMoreInfo(tab, HelpMenu(), {
addons: EXPECTED_ADDONS,
});
closeTab(tab);
await addonCleanup();
});

View File

@@ -1,126 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* Tests to ensure that the right data is sent for
* private windows and when ETP blocks content.
*/
/* import-globals-from send.js */
/* import-globals-from send_more_info.js */
"use strict";
Services.scriptloader.loadSubScript(
getRootDirectory(gTestPath) + "send_more_info.js",
this
);
add_common_setup();
add_task(setupStrictETP);
function getEtpCategory() {
return Services.prefs.getStringPref(
"browser.contentblocking.category",
"standard"
);
}
add_task(async function testSendButton() {
ensureReportBrokenSitePreffedOn();
ensureReasonOptional();
const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
const blockedPromise = waitForContentBlockingEvent(3, win);
const tab = await openTab(REPORTABLE_PAGE_URL3, win);
await blockedPromise;
await testSend(tab, AppMenu(win), {
breakageCategory: "adblocker",
description: "another test description",
antitracking: {
blockList: "strict",
blockedOrigins: null,
isPrivateBrowsing: true,
hasTrackingContentBlocked: true,
hasMixedActiveContentBlocked: true,
hasMixedDisplayContentBlocked: true,
btpHasPurgedSite: false,
etpCategory: getEtpCategory(),
},
frameworks: {
fastclick: true,
marfeel: true,
mobify: true,
},
});
await BrowserTestUtils.closeWindow(win);
});
add_task(async function testSendingMoreInfo() {
ensureReportBrokenSitePreffedOn();
ensureSendMoreInfoEnabled();
const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
const blockedPromise = waitForContentBlockingEvent(3, win);
const tab = await openTab(REPORTABLE_PAGE_URL3, win);
await blockedPromise;
await testSendMoreInfo(tab, HelpMenu(win), {
antitracking: {
blockList: "strict",
blockedOrigins: ["https://trackertest.org"],
isPrivateBrowsing: true,
hasTrackingContentBlocked: true,
hasMixedActiveContentBlocked: true,
hasMixedDisplayContentBlocked: true,
btpHasPurgedSite: false,
etpCategory: getEtpCategory(),
},
frameworks: { fastclick: true, mobify: true, marfeel: true },
consoleLog: [
{
level: "error",
log(actual) {
// "Blocked loading mixed display content http://example.com/tests/image/test/mochitest/blue.png"
return (
Array.isArray(actual) &&
actual.length == 1 &&
actual[0].includes("blue.png")
);
},
pos: "0:1",
uri: REPORTABLE_PAGE_URL3,
},
{
level: "error",
log(actual) {
// "Blocked loading mixed active content http://tracking.example.org/browser/browser/base/content/test/protectionsUI/benignPage.html",
return (
Array.isArray(actual) &&
actual.length == 1 &&
actual[0].includes("benignPage.html")
);
},
pos: "0:1",
uri: REPORTABLE_PAGE_URL3,
},
{
level: "warn",
log(actual) {
// "The resource at https://trackertest.org/ was blocked because content blocking is enabled.",
return (
Array.isArray(actual) &&
actual.length == 1 &&
actual[0].includes("trackertest.org")
);
},
pos: "0:1",
uri: REPORTABLE_PAGE_URL3,
},
],
});
await BrowserTestUtils.closeWindow(win);
});

View File

@@ -1,34 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* Tests to ensure that Report Broken Site popups will be
* reset to whichever tab the user is on as they change
* between windows and tabs. */
"use strict";
add_common_setup();
add_task(async function testBackButtonsAreAdded() {
ensureReportBrokenSitePreffedOn();
await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
let rbs = await AppMenu().openReportBrokenSite();
rbs.isBackButtonEnabled();
await rbs.clickBack();
await rbs.close();
rbs = await HelpMenu().openReportBrokenSite();
ok(!rbs.backButton, "Back button is not shown for Help Menu");
await rbs.close();
rbs = await ProtectionsPanel().openReportBrokenSite();
rbs.isBackButtonEnabled();
await rbs.clickBack();
await rbs.close();
rbs = await HelpMenu().openReportBrokenSite();
ok(!rbs.backButton, "Back button is not shown for Help Menu");
await rbs.close();
});
});

View File

@@ -1,64 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* Test that the Report Broken Site errors messages are shown on
* the UI if the user enters an invalid URL or clicks the send
* button while it is disabled due to not selecting a "reason"
*/
"use strict";
add_common_setup();
add_task(async function test() {
ensureReportBrokenSitePreffedOn();
ensureReasonRequired();
await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
for (const menu of [AppMenu(), ProtectionsPanel(), HelpMenu()]) {
const rbs = await menu.openReportBrokenSite();
const { sendButton, URLInput } = rbs;
rbs.isURLInvalidMessageHidden();
rbs.isReasonNeededMessageHidden();
rbs.setURL("");
window.document.activeElement.blur();
rbs.isURLInvalidMessageShown();
rbs.isReasonNeededMessageHidden();
rbs.setURL("https://asdf");
window.document.activeElement.blur();
rbs.isURLInvalidMessageHidden();
rbs.isReasonNeededMessageHidden();
rbs.setURL("http:/ /asdf");
window.document.activeElement.blur();
rbs.isURLInvalidMessageShown();
rbs.isReasonNeededMessageHidden();
rbs.setURL("https://asdf");
const selectPromise = BrowserTestUtils.waitForSelectPopupShown(window);
EventUtils.synthesizeMouseAtCenter(sendButton, {}, window);
await selectPromise;
rbs.isURLInvalidMessageHidden();
rbs.isReasonNeededMessageShown();
await rbs.dismissDropdownPopup();
rbs.chooseReason("slow");
rbs.isURLInvalidMessageHidden();
rbs.isReasonNeededMessageHidden();
rbs.setURL("");
rbs.chooseReason("choose");
window.ownerGlobal.document.activeElement?.blur();
const focusPromise = BrowserTestUtils.waitForEvent(URLInput, "focus");
EventUtils.synthesizeMouseAtCenter(sendButton, {}, window);
await focusPromise;
rbs.isURLInvalidMessageShown();
rbs.isReasonNeededMessageShown();
rbs.clickCancel();
}
});
});

View File

@@ -1,88 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* Tests to ensure that the right data is sent for
* private windows and when ETP blocks content.
*/
/* import-globals-from send.js */
/* import-globals-from send_more_info.js */
"use strict";
Services.scriptloader.loadSubScript(
getRootDirectory(gTestPath) + "send_more_info.js",
this
);
const { ExperimentAPI } = ChromeUtils.importESModule(
"resource://nimbus/ExperimentAPI.sys.mjs"
);
const { NimbusTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/NimbusTestUtils.sys.mjs"
);
add_common_setup();
const EXPECTED_EXPERIMENTS_IN_REPORT = [
{ slug: "test-experiment", branch: "branch", kind: "nimbusExperiment" },
{ slug: "test-experiment-rollout", branch: "branch", kind: "nimbusRollout" },
];
let EXPERIMENT_CLEANUPS;
add_setup(async function () {
await ExperimentAPI.ready();
EXPERIMENT_CLEANUPS = [
await NimbusTestUtils.enrollWithFeatureConfig(
{ featureId: "no-feature-firefox-desktop", value: {} },
{ slug: "test-experiment", branchSlug: "branch" }
),
await NimbusTestUtils.enrollWithFeatureConfig(
{ featureId: "no-feature-firefox-desktop", value: {} },
{ slug: "test-experiment-rollout", isRollout: true, branchSlug: "branch" }
),
async () => {
ExperimentAPI.manager.store._deleteForTests("test-experiment-disabled");
await NimbusTestUtils.flushStore();
},
];
await NimbusTestUtils.enrollWithFeatureConfig(
{ featureId: "no-feature-firefox-desktop", value: {} },
{ slug: "test-experiment-disabled" }
);
await ExperimentAPI.manager.unenroll("test-experiment-disabled");
});
add_task(async function testSendButton() {
ensureReportBrokenSitePreffedOn();
ensureReasonOptional();
const tab = await openTab(REPORTABLE_PAGE_URL);
await testSend(tab, AppMenu(), {
experiments: EXPECTED_EXPERIMENTS_IN_REPORT,
});
closeTab(tab);
});
add_task(async function testSendingMoreInfo() {
ensureReportBrokenSitePreffedOn();
ensureSendMoreInfoEnabled();
const tab = await openTab(REPORTABLE_PAGE_URL);
await testSendMoreInfo(tab, HelpMenu(), {
experiments: EXPECTED_EXPERIMENTS_IN_REPORT,
});
closeTab(tab);
});
add_task(async function teardown() {
for (const cleanup of EXPERIMENT_CLEANUPS) {
await cleanup();
}
});

View File

@@ -1,107 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* Tests to ensure that sending or canceling reports with
* the Send and Cancel buttons work (as well as the Okay button)
*/
"use strict";
add_common_setup();
requestLongerTimeout(2);
async function testPressingKey(key, tabToMatch, makePromise, followUp) {
await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
for (const menu of [AppMenu(), ProtectionsPanel(), HelpMenu()]) {
info(
`Opening RBS to test pressing ${key} for ${tabToMatch} on ${menu.menuDescription}`
);
const rbs = await menu.openReportBrokenSite();
const promise = makePromise(rbs);
if (tabToMatch) {
if (await tabTo(tabToMatch)) {
await pressKeyAndAwait(promise, key);
followUp && (await followUp(rbs));
await rbs.close();
ok(true, `was able to activate ${tabToMatch} with keyboard`);
} else {
await rbs.close();
ok(false, `could not tab to ${tabToMatch}`);
}
} else {
await pressKeyAndAwait(promise, key);
followUp && (await followUp(rbs));
await rbs.close();
ok(true, `was able to use keyboard`);
}
}
});
}
add_task(async function testSendMoreInfo() {
ensureReportBrokenSitePreffedOn();
ensureSendMoreInfoEnabled();
await testPressingKey(
"KEY_Enter",
"#report-broken-site-popup-send-more-info-link",
rbs => rbs.waitForSendMoreInfoTab(),
() => gBrowser.removeCurrentTab()
);
});
add_task(async function testCancel() {
ensureReportBrokenSitePreffedOn();
await testPressingKey(
"KEY_Enter",
"#report-broken-site-popup-cancel-button",
rbs => BrowserTestUtils.waitForEvent(rbs.mainView, "ViewHiding")
);
});
add_task(async function testSendAndOkay() {
ensureReportBrokenSitePreffedOn();
await testPressingKey(
"KEY_Enter",
"#report-broken-site-popup-send-button",
rbs => rbs.awaitReportSentViewOpened(),
async rbs => {
await tabTo("#report-broken-site-popup-okay-button");
const promise = BrowserTestUtils.waitForEvent(rbs.sentView, "ViewHiding");
await pressKeyAndAwait(promise, "KEY_Enter");
}
);
});
add_task(async function testESCOnMain() {
ensureReportBrokenSitePreffedOn();
await testPressingKey("KEY_Escape", undefined, rbs =>
BrowserTestUtils.waitForEvent(rbs.mainView, "ViewHiding")
);
});
add_task(async function testESCOnSent() {
ensureReportBrokenSitePreffedOn();
await testPressingKey(
"KEY_Enter",
"#report-broken-site-popup-send-button",
rbs => rbs.awaitReportSentViewOpened(),
async rbs => {
const promise = BrowserTestUtils.waitForEvent(rbs.sentView, "ViewHiding");
await pressKeyAndAwait(promise, "KEY_Escape");
}
);
});
add_task(async function testBackButtons() {
ensureReportBrokenSitePreffedOn();
await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
for (const menu of [AppMenu(), ProtectionsPanel()]) {
await menu.openReportBrokenSite();
await tabTo("#report-broken-site-popup-mainView .subviewbutton-back");
const promise = BrowserTestUtils.waitForEvent(menu.popup, "ViewShown");
await pressKeyAndAwait(promise, "KEY_Enter");
menu.close();
}
});
});

View File

@@ -1,36 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* Tests to ensure that the reason dropdown is shown or hidden
* based on its pref, and that its optional and required modes affect
* the Send button and report appropriately.
*/
"use strict";
add_common_setup();
async function ensureLearnMoreLinkWorks(menu) {
const rbs = await menu.openReportBrokenSite();
const { win, mainView, learnMoreLink } = rbs;
ok(learnMoreLink, "Found a learn more link");
const promises = [
BrowserTestUtils.waitForEvent(mainView, "ViewHiding"),
BrowserTestUtils.waitForNewTab(win.gBrowser, LEARN_MORE_TEST_URL),
];
EventUtils.synthesizeMouseAtCenter(learnMoreLink, {}, win);
const results = await Promise.all(promises);
gBrowser.removeTab(results[1]);
}
add_task(async function testLearnMoreLink() {
ensureReportBrokenSitePreffedOn();
await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
await ensureLearnMoreLinkWorks(AppMenu());
await ensureLearnMoreLinkWorks(HelpMenu());
await ensureLearnMoreLinkWorks(ProtectionsPanel());
});
const telemetry = Glean.webcompatreporting.learnMore.testGetValue();
is(telemetry.length, 3, "Got telemetry");
});

View File

@@ -1,96 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* Test that the Report Broken Site menu items are disabled
* when the active tab is not on a reportable URL, and is hidden
* when the feature is disabled via pref. Also ensure that the
* Report Broken Site item that is automatically generated in
* the app menu's help sub-menu is hidden.
*/
"use strict";
add_common_setup();
add_task(async function testMenus() {
ensureReportBrokenSitePreffedOff();
const appMenu = AppMenu();
const menus = [appMenu, ProtectionsPanel(), HelpMenu()];
async function forceMenuItemStateUpdate() {
ReportBrokenSite.enableOrDisableMenuitems(window);
// the hidden/disabled state of all of the menuitems may not update until one
// is rendered; then the related <command>'s state is propagated to them all.
await appMenu.open();
await appMenu.close();
}
await BrowserTestUtils.withNewTab("about:blank", async function () {
await forceMenuItemStateUpdate();
for (const { menuDescription, reportBrokenSite } of menus) {
isMenuItemHidden(
reportBrokenSite,
`${menuDescription} option hidden on invalid page when preffed off`
);
}
});
await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
await forceMenuItemStateUpdate();
for (const { menuDescription, reportBrokenSite } of menus) {
isMenuItemHidden(
reportBrokenSite,
`${menuDescription} option hidden on valid page when preffed off`
);
}
});
ensureReportBrokenSitePreffedOn();
await BrowserTestUtils.withNewTab("about:blank", async function () {
await forceMenuItemStateUpdate();
for (const { menuDescription, reportBrokenSite } of menus) {
isMenuItemDisabled(
reportBrokenSite,
`${menuDescription} option disabled on invalid page when preffed on`
);
}
});
await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
await forceMenuItemStateUpdate();
for (const { menuDescription, reportBrokenSite } of menus) {
isMenuItemEnabled(
reportBrokenSite,
`${menuDescription} option enabled on valid page when preffed on`
);
}
});
ensureReportBrokenSitePreffedOff();
await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
await forceMenuItemStateUpdate();
for (const { menuDescription, reportBrokenSite } of menus) {
isMenuItemHidden(
reportBrokenSite,
`${menuDescription} option hidden again when pref toggled back off`
);
}
});
ensureReportBrokenSitePreffedOn();
ensureReportBrokenSiteDisabledByPolicy();
await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
await forceMenuItemStateUpdate();
for (const { menuDescription, reportBrokenSite } of menus) {
isMenuItemHidden(
reportBrokenSite,
`${menuDescription} option hidden when disabled by DisableFeedbackCommands enterprise policy`
);
}
});
});

View File

@@ -1,56 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* Test that the background color of the "report sent"
* view is not green in non-default contrast modes.
*/
"use strict";
add_common_setup();
const HIGH_CONTRAST_MODE_OFF = [[PREFS.USE_ACCESSIBILITY_THEME, 0]];
const HIGH_CONTRAST_MODE_ON = [[PREFS.USE_ACCESSIBILITY_THEME, 1]];
add_task(async function testReportSentViewBGColor() {
ensureReportBrokenSitePreffedOn();
ensureReasonDisabled();
await BrowserTestUtils.withNewTab(
REPORTABLE_PAGE_URL,
async function (browser) {
const { defaultView } = browser.ownerGlobal.document;
const menu = AppMenu();
await SpecialPowers.pushPrefEnv({ set: HIGH_CONTRAST_MODE_OFF });
const rbs = await menu.openReportBrokenSite();
const { mainView, sentView } = rbs;
mainView.style.backgroundColor = "var(--background-color-success)";
const expectedReportSentBGColor =
defaultView.getComputedStyle(mainView).backgroundColor;
mainView.style.backgroundColor = "";
const expectedPrefersReducedBGColor =
defaultView.getComputedStyle(mainView).backgroundColor;
await rbs.clickSend();
is(
defaultView.getComputedStyle(sentView).backgroundColor,
expectedReportSentBGColor,
"Using green bgcolor when not prefers-contrast"
);
await rbs.clickOkay();
await SpecialPowers.pushPrefEnv({ set: HIGH_CONTRAST_MODE_ON });
await menu.openReportBrokenSite();
await rbs.clickSend();
is(
defaultView.getComputedStyle(sentView).backgroundColor,
expectedPrefersReducedBGColor,
"Using default bgcolor when prefers-contrast"
);
await rbs.clickOkay();
}
);
});

View File

@@ -1,156 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* Tests to ensure that the reason dropdown is shown or hidden
* based on its pref, and that its optional and required modes affect
* the Send button and report appropriately.
*/
"use strict";
add_common_setup();
requestLongerTimeout(2);
async function clickSendAndCheckPing(rbs, expectedReason = null) {
await GleanPings.brokenSiteReport.testSubmission(
() =>
Assert.equal(
Glean.brokenSiteReport.breakageCategory.testGetValue(),
expectedReason
),
() => rbs.clickSend()
);
}
add_task(async function testReasonDropdown() {
ensureReportBrokenSitePreffedOn();
await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
ensureReasonDisabled();
let rbs = await AppMenu().openReportBrokenSite();
await rbs.isReasonHidden();
await rbs.isSendButtonEnabled();
await clickSendAndCheckPing(rbs);
await rbs.clickOkay();
ensureReasonOptional();
rbs = await AppMenu().openReportBrokenSite();
await rbs.isReasonOptional();
await rbs.isSendButtonEnabled();
await clickSendAndCheckPing(rbs);
await rbs.clickOkay();
rbs = await AppMenu().openReportBrokenSite();
await rbs.isReasonOptional();
rbs.chooseReason("slow");
await rbs.isSendButtonEnabled();
await clickSendAndCheckPing(rbs, "slow");
await rbs.clickOkay();
ensureReasonRequired();
rbs = await AppMenu().openReportBrokenSite();
await rbs.isReasonRequired();
await rbs.isSendButtonEnabled();
const selectPromise = BrowserTestUtils.waitForSelectPopupShown(window);
EventUtils.synthesizeMouseAtCenter(rbs.sendButton, {}, window);
await selectPromise;
rbs.chooseReason("media");
await rbs.dismissDropdownPopup();
await rbs.isSendButtonEnabled();
await clickSendAndCheckPing(rbs, "media");
await rbs.clickOkay();
});
});
async function getListItems(rbs) {
const items = Array.from(rbs.reasonInput.querySelectorAll("option")).map(i =>
i.id.replace("report-broken-site-popup-reason-", "")
);
Assert.equal(items[0], "choose", "First option is always 'choose'");
return items.join(",");
}
add_task(async function testReasonDropdownRandomized() {
ensureReportBrokenSitePreffedOn();
ensureReasonOptional();
const USER_ID_PREF = "app.normandy.user_id";
const RANDOMIZE_PREF = "ui.new-webcompat-reporter.reason-dropdown.randomized";
const origNormandyUserID = Services.prefs.getCharPref(
USER_ID_PREF,
undefined
);
await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
// confirm that the default order is initially used
Services.prefs.setBoolPref(RANDOMIZE_PREF, false);
const rbs = await AppMenu().openReportBrokenSite();
const defaultOrder = [
"choose",
"checkout",
"load",
"slow",
"media",
"content",
"account",
"adblocker",
"notsupported",
"other",
];
Assert.deepEqual(
await getListItems(rbs),
defaultOrder,
"non-random order is correct"
);
// confirm that a random order happens per user
let randomOrder;
let isRandomized = false;
Services.prefs.setBoolPref(RANDOMIZE_PREF, true);
// This becomes ClientEnvironment.randomizationId, which we can set to
// any value which results in a different order from the default ordering.
Services.prefs.setCharPref("app.normandy.user_id", "dummy");
// clicking cancel triggers a reset, which is when the randomization
// logic is called. so we must click cancel after pref-changes here.
rbs.clickCancel();
await AppMenu().openReportBrokenSite();
randomOrder = await getListItems(rbs);
Assert.notEqual(
randomOrder,
defaultOrder,
"options are randomized with pref on"
);
// confirm that the order doesn't change per user
isRandomized = false;
for (let attempt = 0; attempt < 5; ++attempt) {
rbs.clickCancel();
await AppMenu().openReportBrokenSite();
const order = await getListItems(rbs);
if (order != randomOrder) {
isRandomized = true;
break;
}
}
Assert.ok(!isRandomized, "options keep the same order per user");
// confirm that the order reverts to the default if pref flipped to false
Services.prefs.setBoolPref(RANDOMIZE_PREF, false);
rbs.clickCancel();
await AppMenu().openReportBrokenSite();
Assert.deepEqual(
defaultOrder,
await getListItems(rbs),
"reverts to non-random order correctly"
);
rbs.clickCancel();
});
Services.prefs.setCharPref(USER_ID_PREF, origNormandyUserID);
});

View File

@@ -1,79 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* Tests to ensure that sending or canceling reports with
* the Send and Cancel buttons work (as well as the Okay button)
*/
/* import-globals-from send.js */
"use strict";
Services.scriptloader.loadSubScript(
getRootDirectory(gTestPath) + "send.js",
this
);
add_common_setup();
requestLongerTimeout(10);
async function testCancel(menu, url, description) {
let rbs = await menu.openAndPrefillReportBrokenSite(url, description);
await rbs.clickCancel();
ok(!rbs.opened, "clicking Cancel closes Report Broken Site");
// re-opening the panel, the url and description should be reset
rbs = await menu.openReportBrokenSite();
rbs.isMainViewResetToCurrentTab();
rbs.close();
}
add_task(async function testSendButton() {
ensureReportBrokenSitePreffedOn();
ensureReasonOptional();
const tab1 = await openTab(REPORTABLE_PAGE_URL);
await testSend(tab1, AppMenu());
const tab2 = await openTab(REPORTABLE_PAGE_URL);
await testSend(tab2, ProtectionsPanel(), {
url: "https://test.org/test/#fake",
breakageCategory: "media",
description: "test description",
});
closeTab(tab1);
closeTab(tab2);
});
add_task(async function testCancelButton() {
ensureReportBrokenSitePreffedOn();
const tab1 = await openTab(REPORTABLE_PAGE_URL);
await testCancel(AppMenu());
await testCancel(ProtectionsPanel());
await testCancel(HelpMenu());
const tab2 = await openTab(REPORTABLE_PAGE_URL);
await testCancel(AppMenu());
await testCancel(ProtectionsPanel());
await testCancel(HelpMenu());
const win2 = await BrowserTestUtils.openNewBrowserWindow();
const tab3 = await openTab(REPORTABLE_PAGE_URL2, win2);
await testCancel(AppMenu(win2));
await testCancel(ProtectionsPanel(win2));
await testCancel(HelpMenu(win2));
closeTab(tab3);
await BrowserTestUtils.closeWindow(win2);
closeTab(tab1);
closeTab(tab2);
});

View File

@@ -1,65 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* Tests that the send more info link appears only when its pref
* is set to true, and that when clicked it will open a tab to
* the webcompat.com endpoint and send the right data.
*/
/* import-globals-from send_more_info.js */
"use strict";
const VIDEO_URL = `${BASE_URL}/videotest.mp4`;
Services.scriptloader.loadSubScript(
getRootDirectory(gTestPath) + "send_more_info.js",
this
);
add_common_setup();
requestLongerTimeout(2);
add_task(async function testSendMoreInfoPref() {
ensureReportBrokenSitePreffedOn();
await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
await changeTab(gBrowser.selectedTab, REPORTABLE_PAGE_URL);
ensureSendMoreInfoDisabled();
let rbs = await AppMenu().openReportBrokenSite();
await rbs.isSendMoreInfoHidden();
await rbs.close();
ensureSendMoreInfoEnabled();
rbs = await AppMenu().openReportBrokenSite();
await rbs.isSendMoreInfoShown();
await rbs.close();
});
});
add_task(async function testSendingMoreInfo() {
ensureReportBrokenSitePreffedOn();
ensureSendMoreInfoEnabled();
const tab = await openTab(REPORTABLE_PAGE_URL);
await testSendMoreInfo(tab, AppMenu());
await changeTab(tab, REPORTABLE_PAGE_URL2);
await testSendMoreInfo(tab, ProtectionsPanel(), {
url: "https://override.com",
description: "another",
expectNoTabDetails: true,
});
// also load a video to ensure system codec
// information is loaded and properly sent
const tab2 = await openTab(VIDEO_URL);
await testSendMoreInfo(tab2, HelpMenu());
closeTab(tab2);
closeTab(tab);
});

View File

@@ -1,134 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* Tests of the expected tab key element focus order */
"use strict";
add_common_setup();
requestLongerTimeout(2);
async function ensureTabOrder(order, win = window) {
const config = { window: win };
for (let matches of order) {
// We need to tab through all elements in each match array in any order
if (!Array.isArray(matches)) {
matches = [matches];
}
let matchesLeft = matches.length;
while (matchesLeft--) {
const target = await pressKeyAndGetFocus("VK_TAB", config);
let foundMatch = false;
for (const [i, selector] of matches.entries()) {
foundMatch = selector && target.matches(selector);
if (foundMatch) {
matches[i] = "";
break;
}
}
ok(
foundMatch,
`Expected [${matches}] next, got id=${target.id}, class=${target.className}, ${target}`
);
if (!foundMatch) {
return false;
}
}
}
return true;
}
async function ensureExpectedTabOrder(
expectBackButton,
expectReason,
expectSendMoreInfo
) {
const { activeElement } = window.document;
is(
activeElement?.id,
"report-broken-site-popup-url",
"URL is already focused"
);
const order = [];
if (expectReason) {
order.push("#report-broken-site-popup-reason");
}
order.push("#report-broken-site-popup-description");
order.push("#report-broken-site-popup-blocked-trackers-checkbox");
if (expectSendMoreInfo) {
order.push("#report-broken-site-popup-send-more-info-link");
}
// moz-button-groups swap the order of buttons to follow
// platform conventions, so the order of send/cancel will vary.
order.push([
"#report-broken-site-popup-cancel-button",
"#report-broken-site-popup-send-button",
]);
if (expectBackButton) {
order.push(".subviewbutton-back");
}
order.push("#report-broken-site-popup-learn-more-link");
order.push("#report-broken-site-popup-url"); // check that we've cycled back
return ensureTabOrder(order);
}
async function testTabOrder(menu) {
ensureReasonDisabled();
ensureSendMoreInfoDisabled();
const { showsBackButton } = menu;
let rbs = await menu.openReportBrokenSite();
await ensureExpectedTabOrder(showsBackButton, false, false);
await rbs.close();
ensureSendMoreInfoEnabled();
rbs = await menu.openReportBrokenSite();
await ensureExpectedTabOrder(showsBackButton, false, true);
await rbs.close();
ensureReasonOptional();
rbs = await menu.openReportBrokenSite();
await ensureExpectedTabOrder(showsBackButton, true, true);
await rbs.close();
ensureReasonRequired();
rbs = await menu.openReportBrokenSite();
await ensureExpectedTabOrder(showsBackButton, true, true);
await rbs.close();
rbs = await menu.openReportBrokenSite();
rbs.chooseReason("slow");
await ensureExpectedTabOrder(showsBackButton, true, true);
await rbs.clickCancel();
ensureSendMoreInfoDisabled();
rbs = await menu.openReportBrokenSite();
await ensureExpectedTabOrder(showsBackButton, true, false);
await rbs.close();
rbs = await menu.openReportBrokenSite();
rbs.chooseReason("slow");
await ensureExpectedTabOrder(showsBackButton, true, false);
await rbs.clickCancel();
ensureReasonOptional();
rbs = await menu.openReportBrokenSite();
await ensureExpectedTabOrder(showsBackButton, true, false);
await rbs.close();
ensureReasonDisabled();
rbs = await menu.openReportBrokenSite();
await ensureExpectedTabOrder(showsBackButton, false, false);
await rbs.close();
}
add_task(async function testTabOrdering() {
ensureReportBrokenSitePreffedOn();
ensureSendMoreInfoEnabled();
await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
await testTabOrder(AppMenu());
await testTabOrder(ProtectionsPanel());
await testTabOrder(HelpMenu());
});
});

View File

@@ -1,81 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* Tests to ensure that Report Broken Site popups will be
* reset to whichever tab the user is on as they change
* between windows and tabs. */
"use strict";
add_common_setup();
add_task(async function testResetsProperlyOnTabSwitch() {
ensureReportBrokenSitePreffedOn();
const badTab = await openTab("about:blank");
const goodTab1 = await openTab(REPORTABLE_PAGE_URL);
const goodTab2 = await openTab(REPORTABLE_PAGE_URL2);
const appMenu = AppMenu();
const protPanel = ProtectionsPanel();
let rbs = await appMenu.openReportBrokenSite();
rbs.isMainViewResetToCurrentTab();
rbs.close();
gBrowser.selectedTab = goodTab1;
rbs = await protPanel.openReportBrokenSite();
rbs.isMainViewResetToCurrentTab();
rbs.close();
gBrowser.selectedTab = badTab;
await appMenu.open();
appMenu.isReportBrokenSiteDisabled();
await appMenu.close();
gBrowser.selectedTab = goodTab1;
rbs = await protPanel.openReportBrokenSite();
rbs.isMainViewResetToCurrentTab();
rbs.close();
closeTab(badTab);
closeTab(goodTab1);
closeTab(goodTab2);
});
add_task(async function testResetsProperlyOnWindowSwitch() {
ensureReportBrokenSitePreffedOn();
const tab1 = await openTab(REPORTABLE_PAGE_URL);
const win2 = await BrowserTestUtils.openNewBrowserWindow();
const tab2 = await openTab(REPORTABLE_PAGE_URL2, win2);
const appMenu1 = AppMenu();
const appMenu2 = ProtectionsPanel(win2);
let rbs2 = await appMenu2.openReportBrokenSite();
rbs2.isMainViewResetToCurrentTab();
rbs2.close();
// flip back to tab1's window and ensure its URL pops up instead of tab2's URL
await switchToWindow(window);
isSelectedTab(window, tab1); // sanity check
let rbs1 = await appMenu1.openReportBrokenSite();
rbs1.isMainViewResetToCurrentTab();
rbs1.close();
// likewise flip back to tab2's window and ensure its URL pops up instead of tab1's URL
await switchToWindow(win2);
isSelectedTab(win2, tab2); // sanity check
rbs2 = await appMenu2.openReportBrokenSite();
rbs2.isMainViewResetToCurrentTab();
rbs2.close();
closeTab(tab1);
closeTab(tab2);
await BrowserTestUtils.closeWindow(win2);
});

View File

@@ -1,45 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* Tests that when Report Broken Site is disabled, it will
* send the user to webcompat.com when clicked and it the
* relevant tab's report data.
*/
/* import-globals-from send_more_info.js */
"use strict";
Services.scriptloader.loadSubScript(
getRootDirectory(gTestPath) + "send_more_info.js",
this
);
add_common_setup();
const VIDEO_URL = `${BASE_URL}/videotest.mp4`;
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [["test.wait300msAfterTabSwitch", true]],
});
});
add_task(async function testWebcompatComFallbacks() {
ensureReportBrokenSitePreffedOff();
const tab = await openTab(REPORTABLE_PAGE_URL);
await testWebcompatComFallback(tab, AppMenu());
await changeTab(tab, REPORTABLE_PAGE_URL2);
await testWebcompatComFallback(tab, ProtectionsPanel());
// also load a video to ensure system codec
// information is loaded and properly sent
const tab2 = await openTab(VIDEO_URL);
await testWebcompatComFallback(tab2, HelpMenu());
closeTab(tab2);
closeTab(tab);
});

View File

@@ -1,22 +0,0 @@
<!DOCTYPE HTML>
<!-- 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/. -->
<html dir="ltr" xml:lang="en-US" lang="en-US">
<head>
<meta charset="utf8">
<script>
window.marfeel = 1;
window.Mobify = { Tag: 1 };
window.FastClick = 1;
</script>
</head>
<body>
<!-- blocked tracking content -->
<iframe src="https://trackertest.org/"></iframe>
<!-- mixed active content -->
<iframe src="http://tracking.example.org/browser/browser/base/content/test/protectionsUI/benignPage.html"></iframe>
<!-- mixed display content -->
<img src="http://example.com/tests/image/test/mochitest/blue.png"></img>
</body>
</html>

View File

@@ -1,918 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const { CustomizableUITestUtils } = ChromeUtils.importESModule(
"resource://testing-common/CustomizableUITestUtils.sys.mjs"
);
const { EnterprisePolicyTesting, PoliciesPrefTracker } =
ChromeUtils.importESModule(
"resource://testing-common/EnterprisePolicyTesting.sys.mjs"
);
const { UrlClassifierTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/UrlClassifierTestUtils.sys.mjs"
);
const { ReportBrokenSite } = ChromeUtils.importESModule(
"moz-src:///browser/components/reportbrokensite/ReportBrokenSite.sys.mjs"
);
const BASE_URL =
"https://example.com/browser/browser/components/reportbrokensite/test/browser/";
const REPORTABLE_PAGE_URL = "https://example.com";
const REPORTABLE_PAGE_URL2 = REPORTABLE_PAGE_URL.replace(".com", ".org");
const REPORTABLE_PAGE_URL3 = `${BASE_URL}example_report_page.html`;
const SUMO_BASE_URL = Services.urlFormatter.formatURLPref(
"app.support.baseURL"
);
const LEARN_MORE_TEST_URL = `${SUMO_BASE_URL}report-broken-site`;
const NEW_REPORT_ENDPOINT_TEST_URL = `${BASE_URL}sendMoreInfoTestEndpoint.html`;
const PREFS = {
DATAREPORTING_ENABLED: "datareporting.healthreport.uploadEnabled",
REPORTER_ENABLED: "ui.new-webcompat-reporter.enabled",
REASON: "ui.new-webcompat-reporter.reason-dropdown",
SEND_MORE_INFO: "ui.new-webcompat-reporter.send-more-info-link",
NEW_REPORT_ENDPOINT: "ui.new-webcompat-reporter.new-report-endpoint",
TOUCH_EVENTS: "dom.w3c_touch_events.enabled",
USE_ACCESSIBILITY_THEME: "ui.useAccessibilityTheme",
};
function add_common_setup() {
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [
[PREFS.NEW_REPORT_ENDPOINT, NEW_REPORT_ENDPOINT_TEST_URL],
// set touch events to auto-detect, as the pref gets set to 1 somewhere
// while tests are running, making hasTouchScreen checks unreliable.
[PREFS.TOUCH_EVENTS, 2],
],
});
registerCleanupFunction(function () {
for (const prefName of Object.values(PREFS)) {
Services.prefs.clearUserPref(prefName);
}
Services.telemetry.clearEvents();
Services.fog.testResetFOG();
});
});
}
function areObjectsEqual(actual, expected, path = "") {
if (typeof expected == "function") {
try {
const passes = expected(actual);
if (!passes) {
info(`${path} not pass check function: ${actual}`);
}
return passes;
} catch (e) {
info(`${path} threw exception:
got: ${typeof actual}, ${actual}
expected: ${typeof expected}, ${expected}
exception: ${e.message}
${e.stack}`);
return false;
}
}
if (typeof actual != typeof expected) {
info(`${path} types do not match:
got: ${typeof actual}, ${actual}
expected: ${typeof expected}, ${expected}`);
return false;
}
if (typeof actual != "object" || actual === null || expected === null) {
if (actual !== expected) {
info(`${path} does not match
got: ${typeof actual}, ${actual}
expected: ${typeof expected}, ${expected}`);
return false;
}
return true;
}
const prefix = path ? `${path}.` : path;
for (const [key, val] of Object.entries(actual)) {
if (!(key in expected)) {
info(`Extra ${prefix}${key}: ${val}`);
return false;
}
}
let result = true;
for (const [key, expectedVal] of Object.entries(expected)) {
if (key in actual) {
if (!areObjectsEqual(actual[key], expectedVal, `${prefix}${key}`)) {
result = false;
}
} else {
info(`Missing ${prefix}${key} (${expectedVal})`);
result = false;
}
}
return result;
}
function clickAndAwait(toClick, evt, target) {
const menuPromise = BrowserTestUtils.waitForEvent(target, evt);
EventUtils.synthesizeMouseAtCenter(toClick, {}, window);
return menuPromise;
}
async function openTab(url, win) {
const options = {
gBrowser:
win?.gBrowser ||
Services.wm.getMostRecentWindow("navigator:browser").gBrowser,
url,
};
return BrowserTestUtils.openNewForegroundTab(options);
}
async function changeTab(tab, url) {
BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, url);
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
}
function closeTab(tab) {
BrowserTestUtils.removeTab(tab);
}
function switchToWindow(win) {
const promises = [
BrowserTestUtils.waitForEvent(win, "focus"),
BrowserTestUtils.waitForEvent(win, "activate"),
];
win.focus();
return Promise.all(promises);
}
function isSelectedTab(win, tab) {
const selectedTab = win.document.querySelector(".tabbrowser-tab[selected]");
is(selectedTab, tab);
}
async function setupPolicyEngineWithJson(json, customSchema) {
PoliciesPrefTracker.restoreDefaultValues();
if (typeof json != "object") {
let filePath = getTestFilePath(json ? json : "non-existing-file.json");
return EnterprisePolicyTesting.setupPolicyEngineWithJson(
filePath,
customSchema
);
}
return EnterprisePolicyTesting.setupPolicyEngineWithJson(json, customSchema);
}
async function ensureReportBrokenSiteDisabledByPolicy() {
await setupPolicyEngineWithJson({
policies: {
DisableFeedbackCommands: true,
},
});
}
registerCleanupFunction(async function resetPolicies() {
if (Services.policies.status != Ci.nsIEnterprisePolicies.INACTIVE) {
await setupPolicyEngineWithJson("");
}
EnterprisePolicyTesting.resetRunOnceState();
PoliciesPrefTracker.restoreDefaultValues();
PoliciesPrefTracker.stop();
});
function ensureReportBrokenSitePreffedOn() {
Services.prefs.setBoolPref(PREFS.DATAREPORTING_ENABLED, true);
Services.prefs.setBoolPref(PREFS.REPORTER_ENABLED, true);
ensureReasonDisabled();
}
function ensureReportBrokenSitePreffedOff() {
Services.prefs.setBoolPref(PREFS.REPORTER_ENABLED, false);
}
function ensureSendMoreInfoEnabled() {
Services.prefs.setBoolPref(PREFS.SEND_MORE_INFO, true);
}
function ensureSendMoreInfoDisabled() {
Services.prefs.setBoolPref(PREFS.SEND_MORE_INFO, false);
}
function ensureReasonDisabled() {
Services.prefs.setIntPref(PREFS.REASON, 0);
}
function ensureReasonOptional() {
Services.prefs.setIntPref(PREFS.REASON, 1);
}
function ensureReasonRequired() {
Services.prefs.setIntPref(PREFS.REASON, 2);
}
function isMenuItemEnabled(menuItem, itemDesc) {
ok(!menuItem.hidden, `${itemDesc} menu item is shown`);
ok(!menuItem.disabled, `${itemDesc} menu item is enabled`);
}
function isMenuItemHidden(menuItem, itemDesc) {
ok(
!menuItem || menuItem.hidden || !BrowserTestUtils.isVisible(menuItem),
`${itemDesc} menu item is hidden`
);
}
function isMenuItemDisabled(menuItem, itemDesc) {
ok(!menuItem.hidden, `${itemDesc} menu item is shown`);
ok(menuItem.disabled, `${itemDesc} menu item is disabled`);
}
function waitForWebcompatComTab(gBrowser) {
return BrowserTestUtils.waitForNewTab(gBrowser, NEW_REPORT_ENDPOINT_TEST_URL);
}
class ReportBrokenSiteHelper {
sourceMenu = undefined;
win = undefined;
constructor(sourceMenu) {
this.sourceMenu = sourceMenu;
this.win = sourceMenu.win;
}
getViewNode(id) {
return PanelMultiView.getViewNode(this.win.document, id);
}
get mainView() {
return this.getViewNode("report-broken-site-popup-mainView");
}
get sentView() {
return this.getViewNode("report-broken-site-popup-reportSentView");
}
get openPanel() {
return this.mainView?.closest("panel");
}
get opened() {
return this.openPanel?.hasAttribute("panelopen");
}
async click(triggerMenuItem) {
const window = triggerMenuItem.ownerGlobal;
await EventUtils.synthesizeMouseAtCenter(triggerMenuItem, {}, window);
}
async open(triggerMenuItem) {
const shownPromise = BrowserTestUtils.waitForEvent(
this.mainView,
"ViewShown"
);
const focusPromise = BrowserTestUtils.waitForEvent(this.URLInput, "focus");
await this.click(triggerMenuItem);
await shownPromise;
await focusPromise;
await BrowserTestUtils.waitForCondition(
() => this.URLInput.selectionStart === 0
);
}
async #assertClickAndViewChanges(button, view, newView, newFocus) {
ok(view.closest("panel").hasAttribute("panelopen"), "Panel is open");
ok(BrowserTestUtils.isVisible(button), "Button is visible");
ok(!button.disabled, "Button is enabled");
const promises = [];
if (newView) {
if (newView.nodeName == "panel") {
promises.push(BrowserTestUtils.waitForEvent(newView, "popupshown"));
} else {
promises.push(BrowserTestUtils.waitForEvent(newView, "ViewShown"));
}
} else {
promises.push(BrowserTestUtils.waitForEvent(view, "ViewHiding"));
}
if (newFocus) {
promises.push(BrowserTestUtils.waitForEvent(newFocus, "focus"));
}
EventUtils.synthesizeMouseAtCenter(button, {}, this.win);
await Promise.all(promises);
}
async awaitReportSentViewOpened() {
await Promise.all([
BrowserTestUtils.waitForEvent(this.sentView, "ViewShown"),
BrowserTestUtils.waitForEvent(this.okayButton, "focus"),
]);
}
async clickSend() {
await this.#assertClickAndViewChanges(
this.sendButton,
this.mainView,
this.sentView,
this.okayButton
);
}
waitForSendMoreInfoTab() {
return BrowserTestUtils.waitForNewTab(
this.win.gBrowser,
NEW_REPORT_ENDPOINT_TEST_URL
);
}
async clickSendMoreInfo() {
const newTabPromise = waitForWebcompatComTab(this.win.gBrowser);
EventUtils.synthesizeMouseAtCenter(this.sendMoreInfoLink, {}, this.win);
const newTab = await newTabPromise;
const receivedData = await SpecialPowers.spawn(
newTab.linkedBrowser,
[],
async function () {
await content.wrappedJSObject.messageArrived;
return content.wrappedJSObject.message;
}
);
this.win.gBrowser.removeCurrentTab();
return receivedData;
}
async clickCancel() {
await this.#assertClickAndViewChanges(this.cancelButton, this.mainView);
}
async clickOkay() {
await this.#assertClickAndViewChanges(this.okayButton, this.sentView);
}
async clickBack() {
await this.#assertClickAndViewChanges(
this.backButton,
this.sourceMenu.popup
);
}
isBackButtonEnabled() {
ok(BrowserTestUtils.isVisible(this.backButton), "Back button is visible");
ok(!this.backButton.disabled, "Back button is enabled");
}
close() {
if (this.opened) {
this.openPanel?.hidePopup(false);
}
this.sourceMenu?.close();
}
// UI element getters
get URLInput() {
return this.getViewNode("report-broken-site-popup-url");
}
get URLInvalidMessage() {
return this.getViewNode("report-broken-site-popup-invalid-url-msg");
}
get reasonInput() {
return this.getViewNode("report-broken-site-popup-reason");
}
get reasonDropdownPopup() {
return this.win.document.getElementById("ContentSelectDropdown").menupopup;
}
get reasonRequiredMessage() {
return this.getViewNode("report-broken-site-popup-missing-reason-msg");
}
get reasonLabelRequired() {
return this.getViewNode("report-broken-site-popup-reason-label");
}
get reasonLabelOptional() {
return this.getViewNode("report-broken-site-popup-reason-optional-label");
}
get descriptionTextarea() {
return this.getViewNode("report-broken-site-popup-description");
}
get learnMoreLink() {
return this.getViewNode("report-broken-site-popup-learn-more-link");
}
get sendMoreInfoLink() {
return this.getViewNode("report-broken-site-popup-send-more-info-link");
}
get backButton() {
return this.mainView.querySelector(".subviewbutton-back");
}
get blockedTrackersCheckbox() {
return this.getViewNode(
"report-broken-site-popup-blocked-trackers-checkbox"
);
}
set blockedTrackersCheckbox(checked) {
this.blockedTrackersCheckbox.checked = checked;
}
get sendButton() {
return this.getViewNode("report-broken-site-popup-send-button");
}
get cancelButton() {
return this.getViewNode("report-broken-site-popup-cancel-button");
}
get okayButton() {
return this.getViewNode("report-broken-site-popup-okay-button");
}
// Test helpers
#setInput(input, value) {
input.value = value;
input.dispatchEvent(
new UIEvent("input", { bubbles: true, view: this.win })
);
}
setURL(value) {
this.#setInput(this.URLInput, value);
}
chooseReason(value) {
const item = this.getViewNode(`report-broken-site-popup-reason-${value}`);
this.reasonInput.selectedIndex = item.index;
}
dismissDropdownPopup() {
const popup = this.reasonDropdownPopup;
const menuPromise = BrowserTestUtils.waitForPopupEvent(popup, "hidden");
popup.hidePopup();
return menuPromise;
}
setDescription(value) {
this.#setInput(this.descriptionTextarea, value);
}
isURL(expected) {
is(this.URLInput.value, expected);
}
isURLInvalidMessageShown() {
ok(
BrowserTestUtils.isVisible(this.URLInvalidMessage),
"'Please enter a valid URL' message is shown"
);
}
isURLInvalidMessageHidden() {
ok(
!BrowserTestUtils.isVisible(this.URLInvalidMessage),
"'Please enter a valid URL' message is hidden"
);
}
isReasonNeededMessageShown() {
ok(
BrowserTestUtils.isVisible(this.reasonRequiredMessage),
"'Please choose a reason' message is shown"
);
}
isReasonNeededMessageHidden() {
ok(
!BrowserTestUtils.isVisible(this.reasonRequiredMessage),
"'Please choose a reason' message is hidden"
);
}
isSendButtonEnabled() {
ok(BrowserTestUtils.isVisible(this.sendButton), "Send button is visible");
ok(!this.sendButton.disabled, "Send button is enabled");
}
isSendButtonDisabled() {
ok(BrowserTestUtils.isVisible(this.sendButton), "Send button is visible");
ok(this.sendButton.disabled, "Send button is disabled");
}
isSendMoreInfoShown() {
ok(
BrowserTestUtils.isVisible(this.sendMoreInfoLink),
"send more info is shown"
);
}
isSendMoreInfoHidden() {
ok(
!BrowserTestUtils.isVisible(this.sendMoreInfoLink),
"send more info is hidden"
);
}
isSendMoreInfoShownOrHiddenAppropriately() {
if (Services.prefs.getBoolPref(PREFS.SEND_MORE_INFO)) {
this.isSendMoreInfoShown();
} else {
this.isSendMoreInfoHidden();
}
}
isReasonHidden() {
ok(
!BrowserTestUtils.isVisible(this.reasonInput),
"reason drop-down is hidden"
);
ok(
!BrowserTestUtils.isVisible(this.reasonLabelOptional),
"optional reason label is hidden"
);
ok(
!BrowserTestUtils.isVisible(this.reasonLabelRequired),
"required reason label is hidden"
);
}
isReasonRequired() {
ok(
BrowserTestUtils.isVisible(this.reasonInput),
"reason drop-down is shown"
);
ok(
!BrowserTestUtils.isVisible(this.reasonLabelOptional),
"optional reason label is hidden"
);
ok(
BrowserTestUtils.isVisible(this.reasonLabelRequired),
"required reason label is shown"
);
}
isReasonOptional() {
ok(
BrowserTestUtils.isVisible(this.reasonInput),
"reason drop-down is shown"
);
ok(
BrowserTestUtils.isVisible(this.reasonLabelOptional),
"optional reason label is shown"
);
ok(
!BrowserTestUtils.isVisible(this.reasonLabelRequired),
"required reason label is hidden"
);
}
isReasonShownOrHiddenAppropriately() {
const pref = Services.prefs.getIntPref(PREFS.REASON);
if (pref == 2) {
this.isReasonOptional();
} else if (pref == 1) {
this.isReasonOptional();
} else {
this.isReasonHidden();
}
}
isDescription(expected) {
return this.descriptionTextarea.value == expected;
}
isMainViewResetToCurrentTab() {
this.isURL(this.win.gBrowser.selectedBrowser.currentURI.spec);
this.isDescription("");
this.isReasonShownOrHiddenAppropriately();
this.isSendMoreInfoShownOrHiddenAppropriately();
}
}
class MenuHelper {
menuDescription = undefined;
win = undefined;
constructor(win = window) {
this.win = win;
}
getViewNode(id) {
return PanelMultiView.getViewNode(this.win.document, id);
}
get showsBackButton() {
return true;
}
get reportBrokenSite() {
throw new Error("Should be defined in derived class");
}
get popup() {
throw new Error("Should be defined in derived class");
}
get opened() {
return this.popup?.hasAttribute("panelopen");
}
async open() {}
async close() {}
isReportBrokenSiteDisabled() {
return isMenuItemDisabled(this.reportBrokenSite, this.menuDescription);
}
isReportBrokenSiteEnabled() {
return isMenuItemEnabled(this.reportBrokenSite, this.menuDescription);
}
isReportBrokenSiteHidden() {
return isMenuItemHidden(this.reportBrokenSite, this.menuDescription);
}
async clickReportBrokenSiteAndAwaitWebCompatTabData() {
const newTabPromise = waitForWebcompatComTab(this.win.gBrowser);
await this.clickReportBrokenSite();
const newTab = await newTabPromise;
const receivedData = await SpecialPowers.spawn(
newTab.linkedBrowser,
[],
async function () {
await content.wrappedJSObject.messageArrived;
return content.wrappedJSObject.message;
}
);
this.win.gBrowser.removeCurrentTab();
return receivedData;
}
async clickReportBrokenSite() {
if (!this.opened) {
await this.open();
}
isMenuItemEnabled(this.reportBrokenSite, this.menuDescription);
const rbs = new ReportBrokenSiteHelper(this);
await rbs.click(this.reportBrokenSite);
return rbs;
}
async openReportBrokenSite() {
if (!this.opened) {
await this.open();
}
isMenuItemEnabled(this.reportBrokenSite, this.menuDescription);
const rbs = new ReportBrokenSiteHelper(this);
await rbs.open(this.reportBrokenSite);
return rbs;
}
async openAndPrefillReportBrokenSite(url = null, description = "") {
let rbs = await this.openReportBrokenSite();
rbs.isMainViewResetToCurrentTab();
if (url) {
rbs.setURL(url);
}
if (description) {
rbs.setDescription(description);
}
return rbs;
}
}
class AppMenuHelper extends MenuHelper {
menuDescription = "AppMenu";
get reportBrokenSite() {
return this.getViewNode("appMenu-report-broken-site-button");
}
get popup() {
return this.win.document.getElementById("appMenu-popup");
}
async open() {
await new CustomizableUITestUtils(this.win).openMainMenu();
}
async close() {
if (this.opened) {
await new CustomizableUITestUtils(this.win).hideMainMenu();
}
}
}
class HelpMenuHelper extends MenuHelper {
menuDescription = "Help Menu";
get showsBackButton() {
return false;
}
get reportBrokenSite() {
return this.win.document.getElementById("help_reportBrokenSite");
}
get popup() {
return this.getViewNode("PanelUI-helpView");
}
get helpMenu() {
return this.win.document.getElementById("menu_HelpPopup");
}
async openReportBrokenSite() {
// We can't actually open the Help menu properly in testing, so the best
// we can do to open its Report Broken Site panel is to force its DOM to be
// prepared, and then soft-click the Report Broken Site menuitem to open it.
await this.open();
const shownPromise = BrowserTestUtils.waitForEvent(
this.win,
"ViewShown",
true,
e => e.target.classList.contains("report-broken-site-view")
);
this.reportBrokenSite.click();
await shownPromise;
return new ReportBrokenSiteHelper(this);
}
async clickReportBrokenSite() {
await this.open();
this.reportBrokenSite.click();
return new ReportBrokenSiteHelper(this);
}
async open() {
const { helpMenu } = this;
const promise = BrowserTestUtils.waitForEvent(helpMenu, "popupshown");
// This event-faking method was copied from browser_title_case_menus.js.
// We can't actually open the Help menu in testing, but this lets us
// force its DOM to be properly built.
helpMenu.dispatchEvent(new MouseEvent("popupshowing", { bubbles: true }));
helpMenu.dispatchEvent(new MouseEvent("popupshown", { bubbles: true }));
await promise;
}
async close() {
const { helpMenu } = this;
const promise = BrowserTestUtils.waitForPopupEvent(helpMenu, "hidden");
// (Also copied from browser_title_case_menus.js)
// Just for good measure, we'll fire the popuphiding/popuphidden events
// after we close the menupopups.
helpMenu.dispatchEvent(new MouseEvent("popuphiding", { bubbles: true }));
helpMenu.dispatchEvent(new MouseEvent("popuphidden", { bubbles: true }));
await promise;
}
}
class ProtectionsPanelHelper extends MenuHelper {
menuDescription = "Protections Panel";
get reportBrokenSite() {
this.win.gProtectionsHandler._initializePopup();
return this.getViewNode("protections-popup-report-broken-site-button");
}
get popup() {
this.win.gProtectionsHandler._initializePopup();
return this.win.document.getElementById("protections-popup");
}
async open() {
const promise = BrowserTestUtils.waitForEvent(
this.win,
"popupshown",
true,
e => e.target.id == "protections-popup"
);
this.win.gProtectionsHandler.showProtectionsPopup();
await promise;
}
async close() {
if (this.opened) {
const popup = this.popup;
const promise = BrowserTestUtils.waitForPopupEvent(popup, "hidden");
PanelMultiView.hidePopup(popup, false);
await promise;
}
}
}
function AppMenu(win = window) {
return new AppMenuHelper(win);
}
function HelpMenu(win = window) {
return new HelpMenuHelper(win);
}
function ProtectionsPanel(win = window) {
return new ProtectionsPanelHelper(win);
}
function pressKeyAndAwait(event, key, config = {}) {
const win = config.window || window;
if (!event.then) {
event = BrowserTestUtils.waitForEvent(win, event, config.timeout || 200);
}
EventUtils.synthesizeKey(key, config, win);
return event;
}
async function pressKeyAndGetFocus(key, config = {}) {
return (await pressKeyAndAwait("focus", key, config)).target;
}
async function tabTo(match, win = window) {
const config = { window: win };
const { activeElement } = win.document;
if (activeElement?.matches(match)) {
return activeElement;
}
let initial = await pressKeyAndGetFocus("VK_TAB", config);
let target = initial;
do {
if (target.matches(match)) {
return target;
}
target = await pressKeyAndGetFocus("VK_TAB", config);
} while (target && target !== initial);
return undefined;
}
function filterFrameworkDetectorFails(ping, expected) {
// the framework detector's frame-script may fail to run in low memory or other
// weird corner-cases, so we ignore the results in that case if they don't match.
if (!areObjectsEqual(ping.frameworks, expected.frameworks)) {
const { fastclick, mobify, marfeel } = ping.frameworks;
if (!fastclick && !mobify && !marfeel) {
console.info("Ignoring failure to get framework data");
expected.frameworks = ping.frameworks;
}
}
}
async function setupStrictETP() {
await UrlClassifierTestUtils.addTestTrackers();
registerCleanupFunction(() => {
UrlClassifierTestUtils.cleanupTestTrackers();
});
await SpecialPowers.pushPrefEnv({
set: [
["security.mixed_content.block_active_content", true],
["security.mixed_content.block_display_content", true],
["security.mixed_content.upgrade_display_content", false],
[
"urlclassifier.trackingTable",
"content-track-digest256,mochitest2-track-simple",
],
["browser.contentblocking.category", "strict"],
],
});
}
// copied from browser/base/content/test/protectionsUI/head.js
function waitForContentBlockingEvent(numChanges = 1, win = null) {
if (!win) {
win = window;
}
return new Promise(resolve => {
let n = 0;
let listener = {
onContentBlockingEvent(webProgress, request, event) {
n = n + 1;
info(
`Received onContentBlockingEvent event: ${event} (${n} of ${numChanges})`
);
if (n >= numChanges) {
win.gBrowser.removeProgressListener(listener);
resolve(n);
}
},
};
win.gBrowser.addProgressListener(listener);
});
}

View File

@@ -1,355 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* Helper methods for testing sending reports with
* the Report Broken Site feature.
*/
/* import-globals-from head.js */
"use strict";
const { Troubleshoot } = ChromeUtils.importESModule(
"resource://gre/modules/Troubleshoot.sys.mjs"
);
function getSysinfoProperty(propertyName, defaultValue) {
try {
return Services.sysinfo.getProperty(propertyName);
} catch (e) {}
return defaultValue;
}
function securityStringToArray(str) {
return str ? str.split(";") : null;
}
function getExpectedGraphicsDevices(snapshot) {
const { graphics } = snapshot;
return [
graphics.adapterDeviceID,
graphics.adapterVendorID,
graphics.adapterDeviceID2,
graphics.adapterVendorID2,
]
.filter(i => i)
.sort();
}
function compareGraphicsDevices(expected, rawActual) {
const actual = rawActual
.map(({ deviceID, vendorID }) => [deviceID, vendorID])
.flat()
.filter(i => i)
.sort();
return areObjectsEqual(actual, expected);
}
function getExpectedGraphicsDrivers(snapshot) {
const { graphics } = snapshot;
const expected = [];
for (let i = 1; i < 3; ++i) {
const version = graphics[`webgl${i}Version`];
if (version && version != "-") {
expected.push(graphics[`webgl${i}Renderer`]);
expected.push(version);
}
}
return expected.filter(i => i).sort();
}
function compareGraphicsDrivers(expected, rawActual) {
const actual = rawActual
.map(({ renderer, version }) => [renderer, version])
.flat()
.filter(i => i)
.sort();
return areObjectsEqual(actual, expected);
}
function getExpectedGraphicsFeatures(snapshot) {
const expected = {};
for (let { name, log, status } of snapshot.graphics.featureLog.features) {
for (const item of log?.reverse() ?? []) {
if (item.failureId && item.status == status) {
status = `${status} (${item.message || item.failureId})`;
}
}
expected[name] = status;
}
return expected;
}
async function getExpectedWebCompatInfo(tab, snapshot, fullAppData = false) {
const gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
const { application, graphics, intl, securitySoftware } = snapshot;
const { fissionAutoStart, memorySizeBytes, updateChannel, userAgent } =
application;
const app = {
defaultLocales: intl.localeService.available,
defaultUseragentString: userAgent,
fissionEnabled: fissionAutoStart,
};
if (fullAppData) {
app.applicationName = application.name;
app.osArchitecture = getSysinfoProperty("arch", null);
app.osName = getSysinfoProperty("name", null);
app.osVersion = getSysinfoProperty("version", null);
app.updateChannel = updateChannel;
app.version = application.version;
}
const hasTouchScreen = graphics.info.ApzTouchInput == 1;
const { registeredAntiVirus, registeredAntiSpyware, registeredFirewall } =
securitySoftware;
const browserInfo = {
addons: [],
app,
experiments: [],
graphics: {
devicesJson(actualStr) {
const expected = getExpectedGraphicsDevices(snapshot);
// If undefined is saved to the Glean value here, we'll get the string "undefined" (invalid JSON).
// We should stop using JSON like this in bug 1875185.
if (!actualStr || actualStr == "undefined") {
return !expected.length;
}
return compareGraphicsDevices(expected, JSON.parse(actualStr));
},
driversJson(actualStr) {
const expected = getExpectedGraphicsDrivers(snapshot);
// If undefined is saved to the Glean value here, we'll get the string "undefined" (invalid JSON).
// We should stop using JSON like this in bug 1875185.
if (!actualStr || actualStr == "undefined") {
return !expected.length;
}
return compareGraphicsDrivers(expected, JSON.parse(actualStr));
},
featuresJson(actualStr) {
const expected = getExpectedGraphicsFeatures(snapshot);
// If undefined is saved to the Glean value here, we'll get the string "undefined" (invalid JSON).
// We should stop using JSON like this in bug 1875185.
if (!actualStr || actualStr == "undefined") {
return !expected.length;
}
return areObjectsEqual(JSON.parse(actualStr), expected);
},
hasTouchScreen,
monitorsJson(actualStr) {
const expected = gfxInfo.getMonitors();
// If undefined is saved to the Glean value here, we'll get the string "undefined" (invalid JSON).
// We should stop using JSON like this in bug 1875185.
if (!actualStr || actualStr == "undefined") {
return !expected.length;
}
return areObjectsEqual(JSON.parse(actualStr), expected);
},
},
prefs: {
cookieBehavior: Services.prefs.getIntPref(
"network.cookie.cookieBehavior",
-1
),
forcedAcceleratedLayers: Services.prefs.getBoolPref(
"layers.acceleration.force-enabled",
false
),
globalPrivacyControlEnabled: Services.prefs.getBoolPref(
"privacy.globalprivacycontrol.enabled",
false
),
installtriggerEnabled: Services.prefs.getBoolPref(
"extensions.InstallTrigger.enabled",
false
),
opaqueResponseBlocking: Services.prefs.getBoolPref(
"browser.opaqueResponseBlocking",
false
),
resistFingerprintingEnabled: Services.prefs.getBoolPref(
"privacy.resistFingerprinting",
false
),
softwareWebrender: Services.prefs.getBoolPref(
"gfx.webrender.software",
false
),
thirdPartyCookieBlockingEnabled: Services.prefs.getBoolPref(
"network.cookie.cookieBehavior.optInPartitioning",
false
),
thirdPartyCookieBlockingEnabledInPbm: Services.prefs.getBoolPref(
"network.cookie.cookieBehavior.optInPartitioning.pbmode",
false
),
},
security: {
antispyware: securityStringToArray(registeredAntiSpyware),
antivirus: securityStringToArray(registeredAntiVirus),
firewall: securityStringToArray(registeredFirewall),
},
system: {
isTablet: getSysinfoProperty("tablet", false),
memory: Math.round(memorySizeBytes / 1024 / 1024),
},
};
const tabInfo = await tab.linkedBrowser.ownerGlobal.SpecialPowers.spawn(
tab.linkedBrowser,
[],
async function () {
return {
devicePixelRatio: `${content.devicePixelRatio}`,
antitracking: {
blockList: "basic",
blockedOrigins: null,
isPrivateBrowsing: false,
hasTrackingContentBlocked: false,
hasMixedActiveContentBlocked: false,
hasMixedDisplayContentBlocked: false,
btpHasPurgedSite: false,
etpCategory: "standard",
},
frameworks: {
fastclick: false,
marfeel: false,
mobify: false,
},
languages: content.navigator.languages,
useragentString: content.navigator.userAgent,
};
}
);
browserInfo.graphics.devicePixelRatio = tabInfo.devicePixelRatio;
delete tabInfo.devicePixelRatio;
return { browserInfo, tabInfo };
}
function extractPingData(branch) {
const data = {};
for (const [name, value] of Object.entries(branch)) {
data[name] = value.testGetValue();
}
return data;
}
function extractBrokenSiteReportFromGleanPing(Glean) {
const ping = extractPingData(Glean.brokenSiteReport);
ping.tabInfo = extractPingData(Glean.brokenSiteReportTabInfo);
ping.tabInfo.antitracking = extractPingData(
Glean.brokenSiteReportTabInfoAntitracking
);
ping.tabInfo.frameworks = extractPingData(
Glean.brokenSiteReportTabInfoFrameworks
);
ping.browserInfo = {
addons: Array.from(Glean.brokenSiteReportBrowserInfo.addons.testGetValue()),
app: extractPingData(Glean.brokenSiteReportBrowserInfoApp),
graphics: extractPingData(Glean.brokenSiteReportBrowserInfoGraphics),
experiments: Array.from(
Glean.brokenSiteReportBrowserInfo.experiments.testGetValue()
),
prefs: extractPingData(Glean.brokenSiteReportBrowserInfoPrefs),
security: extractPingData(Glean.brokenSiteReportBrowserInfoSecurity),
system: extractPingData(Glean.brokenSiteReportBrowserInfoSystem),
};
return ping;
}
async function testSend(tab, menu, expectedOverrides = {}) {
const url = expectedOverrides.url ?? menu.win.gBrowser.currentURI.spec;
const description = expectedOverrides.description ?? "";
const breakageCategory = expectedOverrides.breakageCategory ?? null;
let rbs = await menu.openAndPrefillReportBrokenSite(url, description);
const snapshot = await Troubleshoot.snapshot();
const expected = await getExpectedWebCompatInfo(tab, snapshot);
expected.url = url;
expected.description = description;
expected.breakageCategory = breakageCategory;
if (expectedOverrides.addons) {
expected.browserInfo.addons = expectedOverrides.addons;
}
if (expectedOverrides.experiments) {
expected.browserInfo.experiments = expectedOverrides.experiments;
}
if (expectedOverrides.antitracking) {
expected.tabInfo.antitracking = expectedOverrides.antitracking;
if (expectedOverrides.antitracking.blockedOrigins) {
rbs.blockedTrackersCheckbox = true;
}
}
if (expectedOverrides.frameworks) {
expected.tabInfo.frameworks = expectedOverrides.frameworks;
}
if (breakageCategory) {
rbs.chooseReason(breakageCategory);
}
Services.fog.testResetFOG();
await GleanPings.brokenSiteReport.testSubmission(
() => {
const ping = extractBrokenSiteReportFromGleanPing(Glean);
// sanity checks
const { browserInfo, tabInfo } = ping;
ok(ping.url?.length, "Got a URL");
ok(
["basic", "strict"].includes(tabInfo.antitracking.blockList),
"Got a blockList"
);
if (rbs.blockedTrackersCheckbox.checked) {
ok(
Array.isArray(tabInfo.antitracking.blockedOrigins),
"Got an array for blockedOrigins"
);
} else {
ok(!tabInfo.antitracking.blockedOrigins, "No blockedOrigins included");
}
ok(tabInfo.useragentString?.length, "Got a final UA string");
ok(
browserInfo.app.defaultUseragentString?.length,
"Got a default UA string"
);
filterFrameworkDetectorFails(ping.tabInfo, expected.tabInfo);
ok(areObjectsEqual(ping, expected), "ping matches expectations");
},
() => rbs.clickSend()
);
await rbs.clickOkay();
const telemetry = Glean.webcompatreporting.send.testGetValue();
is(telemetry?.length, 1, "Got a 'send' telemetry event");
is(
telemetry[0].extra.sent_with_blocked_trackers,
String(!!expectedOverrides.antitracking?.blockedOrigins),
"Got correct 'sent_with_blocked_trackers' flag"
);
// re-opening the panel, the url and description should be reset
rbs = await menu.openReportBrokenSite();
rbs.isMainViewResetToCurrentTab();
ok(
!rbs.blockedTrackersCheckbox.checked,
"blocked trackers checkbox is reset"
);
rbs.close();
}

View File

@@ -1,27 +0,0 @@
<!DOCTYPE HTML>
<!-- 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/. -->
<html dir="ltr" xml:lang="en-US" lang="en-US">
<head>
<meta charset="utf8">
</head>
<body>
<script>
let ready;
window.wrtReady = new Promise(r => ready = r);
let arrived;
window.messageArrived = new Promise(r => arrived = r);
window.addEventListener("message", e => {
window.message = e.data;
arrived();
});
window.addEventListener("load", () => {
setTimeout(ready, 100);
});
</script>
</body>
</html>

View File

@@ -1,307 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* Helper methods for testing the "send more info" link
* of the Report Broken Site feature.
*/
/* import-globals-from head.js */
/* import-globals-from send.js */
"use strict";
Services.scriptloader.loadSubScript(
getRootDirectory(gTestPath) + "send.js",
this
);
async function reformatExpectedWebCompatInfo(tab, overrides) {
const gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
const snapshot = await Troubleshoot.snapshot();
const expected = await getExpectedWebCompatInfo(tab, snapshot, true);
const { browserInfo, tabInfo } = expected;
const { app, graphics, prefs, security } = browserInfo;
const {
applicationName,
defaultUseragentString,
fissionEnabled,
osArchitecture,
osName,
osVersion,
updateChannel,
version,
} = app;
const { devicePixelRatio, hasTouchScreen } = graphics;
const { antitracking, languages, useragentString } = tabInfo;
const addons = overrides.addons || [];
const experiments = overrides.experiments || [];
const atOverrides = overrides.antitracking;
const blockList = atOverrides?.blockList ?? antitracking.blockList;
const blockedOrigins =
atOverrides?.blockedOrigins ?? antitracking.blockedOrigins ?? [];
const hasMixedActiveContentBlocked =
atOverrides?.hasMixedActiveContentBlocked ??
antitracking.hasMixedActiveContentBlocked;
const hasMixedDisplayContentBlocked =
atOverrides?.hasMixedDisplayContentBlocked ??
antitracking.hasMixedDisplayContentBlocked;
const hasTrackingContentBlocked =
atOverrides?.hasTrackingContentBlocked ??
antitracking.hasTrackingContentBlocked;
const isPrivateBrowsing =
atOverrides?.isPrivateBrowsing ?? antitracking.isPrivateBrowsing;
const btpHasPurgedSite =
atOverrides?.btpHasPurgedSite ?? antitracking.btpHasPurgedSite;
const etpCategory = atOverrides?.etpCategory ?? antitracking.etpCategory;
const extra_labels = [];
const frameworks = overrides.frameworks ?? {
fastclick: false,
mobify: false,
marfeel: false,
};
// ignore the console log unless explicily testing for it.
const consoleLog = overrides.consoleLog ?? (() => true);
const finalPrefs = {};
for (const [key, pref] of Object.entries({
cookieBehavior: "network.cookie.cookieBehavior",
forcedAcceleratedLayers: "layers.acceleration.force-enabled",
globalPrivacyControlEnabled: "privacy.globalprivacycontrol.enabled",
installtriggerEnabled: "extensions.InstallTrigger.enabled",
opaqueResponseBlocking: "browser.opaqueResponseBlocking",
resistFingerprintingEnabled: "privacy.resistFingerprinting",
softwareWebrender: "gfx.webrender.software",
thirdPartyCookieBlockingEnabled:
"network.cookie.cookieBehavior.optInPartitioning",
thirdPartyCookieBlockingEnabledInPbm:
"network.cookie.cookieBehavior.optInPartitioning.pbmode",
})) {
if (key in prefs) {
finalPrefs[pref] = prefs[key];
}
}
const reformatted = {
blockList,
details: {
additionalData: {
addons,
applicationName,
blockList,
blockedOrigins,
buildId: snapshot.application.buildID,
devicePixelRatio: parseInt(devicePixelRatio),
experiments,
finalUserAgent: useragentString,
fissionEnabled,
gfxData: {
devices(actual) {
const devices = getExpectedGraphicsDevices(snapshot);
return compareGraphicsDevices(devices, actual);
},
drivers(actual) {
const drvs = getExpectedGraphicsDrivers(snapshot);
return compareGraphicsDrivers(drvs, actual);
},
features(actual) {
const features = getExpectedGraphicsFeatures(snapshot);
return areObjectsEqual(actual, features);
},
hasTouchScreen,
monitors(actual) {
return areObjectsEqual(actual, gfxInfo.getMonitors());
},
},
hasMixedActiveContentBlocked,
hasMixedDisplayContentBlocked,
hasTrackingContentBlocked,
btpHasPurgedSite,
isPB: isPrivateBrowsing,
etpCategory,
languages,
locales: snapshot.intl.localeService.available,
memoryMB: browserInfo.system.memory,
osArchitecture,
osName,
osVersion,
prefs: finalPrefs,
version,
},
blockList,
channel: updateChannel,
consoleLog,
defaultUserAgent: defaultUseragentString,
frameworks,
hasTouchScreen,
"gfx.webrender.software": prefs.softwareWebrender,
"mixed active content blocked": hasMixedActiveContentBlocked,
"mixed passive content blocked": hasMixedDisplayContentBlocked,
"tracking content blocked": hasTrackingContentBlocked
? `true (${blockList})`
: "false",
"btp has purged site": btpHasPurgedSite,
},
extra_labels,
src: "desktop-reporter",
utm_campaign: "report-broken-site",
utm_source: "desktop-reporter",
};
const { gfxData } = reformatted.details.additionalData;
for (const optional of [
"directWriteEnabled",
"directWriteVersion",
"clearTypeParameters",
"targetFrameRate",
]) {
if (optional in snapshot.graphics) {
gfxData[optional] = snapshot.graphics[optional];
}
}
// We only care about this pref on Linux right now on webcompat.com.
if (AppConstants.platform != "linux") {
delete finalPrefs["layers.acceleration.force-enabled"];
} else {
reformatted.details["layers.acceleration.force-enabled"] =
finalPrefs["layers.acceleration.force-enabled"];
}
// Only bother adding the security key if it has any data
if (Object.values(security).filter(e => e).length) {
reformatted.details.additionalData.sec = security;
}
const expectedCodecs = snapshot.media.codecSupportInfo
.replaceAll(" NONE", "")
.split("\n")
.sort()
.join("\n");
if (expectedCodecs) {
reformatted.details.additionalData.gfxData.codecSupport = rawActual => {
const actual = Object.entries(rawActual)
.map(
([
name,
{ hardwareDecode, softwareDecode, hardwareEncode, softwareEncode },
]) =>
(
`${name} ` +
`${softwareDecode ? "SWDEC " : ""}` +
`${hardwareDecode ? "HWDEC " : ""}` +
`${softwareEncode ? "SWENC " : ""}` +
`${hardwareEncode ? "HWENC " : ""}`
).trim()
)
.sort()
.join("\n");
return areObjectsEqual(actual, expectedCodecs);
};
}
if (blockList != "basic") {
extra_labels.push(`type-tracking-protection-${blockList}`);
}
if (overrides.expectNoTabDetails) {
delete reformatted.details.frameworks;
delete reformatted.details.consoleLog;
delete reformatted.details["mixed active content blocked"];
delete reformatted.details["mixed passive content blocked"];
delete reformatted.details["tracking content blocked"];
delete reformatted.details["btp has purged site"];
} else {
const { fastclick, mobify, marfeel } = frameworks;
if (fastclick) {
extra_labels.push("type-fastclick");
reformatted.details.fastclick = true;
}
if (mobify) {
extra_labels.push("type-mobify");
reformatted.details.mobify = true;
}
if (marfeel) {
extra_labels.push("type-marfeel");
reformatted.details.marfeel = true;
}
}
extra_labels.sort();
return reformatted;
}
async function testSendMoreInfo(tab, menu, expectedOverrides = {}) {
const url = expectedOverrides.url ?? menu.win.gBrowser.currentURI.spec;
const description = expectedOverrides.description ?? "";
let rbs = await menu.openAndPrefillReportBrokenSite(url, description);
const receivedData = await rbs.clickSendMoreInfo();
await checkWebcompatComPayload(
tab,
url,
description,
expectedOverrides,
receivedData
);
// re-opening the panel, the url and description should be reset
rbs = await menu.openReportBrokenSite();
rbs.isMainViewResetToCurrentTab();
rbs.close();
}
async function testWebcompatComFallback(tab, menu) {
const url = menu.win.gBrowser.currentURI.spec;
const receivedData =
await menu.clickReportBrokenSiteAndAwaitWebCompatTabData();
await checkWebcompatComPayload(tab, url, "", {}, receivedData);
menu.close();
}
async function checkWebcompatComPayload(
tab,
url,
description,
expectedOverrides,
receivedData
) {
const expected = await reformatExpectedWebCompatInfo(tab, expectedOverrides);
expected.url = url;
expected.description = description;
// sanity checks
const { message } = receivedData;
const { details } = message;
const { additionalData } = details;
ok(message.url?.length, "Got a URL");
ok(["basic", "strict"].includes(details.blockList), "Got a blockList");
ok(additionalData.applicationName?.length, "Got an app name");
ok(additionalData.osArchitecture?.length, "Got an OS arch");
ok(additionalData.osName?.length, "Got an OS name");
ok(additionalData.osVersion?.length, "Got an OS version");
ok(additionalData.version?.length, "Got an app version");
ok(details.channel?.length, "Got an app channel");
ok(details.defaultUserAgent?.length, "Got a default UA string");
ok(additionalData.finalUserAgent?.length, "Got a final UA string");
// If we're sending any tab-specific data (which includes console logs),
// check that there is also a valid screenshot.
if ("consoleLog" in details) {
const isScreenshotValid = await new Promise(done => {
var image = new Image();
image.onload = () => done(image.width > 0);
image.onerror = () => done(false);
image.src = receivedData.screenshot;
});
ok(isScreenshotValid, "Got a valid screenshot");
}
filterFrameworkDetectorFails(message.details, expected.details);
ok(areObjectsEqual(message, expected), "sent info matches expectations");
}

View File

@@ -1,14 +0,0 @@
[DEFAULT]
support-files = [
"head.js",
"empty_file.html",
]
["browser_bug400731.js"]
["browser_bug415846.js"]
skip-if = ["true"] # Bug 1248632
["browser_mixedcontent_aboutblocked.js"]
["browser_whitelisted.js"]

View File

@@ -1,65 +0,0 @@
/* Check presence of the "Ignore this warning" button */
function checkWarningState() {
return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
return !!content.document.getElementById("ignore_warning_link");
});
}
add_task(async function testMalware() {
await new Promise(resolve => waitForDBInit(resolve));
await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
const url = "http://www.itisatrap.org/firefox/its-an-attack.html";
BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, url);
await BrowserTestUtils.browserLoaded(
gBrowser.selectedBrowser,
false,
url,
true
);
let buttonPresent = await checkWarningState();
ok(buttonPresent, "Ignore warning link should be present for malware");
});
add_task(async function testUnwanted() {
Services.prefs.setBoolPref("browser.safebrowsing.allowOverride", false);
// Now launch the unwanted software test
const url = "http://www.itisatrap.org/firefox/unwanted.html";
BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, url);
await BrowserTestUtils.browserLoaded(
gBrowser.selectedBrowser,
false,
url,
true
);
// Confirm that "Ignore this warning" is visible - bug 422410
let buttonPresent = await checkWarningState();
ok(
!buttonPresent,
"Ignore warning link should be missing for unwanted software"
);
});
add_task(async function testPhishing() {
Services.prefs.setBoolPref("browser.safebrowsing.allowOverride", true);
// Now launch the phishing test
const url = "http://www.itisatrap.org/firefox/its-a-trap.html";
BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, url);
await BrowserTestUtils.browserLoaded(
gBrowser.selectedBrowser,
false,
url,
true
);
let buttonPresent = await checkWarningState();
ok(buttonPresent, "Ignore warning link should be present for phishing");
gBrowser.removeCurrentTab();
});

View File

@@ -1,98 +0,0 @@
/* Check for the correct behaviour of the report web forgery/not a web forgery
menu items.
Mac makes this astonishingly painful to test since their help menu is special magic,
but we can at least test it on the other platforms.*/
const NORMAL_PAGE = "http://example.com";
const PHISH_PAGE = "http://www.itisatrap.org/firefox/its-a-trap.html";
/**
* Opens a new tab and browses to some URL, tests for the existence
* of the phishing menu items, and then runs a test function to check
* the state of the menu once opened. This function will take care of
* opening and closing the menu.
*
* @param url (string)
* The URL to browse the tab to.
* @param testFn (function)
* The function to run once the menu has been opened. This
* function will be passed the "reportMenu" and "errorMenu"
* DOM nodes as arguments, in that order. This function
* should not yield anything.
* @returns Promise
*/
function check_menu_at_page(url, testFn) {
return BrowserTestUtils.withNewTab(
{
gBrowser,
url: "about:blank",
},
async function (browser) {
// We don't get load events when the DocShell redirects to error
// pages, but we do get DOMContentLoaded, so we'll wait for that.
let dclPromise = SpecialPowers.spawn(browser, [], async function () {
await ContentTaskUtils.waitForEvent(this, "DOMContentLoaded", false);
});
BrowserTestUtils.startLoadingURIString(browser, url);
await dclPromise;
let menu = document.getElementById("menu_HelpPopup");
ok(menu, "Help menu should exist");
let reportMenu = document.getElementById(
"menu_HelpPopup_reportPhishingtoolmenu"
);
ok(reportMenu, "Report phishing menu item should exist");
let errorMenu = document.getElementById(
"menu_HelpPopup_reportPhishingErrortoolmenu"
);
ok(errorMenu, "Report phishing error menu item should exist");
let menuOpen = BrowserTestUtils.waitForEvent(menu, "popupshown");
menu.openPopup(null, "", 0, 0, false, null);
await menuOpen;
testFn(reportMenu, errorMenu);
let menuClose = BrowserTestUtils.waitForEvent(menu, "popuphidden");
menu.hidePopup();
await menuClose;
}
);
}
/**
* Tests that we show the "Report this page" menu item at a normal
* page.
*/
add_task(async function () {
await check_menu_at_page(NORMAL_PAGE, (reportMenu, errorMenu) => {
ok(
!reportMenu.hidden,
"Report phishing menu should be visible on normal sites"
);
ok(
errorMenu.hidden,
"Report error menu item should be hidden on normal sites"
);
});
});
/**
* Tests that we show the "Report this page is okay" menu item at
* a reported attack site.
*/
add_task(async function () {
await check_menu_at_page(PHISH_PAGE, (reportMenu, errorMenu) => {
ok(
reportMenu.hidden,
"Report phishing menu should be hidden on phishing sites"
);
ok(
!errorMenu.hidden,
"Report error menu item should be visible on phishing sites"
);
});
});

View File

@@ -1,47 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const SECURE_CONTAINER_URL =
"https://example.com/browser/browser/components/safebrowsing/content/test/empty_file.html";
add_task(async function testNormalBrowsing() {
await SpecialPowers.pushPrefEnv({
set: [["browser.safebrowsing.only_top_level", false]],
});
await BrowserTestUtils.withNewTab(
SECURE_CONTAINER_URL,
async function (browser) {
// Before we load the phish url, we have to make sure the hard-coded
// black list has been added to the database.
await new Promise(resolve => waitForDBInit(resolve));
let promise = new Promise(resolve => {
// Register listener before loading phish URL.
let removeFunc = BrowserTestUtils.addContentEventListener(
browser,
"AboutBlockedLoaded",
() => {
removeFunc();
resolve();
},
{ wantUntrusted: true }
);
});
await SpecialPowers.spawn(
browser,
[PHISH_URL],
async function (aPhishUrl) {
// Create an iframe which is going to load a phish url.
let iframe = content.document.createElement("iframe");
iframe.src = aPhishUrl;
content.document.body.appendChild(iframe);
}
);
await promise;
ok(true, "about:blocked is successfully loaded!");
}
);
});

View File

@@ -1,46 +0,0 @@
/* Ensure that hostnames in the whitelisted pref are not blocked. */
const PREF_WHITELISTED_HOSTNAMES = "urlclassifier.skipHostnames";
const TEST_PAGE = "http://www.itisatrap.org/firefox/its-an-attack.html";
var tabbrowser = null;
registerCleanupFunction(function () {
tabbrowser = null;
Services.prefs.clearUserPref(PREF_WHITELISTED_HOSTNAMES);
while (gBrowser.tabs.length > 1) {
gBrowser.removeCurrentTab();
}
});
function testBlockedPage() {
info("Non-whitelisted pages must be blocked");
ok(true, "about:blocked was shown");
}
function testWhitelistedPage(window) {
info("Whitelisted pages must be skipped");
var getmeout_button = window.document.getElementById("getMeOutButton");
var ignorewarning_button = window.document.getElementById(
"ignoreWarningButton"
);
ok(!getmeout_button, "GetMeOut button not present");
ok(!ignorewarning_button, "IgnoreWarning button not present");
}
add_task(async function testNormalBrowsing() {
tabbrowser = gBrowser;
let tab = (tabbrowser.selectedTab = BrowserTestUtils.addTab(tabbrowser));
info("Load a test page that's whitelisted");
Services.prefs.setCharPref(
PREF_WHITELISTED_HOSTNAMES,
"example.com,www.ItIsaTrap.org,example.net"
);
await promiseTabLoadEvent(tab, TEST_PAGE, "load");
testWhitelistedPage(tab.ownerGlobal);
info("Load a test page that's no longer whitelisted");
Services.prefs.setCharPref(PREF_WHITELISTED_HOSTNAMES, "");
await promiseTabLoadEvent(tab, TEST_PAGE, "AboutBlockedLoaded");
testBlockedPage(tab.ownerGlobal);
});

View File

@@ -1 +0,0 @@
<html><body></body></html>

View File

@@ -1,103 +0,0 @@
// This url must sync with the table, url in SafeBrowsing.sys.mjs addMozEntries
const PHISH_TABLE = "moztest-phish-simple";
const PHISH_URL = "https://www.itisatrap.org/firefox/its-a-trap.html";
/**
* Waits for a load (or custom) event to finish in a given tab. If provided
* load an uri into the tab.
*
* @param tab
* The tab to load into.
* @param [optional] url
* The url to load, or the current url.
* @param [optional] event
* The load event type to wait for. Defaults to "load".
* @return {Promise} resolved when the event is handled.
* @resolves to the received event
* @rejects if a valid load event is not received within a meaningful interval
*/
function promiseTabLoadEvent(tab, url, eventType = "load") {
info(`Wait tab event: ${eventType}`);
function handle(loadedUrl) {
if (loadedUrl === "about:blank" || (url && loadedUrl !== url)) {
info(`Skipping spurious load event for ${loadedUrl}`);
return false;
}
info("Tab event received: load");
return true;
}
let loaded;
if (eventType === "load") {
loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, handle);
} else {
// No need to use handle.
loaded = BrowserTestUtils.waitForContentEvent(
tab.linkedBrowser,
eventType,
true,
undefined,
true
);
}
if (url) {
BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, url);
}
return loaded;
}
// This function is mostly ported from classifierCommon.js
// under toolkit/components/url-classifier/tests/mochitest.
function waitForDBInit(callback) {
// Since there are two cases that may trigger the callback,
// we have to carefully avoid multiple callbacks and observer
// leaking.
let didCallback = false;
function callbackOnce() {
if (!didCallback) {
Services.obs.removeObserver(obsFunc, "mozentries-update-finished");
callback();
}
didCallback = true;
}
// The first part: listen to internal event.
function obsFunc() {
ok(true, "Received internal event!");
callbackOnce();
}
Services.obs.addObserver(obsFunc, "mozentries-update-finished");
// The second part: we might have missed the event. Just do
// an internal database lookup to confirm if the url has been
// added.
let principal = Services.scriptSecurityManager.createContentPrincipal(
Services.io.newURI(PHISH_URL),
{}
);
let dbService = Cc["@mozilla.org/url-classifier/dbservice;1"].getService(
Ci.nsIUrlClassifierDBService
);
dbService.lookup(principal, PHISH_TABLE, value => {
if (value === PHISH_TABLE) {
ok(true, "DB lookup success!");
callbackOnce();
}
});
}
Services.prefs.setCharPref(
"urlclassifier.malwareTable",
"moztest-malware-simple,moztest-unwanted-simple,moztest-harmful-simple"
);
Services.prefs.setCharPref("urlclassifier.phishTable", "moztest-phish-simple");
Services.prefs.setCharPref(
"urlclassifier.blockedTable",
"moztest-block-simple"
);
SafeBrowsing.init();

View File

@@ -1,103 +0,0 @@
[DEFAULT]
["browser_1119088.js"]
disabled="Disabled by import_external_tests.py"
support-files = ["mac_desktop_image.py"]
run-if = ["os == 'mac'"]
tags = "os_integration"
skip-if = ["os == 'mac' && os_version == '14.70' && processor == 'x86_64'"] # Bug 1869703
["browser_420786.js"]
run-if = ["os == 'linux'"]
["browser_633221.js"]
run-if = ["os == 'linux'"]
["browser_createWindowsShortcut.js"]
run-if = ["os == 'win'"]
["browser_doesAppNeedPin.js"]
["browser_headless_screenshot_1.js"]
support-files = [
"head.js",
"headless.html",
]
skip-if = [
"os == 'win'",
"ccov",
"tsan", # Bug 1429950, Bug 1583315, Bug 1696109, Bug 1701449
]
tags = "os_integration"
["browser_headless_screenshot_2.js"]
support-files = [
"head.js",
"headless.html",
]
skip-if = [
"os == 'win'",
"ccov",
"tsan", # Bug 1429950, Bug 1583315, Bug 1696109, Bug 1701449
]
["browser_headless_screenshot_3.js"]
support-files = [
"head.js",
"headless.html",
]
skip-if = [
"os == 'win'",
"ccov",
"tsan", # Bug 1429950, Bug 1583315, Bug 1696109, Bug 1701449
]
["browser_headless_screenshot_4.js"]
support-files = [
"head.js",
"headless.html",
]
skip-if = [
"os == 'win'",
"ccov",
"tsan", # Bug 1429950, Bug 1583315, Bug 1696109, Bug 1701449
]
["browser_headless_screenshot_cross_origin.js"]
support-files = [
"head.js",
"headless_cross_origin.html",
"headless_iframe.html",
]
skip-if = [
"os == 'win'",
"ccov",
"tsan", # Bug 1429950, Bug 1583315, Bug 1696109, Bug 1701449
]
["browser_headless_screenshot_redirect.js"]
support-files = [
"head.js",
"headless.html",
"headless_redirect.html",
"headless_redirect.html^headers^",
]
skip-if = [
"os == 'win'",
"ccov",
"tsan", # Bug 1429950, Bug 1583315, Bug 1696109, Bug 1701449
]
["browser_processAUMID.js"]
run-if = ["os == 'win'"]
["browser_setDefaultBrowser.js"]
tags = "os_integration"
["browser_setDefaultPDFHandler.js"]
run-if = ["os == 'win'"]
tags = "os_integration"
["browser_setDesktopBackgroundPreview.js"]
disabled="Disabled by import_external_tests.py"
tags = "os_integration"

View File

@@ -1,173 +0,0 @@
// Where we save the desktop background to (~/Pictures).
const NS_OSX_PICTURE_DOCUMENTS_DIR = "Pct";
// Paths used to run the CLI command (python script) that is used to
// 1) check the desktop background image matches what we set it to via
// nsIShellService::setDesktopBackground() and
// 2) revert the desktop background image to the OS default
let kPythonPath = "/usr/bin/python";
if (AppConstants.isPlatformAndVersionAtLeast("macosx", 23.0)) {
kPythonPath = "/usr/local/bin/python3";
}
const kDesktopCheckerScriptPath =
"browser/browser/components/shell/test/mac_desktop_image.py";
const kDefaultBackgroundImage =
"/System/Library/Desktop Pictures/Solid Colors/Teal.png";
ChromeUtils.defineESModuleGetters(this, {
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
});
function getPythonExecutableFile() {
let python = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
info(`Using python at location ${kPythonPath}`);
python.initWithPath(kPythonPath);
return python;
}
function createProcess() {
return Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
}
// Use a CLI command to set the desktop background to |imagePath|. Returns the
// exit code of the CLI command which reflects whether or not the background
// image was successfully set. Returns 0 on success.
function setDesktopBackgroundCLI(imagePath) {
let setBackgroundProcess = createProcess();
setBackgroundProcess.init(getPythonExecutableFile());
let args = [
kDesktopCheckerScriptPath,
"--verbose",
"--set-background-image",
imagePath,
];
setBackgroundProcess.run(true, args, args.length);
return setBackgroundProcess.exitValue;
}
// Check the desktop background is |imagePath| using a CLI command.
// Returns the exit code of the CLI command which reflects whether or not
// the provided image path matches the path of the current desktop background
// image. A return value of 0 indicates success/match.
function checkDesktopBackgroundCLI(imagePath) {
let checkBackgroundProcess = createProcess();
checkBackgroundProcess.init(getPythonExecutableFile());
let args = [
kDesktopCheckerScriptPath,
"--verbose",
"--check-background-image",
imagePath,
];
checkBackgroundProcess.run(true, args, args.length);
return checkBackgroundProcess.exitValue;
}
// Use the python script to set/check the desktop background is |imagePath|
function setAndCheckDesktopBackgroundCLI(imagePath) {
Assert.ok(FileUtils.File(imagePath).exists(), `${imagePath} exists`);
let setExitCode = setDesktopBackgroundCLI(imagePath);
Assert.equal(setExitCode, 0, `Setting background via CLI to ${imagePath}`);
let checkExitCode = checkDesktopBackgroundCLI(imagePath);
Assert.equal(checkExitCode, 0, `Checking background via CLI is ${imagePath}`);
}
// Restore the automation default background image. i.e., the default used
// in the automated test environment, not the OS default.
function restoreDefaultBackground() {
let defaultBackgroundPath;
defaultBackgroundPath = kDefaultBackgroundImage;
setAndCheckDesktopBackgroundCLI(defaultBackgroundPath);
}
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [["test.wait300msAfterTabSwitch", true]],
});
});
/**
* Tests "Set As Desktop Background" platform implementation on macOS.
*
* Sets the desktop background image to the browser logo from the about:logo
* page and verifies it was set successfully. Setting the desktop background
* (which uses the nsIShellService::setDesktopBackground() interface method)
* downloads the image to ~/Pictures using a unique file name and sets the
* desktop background to the downloaded file leaving the download in place.
* After setDesktopBackground() is called, the test uses a python script to
* validate that the current desktop background is in fact set to the
* downloaded logo.
*/
add_task(async function () {
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: "about:logo",
},
async () => {
let dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService(
Ci.nsIDirectoryServiceProvider
);
let uuidGenerator = Services.uuid;
let shellSvc = Cc["@mozilla.org/browser/shell-service;1"].getService(
Ci.nsIShellService
);
// Ensure we are starting with the default background. Log a
// failure if we can not set the background to the default, but
// ignore the case where the background is not already set as that
// that may be due to a previous test failure.
restoreDefaultBackground();
// Generate a UUID (with non-alphanumberic characters removed) to build
// up a filename for the desktop background. Use a UUID to distinguish
// between runs so we won't be confused by images that were not properly
// cleaned up after previous runs.
let uuid = uuidGenerator.generateUUID().toString().replace(/\W/g, "");
// Set the background image path to be $HOME/Pictures/<UUID>.png.
// nsIShellService.setDesktopBackground() downloads the image to this
// path and then sets it as the desktop background image, leaving the
// image in place.
let backgroundImage = dirSvc.getFile(NS_OSX_PICTURE_DOCUMENTS_DIR, {});
backgroundImage.append(uuid + ".png");
if (backgroundImage.exists()) {
backgroundImage.remove(false);
}
// For simplicity, we're going to reach in and access the image on the
// page directly, which means the page shouldn't be running in a remote
// browser. Thankfully, about:logo runs in the parent process for now.
Assert.ok(
!gBrowser.selectedBrowser.isRemoteBrowser,
"image can be accessed synchronously from the parent process"
);
let image = gBrowser.selectedBrowser.contentDocument.images[0];
info(`Setting/saving desktop background to ${backgroundImage.path}`);
// Saves the file in ~/Pictures
shellSvc.setDesktopBackground(image, 0, backgroundImage.leafName);
await BrowserTestUtils.waitForCondition(() => backgroundImage.exists());
info(`${backgroundImage.path} downloaded`);
Assert.ok(
FileUtils.File(backgroundImage.path).exists(),
`${backgroundImage.path} exists`
);
// Check that the desktop background image is the image we set above.
let exitCode = checkDesktopBackgroundCLI(backgroundImage.path);
Assert.equal(exitCode, 0, `background should be ${backgroundImage.path}`);
// Restore the background image to the Mac default.
restoreDefaultBackground();
// We no longer need the downloaded image.
backgroundImage.remove(false);
}
);
});

View File

@@ -1,101 +0,0 @@
const DG_BACKGROUND = "/desktop/gnome/background";
const DG_IMAGE_KEY = DG_BACKGROUND + "/picture_filename";
const DG_OPTION_KEY = DG_BACKGROUND + "/picture_options";
const DG_DRAW_BG_KEY = DG_BACKGROUND + "/draw_background";
const GS_BG_SCHEMA = "org.gnome.desktop.background";
const GS_IMAGE_KEY = "picture-uri";
const GS_OPTION_KEY = "picture-options";
const GS_DRAW_BG_KEY = "draw-background";
add_task(async function () {
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: "about:logo",
},
() => {
var brandName = Services.strings
.createBundle("chrome://branding/locale/brand.properties")
.GetStringFromName("brandShortName");
var dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService(
Ci.nsIDirectoryServiceProvider
);
var homeDir = dirSvc.getFile("Home", {});
var wpFile = homeDir.clone();
wpFile.append(brandName + "_wallpaper.png");
// Backup the existing wallpaper so that this test doesn't change the user's
// settings.
var wpFileBackup = homeDir.clone();
wpFileBackup.append(brandName + "_wallpaper.png.backup");
if (wpFileBackup.exists()) {
wpFileBackup.remove(false);
}
if (wpFile.exists()) {
wpFile.copyTo(null, wpFileBackup.leafName);
}
var shell = Cc["@mozilla.org/browser/shell-service;1"]
.getService(Ci.nsIShellService)
.QueryInterface(Ci.nsIGNOMEShellService);
// For simplicity, we're going to reach in and access the image on the
// page directly, which means the page shouldn't be running in a remote
// browser. Thankfully, about:logo runs in the parent process for now.
Assert.ok(
!gBrowser.selectedBrowser.isRemoteBrowser,
"image can be accessed synchronously from the parent process"
);
var image = content.document.images[0];
let checkWallpaper, restoreSettings;
try {
const prevImage = shell.getGSettingsString(GS_BG_SCHEMA, GS_IMAGE_KEY);
const prevOption = shell.getGSettingsString(
GS_BG_SCHEMA,
GS_OPTION_KEY
);
checkWallpaper = function (position, expectedGSettingsPosition) {
shell.setDesktopBackground(image, position, "");
ok(wpFile.exists(), "Wallpaper was written to disk");
is(
shell.getGSettingsString(GS_BG_SCHEMA, GS_IMAGE_KEY),
encodeURI("file://" + wpFile.path),
"Wallpaper file GSettings key is correct"
);
is(
shell.getGSettingsString(GS_BG_SCHEMA, GS_OPTION_KEY),
expectedGSettingsPosition,
"Wallpaper position GSettings key is correct"
);
};
restoreSettings = function () {
shell.setGSettingsString(GS_BG_SCHEMA, GS_IMAGE_KEY, prevImage);
shell.setGSettingsString(GS_BG_SCHEMA, GS_OPTION_KEY, prevOption);
};
} catch (e) {}
checkWallpaper(Ci.nsIShellService.BACKGROUND_TILE, "wallpaper");
checkWallpaper(Ci.nsIShellService.BACKGROUND_STRETCH, "stretched");
checkWallpaper(Ci.nsIShellService.BACKGROUND_CENTER, "centered");
checkWallpaper(Ci.nsIShellService.BACKGROUND_FILL, "zoom");
checkWallpaper(Ci.nsIShellService.BACKGROUND_FIT, "scaled");
checkWallpaper(Ci.nsIShellService.BACKGROUND_SPAN, "spanned");
restoreSettings();
// Restore files
if (wpFileBackup.exists()) {
wpFileBackup.moveTo(null, wpFile.leafName);
}
}
);
});

View File

@@ -1,11 +0,0 @@
function test() {
ShellService.setDefaultBrowser(false);
ok(
ShellService.isDefaultBrowser(true, false),
"we got here and are the default browser"
);
ok(
ShellService.isDefaultBrowser(true, true),
"we got here and are the default browser"
);
}

View File

@@ -1,218 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
ChromeUtils.defineESModuleGetters(this, {
FileTestUtils: "resource://testing-common/FileTestUtils.sys.mjs",
MockRegistrar: "resource://testing-common/MockRegistrar.sys.mjs",
});
const gBase = Services.dirsvc.get("ProfD", Ci.nsIFile);
gBase.append("CreateWindowsShortcut");
createDirectory(gBase);
const gTmpDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
const gDirectoryServiceProvider = {
getFile(prop, persistent) {
persistent.value = false;
// We only expect a narrow range of calls.
let folder = gBase.clone();
switch (prop) {
case "Progs":
folder.append("Programs");
break;
case "Desk":
folder.append("Desktop");
break;
case "UpdRootD":
// We really want DataRoot, but UpdateSubdir is what we usually get.
folder.append("DataRoot");
folder.append("UpdateDir");
folder.append("UpdateSubdir");
break;
case "ProfD":
// Used by test infrastructure.
folder = folder.parent;
break;
case "TmpD":
// Used by FileTestUtils.
folder = gTmpDir;
break;
default:
console.error(`Access to unexpected directory '${prop}'`);
return Cr.NS_ERROR_FAILURE;
}
createDirectory(folder);
return folder;
},
QueryInterface: ChromeUtils.generateQI([Ci.nsIDirectoryServiceProvider]),
};
add_setup(() => {
Services.dirsvc
.QueryInterface(Ci.nsIDirectoryService)
.registerProvider(gDirectoryServiceProvider);
});
registerCleanupFunction(() => {
gBase.remove(true);
Services.dirsvc
.QueryInterface(Ci.nsIDirectoryService)
.unregisterProvider(gDirectoryServiceProvider);
});
add_task(async function test_CreateWindowsShortcut() {
const DEST = "browser_createWindowsShortcut_TestFile.lnk";
const file = FileTestUtils.getTempFile("program.exe");
const iconPath = FileTestUtils.getTempFile("program.ico");
let shortcut;
const defaults = {
shellService: Cc["@mozilla.org/toolkit/shell-service;1"].getService(),
targetFile: file,
iconFile: iconPath,
description: "made by browser_createWindowsShortcut.js",
aumid: "TESTTEST",
};
shortcut = Services.dirsvc.get("Progs", Ci.nsIFile);
shortcut.append(DEST);
await testShortcut({
shortcutFile: shortcut,
relativePath: DEST,
specialFolder: "Programs",
logHeader: "STARTMENU",
...defaults,
});
let subdir = Services.dirsvc.get("Progs", Ci.nsIFile);
subdir.append("Shortcut Test");
tryRemove(subdir);
shortcut = subdir.clone();
shortcut.append(DEST);
await testShortcut({
shortcutFile: shortcut,
relativePath: "Shortcut Test\\" + DEST,
specialFolder: "Programs",
logHeader: "STARTMENU",
...defaults,
});
tryRemove(subdir);
shortcut = Services.dirsvc.get("Desk", Ci.nsIFile);
shortcut.append(DEST);
await testShortcut({
shortcutFile: shortcut,
relativePath: DEST,
specialFolder: "Desktop",
logHeader: "DESKTOP",
...defaults,
});
});
async function testShortcut({
shortcutFile,
relativePath,
specialFolder,
logHeader,
// Generally provided by the defaults.
shellService,
targetFile,
iconFile,
description,
aumid,
}) {
// If it already exists, remove it.
tryRemove(shortcutFile);
await shellService.createShortcut(
targetFile,
[],
description,
iconFile,
0,
aumid,
specialFolder,
relativePath
);
ok(
shortcutFile.exists(),
`${specialFolder}\\${relativePath}: Shortcut should exist`
);
ok(
queryShortcutLog(relativePath, logHeader),
`${specialFolder}\\${relativePath}: Shortcut log entry was added`
);
await shellService.deleteShortcut(specialFolder, relativePath);
ok(
!shortcutFile.exists(),
`${specialFolder}\\${relativePath}: Shortcut does not exist after deleting`
);
ok(
!queryShortcutLog(relativePath, logHeader),
`${specialFolder}\\${relativePath}: Shortcut log entry was removed`
);
}
function queryShortcutLog(aShortcutName, aSection) {
const parserFactory = Cc[
"@mozilla.org/xpcom/ini-parser-factory;1"
].createInstance(Ci.nsIINIParserFactory);
const dir = Services.dirsvc.get("UpdRootD", Ci.nsIFile).parent.parent;
const enumerator = dir.directoryEntries;
for (const file of enumerator) {
// We don't know the user's SID from JS-land, so just look at all of them.
if (!file.path.match(/[^_]+_S[^_]*_shortcuts.ini/)) {
continue;
}
const parser = parserFactory.createINIParser(file);
parser.QueryInterface(Ci.nsIINIParser);
parser.QueryInterface(Ci.nsIINIParserWriter);
for (let i = 0; ; i++) {
try {
let string = parser.getString(aSection, `Shortcut${i}`);
if (string == aShortcutName) {
enumerator.close();
return true;
}
} catch (e) {
// The key didn't exist, stop here.
break;
}
}
}
enumerator.close();
return false;
}
function createDirectory(aFolder) {
try {
aFolder.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
} catch (e) {
if (e.result != Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
throw e;
}
}
}
function tryRemove(file) {
try {
file.remove(false);
return true;
} catch (e) {
return false;
}
}

View File

@@ -1,54 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
ChromeUtils.defineESModuleGetters(this, {
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
NimbusTestUtils: "resource://testing-common/NimbusTestUtils.sys.mjs",
});
let defaultValue;
add_task(async function default_need() {
defaultValue = await ShellService.doesAppNeedPin();
Assert.notStrictEqual(
defaultValue,
undefined,
"Got a default app need pin value"
);
});
add_task(async function remote_disable() {
if (defaultValue === false) {
info("Default pin already false, so nothing to test");
return;
}
let doCleanup = await NimbusTestUtils.enrollWithFeatureConfig(
{
featureId: NimbusFeatures.shellService.featureId,
value: { disablePin: true, enabled: true },
},
{ isRollout: true }
);
Assert.equal(
await ShellService.doesAppNeedPin(),
false,
"Pinning disabled via nimbus"
);
await doCleanup();
});
add_task(async function restore_default() {
if (defaultValue === undefined) {
info("No default pin value set, so nothing to test");
return;
}
Assert.equal(
await ShellService.doesAppNeedPin(),
defaultValue,
"Pinning restored to original"
);
});

View File

@@ -1,74 +0,0 @@
"use strict";
add_task(async function () {
// Test all four basic variations of the "screenshot" argument
// when a file path is specified.
await testFileCreationPositive(
[
"-url",
"http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
"-screenshot",
screenshotPath,
],
screenshotPath
);
await testFileCreationPositive(
[
"-url",
"http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
`-screenshot=${screenshotPath}`,
],
screenshotPath
);
await testFileCreationPositive(
[
"-url",
"http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
"--screenshot",
screenshotPath,
],
screenshotPath
);
await testFileCreationPositive(
[
"-url",
"http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
`--screenshot=${screenshotPath}`,
],
screenshotPath
);
// Test when the requested URL redirects
await testFileCreationPositive(
[
"-url",
"http://mochi.test:8888/browser/browser/components/shell/test/headless_redirect.html",
"-screenshot",
screenshotPath,
],
screenshotPath
);
// Test with additional command options
await testFileCreationPositive(
[
"-url",
"http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
"-screenshot",
screenshotPath,
"-attach-console",
],
screenshotPath
);
await testFileCreationPositive(
[
"-url",
"http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
"-attach-console",
"-screenshot",
screenshotPath,
"-headless",
],
screenshotPath
);
});

View File

@@ -1,48 +0,0 @@
"use strict";
add_task(async function () {
const cwdScreenshotPath = PathUtils.join(
Services.dirsvc.get("CurWorkD", Ci.nsIFile).path,
"screenshot.png"
);
// Test variations of the "screenshot" argument when a file path
// isn't specified.
await testFileCreationPositive(
[
"-screenshot",
"http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
],
cwdScreenshotPath
);
await testFileCreationPositive(
[
"http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
"-screenshot",
],
cwdScreenshotPath
);
await testFileCreationPositive(
[
"--screenshot",
"http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
],
cwdScreenshotPath
);
await testFileCreationPositive(
[
"http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
"--screenshot",
],
cwdScreenshotPath
);
// Test with additional command options
await testFileCreationPositive(
[
"--screenshot",
"http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
"-attach-console",
],
cwdScreenshotPath
);
});

View File

@@ -1,59 +0,0 @@
"use strict";
add_task(async function () {
const cwdScreenshotPath = PathUtils.join(
Services.dirsvc.get("CurWorkD", Ci.nsIFile).path,
"screenshot.png"
);
// Test invalid URL arguments (either no argument or too many arguments).
await testFileCreationNegative(["-screenshot"], cwdScreenshotPath);
await testFileCreationNegative(
[
"http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
"http://mochi.test:8888/headless.html",
"-screenshot",
],
cwdScreenshotPath
);
// Test all four basic variations of the "window-size" argument.
await testFileCreationPositive(
[
"-url",
"http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
"-screenshot",
"-window-size",
"800",
],
cwdScreenshotPath
);
await testFileCreationPositive(
[
"-url",
"http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
"-screenshot",
"-window-size=800",
],
cwdScreenshotPath
);
await testFileCreationPositive(
[
"-url",
"http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
"-screenshot",
"--window-size",
"800",
],
cwdScreenshotPath
);
await testFileCreationPositive(
[
"-url",
"http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
"-screenshot",
"--window-size=800",
],
cwdScreenshotPath
);
});

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