Compare commits

..

13 Commits

Author SHA1 Message Date
mr. m
c5058746de feat: Added lint rules to ignore mochi tests, b=no-bug, c=tests 2025-10-22 01:18:49 +02:00
mr. m
1d509e30a9 test: Import some mochitests from firefox, b=no-bug, c=tests, scripts, tabs 2025-10-21 16:03:13 +02:00
mr. m
fbad844a08 feat: Animate glances comming from unknown sources, p=#10894, c=glance
* feat: Animate glances comming from unknown sources, b=no-bug, c=glance

* chore: Fixed linter, b=no-bug, c=glance
2025-10-21 12:16:32 +02:00
mr. m
f52f913043 Merge branch 'dev' of https://github.com/zen-browser/desktop into dev 2025-10-21 00:03:51 +02:00
mr. m
346ef9c158 feat: Dont hide browsers with pending paints and align autmatic glance to the tabs panel, b=no-bug, c=glance 2025-10-21 00:03:43 +02:00
mr. m
5038bd5cf7 feat: Only disable toolbar compact instead of hiding it, p=#10887, c=compact-mode 2025-10-20 23:06:57 +02:00
mr. m
b63cefa98b feat: Glance preview should not take the full height of the container, p=#10886, c=glance 2025-10-20 20:35:12 +02:00
mr. m
f8375a1155 feat: Dont wait for the next frame to animate, b=no-bug, c=glance 2025-10-20 18:44:25 +02:00
mr. m
34424a8f95 fix: Fixed the urlbar not always hiding on compact mode, p=#10884, c=compact-mode 2025-10-20 17:35:49 +02:00
mr. m
c883572de8 feat: Make use of the has polyfill for toolbar compact mode, p=#10883, c=compact-mode, tabs, workspaces 2025-10-20 17:17:59 +02:00
mr. m
a53b4f9c94 chore: New translations zen-general.ftl (German), p=#10882 2025-10-20 17:12:41 +02:00
mr. m
d1bba0a8e2 Merge branch 'dev' of https://github.com/zen-browser/desktop into dev 2025-10-20 12:36:52 +02:00
mr. m
d21e127bd7 feat: Make the top buttons smaller in height, b=no-bug, c=tabs 2025-10-20 12:35:49 +02:00
235 changed files with 28385 additions and 496 deletions

View File

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

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

@@ -38,7 +38,7 @@ zen-library-sidebar-workspaces =
zen-library-sidebar-mods =
.label = Mods
zen-toggle-compact-mode-button =
.label = Compact Mode
.label = Kompakter Modus
.tooltiptext = Compact Mode umschalten
# note: Do not translate the "<br/>" tags in the following string

View File

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

@@ -0,0 +1,115 @@
# 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,7 @@ IGNORE_PREFS_FILE_OUT = os.path.join(
'engine', 'testing', 'mochitest', 'ignorePrefs.json'
)
MOCHITEST_NAME = "mochitests"
class JSONWithCommentsDecoder(json.JSONDecoder):
def __init__(self, **kw):
@@ -68,7 +69,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

@@ -58,4 +58,3 @@
<script type="text/javascript" src="chrome://browser/content/zen-components/ZenDownloadAnimation.mjs"></script>
<script type="text/javascript" src="chrome://browser/content/zen-components/ZenEmojiPicker.mjs"></script>
<script type="text/javascript" src="chrome://browser/content/zen-components/ZenWorkspaceCreation.mjs"></script>
<script type="text/javascript" src="chrome://browser/content/zen-components/ZenWindowSyncing.mjs"></script>

View File

@@ -42,7 +42,6 @@
content/browser/zen-components/ZenWorkspaceIcons.mjs (../../zen/workspaces/ZenWorkspaceIcons.mjs)
content/browser/zen-components/ZenWorkspace.mjs (../../zen/workspaces/ZenWorkspace.mjs)
content/browser/zen-components/ZenWorkspaces.mjs (../../zen/workspaces/ZenWorkspaces.mjs)
content/browser/zen-components/ZenWindowSyncing.mjs (../../zen/workspaces/ZenWindowSyncing.mjs)
content/browser/zen-components/ZenWorkspaceCreation.mjs (../../zen/workspaces/ZenWorkspaceCreation.mjs)
content/browser/zen-components/ZenWorkspacesStorage.mjs (../../zen/workspaces/ZenWorkspacesStorage.mjs)
content/browser/zen-components/ZenWorkspacesSync.mjs (../../zen/workspaces/ZenWorkspacesSync.mjs)

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/customizableui/CustomizableUI.sys.mjs b/browser/components/customizableui/CustomizableUI.sys.mjs
index d9a059f608779fea7cd8c595a432f6fe95183e0c..09a7c4045afd0b96027d0bbbad22e02e52fd7b22 100644
index d9a059f608779fea7cd8c595a432f6fe95183e0c..31c43bc3d5b05713299c1b822b9774909445e862 100644
--- a/browser/components/customizableui/CustomizableUI.sys.mjs
+++ b/browser/components/customizableui/CustomizableUI.sys.mjs
@@ -14,6 +14,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
@@ -158,7 +158,7 @@ index d9a059f608779fea7cd8c595a432f6fe95183e0c..09a7c4045afd0b96027d0bbbad22e02e
continue;
}
- sum += parseFloat(style.marginLeft) + parseFloat(style.marginRight);
+ sum += parseFloat(style.marginLeft) + Math.max(0, parseFloat(style.marginRight));
+ sum += parseFloat(style.marginLeft) + (win.gZenVerticalTabsManager._hasSetSingleToolbar ? Math.max(0, parseFloat(style.marginRight)) : parseFloat(style.marginRight));
if (child != aExceptChild) {
sum += getInlineSize(child);
}

View File

@@ -1,8 +1,16 @@
diff --git a/browser/themes/shared/tabbrowser/content-area.css b/browser/themes/shared/tabbrowser/content-area.css
index e06addf1602dc26ff4e75a8db6251231690f3f80..86e2cd0194bb37fa140a2f93eccfdd61419a9aec 100644
index e06addf1602dc26ff4e75a8db6251231690f3f80..ffac005d5040852eda8f574f65f2eadf5ecbd642 100644
--- a/browser/themes/shared/tabbrowser/content-area.css
+++ b/browser/themes/shared/tabbrowser/content-area.css
@@ -276,7 +276,7 @@
@@ -134,7 +134,6 @@
}
browser:is([blank], [pendingpaint]) {
- opacity: 0;
}
browser[type="content"] {
@@ -276,7 +275,7 @@
.dialogStack {
z-index: var(--browser-stack-z-index-dialog-stack);

View File

@@ -24,11 +24,11 @@
const { exists: shouldExist = true } = descendantSelectors;
if (exists === shouldExist) {
if (!element.hasAttribute(stateAttribute)) {
element.setAttribute(stateAttribute, 'true');
gZenCompactModeManager._setElementExpandAttribute(element, true, stateAttribute);
}
} else {
if (element.hasAttribute(stateAttribute)) {
element.removeAttribute(stateAttribute);
gZenCompactModeManager._setElementExpandAttribute(element, false, stateAttribute);
}
}
};

View File

@@ -143,6 +143,7 @@ var gZenCompactModeManager = {
},
addHasPolyfillObserver() {
const attributes = ['panelopen', 'open', 'breakout-extend', 'zen-floating-urlbar'];
this.sidebarObserverId = ZenHasPolyfill.observeSelectorExistence(
this.sidebar,
[
@@ -152,8 +153,21 @@ var gZenCompactModeManager = {
},
],
'zen-compact-mode-active',
['panelopen', 'open', 'breakout-extend', 'zen-floating-urlbar']
attributes
);
this.toolbarObserverId = ZenHasPolyfill.observeSelectorExistence(
document.getElementById('zen-appcontent-navbar-wrapper'),
[
{
selector:
":is([panelopen='true'], [open='true'], #urlbar:focus-within, [breakout-extend='true']):not(.zen-compact-mode-ignore)",
},
],
'zen-compact-mode-active',
attributes
);
// Always connect this observer, we need it even if compact mode is disabled
ZenHasPolyfill.connectObserver(this.toolbarObserverId);
},
flashSidebarIfNecessary(aInstant = false) {
@@ -202,7 +216,7 @@ var gZenCompactModeManager = {
},
updateCompactModeContext(isSingleToolbar) {
isSingleToolbar ||= this.checkIfIllegalState();
const isIllegalState = this.checkIfIllegalState();
const menuitem = document.getElementById('zen-context-menu-compact-mode-toggle');
const menu = document.getElementById('zen-context-menu-compact-mode');
if (isSingleToolbar) {
@@ -212,6 +226,14 @@ var gZenCompactModeManager = {
menu.removeAttribute('hidden');
menu.querySelector('menupopup').prepend(menuitem);
}
const hideToolbarMenuItem = document.getElementById(
'zen-context-menu-compact-mode-hide-toolbar'
);
if (isIllegalState) {
hideToolbarMenuItem.setAttribute('disabled', 'true');
} else {
hideToolbarMenuItem.removeAttribute('disabled');
}
},
hideSidebar() {
@@ -601,7 +623,7 @@ var gZenCompactModeManager = {
},
_setElementExpandAttribute(element, value, attr = 'zen-has-hover') {
const kVerifiedAttributes = ['zen-has-hover', 'has-popup-menu'];
const kVerifiedAttributes = ['zen-has-hover', 'has-popup-menu', 'zen-compact-mode-active'];
const isToolbar = element.id === 'zen-appcontent-navbar-wrapper';
if (value) {
element.setAttribute(attr, 'true');
@@ -612,8 +634,7 @@ var gZenCompactModeManager = {
document.documentElement.hasAttribute('zen-has-bookmarks'))) ||
(this.preference &&
Services.prefs.getBoolPref('zen.view.compact.hide-toolbar') &&
!gZenVerticalTabsManager._hasSetSingleToolbar &&
!gURLBar.hasAttribute('breakout-extend')))
!gZenVerticalTabsManager._hasSetSingleToolbar))
) {
gBrowser.tabpanels.setAttribute('has-toolbar-hovered', 'true');
}

View File

@@ -35,7 +35,8 @@
overflow: clip;
& #urlbar:not([breakout-extend='true']) {
opacity: 0;
/* Sometimes, "opacity: 1" is forced elsewhere */
opacity: 0 !important;
pointer-events: none;
transition: opacity var(--zen-hidden-toolbar-transition);
}
@@ -48,20 +49,18 @@
}
}
& #zen-appcontent-navbar-wrapper[zen-has-hover],
& #zen-appcontent-navbar-wrapper[has-popup-menu],
&
#zen-appcontent-navbar-wrapper:has(
*:is([panelopen='true'], [open='true'], #urlbar:focus-within, [breakout-extend='true']):not(.zen-compact-mode-ignore)
) {
& #zen-appcontent-navbar-wrapper:is(
[zen-has-hover],
[has-popup-menu],
[zen-compact-mode-active]
) {
height: var(--zen-toolbar-height-with-bookmarks);
overflow: inherit;
%include windows-captions-fix-active.inc.css
& #urlbar {
opacity: 1;
opacity: 1 !important;
pointer-events: auto;
}

