Compare commits

..

5 Commits

Author SHA1 Message Date
mr-cheffy
b61cafaed0 chore: Sync upstream to Firefox 146.0.1 2025-12-18 18:53:32 +00:00
mr. m
4baca9cfc9 fix: Fixed addons manager opening when all addons are pinned, p=#11635, c=no-component 2025-12-16 15:45:33 +01:00
mr. m
0ae7c19c30 test: Import some mochitests from firefox, p=#10897
* test: Import some mochitests from firefox, b=no-bug, c=tests, scripts, tabs

* feat: Added lint rules to ignore mochi tests, b=no-bug, c=tests

* chore: Finish importing tests, b=no-bug, c=workflows, tests, scripts, tabs
2025-12-15 12:09:42 +01:00
Blake Gearin
e525b32c18 feat: Enable original extensions panel on hide-unified-extensions-button: false, p=#11335
* feat: Enable original extensions panel on hide-unified-extensions-button: false

* Update to fix prettier issues

* Add site-data attribute for CSS usage

* Update to set gUnifiedExtensions._panel

* Update to fix prettier issues

* Set default panel on onToolbarVisibilityChange

* Update panel initialization

* Remove extra char

* Restore unified-extensions-panel-template deletion

* Reduce reimplementation

* Update patch against Firefox 145.0.2

* Fix conflict

* Add panelUIPosition case

* Fix lint

* feat: Improve and reduce patch sizes, b=no-bug, c=common

---------

Signed-off-by: Blake Gearin <hello@blakeg.me>
Co-authored-by: mr. m <mr.m@tuta.com>
2025-12-15 11:19:03 +01:00
mr. m
e3d13d534e chore: Stick closer to firefox default preferences, p=#11611, c=no-component 2025-12-14 18:21:28 +01:00
87 changed files with 6428 additions and 70 deletions

View File

@@ -95,6 +95,10 @@ 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,6 +17,8 @@ engine/
surfer.json
src/zen/tests/mochitests/*
src/browser/app/profile/*.js
pnpm-lock.yaml

View File

@@ -34,7 +34,7 @@ Zen is a firefox-based browser with the aim of pushing your productivity to a ne
### Firefox Versions
- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `146.0`! 🚀
- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `146.0.1`! 🚀
- [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 146.0`!
### Contributing

View File

@@ -1 +1 @@
8908c8eb566c64521e2a777ad8a80b62bd6aa193
b6f4d18893d4f547f01d5e8aa8a4b364b168c84f

View File

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

View File

@@ -191,6 +191,9 @@ 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

@@ -8,9 +8,6 @@
- name: browser.sessionstore.restore_pinned_tabs_on_demand
value: true
- name: browser.tabs.loadBookmarksInTabs
value: false
- name: browser.tabs.hoverPreview.enabled
value: false
@@ -23,18 +20,6 @@
- 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
@@ -58,9 +43,6 @@
- name: browser.download.manager.addToRecentDocs
value: false
- name: browser.download.open_pdf_attachments_inline
value: true
- name: browser.download.alwaysOpenPanel
value: false

View File

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

View File

@@ -0,0 +1,124 @@
# 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,6 +15,8 @@ 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...")
@@ -59,7 +61,9 @@ 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()]
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_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..7454b801b4ad892d6ad122277eb7c7736e976f9f 100644
index fadcbfca95ee28140579430c0371baad0e2f216a..1947b4ab1c0b4ba0099147c9f13ae78d6e425bdf 100644
--- a/browser/base/content/browser-addons.js
+++ b/browser/base/content/browser-addons.js
@@ -1069,7 +1069,7 @@ var gXPInstallObserver = {
@@ -38,20 +38,92 @@ index fadcbfca95ee28140579430c0371baad0e2f216a..7454b801b4ad892d6ad122277eb7c773
}
return anchorID;
@@ -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"
@@ -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"
);
@@ -2714,6 +2710,7 @@ var gUnifiedExtensions = {
@@ -2662,37 +2662,41 @@ var gUnifiedExtensions = {
);
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) {
if (
@@ -2714,6 +2718,7 @@ var gUnifiedExtensions = {
// and no alternative content is available for display in the panel.
const policies = this.getActivePolicies();
if (
@@ -59,16 +131,46 @@ index fadcbfca95ee28140579430c0371baad0e2f216a..7454b801b4ad892d6ad122277eb7c773
policies.length &&
!this.hasExtensionsInPanel(policies) &&
!this.isPrivateWindowMissingExtensionsWithoutPBMAccess() &&
@@ -2754,7 +2751,7 @@ var gUnifiedExtensions = {
@@ -2729,32 +2734,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;
this.recordButtonTelemetry(reason || "extensions_panel_showing");
this.ensureButtonShownBeforeAttachingPanel(panel);
PanelMultiView.openPopup(panel, this._button, {
- PanelMultiView.openPopup(panel, this._button, {
- position: "bottomright topright",
+ position: gZenUIManager.panelUIPosition(panel, this._button),
+ PanelMultiView.openPopup(panel, button, {
+ position: gZenUIManager.panelUIPosition(panel, button),
triggerEvent: aEvent,
});
}
@@ -2941,18 +2938,20 @@ var gUnifiedExtensions = {
@@ -2941,18 +2944,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..d9a3404905b73db7c8f202ab166d5f3c625351f6 100644
index 7b776b15d52367a008ce6bf53dcfcbbe007b7453..da23f716c753f5a43f17bb5ed7a3d335891168c2 100644
--- a/browser/base/content/navigator-toolbox.js
+++ b/browser/base/content/navigator-toolbox.js
@@ -6,7 +6,7 @@
@@ -27,21 +27,28 @@ index 7b776b15d52367a008ce6bf53dcfcbbe007b7453..d9a3404905b73db7c8f202ab166d5f3c
gBrowser.handleNewTabMiddleClick(element, event);
break;
@@ -315,7 +317,7 @@ document.addEventListener(
#pageActionButton,
@@ -316,6 +318,7 @@ document.addEventListener(
#downloads-button,
#fxa-toolbar-menu-button,
- #unified-extensions-button,
#unified-extensions-button,
+ #zen-site-data-icon-button,
#library-button
`);
if (!element) {
@@ -394,7 +396,7 @@ document.addEventListener(
gSync.toggleAccountPanel(element, event);
break;
- case "unified-extensions-button":
+ case "zen-site-data-icon-button":
@@ -398,6 +401,16 @@ document.addEventListener(
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

@@ -32,7 +32,7 @@
command="Browser:AddBookmarkAs"
flex="1" />
</hbox>
<vbox class="zen-site-data-section">
<vbox id="zen-site-data-section-addons" 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" />

View File

@@ -669,7 +669,9 @@ window.gZenUIManager = {
}
if (
(gZenVerticalTabsManager._hasSetSingleToolbar && gZenVerticalTabsManager._prefsRightSide) ||
(panel?.id === 'zen-unified-site-data-panel' && !gZenVerticalTabsManager._hasSetSingleToolbar)
(panel?.id === 'zen-unified-site-data-panel' &&
!gZenVerticalTabsManager._hasSetSingleToolbar) ||
(panel?.id === 'unified-extensions-panel' && gZenVerticalTabsManager._hasSetSingleToolbar)
) {
block = 'bottomright';
inline = 'topright';

View File

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

View File

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

View File

@@ -0,0 +1,30 @@
# 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

@@ -0,0 +1,14 @@
# 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

@@ -0,0 +1,57 @@
[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

@@ -0,0 +1,99 @@
/* 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

@@ -0,0 +1,126 @@
/* 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

@@ -0,0 +1,34 @@
/* 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

@@ -0,0 +1,64 @@
/* 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

@@ -0,0 +1,88 @@
/* 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

@@ -0,0 +1,107 @@
/* 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

@@ -0,0 +1,36 @@
/* 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

@@ -0,0 +1,96 @@
/* 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

@@ -0,0 +1,56 @@
/* 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

@@ -0,0 +1,156 @@
/* 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

@@ -0,0 +1,79 @@
/* 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

@@ -0,0 +1,65 @@
/* 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

@@ -0,0 +1,134 @@
/* 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

@@ -0,0 +1,81 @@
/* 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

@@ -0,0 +1,45 @@
/* 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

@@ -0,0 +1,22 @@
<!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

@@ -0,0 +1,918 @@
/* 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

@@ -0,0 +1,355 @@
/* 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

@@ -0,0 +1,27 @@
<!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

@@ -0,0 +1,307 @@
/* 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

@@ -0,0 +1,14 @@
[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

@@ -0,0 +1,65 @@
/* 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

@@ -0,0 +1,98 @@
/* 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

@@ -0,0 +1,47 @@
/* 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

@@ -0,0 +1,46 @@
/* 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

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

View File

@@ -0,0 +1,103 @@
// 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

@@ -0,0 +1,103 @@
[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

@@ -0,0 +1,173 @@
// 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

@@ -0,0 +1,101 @@
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

@@ -0,0 +1,11 @@
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

@@ -0,0 +1,218 @@
/* 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

@@ -0,0 +1,54 @@
/* 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

@@ -0,0 +1,74 @@
"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

@@ -0,0 +1,48 @@
"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

@@ -0,0 +1,59 @@
"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
);
});

View File

@@ -0,0 +1,31 @@
"use strict";
add_task(async function () {
const cwdScreenshotPath = PathUtils.join(
Services.dirsvc.get("CurWorkD", Ci.nsIFile).path,
"screenshot.png"
);
// Test other variations of the "window-size" argument.
await testWindowSizePositive(800, 600);
await testWindowSizePositive(1234);
await testFileCreationNegative(
[
"-url",
"http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
"-screenshot",
"-window-size",
"hello",
],
cwdScreenshotPath
);
await testFileCreationNegative(
[
"-url",
"http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
"-screenshot",
"-window-size",
"800,",
],
cwdScreenshotPath
);
});

View File

@@ -0,0 +1,9 @@
"use strict";
add_task(async function () {
// Test cross origin iframes work.
await testGreen(
"http://mochi.test:8888/browser/browser/components/shell/test/headless_cross_origin.html",
screenshotPath
);
});

View File

@@ -0,0 +1,14 @@
"use strict";
add_task(async function () {
// Test when the requested URL redirects
await testFileCreationPositive(
[
"-url",
"http://mochi.test:8888/browser/browser/components/shell/test/headless_redirect.html",
"-screenshot",
screenshotPath,
],
screenshotPath
);
});

View File

@@ -0,0 +1,27 @@
/* Any copyright is dedicated to the Public Domain.
* https://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Bug 1950734 tracks how calling PinCurrentAppToTaskbarWin11
* on MSIX may cause the process AUMID to be unnecessarily changed.
* This test verifies that the behaviour will no longer happen
*/
ChromeUtils.defineESModuleGetters(this, {
ShellService: "moz-src:///browser/components/shell/ShellService.sys.mjs",
});
add_task(async function test_processAUMID() {
let processAUMID = ShellService.checkCurrentProcessAUMIDForTesting();
// This function will trigger the relevant code paths that
// incorrectly changes the process AUMID on MSIX, prior to
// Bug 1950734 being fixed
await ShellService.checkPinCurrentAppToTaskbarAsync(false);
is(
processAUMID,
ShellService.checkCurrentProcessAUMIDForTesting(),
"The process AUMID should not be changed"
);
});