View File

@@ -24,6 +24,9 @@
#duringOpening = false;
#ignoreClose = false;
// Click handling
#lastLinkClickData = { clientX: 0, clientY: 0, height: 0, width: 0 };
// Arc animation configuration
#ARC_CONFIG = Object.freeze({
ARC_STEPS: 70, // Increased for smoother bounce
@@ -268,10 +271,31 @@
data.height
);
return await this.#imageBitmapToBase64(
await window.browsingContext.currentWindowGlobal.drawSnapshot(rect, 1, 'transparent', true)
await window.browsingContext.currentWindowGlobal.drawSnapshot(
rect,
1,
'transparent',
undefined
)
);
}
/**
* Set the last link click data
* @param {Object} data - The link click data
*/
set lastLinkClickData(data) {
this.#lastLinkClickData = data;
}
/**
* Get the last link click data
* @returns {Object} The last link click data
*/
get lastLinkClickData() {
return this.#lastLinkClickData;
}
/**
* Open a glance overlay with the specified data
* @param {Object} data - Glance data including URL, position, and dimensions
@@ -289,6 +313,13 @@
return;
}
if (!data.height || !data.width) {
data = {
...data,
...this.lastLinkClickData,
};
}
this.#setAnimationState(true);
const currentTab = ownerTab ?? gBrowser.selectedTab;
const browserElement = this.#createBrowserElement(data.url, currentTab, existingTab);
@@ -325,12 +356,13 @@
gZenViewSplitter.onLocationChange(browserElement);
this.#prepareGlanceAnimation(data, browserElement);
if (data.width && data.height) {
// It is guaranteed that we will animate this opacity later on
// when we start animating the glance.
this.contentWrapper.style.opacity = 0;
data.elementData = await this.#getElementPreviewData(data);
}
this.#glances.get(this.#currentGlanceID).elementData = data.elementData;
window.requestAnimationFrame(() => {
this.#executeGlanceAnimation(data, browserElement, resolve);
});
this.#executeGlanceAnimation(data, browserElement, resolve);
});
}
@@ -344,7 +376,6 @@
const newButtons = this.#createNewOverlayButtons();
this.browserWrapper.appendChild(newButtons);
this.#animateParentBackground();
this.#setupGlancePositioning(data);
this.#configureBrowserElement(browserElement);
}
@@ -488,7 +519,6 @@
// nice fade-in effect to the content. But if it doesn't exist,
// we just fall back to always showing the browser directly.
if (data.elementData) {
this.contentWrapper.style.opacity = 0;
gZenUIManager.motion
.animate(
this.contentWrapper,
@@ -503,6 +533,7 @@
});
}
this.#animateParentBackground();
gZenUIManager.motion
.animate(this.browserWrapper, arcSequence, {
duration: gZenUIManager.testingEnabled ? 0 : 0.4,
@@ -990,7 +1021,7 @@
if (!onTabClose) {
this.quickCloseGlance({ clearID: false });
}
this.browserWrapper.style.display = 'none';
this.overlay.style.display = 'none';
this.overlay.removeAttribute('fade-out');
this.browserWrapper.removeAttribute('animate');
@@ -1357,18 +1388,9 @@
* @param {Tab} tab - The tab to open glance for
*/
#openGlanceForTab(tab) {
const browserRect = window.windowUtils.getBoundsWithoutFlushing(gBrowser.tabbox);
const clickPosition = gZenUIManager._lastClickPosition || {
clientX: browserRect.width / 2,
clientY: browserRect.height / 2,
};
this.openGlance(
{
url: undefined,
...clickPosition,
width: 0,
height: 0,
},
tab,
tab.owner

View File

@@ -35,22 +35,30 @@ export class ZenGlanceChild extends JSWindowActorChild {
return !(event.ctrlKey ^ event.altKey ^ event.shiftKey ^ event.metaKey);
}
openGlance(target, originalTarget) {
#openGlance(target) {
let url = target.href;
// Add domain to relative URLs
if (!url.match(/^(?:[a-z]+:)?\/\//i)) {
url = this.contentWindow.location.origin + url;
}
this.sendAsyncMessage('ZenGlance:OpenGlance', {
url,
});
}
#sendClickDataToParent(target, element) {
if (!element || !target) {
return;
}
// Get the largest element we can get. If the `A` element
// is a parent of the original target, use the anchor element,
// otherwise use the original target.
let rect = originalTarget.getBoundingClientRect();
let rect = element.getBoundingClientRect();
const anchorRect = target.getBoundingClientRect();
if (anchorRect.width * anchorRect.height > rect.width * rect.height) {
rect = anchorRect;
}
this.sendAsyncMessage('ZenGlance:OpenGlance', {
url,
this.sendAsyncMessage('ZenGlance:RecordLinkClickData', {
clientX: rect.left,
clientY: rect.top,
width: rect.width,
@@ -59,7 +67,19 @@ export class ZenGlanceChild extends JSWindowActorChild {
}
handleClick(event) {
if (this.ensureOnlyKeyModifiers(event) || event.button !== 0 || event.defaultPrevented) {
if (event.button !== 0 || event.defaultPrevented) {
return;
}
// get closest A element
const target = event.target.closest('A');
const elementToRecord = event.originalTarget || event.target;
// We record the link data anyway, even if the glance may be invoked
// or not. We have some cases where glance would open, for example,
// when clicking on a link with a different domain where glance would open.
// The problem is that at that stage we don't know the rect or even what
// element has been clicked, so we send the data here.
this.#sendClickDataToParent(target, elementToRecord);
if (this.ensureOnlyKeyModifiers(event)) {
return;
}
const activationMethod = this.#activationMethod;
@@ -72,13 +92,11 @@ export class ZenGlanceChild extends JSWindowActorChild {
} else if (activationMethod === 'meta' && !event.metaKey) {
return;
}
// get closest A element
const target = event.target.closest('A');
if (target) {
event.preventDefault();
event.stopPropagation();
this.openGlance(target, event.originalTarget || event.target);
this.#openGlance(target);
}
}

View File

@@ -23,6 +23,10 @@ export class ZenGlanceParent extends JSWindowActorParent {
this.browsingContext.topChromeWindow.gZenGlanceManager.closeGlance(params);
break;
}
case 'ZenGlance:RecordLinkClickData': {
this.browsingContext.topChromeWindow.gZenGlanceManager.lastLinkClickData = message.data;
break;
}
default:
console.warn(`[glance]: Unknown message: ${message.name}`);
}

View File

@@ -171,10 +171,10 @@
position: absolute;
pointer-events: none;
width: 100%;
height: 100%;
z-index: 0;
border-radius: var(--zen-native-inner-radius);
inset: 50%;
translate: -50% -50%;
top: 0%;
left: 50%;
translate: -50% 0%;
will-change: transform, opacity;
}

View File

@@ -1,27 +0,0 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
const FILE_NAME = 'zen-sessions.jsonlz4';
export class nsZenSessionFile {
#path;
#windows;
constructor() {
this.#path = PathUtils.join(profileDir, FILE_NAME);
}
async read() {
try {
return await IOUtils.readJSON(this.#path, { compress: true });
} catch (e) {
return {};
}
}
async write(data) {
await IOUtils.writeJSON(this.#path, data, { compress: true });
}
}

View File

@@ -1,50 +0,0 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import {
cancelIdleCallback,
clearTimeout,
requestIdleCallback,
setTimeout,
} from 'resource://gre/modules/Timer.sys.mjs';
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ZenSessionFile: 'resource://gre/modules/ZenSessionFile.sys.mjs',
PrivateBrowsingUtils: 'resource://gre/modules/PrivateBrowsingUtils.sys.mjs',
RunState: 'resource:///modules/sessionstore/RunState.sys.mjs',
});
class nsZenSessionManager {
#file;
constructor() {
this.#file = null;
}
get file() {
if (!this.#file) {
this.#file = lazy.ZenSessionFile;
}
return this.#file;
}
/**
* Saves the current session state. Collects data and writes to disk.
*
* @param forceUpdateAllWindows (optional)
* Forces us to recollect data for all windows and will bypass and
* update the corresponding caches.
*/
saveState(forceUpdateAllWindows = false) {
if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) {
// Don't save (or even collect) anything in permanent private
// browsing mode
return Promise.resolve();
}
}
}
export const ZenSessionStore = new nsZenSessionManager();

View File

@@ -1,35 +0,0 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
export class ZenSessionWindow {
#id;
#selectedWorkspace;
#selectedTab;
constructor(id) {
this.#id = id;
this.#selectedWorkspace = null;
this.#selectedTab = null;
}
get id() {
return this.#id;
}
get selectedWorkspace() {
return this.#selectedWorkspace;
}
set selectedWorkspace(workspace) {
this.#selectedWorkspace = workspace;
}
get selectedTab() {
return this.#selectedTab;
}
set selectedTab(tab) {
this.#selectedTab = tab;
}
}

View File

@@ -101,7 +101,6 @@
}
onTabIconChanged(tab, url = null) {
tab.dispatchEvent(new CustomEvent('ZenTabIconChanged', { bubbles: true, detail: { tab } }));
const iconUrl = url ?? tab.iconImage.src;
if (!iconUrl && tab.hasAttribute('zen-pin-id')) {
try {
@@ -1556,7 +1555,6 @@
}
async onTabLabelChanged(tab) {
tab.dispatchEvent(new CustomEvent('ZenTabLabelChanged', { detail: { tab } }));
if (!this._pinsCache) {
return;
}

View File

@@ -22,9 +22,7 @@ z-index: 1;
%include ../../compact-mode/windows-captions-fix-active.inc.css
&:not([zen-has-hover='true']):not([has-popup-menu]):not(:focus-within):not(
:has(*:is([panelopen='true'], [open='true']))
) {
&:not([zen-has-hover='true']):not([has-popup-menu]):not([zen-compact-mode-active]) {
height: var(--zen-element-separation);
opacity: 0;
& #zen-appcontent-navbar-container {

View File

@@ -963,7 +963,7 @@
:root[zen-single-toolbar='true'] & {
--zen-toolbar-height: 36px;
@media (-moz-platform: macos) {
--zen-toolbar-height: 42px;
--zen-toolbar-height: 38px;
}
& #PanelUI-button {

View File

@@ -0,0 +1,29 @@
# 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
[tooltiptext]
source = "toolkit/components/tooltiptext"
[translations]
source = "browser/components/translations/tests/browser"
is_direct_path = true

View File

@@ -0,0 +1,15 @@
# 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",
"translations/browser.toml",
]

View File

@@ -0,0 +1,50 @@
[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
["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,117 @@
/* 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);
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",
isPrivateBrowsing: true,
hasTrackingContentBlocked: true,
hasMixedActiveContentBlocked: true,
hasMixedDisplayContentBlocked: true,
btpHasPurgedSite: false,
etpCategory: "strict",
},
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",
isPrivateBrowsing: true,
hasTrackingContentBlocked: true,
hasMixedActiveContentBlocked: true,
hasMixedDisplayContentBlocked: true,
btpHasPurgedSite: false,
etpCategory: "strict",
},
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,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,132 @@
/* 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");
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-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,897 @@
/* 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 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);
}
});
});
}
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 sendMoreInfoLink() {
return this.getViewNode("report-broken-site-popup-send-more-info-link");
}
get backButton() {
return this.mainView.querySelector(".subviewbutton-back");
}
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,330 @@
/* 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",
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.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"
);
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();
// re-opening the panel, the url and description should be reset
rbs = await menu.openReportBrokenSite();
rbs.isMainViewResetToCurrentTab();
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,305 @@
/* 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 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,
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 [
"direct2DEnabled",
"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,101 @@
[DEFAULT]
["browser_1119088.js"]
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"]
tags = "os_integration"

View File

@@ -0,0 +1,179 @@
// 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_10_14 =
"/Library/Desktop Pictures/Solid Colors/Teal.png";
const kDefaultBackgroundImage_10_15 =
"/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;
if (AppConstants.isPlatformAndVersionAtLeast("macosx", 19)) {
defaultBackgroundPath = kDefaultBackgroundImage_10_15;
} else {
defaultBackgroundPath = kDefaultBackgroundImage_10_14;
}
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",
"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,6 @@
[DEFAULT]
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

@@ -0,0 +1,285 @@
[DEFAULT]
subsuite = "translations"
support-files = [
"head.js",
"!/toolkit/components/translations/tests/browser/shared-head.js",
"!/toolkit/components/translations/tests/browser/translations-test.mjs",
]
["browser_translations_about_preferences_manage_downloaded_languages.js"]
["browser_translations_about_preferences_settings_always_translate_languages.js"]
["browser_translations_about_preferences_settings_download_languages_all_ui.js"]
["browser_translations_about_preferences_settings_download_languages_error_ui.js"]
["browser_translations_about_preferences_settings_download_languages_ui.js"]
["browser_translations_about_preferences_settings_never_translate_languages.js"]
["browser_translations_about_preferences_settings_never_translate_sites.js"]
["browser_translations_about_preferences_settings_ui.js"]
["browser_translations_about_preferences_settings_ui_keyboard_a11y.js"]
["browser_translations_about_preferences_settings_ui_tab.js"]
["browser_translations_e2e_full_page_translate_with_lexical_shortlist.js"]
["browser_translations_e2e_full_page_translate_without_lexical_shortlist.js"]
["browser_translations_full_page_intersection_content_eager.js"]
["browser_translations_full_page_intersection_find_bar.js"]
["browser_translations_full_page_intersection_find_bar_move_tab_to_new_window.js"]
["browser_translations_full_page_intersection_find_bar_multi_tab.js"]
["browser_translations_full_page_intersection_lazy.js"]
["browser_translations_full_page_intersection_mutations_content_eager.js"]
["browser_translations_full_page_intersection_mutations_lazy.js"]
["browser_translations_full_page_language_id_behavior.js"]
["browser_translations_full_page_move_tab_to_new_window.js"]
["browser_translations_full_page_moz_extension.js"]
["browser_translations_full_page_multiple_windows.js"]
skip-if = ["os == 'linux' && os_version == '24.04' && processor == 'x86_64' && display == 'x11' && ccov"] # Bug 1893021
["browser_translations_full_page_panel_a11y_focus.js"]
["browser_translations_full_page_panel_always_translate_language_bad_data.js"]
["browser_translations_full_page_panel_always_translate_language_basic.js"]
["browser_translations_full_page_panel_always_translate_language_manual.js"]
["browser_translations_full_page_panel_always_translate_language_restore.js"]
["browser_translations_full_page_panel_app_menu_never_translate_language.js"]
["browser_translations_full_page_panel_app_menu_never_translate_site.js"]
["browser_translations_full_page_panel_auto_translate_error_view.js"]
["browser_translations_full_page_panel_auto_translate_revisit_view.js"]
["browser_translations_full_page_panel_basics.js"]
["browser_translations_full_page_panel_button.js"]
["browser_translations_full_page_panel_cancel.js"]
["browser_translations_full_page_panel_change_app_locale.js"]
["browser_translations_full_page_panel_close_panel_never_translate_language_with_translations_active.js"]
["browser_translations_full_page_panel_close_panel_never_translate_language_with_translations_inactive.js"]
["browser_translations_full_page_panel_close_panel_never_translate_site.js"]
["browser_translations_full_page_panel_engine_destroy.js"]
["browser_translations_full_page_panel_engine_destroy_pending.js"]
skip-if = ["os == 'linux' && os_version == '24.04' && processor == 'x86_64' && display == 'x11' && ccov"] # Bug 1893021
["browser_translations_full_page_panel_engine_unsupported.js"]
["browser_translations_full_page_panel_firstrun.js"]
["browser_translations_full_page_panel_firstrun_revisit.js"]
["browser_translations_full_page_panel_flip_lexical_shortlist.js"]
["browser_translations_full_page_panel_fuzzing.js"]
skip-if = ["true"]
["browser_translations_full_page_panel_gear.js"]
["browser_translations_full_page_panel_init_failure.js"]
["browser_translations_full_page_panel_modify_available_language_models.js"]
["browser_translations_full_page_panel_never_translate_language.js"]
["browser_translations_full_page_panel_never_translate_site_auto.js"]
["browser_translations_full_page_panel_never_translate_site_basic.js"]
["browser_translations_full_page_panel_never_translate_site_manual.js"]
["browser_translations_full_page_panel_retry.js"]
["browser_translations_full_page_panel_script_tags.js"]
["browser_translations_full_page_panel_settings_unsupported_lang.js"]
["browser_translations_full_page_panel_switch_languages.js"]
["browser_translations_full_page_panel_switch_tabs_before_engine_ready.js"]
["browser_translations_full_page_panel_target_language_persists_on_reopen.js"]
["browser_translations_full_page_panel_unsupported_lang.js"]
["browser_translations_full_page_panel_weblanguage_differs_from_app.js"]
["browser_translations_full_page_reader_mode.js"]
["browser_translations_full_page_telemetry_auto_translate.js"]
["browser_translations_full_page_telemetry_basics.js"]
["browser_translations_full_page_telemetry_open_panel.js"]
["browser_translations_full_page_telemetry_panel_auto_offer.js"]
["browser_translations_full_page_telemetry_panel_auto_offer_settings.js"]
["browser_translations_full_page_telemetry_retranslate.js"]
["browser_translations_full_page_telemetry_switch_languages.js"]
["browser_translations_full_page_telemetry_translation_failure.js"]
["browser_translations_full_page_telemetry_translation_request.js"]
["browser_translations_full_page_telemetry_unsupported_lang.js"]
["browser_translations_recent_language_memory_auto_translate.js"]
["browser_translations_recent_language_memory_full_page_and_select.js"]
["browser_translations_recent_language_memory_full_page_and_select_multi_window.js"]
skip-if = ["os == 'linux' && os_version == '24.04' && processor == 'x86_64' && display == 'x11' && ccov"] # Bug 1972458
["browser_translations_recent_language_memory_full_page_multi_window.js"]
["browser_translations_recent_language_memory_full_page_multi_window_multi_tab.js"]
["browser_translations_recent_language_memory_full_page_navigate.js"]
["browser_translations_recent_language_memory_full_page_retranslate.js"]
["browser_translations_select_context_menu_engine_unsupported.js"]
["browser_translations_select_context_menu_feature_disabled.js"]
["browser_translations_select_context_menu_preferred_app_locales.js"]
["browser_translations_select_context_menu_preferred_language_edge_cases.js"]
["browser_translations_select_context_menu_preferred_web_languages.js"]
["browser_translations_select_context_menu_with_full_page_translations_active.js"]
["browser_translations_select_context_menu_with_hyperlink.js"]
["browser_translations_select_context_menu_with_no_text_selected.js"]
["browser_translations_select_context_menu_with_text_selected.js"]
["browser_translations_select_panel_a11y_utils.js"]
["browser_translations_select_panel_change_app_locale.js"]
["browser_translations_select_panel_close_on_new_tab.js"]
["browser_translations_select_panel_copy_button.js"]
["browser_translations_select_panel_engine_cache.js"]
["browser_translations_select_panel_fallback_to_doc_language.js"]
["browser_translations_select_panel_flip_lexical_shortlist.js"]
["browser_translations_select_panel_init_failure.js"]
["browser_translations_select_panel_modify_available_language_models.js"]
["browser_translations_select_panel_pdf.js"]
["browser_translations_select_panel_reader_mode.js"]
["browser_translations_select_panel_retranslate_on_change_language_directly.js"]
["browser_translations_select_panel_retranslate_on_change_language_from_dropdown_menu.js"]
["browser_translations_select_panel_script_tags.js"]
["browser_translations_select_panel_select_current_language_directly.js"]
["browser_translations_select_panel_select_current_language_from_dropdown_menu.js"]
["browser_translations_select_panel_select_same_from_and_to_languages_directly.js"]
["browser_translations_select_panel_select_same_from_and_to_languages_from_dropdown_menu.js"]
["browser_translations_select_panel_settings_menu.js"]
["browser_translations_select_panel_translate_full_page_button.js"]
["browser_translations_select_panel_translate_on_change_language_directly.js"]
["browser_translations_select_panel_translate_on_change_language_from_dropdown_menu.js"]
["browser_translations_select_panel_translate_on_change_language_multiple_times_directly.js"]
["browser_translations_select_panel_translate_on_change_language_multiple_times_from_dropdown_menu.js"]
["browser_translations_select_panel_translate_on_open.js"]
["browser_translations_select_panel_translation_failure_after_unsupported_language.js"]
["browser_translations_select_panel_translation_failure_on_open.js"]
["browser_translations_select_panel_translation_failure_on_retranslate.js"]
["browser_translations_select_panel_unsupported_language.js"]
["browser_translations_select_telemetry_change_both_languages_together.js"]
["browser_translations_select_telemetry_change_languages_multiple_times.js"]
["browser_translations_select_telemetry_init_failure_ui_then_cancel.js"]
["browser_translations_select_telemetry_init_failure_ui_then_succeed.js"]
["browser_translations_select_telemetry_keypresses_cancel_button.js"]
["browser_translations_select_telemetry_keypresses_copy_button.js"]
["browser_translations_select_telemetry_keypresses_done_button.js"]
["browser_translations_select_telemetry_keypresses_settings_button.js"]
["browser_translations_select_telemetry_keypresses_translate_button.js"]
["browser_translations_select_telemetry_keypresses_translate_full_page_button.js"]
["browser_translations_select_telemetry_keypresses_try_again_button_init_failure.js"]
["browser_translations_select_telemetry_keypresses_try_again_button_translation_failure.js"]
["browser_translations_select_telemetry_multi_page.js"]
["browser_translations_select_telemetry_primary_ui.js"]
["browser_translations_select_telemetry_settings_menu.js"]
["browser_translations_select_telemetry_translation_failure_ui_then_cancel.js"]
["browser_translations_select_telemetry_translation_failure_ui_then_succeed.js"]
["browser_translations_select_telemetry_translation_failure_with_full_page_translations_active.js"]
["browser_translations_select_telemetry_translation_success_with_full_page_translations_active.js"]
["browser_translations_select_telemetry_unsupported_language_ui.js"]

View File

@@ -0,0 +1,225 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
function getFrenchModels() {
return languageModelNames([
{ fromLang: "fr", toLang: "en" },
{ fromLang: "en", toLang: "fr" },
]);
}
add_task(async function test_about_preferences_manage_languages() {
await testWithAndWithoutLexicalShortlist(async lexicalShortlistPrefs => {
const {
cleanup,
remoteClients,
elements: {
downloadAllLabel,
downloadAll,
deleteAll,
frenchLabel,
frenchDownload,
frenchDelete,
spanishLabel,
spanishDownload,
spanishDelete,
ukrainianLabel,
ukrainianDownload,
ukrainianDelete,
},
} = await setupAboutPreferences(LANGUAGE_PAIRS, {
prefs: [
["browser.translations.newSettingsUI.enable", false],
...lexicalShortlistPrefs,
],
});
is(
downloadAllLabel.getAttribute("data-l10n-id"),
"translations-manage-download-description",
"The first row is all of the languages."
);
is(frenchLabel.textContent, "French", "There is a French row.");
is(spanishLabel.textContent, "Spanish", "There is a Spanish row.");
is(ukrainianLabel.textContent, "Ukrainian", "There is a Ukrainian row.");
await ensureVisibility({
message: "Everything starts out as available to download",
visible: {
downloadAll,
frenchDownload,
spanishDownload,
ukrainianDownload,
},
hidden: { deleteAll, frenchDelete, spanishDelete, ukrainianDelete },
});
click(frenchDownload, "Downloading French");
const frenchModels = getFrenchModels();
Assert.deepEqual(
await remoteClients.translationModels.resolvePendingDownloads(
frenchModels.length
),
frenchModels,
"French models were downloaded."
);
await ensureVisibility({
message: "French can now be deleted, and delete all is available.",
visible: {
downloadAll,
deleteAll,
frenchDelete,
spanishDownload,
ukrainianDownload,
},
hidden: { frenchDownload, spanishDelete, ukrainianDelete },
});
click(frenchDelete, "Deleting French");
await ensureVisibility({
message: "Everything can be downloaded.",
visible: {
downloadAll,
frenchDownload,
spanishDownload,
ukrainianDownload,
},
hidden: { deleteAll, frenchDelete, spanishDelete, ukrainianDelete },
});
click(downloadAll, "Downloading all languages.");
const allModels = languageModelNames(LANGUAGE_PAIRS);
Assert.deepEqual(
await remoteClients.translationModels.resolvePendingDownloads(
allModels.length
),
allModels,
"All models were downloaded."
);
Assert.deepEqual(
await remoteClients.translationsWasm.resolvePendingDownloads(1),
["bergamot-translator"],
"Wasm was downloaded."
);
await ensureVisibility({
message: "Everything can be deleted.",
visible: { deleteAll, frenchDelete, spanishDelete, ukrainianDelete },
hidden: {
downloadAll,
frenchDownload,
spanishDownload,
ukrainianDownload,
},
});
click(deleteAll, "Deleting all languages.");
await ensureVisibility({
message: "Everything can be downloaded again",
visible: {
downloadAll,
frenchDownload,
spanishDownload,
ukrainianDownload,
},
hidden: { deleteAll, frenchDelete, spanishDelete, ukrainianDelete },
});
click(frenchDownload, "Downloading French.");
click(spanishDownload, "Downloading Spanish.");
click(ukrainianDownload, "Downloading Ukrainian.");
Assert.deepEqual(
await remoteClients.translationModels.resolvePendingDownloads(
allModels.length
),
allModels,
"All models were downloaded again."
);
remoteClients.translationsWasm.assertNoNewDownloads();
await ensureVisibility({
message: "Everything is downloaded again.",
visible: { deleteAll, frenchDelete, spanishDelete, ukrainianDelete },
hidden: {
downloadAll,
frenchDownload,
spanishDownload,
ukrainianDownload,
},
});
await cleanup();
});
});
add_task(async function test_about_preferences_download_reject() {
const {
cleanup,
remoteClients,
elements: { document, frenchDownload },
} = await setupAboutPreferences(LANGUAGE_PAIRS, {
prefs: [["browser.translations.newSettingsUI.enable", false]],
});
click(frenchDownload, "Downloading French");
is(
maybeGetByL10nId("translations-manage-error-download", document),
null,
"No error messages are present."
);
const failureErrors = await captureTranslationsError(() =>
remoteClients.translationModels.rejectPendingDownloads(
getFrenchModels().length
)
);
ok(
!!failureErrors.length,
`The errors for download should have been reported, found ${failureErrors.length} errors`
);
for (const { error } of failureErrors) {
is(
error?.message,
"Failed to download file.",
"The error reported was a download error."
);
}
await waitForCondition(
() => maybeGetByL10nId("translations-manage-error-download", document),
"The error message is now visible."
);
click(frenchDownload, "Attempting to download French again", document);
is(
maybeGetByL10nId("translations-manage-error-download", document),
null,
"The error message is hidden again."
);
const successErrors = await captureTranslationsError(() =>
remoteClients.translationModels.resolvePendingDownloads(
getFrenchModels().length
)
);
is(
successErrors.length,
0,
"Expected no errors downloading French the second time"
);
await cleanup();
});

View File

@@ -0,0 +1,97 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(
async function test_about_preferences_always_translate_language_settings() {
const {
cleanup,
elements: { settingsButton },
} = await setupAboutPreferences(LANGUAGE_PAIRS, {
prefs: [["browser.translations.newSettingsUI.enable", false]],
});
info("Ensuring the list of always-translate languages is empty");
is(
getAlwaysTranslateLanguagesFromPref().length,
0,
"The list of always-translate languages is empty"
);
info("Adding two languages to the alwaysTranslateLanguages pref");
Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, "fr,de");
const dialogWindow = await waitForOpenDialogWindow(
"chrome://browser/content/preferences/dialogs/translations.xhtml",
() => {
click(
settingsButton,
"Opening the about:preferences Translations Settings"
);
}
);
let tree = dialogWindow.document.getElementById(
"alwaysTranslateLanguagesTree"
);
let remove = dialogWindow.document.getElementById(
"removeAlwaysTranslateLanguage"
);
let removeAll = dialogWindow.document.getElementById(
"removeAllAlwaysTranslateLanguages"
);
is(
tree.view.rowCount,
2,
"The always-translate languages list has 2 items"
);
ok(remove.disabled, "The 'Remove Language' button is disabled");
ok(!removeAll.disabled, "The 'Remove All Languages' button is enabled");
info("Selecting the first always-translate language.");
tree.view.selection.select(0);
ok(!remove.disabled, "The 'Remove Language' button is enabled");
click(remove, "Clicking the remove-language button");
is(
tree.view.rowCount,
1,
"The always-translate languages list now contains 1 item"
);
is(
getAlwaysTranslateLanguagesFromPref().length,
1,
"One language tag in the pref"
);
info("Removing all languages from the alwaysTranslateLanguages pref");
Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, "");
is(tree.view.rowCount, 0, "The always-translate languages list is empty");
ok(remove.disabled, "The 'Remove Language' button is disabled");
ok(removeAll.disabled, "The 'Remove All Languages' button is disabled");
info("Adding more languages to the alwaysTranslateLanguages pref");
Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, "fr,en,es");
is(
tree.view.rowCount,
3,
"The always-translate languages list has 3 items"
);
ok(remove.disabled, "The 'Remove Language' button is disabled");
ok(!removeAll.disabled, "The 'Remove All Languages' button is enabled");
click(removeAll, "Clicking the remove-all languages button");
is(tree.view.rowCount, 0, "The always-translate languages list is empty");
ok(remove.disabled, "The 'Remove Language' button is disabled");
ok(removeAll.disabled, "The 'Remove All Languages' button is disabled");
is(
getAlwaysTranslateLanguagesFromPref().length,
0,
"There are no languages in the alwaysTranslateLanguages pref"
);
await waitForCloseDialogWindow(dialogWindow);
await cleanup();
}
);

View File

@@ -0,0 +1,159 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(async function test_translations_settings_download_languages_all() {
const {
cleanup,
remoteClients,
elements: { settingsButton },
} = await setupAboutPreferences(LANGUAGE_PAIRS, {
prefs: [["browser.translations.newSettingsUI.enable", true]],
});
const frenchModels = languageModelNames([
{ fromLang: "fr", toLang: "en" },
{ fromLang: "en", toLang: "fr" },
]);
const spanishModels = languageModelNames([
{ fromLang: "es", toLang: "en" },
{ fromLang: "en", toLang: "es" },
]);
const ukrainianModels = languageModelNames([
{ fromLang: "uk", toLang: "en" },
{ fromLang: "en", toLang: "uk" },
]);
assertVisibility({
message: "Expect paneGeneral elements to be visible.",
visible: { settingsButton },
});
info(
"Open translations settings page by clicking on translations settings button."
);
const { downloadLanguageList } =
await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane(
settingsButton
);
info(
"Install each language French, Spanish and Ukrainian and check if All language state changes to 'all language downloaded' by changing the all language button icon to 'remove icon'"
);
info("Download French language model.");
let langFr = Array.from(downloadLanguageList.querySelectorAll("label")).find(
el => el.getAttribute("value") === "fr"
);
let clickButton = BrowserTestUtils.waitForEvent(
langFr.parentNode.querySelector("moz-button"),
"click"
);
langFr.parentNode.querySelector("moz-button").click();
await clickButton;
Assert.deepEqual(
await remoteClients.translationModels.resolvePendingDownloads(
frenchModels.length
),
frenchModels,
"French models were downloaded."
);
await TranslationsSettingsTestUtils.downaloadButtonClick(
langFr,
"translations-settings-remove-icon",
"Delete icon is visible for French language hence downloaded"
);
info("Download Spanish language model.");
let langEs = Array.from(downloadLanguageList.querySelectorAll("label")).find(
el => el.getAttribute("value") === "es"
);
clickButton = BrowserTestUtils.waitForEvent(
langEs.parentNode.querySelector("moz-button"),
"click"
);
langEs.parentNode.querySelector("moz-button").click();
await clickButton;
Assert.deepEqual(
await remoteClients.translationModels.resolvePendingDownloads(
spanishModels.length
),
spanishModels,
"Spanish models were downloaded."
);
await TranslationsSettingsTestUtils.downaloadButtonClick(
langEs,
"translations-settings-remove-icon",
"Delete icon is visible for Spanish language hence downloaded"
);
info("Download Ukrainian language model.");
let langUk = Array.from(downloadLanguageList.querySelectorAll("label")).find(
el => el.getAttribute("value") === "uk"
);
clickButton = BrowserTestUtils.waitForEvent(
langUk.parentNode.querySelector("moz-button"),
"click"
);
langUk.parentNode.querySelector("moz-button").click();
await clickButton;
Assert.deepEqual(
await remoteClients.translationModels.resolvePendingDownloads(
ukrainianModels.length
),
ukrainianModels,
"Ukrainian models were downloaded."
);
await TranslationsSettingsTestUtils.downaloadButtonClick(
langUk,
"translations-settings-remove-icon",
"Delete icon is visible for Ukranian language hence downloaded."
);
// Download "All languages" is the first child
let langAll = downloadLanguageList.children[0];
ok(
langAll
.querySelector("moz-button")
.classList.contains("translations-settings-remove-icon"),
"Delete icon is visible for All Languages after all individual language models were downloaded."
);
info(
"Remove one language ensure that All Languages change state changes to 'removed' to indicate that all languages are not downloaded."
);
info("Remove Spanish language model.");
langEs.parentNode.querySelector("moz-button").click();
await clickButton;
await TranslationsSettingsTestUtils.downaloadButtonClick(
langEs,
"translations-settings-download-icon",
"Download icon is visible for Spanish language hence removed"
);
ok(
langAll
.querySelector("moz-button")
.classList.contains("translations-settings-download-icon"),
"Download icon is visible for all languages i.e. all languages are not downloaded since one language, Spanish was removed."
);
await cleanup();
});

View File

@@ -0,0 +1,173 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(
async function test_translations_settings_download_languages_error_handling() {
const {
cleanup,
remoteClients,
elements: { settingsButton },
} = await setupAboutPreferences(LANGUAGE_PAIRS, {
prefs: [["browser.translations.newSettingsUI.enable", true]],
});
const frenchModels = languageModelNames([
{ fromLang: "fr", toLang: "en" },
{ fromLang: "en", toLang: "fr" },
]);
const spanishModels = languageModelNames([
{ fromLang: "es", toLang: "en" },
{ fromLang: "en", toLang: "es" },
]);
const allModels = languageModelNames(LANGUAGE_PAIRS);
assertVisibility({
message: "Expect paneGeneral elements to be visible.",
visible: { settingsButton },
});
info(
"Open translations settings page by clicking on translations settings button."
);
const { downloadLanguageList } =
await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane(
settingsButton
);
info("Test French language model for download error");
let langFr = Array.from(
downloadLanguageList.querySelectorAll("label")
).find(el => el.getAttribute("value") === "fr");
let clickButton = BrowserTestUtils.waitForEvent(
langFr.parentNode.querySelector("moz-button"),
"click"
);
langFr.parentNode.querySelector("moz-button").click();
await clickButton;
await captureTranslationsError(() =>
remoteClients.translationModels.rejectPendingDownloads(
frenchModels.length
)
);
const errorElement = gBrowser.selectedBrowser.contentDocument.querySelector(
".translations-settings-language-error"
);
assertVisibility({
message: "Moz-message-bar with error message is visible",
visible: { errorElement },
});
is(
document.l10n.getAttributes(errorElement).id,
"translations-settings-language-download-error",
"Error message correctly shows download error"
);
is(
document.l10n.getAttributes(errorElement).args.name,
"French",
"Error message correctly shows download error for French language"
);
await TranslationsSettingsTestUtils.downaloadButtonClick(
langFr,
"translations-settings-download-icon",
"Download icon is visible on French button"
);
remoteClients.translationsWasm.assertNoNewDownloads();
info("Download Spanish language model successfully.");
let langEs = Array.from(
downloadLanguageList.querySelectorAll("label")
).find(el => el.getAttribute("value") === "es");
clickButton = BrowserTestUtils.waitForEvent(
langEs.parentNode.querySelector("moz-button"),
"click"
);
langEs.parentNode.querySelector("moz-button").click();
await clickButton;
const errorElementEs =
gBrowser.selectedBrowser.contentDocument.querySelector(
".translations-settings-language-error"
);
ok(
!errorElementEs,
"Previous error is remove when new action occured, i.e. click download Spanish button"
);
Assert.deepEqual(
await remoteClients.translationModels.resolvePendingDownloads(
spanishModels.length
),
spanishModels,
"Spanish models were downloaded."
);
await TranslationsSettingsTestUtils.downaloadButtonClick(
langEs,
"translations-settings-remove-icon",
"Delete icon is visible for Spanish language hence downloaded"
);
info("Test All language models download error");
// Download "All languages" is the first child
let langAll = downloadLanguageList.children[0];
let clickButtonAll = BrowserTestUtils.waitForEvent(
langAll.querySelector("moz-button"),
"click"
);
langAll.querySelector("moz-button").click();
await clickButtonAll;
await captureTranslationsError(() =>
remoteClients.translationModels.rejectPendingDownloads(allModels.length)
);
await captureTranslationsError(() =>
remoteClients.translationsWasm.rejectPendingDownloads(allModels.length)
);
remoteClients.translationsWasm.assertNoNewDownloads();
await TranslationsSettingsTestUtils.downaloadButtonClick(
langAll,
"translations-settings-download-icon",
"Download icon is visible for 'all languages'"
);
const errorElementAll =
gBrowser.selectedBrowser.contentDocument.querySelector(
".translations-settings-language-error"
);
assertVisibility({
message: "Moz-message-bar with error message is visible",
visible: { errorElementAll },
});
is(
document.l10n.getAttributes(errorElementAll).id,
"translations-settings-language-download-error",
"Error message correctly shows download error"
);
is(
document.l10n.getAttributes(errorElementAll).args.name,
"all",
"Error message correctly shows download error for all language"
);
await cleanup();
}
);

View File

@@ -0,0 +1,117 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(async function test_translations_settings_download_languages() {
await testWithAndWithoutLexicalShortlist(async lexicalShortlistPrefs => {
const {
cleanup,
remoteClients,
elements: { settingsButton },
} = await setupAboutPreferences(LANGUAGE_PAIRS, {
prefs: [
["browser.translations.newSettingsUI.enable", true],
...lexicalShortlistPrefs,
],
});
assertVisibility({
message: "Expect paneGeneral elements to be visible.",
visible: { settingsButton },
});
info(
"Open translations settings page by clicking on translations settings button."
);
const { downloadLanguageList } =
await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane(
settingsButton
);
info("Test French language model install and uninstall function.");
let langFr = Array.from(
downloadLanguageList.querySelectorAll("label")
).find(el => el.getAttribute("value") === "fr");
let clickButton = BrowserTestUtils.waitForEvent(
langFr.parentNode.querySelector("moz-button"),
"click"
);
langFr.parentNode.querySelector("moz-button").click();
await clickButton;
const frenchModels = languageModelNames([
{ fromLang: "fr", toLang: "en" },
{ fromLang: "en", toLang: "fr" },
]);
Assert.deepEqual(
await remoteClients.translationModels.resolvePendingDownloads(
frenchModels.length
),
frenchModels,
"French models were downloaded."
);
await TranslationsSettingsTestUtils.downaloadButtonClick(
langFr,
"translations-settings-remove-icon",
"Delete icon is visible on French button."
);
langFr.parentNode.querySelector("moz-button").click();
await clickButton;
await TranslationsSettingsTestUtils.downaloadButtonClick(
langFr,
"translations-settings-download-icon",
"Download icon is visible on French Button."
);
info("Test 'All language' models install and uninstall function");
// Download "All languages" is the first child
let langAll = downloadLanguageList.children[0];
let clickButtonAll = BrowserTestUtils.waitForEvent(
langAll.querySelector("moz-button"),
"click"
);
langAll.querySelector("moz-button").click();
await clickButtonAll;
const allModels = languageModelNames(LANGUAGE_PAIRS);
Assert.deepEqual(
await remoteClients.translationModels.resolvePendingDownloads(
allModels.length
),
allModels,
"All models were downloaded."
);
Assert.deepEqual(
await remoteClients.translationsWasm.resolvePendingDownloads(1),
["bergamot-translator"],
"Wasm was downloaded."
);
await TranslationsSettingsTestUtils.downaloadButtonClick(
langAll,
"translations-settings-remove-icon",
"Delete icon is visible on 'All languages' button"
);
langAll.querySelector("moz-button").click();
await clickButton;
await TranslationsSettingsTestUtils.downaloadButtonClick(
langAll,
"translations-settings-download-icon",
"Download icon is visible on 'All Language' button."
);
await cleanup();
});
});

View File

@@ -0,0 +1,89 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(
async function test_about_preferences_never_translate_language_settings() {
const {
cleanup,
elements: { settingsButton },
} = await setupAboutPreferences(LANGUAGE_PAIRS, {
prefs: [["browser.translations.newSettingsUI.enable", false]],
});
info("Ensuring the list of never-translate languages is empty");
is(
getNeverTranslateLanguagesFromPref().length,
0,
"The list of never-translate languages is empty"
);
info("Adding two languages to the neverTranslateLanguages pref");
Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, "fr,de");
const dialogWindow = await waitForOpenDialogWindow(
"chrome://browser/content/preferences/dialogs/translations.xhtml",
() => {
click(
settingsButton,
"Opening the about:preferences Translations Settings"
);
}
);
let tree = dialogWindow.document.getElementById(
"neverTranslateLanguagesTree"
);
let remove = dialogWindow.document.getElementById(
"removeNeverTranslateLanguage"
);
let removeAll = dialogWindow.document.getElementById(
"removeAllNeverTranslateLanguages"
);
is(tree.view.rowCount, 2, "The never-translate languages list has 2 items");
ok(remove.disabled, "The 'Remove Language' button is disabled");
ok(!removeAll.disabled, "The 'Remove All Languages' button is enabled");
info("Selecting the first never-translate language.");
tree.view.selection.select(0);
ok(!remove.disabled, "The 'Remove Language' button is enabled");
click(remove, "Clicking the remove-language button");
is(
tree.view.rowCount,
1,
"The never-translate languages list now contains 1 item"
);
is(
getNeverTranslateLanguagesFromPref().length,
1,
"One language tag in the pref"
);
info("Removing all languages from the neverTranslateLanguages pref");
Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, "");
is(tree.view.rowCount, 0, "The never-translate languages list is empty");
ok(remove.disabled, "The 'Remove Language' button is disabled");
ok(removeAll.disabled, "The 'Remove All Languages' button is disabled");
info("Adding more languages to the neverTranslateLanguages pref");
Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, "fr,en,es");
is(tree.view.rowCount, 3, "The never-translate languages list has 3 items");
ok(remove.disabled, "The 'Remove Language' button is disabled");
ok(!removeAll.disabled, "The 'Remove All Languages' button is enabled");
click(removeAll, "Clicking the remove-all languages button");
is(tree.view.rowCount, 0, "The never-translate languages list is empty");
ok(remove.disabled, "The 'Remove Language' button is disabled");
ok(removeAll.disabled, "The 'Remove All Languages' button is disabled");
is(
getNeverTranslateLanguagesFromPref().length,
0,
"There are no languages in the neverTranslateLanguages pref"
);
await waitForCloseDialogWindow(dialogWindow);
await cleanup();
}
);

View File

@@ -0,0 +1,124 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { PermissionTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/PermissionTestUtils.sys.mjs"
);
add_task(async function test_about_preferences_never_translate_site_settings() {
const {
cleanup,
elements: { settingsButton },
} = await setupAboutPreferences(LANGUAGE_PAIRS, {
prefs: [["browser.translations.newSettingsUI.enable", false]],
permissionsUrls: [
"https://example.com",
"https://example.org",
"https://example.net",
],
});
info("Ensuring the list of never-translate sites is empty");
is(
getNeverTranslateSitesFromPerms().length,
0,
"The list of never-translate sites is empty"
);
info("Adding two sites to the neverTranslateSites perms");
PermissionTestUtils.add(
"https://example.com",
TRANSLATIONS_PERMISSION,
Services.perms.DENY_ACTION
);
PermissionTestUtils.add(
"https://example.org",
TRANSLATIONS_PERMISSION,
Services.perms.DENY_ACTION
);
PermissionTestUtils.add(
"https://example.net",
TRANSLATIONS_PERMISSION,
Services.perms.DENY_ACTION
);
const dialogWindow = await waitForOpenDialogWindow(
"chrome://browser/content/preferences/dialogs/translations.xhtml",
() => {
click(
settingsButton,
"Opening the about:preferences Translations Settings"
);
}
);
let tree = dialogWindow.document.getElementById("neverTranslateSitesTree");
let remove = dialogWindow.document.getElementById("removeNeverTranslateSite");
let removeAll = dialogWindow.document.getElementById(
"removeAllNeverTranslateSites"
);
is(tree.view.rowCount, 3, "The never-translate sites list has 2 items");
ok(remove.disabled, "The 'Remove Site' button is disabled");
ok(!removeAll.disabled, "The 'Remove All Sites' button is enabled");
info("Selecting the first never-translate site.");
tree.view.selection.select(0);
ok(!remove.disabled, "The 'Remove Site' button is enabled");
click(remove, "Clicking the remove-site button");
is(
tree.view.rowCount,
2,
"The never-translate sites list now contains 2 items"
);
is(
getNeverTranslateSitesFromPerms().length,
2,
"There are 2 sites with permissions"
);
info("Removing all sites from the neverTranslateSites perms");
PermissionTestUtils.remove("https://example.com", TRANSLATIONS_PERMISSION);
PermissionTestUtils.remove("https://example.org", TRANSLATIONS_PERMISSION);
PermissionTestUtils.remove("https://example.net", TRANSLATIONS_PERMISSION);
is(tree.view.rowCount, 0, "The never-translate sites list is empty");
ok(remove.disabled, "The 'Remove Site' button is disabled");
ok(removeAll.disabled, "The 'Remove All Sites' button is disabled");
info("Adding more sites to the neverTranslateSites perms");
PermissionTestUtils.add(
"https://example.org",
TRANSLATIONS_PERMISSION,
Services.perms.DENY_ACTION
);
PermissionTestUtils.add(
"https://example.com",
TRANSLATIONS_PERMISSION,
Services.perms.DENY_ACTION
);
PermissionTestUtils.add(
"https://example.net",
TRANSLATIONS_PERMISSION,
Services.perms.DENY_ACTION
);
is(tree.view.rowCount, 3, "The never-translate sites list has 3 items");
ok(remove.disabled, "The 'Remove Site' button is disabled");
ok(!removeAll.disabled, "The 'Remove All Sites' button is enabled");
click(removeAll, "Clicking the remove-all sites button");
is(tree.view.rowCount, 0, "The never-translate sites list is empty");
ok(remove.disabled, "The 'Remove Site' button is disabled");
ok(removeAll.disabled, "The 'Remove All Sites' button is disabled");
is(
getNeverTranslateSitesFromPerms().length,
0,
"There are no sites in the neverTranslateSites perms"
);
await waitForCloseDialogWindow(dialogWindow);
await cleanup();
});

View File

@@ -0,0 +1,462 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(async function test_translations_settings_pane_elements() {
const {
cleanup,
elements: { settingsButton },
} = await setupAboutPreferences(LANGUAGE_PAIRS, {
prefs: [["browser.translations.newSettingsUI.enable", true]],
});
assertVisibility({
message: "Expect paneGeneral elements to be visible.",
visible: { settingsButton },
});
info(
"Open translations settings page by clicking on translations settings button."
);
const {
translationsSettingsBackButton,
translationsSettingsHeader,
translationsSettingsDescription,
translateAlwaysHeader,
translateNeverHeader,
alwaysTranslateMenuList,
neverTranslateMenuList,
translateNeverSiteHeader,
translateNeverSiteDesc,
downloadLanguageSection,
translateDownloadLanguagesLearnMore,
} =
await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane(
settingsButton
);
const translateDownloadLanguagesHeader =
downloadLanguageSection.querySelector("h2");
assertVisibility({
message: "Expect paneTranslations elements to be visible.",
visible: {
translationsSettingsBackButton,
translationsSettingsHeader,
translationsSettingsDescription,
translateAlwaysHeader,
translateNeverHeader,
alwaysTranslateMenuList,
neverTranslateMenuList,
translateNeverSiteHeader,
translateNeverSiteDesc,
translateDownloadLanguagesLearnMore,
},
hidden: {
settingsButton,
},
});
info(
"In translations settings page, click on back button to go back to main preferences page."
);
const paneEvent = BrowserTestUtils.waitForEvent(
document,
"paneshown",
false,
event => event.detail.category === "paneGeneral"
);
click(translationsSettingsBackButton);
await paneEvent;
assertVisibility({
message: "Expect paneGeneral elements to be visible.",
visible: {
settingsButton,
},
hidden: {
translationsSettingsBackButton,
translationsSettingsHeader,
translationsSettingsDescription,
translateAlwaysHeader,
translateNeverHeader,
alwaysTranslateMenuList,
neverTranslateMenuList,
translateNeverSiteHeader,
translateNeverSiteDesc,
translateDownloadLanguagesHeader,
translateDownloadLanguagesLearnMore,
},
});
await cleanup();
});
add_task(async function test_translations_settings_always_translate() {
const {
cleanup,
elements: { settingsButton },
} = await setupAboutPreferences(LANGUAGE_PAIRS, {
prefs: [["browser.translations.newSettingsUI.enable", true]],
});
assertVisibility({
message: "Expect paneGeneral elements to be visible.",
visible: { settingsButton },
});
info(
"Open translations settings page by clicking on translations settings button."
);
const {
alwaysTranslateMenuList,
alwaysTranslateLanguageList,
alwaysTranslateMenuPopup,
} =
await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane(
settingsButton
);
info("Testing the Always translate langauge settings");
await testLanguageList(
alwaysTranslateLanguageList,
alwaysTranslateMenuList,
alwaysTranslateMenuPopup,
ALWAYS_TRANSLATE_LANGS_PREF,
"Always"
);
await testLanguageListWithPref(
alwaysTranslateLanguageList,
ALWAYS_TRANSLATE_LANGS_PREF,
"Always"
);
await cleanup();
});
add_task(async function test_translations_settings_never_translate() {
const {
cleanup,
elements: { settingsButton },
} = await setupAboutPreferences(LANGUAGE_PAIRS, {
prefs: [["browser.translations.newSettingsUI.enable", true]],
});
info(
"Open translations settings page by clicking on translations settings button."
);
assertVisibility({
message: "Expect paneGeneral elements to be visible.",
visible: { settingsButton },
});
const {
neverTranslateMenuList,
neverTranslateLanguageList,
neverTranslateMenuPopup,
} =
await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane(
settingsButton
);
info("Testing the Never translate langauge settings");
await testLanguageList(
neverTranslateLanguageList,
neverTranslateMenuList,
neverTranslateMenuPopup,
NEVER_TRANSLATE_LANGS_PREF,
"Never"
);
await testLanguageListWithPref(
neverTranslateLanguageList,
NEVER_TRANSLATE_LANGS_PREF,
"Never"
);
await cleanup();
});
function getLangsFromPref(pref) {
let rawLangs = Services.prefs.getCharPref(pref);
if (!rawLangs) {
return [];
}
let langArr = rawLangs.split(",");
return langArr;
}
async function testLanguageList(
languageList,
menuList,
menuPopup,
pref,
sectionName
) {
info("Ensure the Always/Never list is empty initially.");
is(
languageList.childElementCount,
0,
`Language list empty in ${sectionName} Translate list`
);
const menuItems = menuPopup.children;
info(
"Click each language on the menulist to add it into the Always/Never list."
);
for (const menuItem of menuItems) {
menuList.open = true;
let clickMenu = BrowserTestUtils.waitForEvent(
menuList.querySelector("menupopup"),
"popuphidden"
);
click(menuItem);
menuList.querySelector("menupopup").hidePopup();
await clickMenu;
/** Languages are always added on the top, so check the firstChild
* for newly added languages.
* the firstChild.querySelector("label").innerText is the language display name
* which is compared with the menulist display name that is selected
*/
let langElem = languageList.firstElementChild;
const displayName = getIntlDisplayName(menuItem.value);
is(
langElem.querySelector("label").innerText,
displayName,
`Language list has element ${displayName}`
);
const langTag = langElem.querySelector("label").getAttribute("value");
ok(
getLangsFromPref(pref).includes(langTag),
`Perferences contains ${langTag}`
);
}
/** The test cases has 4 languages, so check if 4 languages are added to the list */
let langNum = languageList.childElementCount;
is(langNum, 4, "Number of languages added is 4");
info(
"Remove each language from the Always/Never list that we added initially."
);
for (let i = 0; i < langNum; i++) {
// Delete the first language in the list
let langElem = languageList.children[0];
let langName = langElem.querySelector("label").innerText;
const langTag = langElem.querySelector("label").getAttribute("value");
let langButton = langElem.querySelector("moz-button");
let clickButton = BrowserTestUtils.waitForEvent(langButton, "click");
langButton.click();
await clickButton;
ok(
!getLangsFromPref(pref).includes(langTag),
`Perferences does not contain ${langTag}`
);
if (i < langNum) {
is(
languageList.childElementCount,
langNum - i - 1,
`${langName} removed from ${sectionName} Translate`
);
}
}
}
async function testLanguageListWithPref(languageList, pref, sectionName) {
const langs = [
"fr",
"de",
"en",
"es",
"fr,de",
"fr,en",
"fr,es",
"de,en",
"de,en,es",
"es,fr,en",
"en,es,fr,de",
];
info("Ensure the Always/Never list is empty initially.");
is(
languageList.childElementCount,
0,
`Language list is empty in ${sectionName} Translate list`
);
info(
"Add languages to the Always/Never list in translations setting by setting the ALWAYS_TRANSLATE_LANGS_PREF/NEVER_TRANSLATE_LANGS_PREF."
);
for (const langOptions of langs) {
Services.prefs.setCharPref(pref, langOptions);
/** Languages are always added on the top, so check the firstChild
* for newly added languages.
* the firstChild.querySelector("label").innerText is the language display name
* which is compared with the menulist display name that is selected
*/
const langsAdded = langOptions.split(",");
is(
languageList.childElementCount,
langsAdded.length,
`Language list has ${langsAdded.length} elements `
);
let langsAddedHtml = Array.from(languageList.querySelectorAll("label"));
for (const lang of langsAdded) {
const langFind = langsAddedHtml
.find(el => el.getAttribute("value") === lang)
.getAttribute("value");
is(langFind, lang, `Language list has element ${lang}`);
}
}
Services.prefs.setCharPref(pref, "");
is(
languageList.childElementCount,
0,
`All removed from ${sectionName} Translate`
);
}
const { PermissionTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/PermissionTestUtils.sys.mjs"
);
add_task(async function test_translations_settings_never_translate_site() {
const {
cleanup,
elements: { settingsButton },
} = await setupAboutPreferences(LANGUAGE_PAIRS, {
prefs: [["browser.translations.newSettingsUI.enable", true]],
});
info(
"Open translations settings page by clicking on translations settings button."
);
const { neverTranslateSiteList } =
await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane(
settingsButton
);
info("Ensuring the list of never-translate sites is empty");
is(
getNeverTranslateSitesFromPerms().length,
0,
"The list of never-translate sites is empty"
);
is(
neverTranslateSiteList.childElementCount,
0,
"The never-translate sites html list is empty"
);
info("Adding sites to the neverTranslateSites perms");
await PermissionTestUtils.add(
"https://example.com",
TRANSLATIONS_PERMISSION,
Services.perms.DENY_ACTION
);
await PermissionTestUtils.add(
"https://example.org",
TRANSLATIONS_PERMISSION,
Services.perms.DENY_ACTION
);
await PermissionTestUtils.add(
"https://example.net",
TRANSLATIONS_PERMISSION,
Services.perms.DENY_ACTION
);
is(
getNeverTranslateSitesFromPerms().length,
3,
"The list of never-translate sites has 3 elements"
);
is(
neverTranslateSiteList.childElementCount,
3,
"The never-translate sites html list has 3 elements"
);
const permissionsUrls = [
"https://example.com",
"https://example.org",
"https://example.net",
];
info(
"Ensure that the Never translate sites in permissions settings are reflected in Never translate sites section of translations settings page"
);
const siteNum = neverTranslateSiteList.children.length;
for (let i = siteNum; i > 0; i--) {
is(
neverTranslateSiteList.children[i - 1].querySelector("label").textContent,
permissionsUrls[permissionsUrls.length - i],
`Never translate URL ${
permissionsUrls[permissionsUrls.length - i]
} is added`
);
}
info(
"Delete each site by clicking the button in Never translate sites section of translations settings page and check if it is removed in the Never translate sites in permissions settings"
);
for (let i = 0; i < siteNum; i++) {
// Delete the first site in the list
let siteElem = neverTranslateSiteList.children[0];
// Delete the first language in the list
let siteName = siteElem.querySelector("label").innerText;
let siteButton = siteElem.querySelector("moz-button");
ok(
neverTranslateSiteList.querySelector(`label[value="${siteName}"]`),
`Site ${siteName} present in the Never transalate site list`
);
ok(
getNeverTranslateSitesFromPerms().find(p => p.origin === siteName),
`Site ${siteName} present in the Never transalate site permissions list`
);
let clickButton = BrowserTestUtils.waitForEvent(siteButton, "click");
siteButton.click();
await clickButton;
ok(
!neverTranslateSiteList.querySelector(`label[value="${siteName}"]`),
`Site ${siteName} removed successfully from the Never transalate site list`
);
ok(
!getNeverTranslateSitesFromPerms().find(p => p.origin === siteName),
`Site ${siteName} removed from successfully from the Never transalate site permissions list`
);
if (i < siteNum) {
is(
neverTranslateSiteList.childElementCount,
siteNum - i - 1,
`${siteName} removed from Never Translate Site`
);
}
const siteLen = siteNum - i - 1;
is(
getNeverTranslateSitesFromPerms().length,
siteLen,
`There are ${siteLen} site in Never translate site`
);
}
await cleanup();
});

View File

@@ -0,0 +1,635 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { PermissionTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/PermissionTestUtils.sys.mjs"
);
add_task(async function test_translations_settings_keyboard_a11y() {
const {
cleanup,
elements: { settingsButton },
} = await setupAboutPreferences(LANGUAGE_PAIRS, {
prefs: [["browser.translations.newSettingsUI.enable", true]],
});
info(
"Open translations settings page by clicking on translations settings button."
);
assertVisibility({
message: "Expect paneGeneral elements to be visible.",
visible: { settingsButton },
});
const {
translationsSettingsBackButton,
alwaysTranslateMenuList,
neverTranslateMenuList,
translateDownloadLanguagesLearnMore,
downloadLanguageList,
} =
await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane(
settingsButton
);
const document = gBrowser.selectedBrowser.contentDocument;
info("Press the Tab key to focus the first page element, the back button");
EventUtils.synthesizeKey("KEY_Tab");
is(
document.activeElement.id,
translationsSettingsBackButton.id,
"Key is focused on back button"
);
info(
"Press the Tab key to focus the next page element, the Always Translate Menulist button"
);
EventUtils.synthesizeKey("KEY_Tab");
is(
document.activeElement.id,
alwaysTranslateMenuList.id,
"Key is focused on Always Translate Menulist button"
);
info(
"Press the Tab key to focus the next page element, the Never Translate Menulist button"
);
EventUtils.synthesizeKey("KEY_Tab");
is(
document.activeElement.id,
neverTranslateMenuList.id,
"Key is focused on Never Translate Menulist button"
);
info(
"Press the Tab key to focus the next page element, the Download Languages' Learn More link"
);
EventUtils.synthesizeKey("KEY_Tab");
is(
document.activeElement.id,
translateDownloadLanguagesLearnMore.id,
"Key is focused on Download Languages' Learn More link"
);
info(
"Press the Tab key to focus the next page element, the Download Languages list section"
);
EventUtils.synthesizeKey("KEY_Tab");
is(
document.activeElement.id,
downloadLanguageList.id,
"Key is focused on Download Languages list section"
);
await cleanup();
});
add_task(async function test_translations_settings_keyboard_download_a11y() {
const {
cleanup,
elements: { settingsButton },
} = await setupAboutPreferences(LANGUAGE_PAIRS, {
prefs: [["browser.translations.newSettingsUI.enable", true]],
});
info(
"Open translations settings page by clicking on translations settings button."
);
assertVisibility({
message: "Expect paneGeneral elements to be visible.",
visible: { settingsButton },
});
const {
translationsSettingsBackButton,
alwaysTranslateMenuList,
neverTranslateMenuList,
translateDownloadLanguagesLearnMore,
downloadLanguageList,
} =
await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane(
settingsButton
);
const document = gBrowser.selectedBrowser.contentDocument;
info("Press the Tab key to focus the first page element, the back button");
EventUtils.synthesizeKey("KEY_Tab");
is(
document.activeElement.id,
translationsSettingsBackButton.id,
"Key is focused on back button"
);
info(
"Press the Tab key to focus the next page element, the Always Translate Menulist button"
);
EventUtils.synthesizeKey("KEY_Tab");
is(
document.activeElement.id,
alwaysTranslateMenuList.id,
"Key is focused on Always Translate Menulist button"
);
info(
"Press the Tab key to focus the next page element, the Never Translate Menulist button"
);
EventUtils.synthesizeKey("KEY_Tab");
is(
document.activeElement.id,
neverTranslateMenuList.id,
"Key is focused on Never Translate Menulist button"
);
info(
"Press the Tab key to focus the next page element, the Download Languages' Learn More link"
);
EventUtils.synthesizeKey("KEY_Tab");
is(
document.activeElement.id,
translateDownloadLanguagesLearnMore.id,
"Key is focused on Download Languages' Learn More link"
);
info(
"Press the Tab key to focus the next page element, the Download Languages list section"
);
EventUtils.synthesizeKey("KEY_Tab");
is(
document.activeElement.id,
downloadLanguageList.id,
"Key is focused on Download Languages list section"
);
info(
"Press the Arrow Down key to focus the first language element in the Download List Section"
);
for (let element of downloadLanguageList.children) {
info(
"Press the Arrow Down key to focus the next language element in the Download List Section"
);
EventUtils.synthesizeKey("KEY_ArrowDown");
is(
document.activeElement.parentNode.id,
element.id,
"Key is focused on the language " +
element.querySelector("label").textContent +
" within the language list"
);
}
is(
document.activeElement.parentNode.id,
downloadLanguageList.lastElementChild.id,
"Key is focused on the last language " +
downloadLanguageList.lastElementChild.querySelector("label").textContent +
" within the language list"
);
info(
"Press the Arrow up key to focus the previous language element in the Download List Section"
);
for (let i = downloadLanguageList.children.length - 2; i >= 0; i--) {
info(
"Press the Arrow up key to focus the previous language element in the Download List Section"
);
EventUtils.synthesizeKey("KEY_ArrowUp");
is(
document.activeElement.parentNode.id,
downloadLanguageList.children[i].id,
"Key is focused on the language " +
downloadLanguageList.children[i].querySelector("label").textContent +
" within the language list"
);
}
is(
document.activeElement.parentNode.id,
downloadLanguageList.firstElementChild.id,
"Key is focused on the first language within the language list"
);
await cleanup();
});
add_task(
async function test_translations_settings_keyboard_never_translate_site_a11y() {
const {
cleanup,
elements: { settingsButton },
} = await setupAboutPreferences(LANGUAGE_PAIRS, {
prefs: [["browser.translations.newSettingsUI.enable", true]],
});
info(
"Open translations settings page by clicking on translations settings button."
);
assertVisibility({
message: "Expect paneGeneral elements to be visible.",
visible: { settingsButton },
});
const {
translationsSettingsBackButton,
alwaysTranslateMenuList,
neverTranslateMenuList,
neverTranslateSiteList,
translateDownloadLanguagesLearnMore,
} =
await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane(
settingsButton
);
is(
neverTranslateSiteList.childElementCount,
0,
"The never-translate sites html list is empty"
);
info("Adding sites to the neverTranslateSites perms");
await PermissionTestUtils.add(
"https://example.com",
TRANSLATIONS_PERMISSION,
Services.perms.DENY_ACTION
);
await PermissionTestUtils.add(
"https://example.org",
TRANSLATIONS_PERMISSION,
Services.perms.DENY_ACTION
);
await PermissionTestUtils.add(
"https://example.net",
TRANSLATIONS_PERMISSION,
Services.perms.DENY_ACTION
);
is(
getNeverTranslateSitesFromPerms().length,
3,
"The list of never-translate sites has 3 elements"
);
const document = gBrowser.selectedBrowser.contentDocument;
info("Press the Tab key to focus the first page element, the back button");
EventUtils.synthesizeKey("KEY_Tab");
is(
document.activeElement.id,
translationsSettingsBackButton.id,
"Key is focused on back button"
);
info(
"Press the Tab key to focus the next page element, the Always Translate Menulist button"
);
EventUtils.synthesizeKey("KEY_Tab");
is(
document.activeElement.id,
alwaysTranslateMenuList.id,
"Key is focused on Always Translate Menulist button"
);
info(
"Press the Tab key to focus the next page element, the Never Translate Menulist button"
);
EventUtils.synthesizeKey("KEY_Tab");
is(
document.activeElement.id,
neverTranslateMenuList.id,
"Key focus is now Never Translate List Menu button"
);
info(
"Press the Tab key to focus the next page element, the Never Translate Site List section"
);
EventUtils.synthesizeKey("KEY_Tab");
is(
document.activeElement.id,
neverTranslateSiteList.id,
"Key focus is now Never Translate Site List"
);
info(
"Press the Arrow Down key to focus the first site element in the Never Translate Site List"
);
for (const site of neverTranslateSiteList.children) {
info(
"Press the Arrow Down key to focus the next site element in the Never Translate Site List"
);
EventUtils.synthesizeKey("KEY_ArrowDown");
is(
document.activeElement.parentNode.id,
site.id,
"Key focus is now Never Translate Site list element " +
site.querySelector("label").textContent
);
}
is(
document.activeElement.parentNode.id,
neverTranslateSiteList.lastElementChild.id,
"Key is focused on the last site " +
neverTranslateSiteList.lastElementChild.querySelector("label")
.textContent +
" within the site list"
);
info(
"Press the Arrow up key to focus the previous site element in the Never Translate Site List"
);
for (let i = neverTranslateSiteList.children.length - 2; i >= 0; i--) {
info(
"Press the Arrow up key to focus the previous site element in the Never Translate Site List"
);
EventUtils.synthesizeKey("KEY_ArrowUp");
is(
document.activeElement.parentNode.id,
neverTranslateSiteList.children[i].id,
"Key is focused on the site " +
neverTranslateSiteList.children[i].querySelector("label")
.textContent +
" within the site list"
);
}
info(
"Press the Tab key to focus the next page element, the Download Languages' Learn More link"
);
EventUtils.synthesizeKey("KEY_Tab");
is(
document.activeElement.id,
translateDownloadLanguagesLearnMore.id,
"Key is focused on Download Languages' Learn More link"
);
await cleanup();
}
);
add_task(
async function test_translations_settings_keyboard_never_translate_a11y() {
const {
cleanup,
elements: { settingsButton },
} = await setupAboutPreferences(LANGUAGE_PAIRS, {
prefs: [["browser.translations.newSettingsUI.enable", true]],
});
info(
"Open translations settings page by clicking on translations settings button."
);
assertVisibility({
message: "Expect paneGeneral elements to be visible.",
visible: { settingsButton },
});
const {
translationsSettingsBackButton,
alwaysTranslateMenuList,
neverTranslateMenuList,
neverTranslateLanguageList,
neverTranslateMenuPopup,
translateDownloadLanguagesLearnMore,
} =
await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane(
settingsButton
);
const document = gBrowser.selectedBrowser.contentDocument;
info("Press the Tab key to focus the first page element, the back button");
EventUtils.synthesizeKey("KEY_Tab");
is(
document.activeElement.id,
translationsSettingsBackButton.id,
"Key is focused on back button"
);
info(
"Press the Tab key to focus the next page element, the Always Translate Menulist button"
);
EventUtils.synthesizeKey("KEY_Tab");
is(
document.activeElement.id,
alwaysTranslateMenuList.id,
"Key is focused on Always Translate Menulist button"
);
info(
"Press the Tab key to focus the next page element, the Never Translate Menulist button."
);
EventUtils.synthesizeKey("KEY_Tab");
is(
document.activeElement.id,
neverTranslateMenuList.id,
"Key is focused on Never Translate Menulist button"
);
info("Press the Arrow Down key to focus on the first list element.");
for (const menuItem of neverTranslateMenuPopup.children) {
if (AppConstants.platform === "macosx") {
info("Opening the menu popup.");
const popupPromise = BrowserTestUtils.waitForEvent(
neverTranslateMenuPopup,
"popupshown"
);
EventUtils.synthesizeKey("KEY_ArrowDown");
await popupPromise;
}
EventUtils.synthesizeKey("KEY_ArrowDown");
if (AppConstants.platform === "macosx") {
info("Closing the menu popup.");
const popupPromise = BrowserTestUtils.waitForEvent(
neverTranslateMenuPopup,
"popuphidden"
);
EventUtils.synthesizeKey("KEY_Enter");
await popupPromise;
} else {
const { promise, resolve } = Promise.withResolvers();
requestAnimationFrame(() => {
requestAnimationFrame(resolve);
});
EventUtils.synthesizeKey("KEY_Enter");
await promise;
}
is(
neverTranslateLanguageList.firstElementChild.querySelector("label")
.textContent,
menuItem.textContent,
menuItem.textContent + "is added to never translate language"
);
}
info(
"Press the Tab key to focus the next page element, the Never Translate list"
);
EventUtils.synthesizeKey("KEY_Tab");
is(
neverTranslateLanguageList.id,
document.activeElement.id,
"Key is focused on Always Translate list."
);
info("Press the Arrow Down key to focus on the first list element.");
for (const lang of neverTranslateLanguageList.children) {
EventUtils.synthesizeKey("KEY_ArrowDown");
is(
document.activeElement.parentNode.id,
lang.id,
"Key is focused on " +
lang.querySelector("label").textContent +
" element of Never Translate list."
);
}
info(
"Press the Tab key to focus the next page element, the Download Languages' Learn More link"
);
EventUtils.synthesizeKey("KEY_Tab");
is(
document.activeElement.id,
translateDownloadLanguagesLearnMore.id,
"Key is focused on Download Languages' Learn More link"
);
await cleanup();
}
);
add_task(
async function test_translations_settings_keyboard_always_translate_a11y() {
const {
cleanup,
elements: { settingsButton },
} = await setupAboutPreferences(LANGUAGE_PAIRS, {
prefs: [["browser.translations.newSettingsUI.enable", true]],
});
info(
"Open translations settings page by clicking on translations settings button."
);
assertVisibility({
message: "Expect paneGeneral elements to be visible.",
visible: { settingsButton },
});
const {
translationsSettingsBackButton,
alwaysTranslateMenuList,
neverTranslateMenuList,
alwaysTranslateLanguageList,
alwaysTranslateMenuPopup,
} =
await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane(
settingsButton
);
const document = gBrowser.selectedBrowser.contentDocument;
info("Press the Tab key to focus the first page element, the back button");
EventUtils.synthesizeKey("KEY_Tab");
is(
document.activeElement.id,
translationsSettingsBackButton.id,
"Key is focused on back button"
);
info(
"Press the Tab key to focus the next page element, the Always Translate Menulist button"
);
EventUtils.synthesizeKey("KEY_Tab");
is(
document.activeElement.id,
alwaysTranslateMenuList.id,
"Key is focused on Always Translate Menulist button"
);
info("Press the Arrow Down key to focus on the first list element.");
for (const menuItem of alwaysTranslateMenuPopup.children) {
if (AppConstants.platform === "macosx") {
info("Opening the menu popup.");
const popupPromise = BrowserTestUtils.waitForEvent(
alwaysTranslateMenuPopup,
"popupshown"
);
EventUtils.synthesizeKey("KEY_ArrowDown");
await popupPromise;
}
EventUtils.synthesizeKey("KEY_ArrowDown");
if (AppConstants.platform === "macosx") {
info("Closing the menu popup.");
const popupPromise = BrowserTestUtils.waitForEvent(
alwaysTranslateMenuPopup,
"popuphidden"
);
EventUtils.synthesizeKey("KEY_Enter");
await popupPromise;
} else {
const { promise, resolve } = Promise.withResolvers();
requestAnimationFrame(() => {
requestAnimationFrame(resolve);
});
EventUtils.synthesizeKey("KEY_Enter");
await promise;
}
is(
alwaysTranslateLanguageList.firstElementChild.querySelector("label")
.textContent,
menuItem.textContent,
menuItem.textContent + "is added to always translate language"
);
}
info(
"Press the Tab key to focus the next page element, the Always Translate list"
);
EventUtils.synthesizeKey("KEY_Tab");
is(
alwaysTranslateLanguageList.id,
document.activeElement.id,
"Key is focused on Always Translate list."
);
info("Press the Arrow Down key to focus on the first list element.");
for (const lang of alwaysTranslateLanguageList.children) {
EventUtils.synthesizeKey("KEY_ArrowDown");
is(
document.activeElement.parentNode.id,
lang.id,
"Key is focused on " +
lang.querySelector("label").textContent +
" element of Always Translate list."
);
}
info(
"Press the Tab key to focus the next page element, the Never Translate list"
);
EventUtils.synthesizeKey("KEY_Tab");
is(
document.activeElement.id,
neverTranslateMenuList.id,
"Key focus is now Never Translate List Menu button"
);
await cleanup();
}
);

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