View File

@@ -0,0 +1,239 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
ChromeUtils.defineESModuleGetters(this, {
ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs",
ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
NimbusTestUtils: "resource://testing-common/NimbusTestUtils.sys.mjs",
sinon: "resource://testing-common/Sinon.sys.mjs",
});
const setDefaultBrowserUserChoiceStub = async () => {
throw Components.Exception("", Cr.NS_ERROR_WDBA_NO_PROGID);
};
const defaultAgentStub = sinon
.stub(ShellService, "defaultAgent")
.value({ setDefaultBrowserUserChoiceAsync: setDefaultBrowserUserChoiceStub });
const _userChoiceImpossibleTelemetryResultStub = sinon
.stub(ShellService, "_userChoiceImpossibleTelemetryResult")
.callsFake(() => null);
const userChoiceStub = sinon
.stub(ShellService, "setAsDefaultUserChoice")
.resolves();
const setDefaultStub = sinon.stub();
const shellStub = sinon
.stub(ShellService, "shellService")
.value({ setDefaultBrowser: setDefaultStub });
const sendTriggerStub = sinon.stub(ASRouter, "sendTriggerMessage");
registerCleanupFunction(() => {
sinon.restore();
});
let defaultUserChoice;
add_task(async function need_user_choice() {
await ShellService.setDefaultBrowser();
defaultUserChoice = userChoiceStub.called;
Assert.notStrictEqual(
defaultUserChoice,
undefined,
"Decided which default browser method to use"
);
Assert.equal(
setDefaultStub.notCalled,
defaultUserChoice,
"Only one default behavior was used"
);
});
add_task(async function remote_disable() {
if (defaultUserChoice === false) {
info("Default behavior already not user choice, so nothing to test");
return;
}
userChoiceStub.resetHistory();
setDefaultStub.resetHistory();
let doCleanup = await NimbusTestUtils.enrollWithFeatureConfig(
{
featureId: NimbusFeatures.shellService.featureId,
value: {
setDefaultBrowserUserChoice: false,
enabled: true,
},
},
{ isRollout: true }
);
await ShellService.setDefaultBrowser();
Assert.ok(
userChoiceStub.notCalled,
"Set default with user choice disabled via nimbus"
);
Assert.ok(setDefaultStub.called, "Used plain set default instead");
await doCleanup();
});
add_task(async function restore_default() {
if (defaultUserChoice === undefined) {
info("No default user choice behavior set, so nothing to test");
return;
}
userChoiceStub.resetHistory();
setDefaultStub.resetHistory();
await ShellService.setDefaultBrowser();
Assert.equal(
userChoiceStub.called,
defaultUserChoice,
"Set default with user choice restored to original"
);
Assert.equal(
setDefaultStub.notCalled,
defaultUserChoice,
"Plain set default behavior restored to original"
);
});
add_task(async function ensure_fallback() {
if (AppConstants.platform != "win") {
info("Nothing to test on non-Windows");
return;
}
let userChoicePromise = Promise.resolve();
userChoiceStub.callsFake(function (...args) {
return (userChoicePromise = userChoiceStub.wrappedMethod.apply(this, args));
});
userChoiceStub.resetHistory();
setDefaultStub.resetHistory();
let doCleanup = await NimbusTestUtils.enrollWithFeatureConfig(
{
featureId: NimbusFeatures.shellService.featureId,
value: {
setDefaultBrowserUserChoice: true,
setDefaultPDFHandler: false,
enabled: true,
},
},
{ isRollout: true }
);
await ShellService.setDefaultBrowser();
Assert.ok(userChoiceStub.called, "Set default with user choice called");
let message = "";
await userChoicePromise.catch(err => (message = err.message || ""));
Assert.ok(
message.includes("ErrExeProgID"),
"Set default with user choice threw an expected error"
);
Assert.ok(setDefaultStub.called, "Fallbacked to plain set default");
await doCleanup();
});
async function setUpNotificationTests(guidanceEnabled, oneClick) {
sinon.reset();
const experimentCleanup = await NimbusTestUtils.enrollWithFeatureConfig(
{
featureId: NimbusFeatures.shellService.featureId,
value: {
setDefaultGuidanceNotifications: guidanceEnabled,
setDefaultBrowserUserChoice: oneClick,
setDefaultBrowserUserChoiceRegRename: oneClick,
enabled: true,
},
},
{ isRollout: true }
);
const doCleanup = async () => {
await experimentCleanup();
sinon.reset();
};
await ShellService.setDefaultBrowser();
return doCleanup;
}
add_task(
async function show_notification_when_set_to_default_guidance_enabled_and_one_click_disabled() {
if (!AppConstants.isPlatformAndVersionAtLeast("win", 10)) {
info("Nothing to test on non-Windows or older Windows versions");
return;
}
const doCleanup = await setUpNotificationTests(
true, // guidance enabled
false // one-click disabled
);
Assert.ok(setDefaultStub.called, "Fallback method used to set default");
Assert.equal(
sendTriggerStub.firstCall.args[0].id,
"deeplinkedToWindowsSettingsUI",
`Set to default guidance message trigger was sent`
);
await doCleanup();
}
);
add_task(
async function do_not_show_notification_when_set_to_default_guidance_disabled_and_one_click_enabled() {
if (!AppConstants.isPlatformAndVersionAtLeast("win", 10)) {
info("Nothing to test on non-Windows or older Windows versions");
return;
}
const doCleanup = await setUpNotificationTests(
false, // guidance disabled
true // one-click enabled
);
Assert.ok(setDefaultStub.notCalled, "Fallback method not called");
Assert.equal(
sendTriggerStub.callCount,
0,
`Set to default guidance message trigger was not sent`
);
await doCleanup();
}
);
add_task(
async function do_not_show_notification_when_set_to_default_guidance_enabled_and_one_click_enabled() {
if (!AppConstants.isPlatformAndVersionAtLeast("win", 10)) {
info("Nothing to test on non-Windows or older Windows versions");
return;
}
const doCleanup = await setUpNotificationTests(
true, // guidance enabled
true // one-click enabled
);
Assert.ok(setDefaultStub.notCalled, "Fallback method not called");
Assert.equal(
sendTriggerStub.callCount,
0,
`Set to default guidance message trigger was not sent`
);
await doCleanup();
}
);

View File

@@ -0,0 +1,279 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
ChromeUtils.defineESModuleGetters(this, {
ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
NimbusTestUtils: "resource://testing-common/NimbusTestUtils.sys.mjs",
sinon: "resource://testing-common/Sinon.sys.mjs",
});
const setDefaultBrowserUserChoiceStub = sinon.stub();
const setDefaultExtensionHandlersUserChoiceStub = sinon
.stub()
.callsFake(() => Promise.resolve());
const defaultAgentStub = sinon.stub(ShellService, "defaultAgent").value({
setDefaultBrowserUserChoiceAsync: setDefaultBrowserUserChoiceStub,
setDefaultExtensionHandlersUserChoice:
setDefaultExtensionHandlersUserChoiceStub,
});
XPCOMUtils.defineLazyServiceGetter(
this,
"XreDirProvider",
"@mozilla.org/xre/directory-provider;1",
Ci.nsIXREDirProvider
);
const _userChoiceImpossibleTelemetryResultStub = sinon
.stub(ShellService, "_userChoiceImpossibleTelemetryResult")
.callsFake(() => null);
// Ensure we don't fall back to a real implementation.
const setDefaultStub = sinon.stub();
// We'll dynamically update this as needed during the tests.
const queryCurrentDefaultHandlerForStub = sinon.stub();
const shellStub = sinon.stub(ShellService, "shellService").value({
setDefaultBrowser: setDefaultStub,
queryCurrentDefaultHandlerFor: queryCurrentDefaultHandlerForStub,
});
registerCleanupFunction(() => {
defaultAgentStub.restore();
_userChoiceImpossibleTelemetryResultStub.restore();
shellStub.restore();
});
add_task(async function ready() {
await ExperimentAPI.ready();
});
// Everything here is Windows.
Assert.equal(AppConstants.platform, "win", "Platform is Windows");
add_task(async function remoteEnableWithPDF() {
let doCleanup = await NimbusTestUtils.enrollWithFeatureConfig(
{
featureId: NimbusFeatures.shellService.featureId,
value: {
setDefaultBrowserUserChoice: true,
setDefaultPDFHandlerOnlyReplaceBrowsers: false,
setDefaultPDFHandler: true,
enabled: true,
},
},
{ isRollout: true }
);
Assert.equal(
NimbusFeatures.shellService.getVariable("setDefaultBrowserUserChoice"),
true
);
Assert.equal(
NimbusFeatures.shellService.getVariable("setDefaultPDFHandler"),
true
);
setDefaultBrowserUserChoiceStub.resetHistory();
await ShellService.setDefaultBrowser();
const aumi = XreDirProvider.getInstallHash();
Assert.ok(setDefaultBrowserUserChoiceStub.called);
Assert.deepEqual(setDefaultBrowserUserChoiceStub.firstCall.args, [
aumi,
[".pdf", "FirefoxPDF"],
]);
await doCleanup();
});
add_task(async function remoteEnableWithPDF_testOnlyReplaceBrowsers() {
let doCleanup = await NimbusTestUtils.enrollWithFeatureConfig(
{
featureId: NimbusFeatures.shellService.featureId,
value: {
setDefaultBrowserUserChoice: true,
setDefaultPDFHandlerOnlyReplaceBrowsers: true,
setDefaultPDFHandler: true,
enabled: true,
},
},
{ isRollout: true }
);
Assert.equal(
NimbusFeatures.shellService.getVariable("setDefaultBrowserUserChoice"),
true
);
Assert.equal(
NimbusFeatures.shellService.getVariable("setDefaultPDFHandler"),
true
);
Assert.equal(
NimbusFeatures.shellService.getVariable(
"setDefaultPDFHandlerOnlyReplaceBrowsers"
),
true
);
const aumi = XreDirProvider.getInstallHash();
// We'll take the default from a missing association or a known browser.
for (let progId of ["", "MSEdgePDF"]) {
queryCurrentDefaultHandlerForStub.callsFake(() => progId);
setDefaultBrowserUserChoiceStub.resetHistory();
await ShellService.setDefaultBrowser();
Assert.ok(setDefaultBrowserUserChoiceStub.called);
Assert.deepEqual(
setDefaultBrowserUserChoiceStub.firstCall.args,
[aumi, [".pdf", "FirefoxPDF"]],
`Will take default from missing association or known browser with ProgID '${progId}'`
);
}
// But not from a non-browser.
queryCurrentDefaultHandlerForStub.callsFake(() => "Acrobat.Document.DC");
setDefaultBrowserUserChoiceStub.resetHistory();
await ShellService.setDefaultBrowser();
Assert.ok(setDefaultBrowserUserChoiceStub.called);
Assert.deepEqual(
setDefaultBrowserUserChoiceStub.firstCall.args,
[aumi, []],
`Will not take default from non-browser`
);
await doCleanup();
});
add_task(async function remoteEnableWithoutPDF() {
let doCleanup = await NimbusTestUtils.enrollWithFeatureConfig(
{
featureId: NimbusFeatures.shellService.featureId,
value: {
setDefaultBrowserUserChoice: true,
setDefaultPDFHandler: false,
enabled: true,
},
},
{ isRollout: true }
);
Assert.equal(
NimbusFeatures.shellService.getVariable("setDefaultBrowserUserChoice"),
true
);
Assert.equal(
NimbusFeatures.shellService.getVariable("setDefaultPDFHandler"),
false
);
setDefaultBrowserUserChoiceStub.resetHistory();
await ShellService.setDefaultBrowser();
const aumi = XreDirProvider.getInstallHash();
Assert.ok(setDefaultBrowserUserChoiceStub.called);
Assert.deepEqual(setDefaultBrowserUserChoiceStub.firstCall.args, [aumi, []]);
await doCleanup();
});
add_task(async function remoteDisable() {
let doCleanup = await NimbusTestUtils.enrollWithFeatureConfig(
{
featureId: NimbusFeatures.shellService.featureId,
value: {
setDefaultBrowserUserChoice: false,
setDefaultPDFHandler: true,
enabled: false,
},
},
{ isRollout: true }
);
Assert.equal(
NimbusFeatures.shellService.getVariable("setDefaultBrowserUserChoice"),
false
);
Assert.equal(
NimbusFeatures.shellService.getVariable("setDefaultPDFHandler"),
true
);
setDefaultBrowserUserChoiceStub.resetHistory();
await ShellService.setDefaultBrowser();
Assert.ok(setDefaultBrowserUserChoiceStub.notCalled);
Assert.ok(setDefaultStub.called);
await doCleanup();
});
add_task(async function test_setAsDefaultPDFHandler_knownBrowser() {
const sandbox = sinon.createSandbox();
const aumi = XreDirProvider.getInstallHash();
const expectedArguments = [aumi, [".pdf", "FirefoxPDF"]];
try {
const pdfHandlerResult = { registered: true, knownBrowser: true };
sandbox
.stub(ShellService, "getDefaultPDFHandler")
.returns(pdfHandlerResult);
info("Testing setAsDefaultPDFHandler(true) when knownBrowser = true");
ShellService.setAsDefaultPDFHandler(true);
Assert.ok(
setDefaultExtensionHandlersUserChoiceStub.called,
"Called default browser agent"
);
Assert.deepEqual(
setDefaultExtensionHandlersUserChoiceStub.firstCall.args,
expectedArguments,
"Called default browser agent with expected arguments"
);
setDefaultExtensionHandlersUserChoiceStub.resetHistory();
info("Testing setAsDefaultPDFHandler(false) when knownBrowser = true");
ShellService.setAsDefaultPDFHandler(false);
Assert.ok(
setDefaultExtensionHandlersUserChoiceStub.called,
"Called default browser agent"
);
Assert.deepEqual(
setDefaultExtensionHandlersUserChoiceStub.firstCall.args,
expectedArguments,
"Called default browser agent with expected arguments"
);
setDefaultExtensionHandlersUserChoiceStub.resetHistory();
pdfHandlerResult.knownBrowser = false;
info("Testing setAsDefaultPDFHandler(true) when knownBrowser = false");
ShellService.setAsDefaultPDFHandler(true);
Assert.ok(
setDefaultExtensionHandlersUserChoiceStub.notCalled,
"Did not call default browser agent"
);
setDefaultExtensionHandlersUserChoiceStub.resetHistory();
info("Testing setAsDefaultPDFHandler(false) when knownBrowser = false");
ShellService.setAsDefaultPDFHandler(false);
Assert.ok(
setDefaultExtensionHandlersUserChoiceStub.called,
"Called default browser agent"
);
Assert.deepEqual(
setDefaultExtensionHandlersUserChoiceStub.firstCall.args,
expectedArguments,
"Called default browser agent with expected arguments"
);
setDefaultExtensionHandlersUserChoiceStub.resetHistory();
} finally {
sandbox.restore();
}
});

View File

@@ -0,0 +1,93 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Check whether the preview image for setDesktopBackground is rendered
* correctly, without stretching
*/
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [["test.wait300msAfterTabSwitch", true]],
});
});
add_task(async function () {
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: "about:logo",
},
async () => {
const dialogLoad = BrowserTestUtils.domWindowOpened(null, async win => {
await BrowserTestUtils.waitForEvent(win, "load");
Assert.equal(
win.document.documentElement.getAttribute("windowtype"),
"Shell:SetDesktopBackground",
"Opened correct window"
);
return true;
});
const image = content.document.images[0];
EventUtils.synthesizeMouseAtCenter(image, { type: "contextmenu" });
const menu = document.getElementById("contentAreaContextMenu");
await BrowserTestUtils.waitForPopupEvent(menu, "shown");
const menuClosed = BrowserTestUtils.waitForPopupEvent(menu, "hidden");
const menuItem = document.getElementById("context-setDesktopBackground");
try {
menu.activateItem(menuItem);
} catch (ex) {
ok(
menuItem.hidden,
"should only fail to activate when menu item is hidden"
);
ok(
!ShellService.canSetDesktopBackground,
"Should only hide when not able to set the desktop background"
);
is(
AppConstants.platform,
"linux",
"Should always be able to set desktop background on non-linux platforms"
);
todo(false, "Skipping test on this configuration");
menu.hidePopup();
await menuClosed;
return;
}
await menuClosed;
const win = await dialogLoad;
/* setDesktopBackground.js does a setTimeout to wait for correct
dimensions. If we don't wait here we could read the preview dimensions
before they're changed to match the screen */
await TestUtils.waitForTick();
const canvas = win.document.getElementById("screen");
const screenRatio = screen.width / screen.height;
const previewRatio = canvas.clientWidth / canvas.clientHeight;
info(`Screen dimensions are ${screen.width}x${screen.height}`);
info(`Screen's raw ratio is ${screenRatio}`);
info(
`Preview dimensions are ${canvas.clientWidth}x${canvas.clientHeight}`
);
info(`Preview's raw ratio is ${previewRatio}`);
Assert.ok(
previewRatio < screenRatio + 0.01 && previewRatio > screenRatio - 0.01,
"Preview's aspect ratio is within ±.01 of screen's"
);
win.close();
await menuClosed;
}
);
});

View File

@@ -0,0 +1,40 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/. */
#include "gtest/gtest.h"
#include "Windows11LimitedAccessFeatures.h"
#include "WinUtils.h"
TEST(LimitedAccessFeature, VerifyGeneratedInfo)
{
// If running on MSIX we have no guarantee that the
// generated LAF info will match the known values.
if (mozilla::widget::WinUtils::HasPackageIdentity()) {
return;
}
LimitedAccessFeatureInfo knownLafInfo = {
// Win11LimitedAccessFeatureType::Taskbar
"Win11LimitedAccessFeatureType::Taskbar"_ns, // debugName
u"com.microsoft.windows.taskbar.pin"_ns, // feature
u"kRFiWpEK5uS6PMJZKmR7MQ=="_ns, // token
u"pcsmm0jrprpb2 has registered their use of "_ns // attestation
u"com.microsoft.windows.taskbar.pin with Microsoft and agrees to the "_ns
u"terms "_ns
u"of use."_ns};
auto generatedLafInfoResult = GenerateLimitedAccessFeatureInfo(
"Win11LimitedAccessFeatureType::Taskbar"_ns,
u"com.microsoft.windows.taskbar.pin"_ns);
ASSERT_TRUE(generatedLafInfoResult.isOk());
LimitedAccessFeatureInfo generatedLafInfo = generatedLafInfoResult.unwrap();
// Check for equality between generated values and known good values
ASSERT_TRUE(knownLafInfo.debugName.Equals(generatedLafInfo.debugName));
ASSERT_TRUE(knownLafInfo.feature.Equals(generatedLafInfo.feature));
ASSERT_TRUE(knownLafInfo.token.Equals(generatedLafInfo.token));
ASSERT_TRUE(knownLafInfo.attestation.Equals(generatedLafInfo.attestation));
}

View File

@@ -0,0 +1,54 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/. */
#include "gtest/gtest.h"
#include "nsDirectoryServiceDefs.h"
#include "nsDirectoryServiceUtils.h"
#include "nsWindowsShellServiceInternal.h"
#include "nsXULAppAPI.h"
TEST(ShellLink, NarrowCharacterArguments)
{
nsCOMPtr<nsIFile> exe;
nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(exe));
ASSERT_TRUE(NS_SUCCEEDED(rv));
RefPtr<IShellLinkW> link;
rv = CreateShellLinkObject(exe, {u"test"_ns}, u"test"_ns, exe, 0, u"aumid"_ns,
getter_AddRefs(link));
ASSERT_TRUE(NS_SUCCEEDED(rv));
ASSERT_TRUE(link != nullptr);
std::wstring testArgs = L"\"test\" ";
wchar_t resultArgs[sizeof(testArgs)];
HRESULT hr = link->GetArguments(resultArgs, sizeof(resultArgs));
ASSERT_TRUE(SUCCEEDED(hr));
ASSERT_TRUE(testArgs == resultArgs);
}
TEST(ShellLink, WideCharacterArguments)
{
nsCOMPtr<nsIFile> exe;
nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(exe));
ASSERT_TRUE(NS_SUCCEEDED(rv));
RefPtr<IShellLinkW> link;
rv = CreateShellLinkObject(exe, {u"Test\\テスト用アカウント\\Test"_ns},
u"test"_ns, exe, 0, u"aumid"_ns,
getter_AddRefs(link));
ASSERT_TRUE(NS_SUCCEEDED(rv));
ASSERT_TRUE(link != nullptr);
std::wstring testArgs = L"\"Test\\テスト用アカウント\\Test\" ";
wchar_t resultArgs[sizeof(testArgs)];
HRESULT hr = link->GetArguments(resultArgs, sizeof(resultArgs));
ASSERT_TRUE(SUCCEEDED(hr));
ASSERT_TRUE(testArgs == resultArgs);
}

View File

@@ -0,0 +1,15 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows":
LOCAL_INCLUDES += ["/browser/components/shell"]
UNIFIED_SOURCES += [
"LimitedAccessFeatureTests.cpp",
"ShellLinkTests.cpp",
]
FINAL_LIBRARY = "xul-gtest"

View File

@@ -0,0 +1,159 @@
"use strict";
const { Subprocess } = ChromeUtils.importESModule(
"resource://gre/modules/Subprocess.sys.mjs"
);
const TEMP_DIR = Services.dirsvc.get("TmpD", Ci.nsIFile).path;
const screenshotPath = PathUtils.join(TEMP_DIR, "headless_test_screenshot.png");
async function runFirefox(args) {
const XRE_EXECUTABLE_FILE = "XREExeF";
const firefoxExe = Services.dirsvc.get(XRE_EXECUTABLE_FILE, Ci.nsIFile).path;
const NS_APP_PREFS_50_FILE = "PrefF";
const mochiPrefsFile = Services.dirsvc.get(NS_APP_PREFS_50_FILE, Ci.nsIFile);
const mochiPrefsPath = mochiPrefsFile.path;
const mochiPrefsName = mochiPrefsFile.leafName;
const profilePath = PathUtils.join(
TEMP_DIR,
"headless_test_screenshot_profile"
);
const prefsPath = PathUtils.join(profilePath, mochiPrefsName);
const firefoxArgs = ["-profile", profilePath];
await IOUtils.makeDirectory(profilePath);
await IOUtils.copy(mochiPrefsPath, prefsPath);
let proc = await Subprocess.call({
command: firefoxExe,
arguments: firefoxArgs.concat(args),
// Disable leak detection to avoid intermittent failure bug 1331152.
environmentAppend: true,
environment: {
ASAN_OPTIONS:
"detect_leaks=0:quarantine_size=50331648:malloc_context_size=5",
// Don't enable Marionette.
MOZ_MARIONETTE: null,
},
});
let stdout;
while ((stdout = await proc.stdout.readString())) {
dump(`>>> ${stdout}\n`);
}
let { exitCode } = await proc.wait();
is(exitCode, 0, "Firefox process should exit with code 0");
await IOUtils.remove(profilePath, { recursive: true });
}
async function testFileCreationPositive(args, path) {
await runFirefox(args);
let saved = IOUtils.exists(path);
ok(saved, "A screenshot should be saved as " + path);
if (!saved) {
return;
}
let info = await IOUtils.stat(path);
Assert.greater(info.size, 0, "Screenshot should not be an empty file");
await IOUtils.remove(path);
}
async function testFileCreationNegative(args, path) {
await runFirefox(args);
let saved = await IOUtils.exists(path);
ok(!saved, "A screenshot should not be saved");
await IOUtils.remove(path);
}
async function testWindowSizePositive(width, height) {
let size = String(width);
if (height) {
size += "," + height;
}
await runFirefox([
"-url",
"http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
"-screenshot",
screenshotPath,
"-window-size",
size,
]);
let saved = await IOUtils.exists(screenshotPath);
ok(saved, "A screenshot should be saved in the tmp directory");
if (!saved) {
return;
}
let data = await IOUtils.read(screenshotPath);
await new Promise(resolve => {
let blob = new Blob([data], { type: "image/png" });
let reader = new FileReader();
reader.onloadend = function () {
let screenshot = new Image();
screenshot.onload = function () {
is(
screenshot.width,
width,
"Screenshot should be " + width + " pixels wide"
);
if (height) {
is(
screenshot.height,
height,
"Screenshot should be " + height + " pixels tall"
);
}
resolve();
};
screenshot.src = reader.result;
};
reader.readAsDataURL(blob);
});
await IOUtils.remove(screenshotPath);
}
async function testGreen(url, path) {
await runFirefox(["-url", url, `--screenshot=${path}`]);
let saved = await IOUtils.exists(path);
ok(saved, "A screenshot should be saved in the tmp directory");
if (!saved) {
return;
}
let data = await IOUtils.read(path);
let image = await new Promise(resolve => {
let blob = new Blob([data], { type: "image/png" });
let reader = new FileReader();
reader.onloadend = function () {
let screenshot = new Image();
screenshot.onload = function () {
resolve(screenshot);
};
screenshot.src = reader.result;
};
reader.readAsDataURL(blob);
});
let canvas = document.createElement("canvas");
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
let ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0);
let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
let rgba = imageData.data;
let found = false;
for (let i = 0; i < rgba.length; i += 4) {
if (rgba[i] === 0 && rgba[i + 1] === 255 && rgba[i + 2] === 0) {
found = true;
break;
}
}
ok(found, "There should be a green pixel in the screenshot.");
await IOUtils.remove(path);
}

View File

@@ -0,0 +1,6 @@
<html>
<head><meta content="text/html; charset=utf-8" http-equiv="Content-Type"></head>
<body style="background-color: rgb(0, 255, 0); color: rgb(0, 0, 255)">
Hi
</body>
</html>

View File

@@ -0,0 +1,7 @@
<html>
<head><meta content="text/html; charset=utf-8" http-equiv="Content-Type"></head>
<body>
<iframe width="300" height="200" src="http://example.com/browser/browser/components/shell/test/headless_iframe.html"></iframe>
Hi
</body>
</html>

View File

@@ -0,0 +1,6 @@
<html>
<head><meta content="text/html; charset=utf-8" http-equiv="Content-Type"></head>
<body style="background-color: rgb(0, 255, 0);">
Hi
</body>
</html>

View File

@@ -0,0 +1,2 @@
HTTP 302 Moved Temporarily
Location: headless.html

View File

@@ -0,0 +1,168 @@
#!/usr/bin/python
# 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/. */
"""
mac_desktop_image.py
Mac-specific utility to get/set the desktop background image or check that
the current background image path matches a provided path.
Depends on Objective-C python binding imports which are in the python import
paths by default when using macOS's /usr/bin/python.
Includes generous amount of logging to aid debugging for use in automated tests.
"""
import argparse
import logging
import os
import sys
#
# These Objective-C bindings imports are included in the import path by default
# for the Mac-bundled python installed in /usr/bin/python. They're needed to
# call the Objective-C API's to set and retrieve the current desktop background
# image.
#
from AppKit import NSScreen, NSWorkspace
from Cocoa import NSURL
def main():
parser = argparse.ArgumentParser(
description="Utility to print, set, or "
+ "check the path to image being used as "
+ "the desktop background image. By "
+ "default, prints the path to the "
+ "current desktop background image."
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="print verbose debugging information",
default=False,
)
group = parser.add_mutually_exclusive_group()
group.add_argument(
"-s",
"--set-background-image",
dest="newBackgroundImagePath",
required=False,
help="path to the new background image to set. A zero "
+ "exit code indicates no errors occurred.",
default=None,
)
group.add_argument(
"-c",
"--check-background-image",
dest="checkBackgroundImagePath",
required=False,
help="check if the provided background image path "
+ "matches the provided path. A zero exit code "
+ "indicates the paths match.",
default=None,
)
args = parser.parse_args()
# Using logging for verbose output
if args.verbose:
logging.basicConfig(level=logging.DEBUG)
else:
logging.basicConfig(level=logging.CRITICAL)
logger = logging.getLogger("desktopImage")
# Print what we're going to do
if args.checkBackgroundImagePath is not None:
logger.debug(
"checking provided desktop image %s matches current "
"image" % args.checkBackgroundImagePath
)
elif args.newBackgroundImagePath is not None:
logger.debug("setting image to %s " % args.newBackgroundImagePath)
else:
logger.debug("retrieving desktop image path")
focussedScreen = NSScreen.mainScreen()
if not focussedScreen:
raise RuntimeError("mainScreen error")
ws = NSWorkspace.sharedWorkspace()
if not ws:
raise RuntimeError("sharedWorkspace error")
# If we're just checking the image path, check it and then return.
# A successful exit code (0) indicates the paths match.
if args.checkBackgroundImagePath is not None:
# Get existing desktop image path and resolve it
existingImageURL = getCurrentDesktopImageURL(focussedScreen, ws, logger)
existingImagePath = existingImageURL.path()
existingImagePathReal = os.path.realpath(existingImagePath)
logger.debug("existing desktop image: %s" % existingImagePath)
logger.debug("existing desktop image realpath: %s" % existingImagePath)
# Resolve the path we're going to check
checkImagePathReal = os.path.realpath(args.checkBackgroundImagePath)
logger.debug("check desktop image: %s" % args.checkBackgroundImagePath)
logger.debug("check desktop image realpath: %s" % checkImagePathReal)
if existingImagePathReal == checkImagePathReal:
print("desktop image path matches provided path")
return True
print("desktop image path does NOT match provided path")
return False
# Log the current desktop image
if args.verbose:
existingImageURL = getCurrentDesktopImageURL(focussedScreen, ws, logger)
logger.debug("existing desktop image: %s" % existingImageURL.path())
# Set the desktop image
if args.newBackgroundImagePath is not None:
newImagePath = args.newBackgroundImagePath
if not os.path.exists(newImagePath):
logger.critical("%s does not exist" % newImagePath)
return False
if not os.access(newImagePath, os.R_OK):
logger.critical("%s is not readable" % newImagePath)
return False
logger.debug("new desktop image to set: %s" % newImagePath)
newImageURL = NSURL.fileURLWithPath_(newImagePath)
logger.debug("new desktop image URL to set: %s" % newImageURL)
status = False
(status, error) = ws.setDesktopImageURL_forScreen_options_error_(
newImageURL, focussedScreen, None, None
)
if not status:
raise RuntimeError("setDesktopImageURL error")
# Print the current desktop image
imageURL = getCurrentDesktopImageURL(focussedScreen, ws, logger)
imagePath = imageURL.path()
imagePathReal = os.path.realpath(imagePath)
logger.debug("updated desktop image URL: %s" % imageURL)
logger.debug("updated desktop image path: %s" % imagePath)
logger.debug("updated desktop image path (resolved): %s" % imagePathReal)
print(imagePathReal)
return True
def getCurrentDesktopImageURL(focussedScreen, workspace, logger):
imageURL = workspace.desktopImageURLForScreen_(focussedScreen)
if not imageURL:
raise RuntimeError("desktopImageURLForScreen returned invalid URL")
if not imageURL.isFileURL():
logger.warning("desktop image URL is not a file URL")
return imageURL
if __name__ == "__main__":
if not main():
sys.exit(1)
else:
sys.exit(0)

View File

@@ -0,0 +1,40 @@
/* Any copyright is dedicated to the Public Domain.
* https://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test the macOS ShowSecurityPreferences shell service method.
*/
"use strict";
// eslint-disable-next-line mozilla/no-redeclare-with-import-autofix
const { AppConstants } = ChromeUtils.importESModule(
"resource://gre/modules/AppConstants.sys.mjs"
);
function killSystemPreferences() {
let killallFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
killallFile.initWithPath("/usr/bin/killall");
let sysPrefsArg = ["System Preferences"];
if (AppConstants.isPlatformAndVersionAtLeast("macosx", 22)) {
sysPrefsArg = ["System Settings"];
}
let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
process.init(killallFile);
process.run(true, sysPrefsArg, 1);
return process.exitValue;
}
add_setup(async function () {
info("Ensure System Preferences isn't already running");
killSystemPreferences();
});
add_task(async function test_prefsOpen() {
let shellSvc = Cc["@mozilla.org/browser/shell-service;1"].getService(
Ci.nsIMacShellService
);
shellSvc.showSecurityPreferences("Privacy_AllFiles");
equal(killSystemPreferences(), 0, "Ensure System Preferences was started");
});

View File

@@ -0,0 +1,7 @@
[DEFAULT]
run-if = ["os != 'android'"]
firefox-appdir = "browser"
tags = "os_integration"
["test_macOS_showSecurityPreferences.js"]
run-if = ["os == 'mac'"]

View File

@@ -0,0 +1,17 @@
[DEFAULT]
["browser_bug329212.js"]
support-files = ["title_test.svg"]
["browser_bug331772_xul_tooltiptext_in_html.js"]
support-files = ["xul_tooltiptext.xhtml"]
["browser_bug561623.js"]
["browser_bug581947.js"]
["browser_input_file_tooltips.js"]
["browser_nac_tooltip.js"]
["browser_shadow_dom_tooltip.js"]

View File

@@ -0,0 +1,48 @@
"use strict";
add_task(async function () {
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: "http://mochi.test:8888/browser/toolkit/components/tooltiptext/tests/title_test.svg",
},
async function (browser) {
await SpecialPowers.spawn(browser, [""], function () {
let tttp = Cc[
"@mozilla.org/embedcomp/default-tooltiptextprovider;1"
].getService(Ci.nsITooltipTextProvider);
function checkElement(id, expectedTooltipText) {
let el = content.document.getElementById(id);
let textObj = {};
let shouldHaveTooltip = expectedTooltipText !== null;
is(
tttp.getNodeText(el, textObj, {}),
shouldHaveTooltip,
"element " +
id +
" should " +
(shouldHaveTooltip ? "" : "not ") +
"have a tooltip"
);
if (shouldHaveTooltip) {
is(
textObj.value,
expectedTooltipText,
"element " + id + " should have the right tooltip text"
);
}
}
checkElement("svg1", "This is a non-root SVG element title");
checkElement("text1", "\n\n\n This is a title\n\n ");
checkElement("text2", null);
checkElement("text3", null);
checkElement("link1", "\n This is a title\n ");
checkElement("text4", "\n This is a title\n ");
checkElement("link2", null);
checkElement("link3", "This is an xlink:title attribute");
checkElement("link4", "This is an xlink:title attribute");
checkElement("text5", null);
});
}
);
});

View File

@@ -0,0 +1,30 @@
/**
* Tests that the tooltiptext attribute is used for XUL elements in an HTML doc.
*/
add_task(async function () {
await SpecialPowers.pushPermissions([
{ type: "allowXULXBL", allow: true, context: "http://mochi.test:8888" },
]);
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: "http://mochi.test:8888/browser/toolkit/components/tooltiptext/tests/xul_tooltiptext.xhtml",
},
async function (browser) {
await SpecialPowers.spawn(browser, [""], function () {
let textObj = {};
let tttp = Cc[
"@mozilla.org/embedcomp/default-tooltiptextprovider;1"
].getService(Ci.nsITooltipTextProvider);
let xulToolbarButton =
content.document.getElementById("xulToolbarButton");
ok(
tttp.getNodeText(xulToolbarButton, textObj, {}),
"should get tooltiptext"
);
is(textObj.value, "XUL tooltiptext");
});
}
);
});

View File

@@ -0,0 +1,33 @@
add_task(async function () {
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: "data:text/html,<!DOCTYPE html><html><body><input id='i'></body></html>",
},
async function (browser) {
await SpecialPowers.spawn(browser, [""], function () {
let tttp = Cc[
"@mozilla.org/embedcomp/default-tooltiptextprovider;1"
].getService(Ci.nsITooltipTextProvider);
let i = content.document.getElementById("i");
ok(
!tttp.getNodeText(i, {}, {}),
"No tooltip should be shown when @title is null"
);
i.title = "foo";
ok(
tttp.getNodeText(i, {}, {}),
"A tooltip should be shown when @title is not the empty string"
);
i.pattern = "bar";
ok(
tttp.getNodeText(i, {}, {}),
"A tooltip should be shown when @title is not the empty string"
);
});
}
);
});

View File

@@ -0,0 +1,107 @@
function check(aBrowser, aElementName, aBarred, aType) {
return SpecialPowers.spawn(
aBrowser,
[[aElementName, aBarred, aType]],
async function ([aElementName, aBarred, aType]) {
let e = content.document.createElement(aElementName);
let contentElement = content.document.getElementById("content");
contentElement.appendChild(e);
if (aType) {
e.type = aType;
}
let tttp = Cc[
"@mozilla.org/embedcomp/default-tooltiptextprovider;1"
].getService(Ci.nsITooltipTextProvider);
ok(
!tttp.getNodeText(e, {}, {}),
"No tooltip should be shown when the element is valid"
);
e.setCustomValidity("foo");
if (aBarred) {
ok(
!tttp.getNodeText(e, {}, {}),
"No tooltip should be shown when the element is barred from constraint validation"
);
} else {
ok(
tttp.getNodeText(e, {}, {}),
e.tagName + " A tooltip should be shown when the element isn't valid"
);
}
e.setAttribute("title", "");
ok(
!tttp.getNodeText(e, {}, {}),
"No tooltip should be shown if the title attribute is set"
);
e.removeAttribute("title");
contentElement.setAttribute("novalidate", "");
ok(
!tttp.getNodeText(e, {}, {}),
"No tooltip should be shown if the novalidate attribute is set on the form owner"
);
contentElement.removeAttribute("novalidate");
e.remove();
}
);
}
function todo_check(aBrowser, aElementName, aBarred) {
return SpecialPowers.spawn(
aBrowser,
[[aElementName, aBarred]],
async function ([aElementName]) {
let e = content.document.createElement(aElementName);
let contentElement = content.document.getElementById("content");
contentElement.appendChild(e);
let caught = false;
try {
e.setCustomValidity("foo");
} catch (e) {
caught = true;
}
todo(!caught, "setCustomValidity should exist for " + aElementName);
e.remove();
}
);
}
add_task(async function () {
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: "data:text/html,<!DOCTYPE html><html><body><form id='content'></form></body></html>",
},
async function (browser) {
let testData = [
/* element name, barred */
["input", false, null],
["textarea", false, null],
["button", true, "button"],
["button", false, "submit"],
["select", false, null],
["output", true, null],
["fieldset", true, null],
["object", true, null],
];
for (let data of testData) {
await check(browser, data[0], data[1], data[2]);
}
let todo_testData = [["keygen", "false"]];
for (let data of todo_testData) {
await todo_check(browser, data[0], data[1]);
}
}
);
});

View File

@@ -0,0 +1,131 @@
/* eslint-disable mozilla/no-arbitrary-setTimeout */
let tempFile;
add_setup(async function () {
await SpecialPowers.pushPrefEnv({ set: [["ui.tooltip.delay_ms", 0]] });
tempFile = createTempFile();
registerCleanupFunction(function () {
tempFile.remove(true);
});
});
add_task(async function test_singlefile_selected() {
await do_test({ value: true, result: "testfile_bug1251809" });
});
add_task(async function test_title_set() {
await do_test({ title: "foo", result: "foo" });
});
add_task(async function test_nofile_selected() {
await do_test({ result: "No file selected." });
});
add_task(async function test_multipleset_nofile_selected() {
await do_test({ multiple: true, result: "No files selected." });
});
add_task(async function test_requiredset() {
await do_test({ required: true, result: "Please select a file." });
});
async function do_test(test) {
info(`starting test ${JSON.stringify(test)}`);
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
info("Moving mouse out of the way.");
await EventUtils.synthesizeAndWaitNativeMouseMove(
tab.linkedBrowser,
300,
300
);
info("creating input field");
await SpecialPowers.spawn(tab.linkedBrowser, [test], async function (test) {
let doc = content.document;
let input = doc.createElement("input");
doc.body.appendChild(input);
input.id = "test_input";
input.setAttribute("style", "position: absolute; top: 0; left: 0;");
input.type = "file";
if (test.title) {
input.setAttribute("title", test.title);
}
if (test.multiple) {
input.multiple = true;
}
if (test.required) {
input.required = true;
}
});
if (test.value) {
info("Creating mock filepicker to select files");
let MockFilePicker = SpecialPowers.MockFilePicker;
MockFilePicker.init(window.browsingContext);
MockFilePicker.returnValue = MockFilePicker.returnOK;
MockFilePicker.displayDirectory = FileUtils.getDir("TmpD", []);
MockFilePicker.setFiles([tempFile]);
MockFilePicker.afterOpenCallback = MockFilePicker.cleanup;
try {
// Open the File Picker dialog (MockFilePicker) to select
// the files for the test.
await BrowserTestUtils.synthesizeMouseAtCenter(
"#test_input",
{},
tab.linkedBrowser
);
info("Waiting for the input to have the requisite files");
await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
let input = content.document.querySelector("#test_input");
await ContentTaskUtils.waitForCondition(
() => input.files.length,
"The input should have at least one file selected"
);
info(`The input has ${input.files.length} file(s) selected.`);
});
} catch (e) {}
} else {
info("No real file selection required.");
}
let awaitTooltipOpen = new Promise(resolve => {
let tooltipId = Services.appinfo.browserTabsRemoteAutostart
? "remoteBrowserTooltip"
: "aHTMLTooltip";
let tooltip = document.getElementById(tooltipId);
tooltip.addEventListener(
"popupshown",
function (event) {
resolve(event.target);
},
{ once: true }
);
});
info("Initial mouse move");
await EventUtils.synthesizeAndWaitNativeMouseMove(tab.linkedBrowser, 50, 5);
info("Waiting");
await new Promise(resolve => setTimeout(resolve, 400));
info("Second mouse move");
await EventUtils.synthesizeAndWaitNativeMouseMove(tab.linkedBrowser, 70, 5);
info("Waiting for tooltip to open");
let tooltip = await awaitTooltipOpen;
is(
tooltip.getAttribute("label"),
test.result,
"tooltip label should match expectation"
);
info("Closing tab");
BrowserTestUtils.removeTab(tab);
}
function createTempFile() {
let file = FileUtils.getDir("TmpD", []);
file.append("testfile_bug1251809");
file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
return file;
}

View File

@@ -0,0 +1,66 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint-disable mozilla/no-arbitrary-setTimeout */
"use strict";
add_setup(async function () {
await SpecialPowers.pushPrefEnv({ set: [["ui.tooltip.delay_ms", 0]] });
});
add_task(async function () {
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: "data:text/html,<!DOCTYPE html>",
},
async function (browser) {
info("Moving mouse out of the way.");
await EventUtils.synthesizeAndWaitNativeMouseMove(browser, 300, 300);
await SpecialPowers.spawn(browser, [], function () {
let widget = content.document.insertAnonymousContent();
widget.root.innerHTML = `<button style="pointer-events: auto; position: absolute; width: 200px; height: 200px;" title="foo">bar</button>`;
let tttp = Cc[
"@mozilla.org/embedcomp/default-tooltiptextprovider;1"
].getService(Ci.nsITooltipTextProvider);
let text = {};
let dir = {};
ok(
tttp.getNodeText(widget.root.querySelector("button"), text, dir),
"A tooltip should be shown for NAC"
);
is(text.value, "foo", "Tooltip text should be correct");
});
let awaitTooltipOpen = new Promise(resolve => {
let tooltipId = Services.appinfo.browserTabsRemoteAutostart
? "remoteBrowserTooltip"
: "aHTMLTooltip";
let tooltip = document.getElementById(tooltipId);
tooltip.addEventListener(
"popupshown",
function (event) {
resolve(event.target);
},
{ once: true }
);
});
info("Initial mouse move");
await EventUtils.synthesizeAndWaitNativeMouseMove(browser, 50, 5);
info("Waiting");
await new Promise(resolve => setTimeout(resolve, 400));
info("Second mouse move");
await EventUtils.synthesizeAndWaitNativeMouseMove(browser, 70, 5);
info("Waiting for tooltip to open");
let tooltip = await awaitTooltipOpen;
is(
tooltip.getAttribute("label"),
"foo",
"tooltip label should match expectation"
);
}
);
});

View File

@@ -0,0 +1,166 @@
/* eslint-disable mozilla/no-arbitrary-setTimeout */
add_setup(async function () {
await SpecialPowers.pushPrefEnv({ set: [["ui.tooltip.delay_ms", 0]] });
});
add_task(async function test_title_in_shadow_dom() {
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
info("Moving mouse out of the way.");
await EventUtils.synthesizeAndWaitNativeMouseMove(
tab.linkedBrowser,
300,
300
);
info("creating host");
await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
let doc = content.document;
let host = doc.createElement("div");
doc.body.appendChild(host);
host.setAttribute("style", "position: absolute; top: 0; left: 0;");
var sr = host.attachShadow({ mode: "closed" });
sr.innerHTML =
"<div title='shadow' style='width: 200px; height: 200px;'>shadow</div>";
});
let awaitTooltipOpen = new Promise(resolve => {
let tooltipId = Services.appinfo.browserTabsRemoteAutostart
? "remoteBrowserTooltip"
: "aHTMLTooltip";
let tooltip = document.getElementById(tooltipId);
tooltip.addEventListener(
"popupshown",
function (event) {
resolve(event.target);
},
{ once: true }
);
});
info("Initial mouse move");
await EventUtils.synthesizeAndWaitNativeMouseMove(tab.linkedBrowser, 50, 5);
info("Waiting");
await new Promise(resolve => setTimeout(resolve, 400));
info("Second mouse move");
await EventUtils.synthesizeAndWaitNativeMouseMove(tab.linkedBrowser, 70, 5);
info("Waiting for tooltip to open");
let tooltip = await awaitTooltipOpen;
is(
tooltip.getAttribute("label"),
"shadow",
"tooltip label should match expectation"
);
info("Closing tab");
BrowserTestUtils.removeTab(tab);
});
add_task(async function test_title_in_light_dom() {
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
info("Moving mouse out of the way.");
await EventUtils.synthesizeAndWaitNativeMouseMove(
tab.linkedBrowser,
300,
300
);
info("creating host");
await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
let doc = content.document;
let host = doc.createElement("div");
host.title = "light";
doc.body.appendChild(host);
host.setAttribute("style", "position: absolute; top: 0; left: 0;");
var sr = host.attachShadow({ mode: "closed" });
sr.innerHTML = "<div style='width: 200px; height: 200px;'>shadow</div>";
});
let awaitTooltipOpen = new Promise(resolve => {
let tooltipId = Services.appinfo.browserTabsRemoteAutostart
? "remoteBrowserTooltip"
: "aHTMLTooltip";
let tooltip = document.getElementById(tooltipId);
tooltip.addEventListener(
"popupshown",
function (event) {
resolve(event.target);
},
{ once: true }
);
});
info("Initial mouse move");
await EventUtils.synthesizeAndWaitNativeMouseMove(tab.linkedBrowser, 50, 5);
info("Waiting");
await new Promise(resolve => setTimeout(resolve, 400));
info("Second mouse move");
await EventUtils.synthesizeAndWaitNativeMouseMove(tab.linkedBrowser, 70, 5);
info("Waiting for tooltip to open");
let tooltip = await awaitTooltipOpen;
is(
tooltip.getAttribute("label"),
"light",
"tooltip label should match expectation"
);
info("Closing tab");
BrowserTestUtils.removeTab(tab);
});
add_task(async function test_title_through_slot() {
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
info("Moving mouse out of the way.");
await EventUtils.synthesizeAndWaitNativeMouseMove(
tab.linkedBrowser,
300,
300
);
info("creating host");
await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
let doc = content.document;
let host = doc.createElement("div");
host.title = "light";
host.innerHTML = "<div style='width: 200px; height: 200px;'>light</div>";
doc.body.appendChild(host);
host.setAttribute("style", "position: absolute; top: 0; left: 0;");
var sr = host.attachShadow({ mode: "closed" });
sr.innerHTML =
"<div title='shadow' style='width: 200px; height: 200px;'><slot></slot></div>";
});
let awaitTooltipOpen = new Promise(resolve => {
let tooltipId = Services.appinfo.browserTabsRemoteAutostart
? "remoteBrowserTooltip"
: "aHTMLTooltip";
let tooltip = document.getElementById(tooltipId);
tooltip.addEventListener(
"popupshown",
function (event) {
resolve(event.target);
},
{ once: true }
);
});
info("Initial mouse move");
await EventUtils.synthesizeAndWaitNativeMouseMove(tab.linkedBrowser, 50, 5);
info("Waiting");
await new Promise(resolve => setTimeout(resolve, 400));
info("Second mouse move");
await EventUtils.synthesizeAndWaitNativeMouseMove(tab.linkedBrowser, 70, 5);
info("Waiting for tooltip to open");
let tooltip = await awaitTooltipOpen;
is(
tooltip.getAttribute("label"),
"shadow",
"tooltip label should match expectation"
);
info("Closing tab");
BrowserTestUtils.removeTab(tab);
});

View File

@@ -0,0 +1,59 @@
<svg width="640px" height="480px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>This is a root SVG element's title</title>
<foreignObject>
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<svg xmlns="http://www.w3.org/2000/svg" id="svg1">
<title>This is a non-root SVG element title</title>
</svg>
</body>
</html>
</foreignObject>
<text id="text1" x="10px" y="32px" font-size="24px">
This contains only &lt;title&gt;
<title>
This is a title
</title>
</text>
<text id="text2" x="10px" y="96px" font-size="24px">
This contains only &lt;desc&gt;
<desc>This is a desc</desc>
</text>
<text id="text3" x="10px" y="128px" font-size="24px" title="ignored for SVG">
This contains nothing.
</text>
<a id="link1" href="#">
This link contains &lt;title&gt;
<title>
This is a title
</title>
<text id="text4" x="10px" y="192px" font-size="24px">
</text>
</a>
<a id="link2" href="#">
<text x="10px" y="192px" font-size="24px">
This text contains &lt;title&gt;
<title>
This is a title
</title>
</text>
</a>
<a id="link3" href="#" xlink:title="This is an xlink:title attribute">
<text x="10px" y="224px" font-size="24px">
This link contains &lt;title&gt; &amp; xlink:title attr.
<title>This is a title</title>
</text>
</a>
<a id="link4" href="#" xlink:title="This is an xlink:title attribute">
<text x="10px" y="256px" font-size="24px">
This link contains xlink:title attr.
</text>
</a>
<text id="text5" x="10px" y="160px" font-size="24px"
xlink:title="This is an xlink:title attribute but it isn't on a link" >
This contains nothing.
</text>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,12 @@
<?xml version="1.0"?>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<xul:toolbox xmlns:html="http://www.w3.org/1999/xhtml"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<toolbar>
<toolbarbutton id="xulToolbarButton"
tooltiptext="XUL tooltiptext"
title="XUL title"/>
</toolbar>
</xul:toolbox>
</html>

View File

@@ -15,3 +15,7 @@ BROWSER_CHROME_MANIFESTS += [
"welcome/browser.toml",
"workspaces/browser.toml",
]
DIRS += [
"mochitests",
]

View File

@@ -4,6 +4,11 @@
import { AppConstants } from 'resource://gre/modules/AppConstants.sys.mjs';
const ADDONS_BUTTONS_HIDDEN = Services.prefs.getBoolPref(
'zen.theme.hide-unified-extensions-button',
true
);
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
@@ -21,7 +26,19 @@ export class nsZenSiteDataPanel {
this.window = window;
this.document = window.document;
this.panel = this.document.getElementById('zen-unified-site-data-panel');
this.unifiedPanel = this.#initUnifiedPanel();
this.unifiedPanelView = 'unified-extensions-view';
this.extensionsPanelView = 'original-unified-extensions-view';
if (ADDONS_BUTTONS_HIDDEN) {
this.window.gUnifiedExtensions._panel = this.unifiedPanel;
// Remove the old permissions dialog
this.document.getElementById('unified-extensions-panel-template').remove();
} else {
this.extensionsPanel = this.#initExtensionsPanel();
}
this.#init();
}
@@ -34,22 +51,23 @@ export class nsZenSiteDataPanel {
`);
this.anchor = button.querySelector('#zen-site-data-icon-button');
this.document.getElementById('identity-icon-box').before(button);
this.window.gUnifiedExtensions._button = this.anchor;
this.extensionsPanelButton = this.document.getElementById('unified-extensions-button');
this.window.gUnifiedExtensions._button = ADDONS_BUTTONS_HIDDEN
? this.anchor
: this.extensionsPanelButton;
this.document
.getElementById('nav-bar')
.setAttribute('addon-webext-overflowbutton', 'zen-site-data-icon-button');
// Remove the old permissions dialog
this.document.getElementById('unified-extensions-panel-template').remove();
this.#initCopyUrlButton();
this.#initEventListeners();
this.#maybeShowFeatureCallout();
}
#initEventListeners() {
this.panel.addEventListener('popupshowing', this);
this.unifiedPanel.addEventListener('popupshowing', this);
this.document.getElementById('zen-site-data-manage-addons').addEventListener('click', this);
this.document.getElementById('zen-site-data-settings-more').addEventListener('click', this);
this.anchor.addEventListener('click', this);
@@ -124,6 +142,24 @@ export class nsZenSiteDataPanel {
}
}
#initExtensionsPanel() {
const panel = this.window.gUnifiedExtensions.panel;
const extensionsView = panel?.querySelector('#unified-extensions-view');
extensionsView.setAttribute('id', this.extensionsPanelView);
const panelMultiView = panel?.querySelector('panelmultiview');
panelMultiView.setAttribute('mainViewId', this.extensionsPanelView);
return panel;
}
#initUnifiedPanel() {
const panel = this.document.getElementById('zen-unified-site-data-panel');
this.window.gUnifiedExtensions.initializePanel(panel);
return panel;
}
#preparePanel() {
this.#setSitePermissions();
this.#setSiteSecurityInfo();
@@ -498,7 +534,7 @@ export class nsZenSiteDataPanel {
this.window.gZenCommonActions.copyCurrentURLToClipboard();
}
if (AppConstants.platform !== 'macosx') {
this.panel.hidePopup();
this.unifiedPanel.hidePopup();
}
}
}
@@ -556,7 +592,13 @@ export class nsZenSiteDataPanel {
break;
}
case 'zen-site-data-icon-button': {
this.window.gUnifiedExtensions.togglePanel(event);
this.window.gUnifiedExtensions.togglePanel(
event,
null,
this.unifiedPanel,
this.unifiedPanelView,
this.anchor
);
break;
}
default: {

View File

@@ -5,7 +5,7 @@
"binaryName": "zen",
"version": {
"product": "firefox",
"version": "146.0",
"version": "146.0.1",
"candidate": "146.0"
},
"buildOptions": {