diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml index be6c53120..e88a8ca6a 100644 --- a/.github/workflows/sync-upstream.yml +++ b/.github/workflows/sync-upstream.yml @@ -95,6 +95,10 @@ jobs: echo "Checking if patches apply cleanly..." npm run import + - name: Import external tests + if: steps.git-check.outputs.files_changed == 'true' + run: python3 scripts/import_external_tests.py + - name: Create pull request uses: peter-evans/create-pull-request@v7 if: steps.git-check.outputs.files_changed == 'true' diff --git a/.prettierignore b/.prettierignore index af7e85d38..df0ad852b 100644 --- a/.prettierignore +++ b/.prettierignore @@ -17,6 +17,8 @@ engine/ surfer.json +src/zen/tests/mochitests/* + src/browser/app/profile/*.js pnpm-lock.yaml diff --git a/eslint.config.mjs b/eslint.config.mjs index 6353523ce..19ed43515 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -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/**']), ]); diff --git a/locales/en-US/browser/browser/preferences/zen-preferences.ftl b/locales/en-US/browser/browser/preferences/zen-preferences.ftl index 2941a5157..ab910a806 100644 --- a/locales/en-US/browser/browser/preferences/zen-preferences.ftl +++ b/locales/en-US/browser/browser/preferences/zen-preferences.ftl @@ -191,6 +191,9 @@ category-zen-CKS = .tooltiptext = { pane-zen-CKS-title } pane-settings-CKS-title = { -brand-short-name } Keyboard Shortcuts +category-zen-marketplace = + .tooltiptext = Zen Mods + zen-settings-CKS-header = Customize your keyboard shortcuts zen-settings-CKS-description = Change the default keyboard shortcuts to your liking and improve your browsing experience diff --git a/scripts/import_external_tests.py b/scripts/import_external_tests.py new file mode 100644 index 000000000..535686c61 --- /dev/null +++ b/scripts/import_external_tests.py @@ -0,0 +1,124 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import tomllib +import shutil + +BASE_PATH = os.path.join("src", "zen", "tests") +EXTERNAL_TESTS_MANIFEST = os.path.join(BASE_PATH, "manifest.toml") +EXTERNAL_TESTS_OUTPUT = os.path.join(BASE_PATH, "mochitests") + +FILE_PREFIX = """ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# This file is autogenerated by scripts/import_external_tests.py +# Do not edit manually. + +BROWSER_CHROME_MANIFESTS += [ +""" + +FILE_SUFFIX = "]" + + +def get_tests_manifest(): + with open(EXTERNAL_TESTS_MANIFEST, "rb") as f: + return tomllib.load(f) + + +def die_with_error(message): + print(f"ERROR: {message}") + exit(1) + + +def validate_tests_path(path, files, ignore_list): + for ignore in ignore_list: + if ignore not in files: + die_with_error(f"Ignore file '{ignore}' not found in tests folder '{path}'") + if "browser.toml" not in files or "browser.js" in ignore_list: + die_with_error(f"'browser.toml' not found in tests folder '{path}'") + + +def disable_and_replace_manifest(manifest, output_path): + toml_file = os.path.join(output_path, "browser.toml") + disabled_tests = manifest.get("disable", []) + with open(toml_file, "r") as f: + data = f.read() + for test in disabled_tests: + segment = f'["{test}"]' + if segment not in data: + die_with_error(f"Could not disable test '{test}' as it was not found in '{toml_file}'") + replace_with = f'["{test}"]\ndisabled="Disabled by import_external_tests.py"' + data = data.replace(segment, replace_with) + for replacement in manifest.get("replace-manifest", {}).keys(): + if replacement not in data: + die_with_error(f"Could not replace manifest entry '{replacement}' as it was not found in '{toml_file}'") + data = data.replace(replacement, manifest["replace-manifest"][replacement]) + with open(toml_file, "w") as f: + f.write(data) + + +def import_test_suite(test_suite, source_path, output_path, ignore_list, manifest, is_direct_path=False): + print(f"Importing test suite '{test_suite}' from '{source_path}'") + tests_folder = os.path.join("engine", source_path) + if not is_direct_path: + tests_folder = os.path.join(tests_folder, "tests") + if not os.path.exists(tests_folder): + die_with_error(f"Tests folder not found: {tests_folder}") + files = os.listdir(tests_folder) + validate_tests_path(tests_folder, files, ignore_list) + if os.path.exists(output_path): + shutil.rmtree(output_path) + os.makedirs(output_path, exist_ok=True) + for item in files: + if item in ignore_list: + continue + s = os.path.join(tests_folder, item) + d = os.path.join(output_path, item) + if os.path.isdir(s): + shutil.copytree(s, d) + else: + shutil.copy2(s, d) + disable_and_replace_manifest(manifest[test_suite], output_path) + + +def write_moz_build_file(manifest): + moz_build_path = os.path.join(EXTERNAL_TESTS_OUTPUT, "moz.build") + print(f"Writing moz.build file to '{moz_build_path}'") + with open(moz_build_path, "w") as f: + f.write(FILE_PREFIX) + for test_suite in manifest.keys(): + f.write(f'\t"{test_suite}/browser.toml",\n') + f.write(FILE_SUFFIX) + + +def make_sure_ordered_tests(manifest): + ordered_tests = sorted(manifest.keys()) + if list(manifest.keys()) != ordered_tests: + die_with_error("Test suites in manifest.toml are not in alphabetical order.") + + +def main(): + manifest = get_tests_manifest() + if os.path.exists(EXTERNAL_TESTS_OUTPUT): + shutil.rmtree(EXTERNAL_TESTS_OUTPUT) + os.makedirs(EXTERNAL_TESTS_OUTPUT, exist_ok=True) + + make_sure_ordered_tests(manifest) + for test_suite, config in manifest.items(): + import_test_suite( + test_suite=test_suite, + source_path=config["source"], + output_path=os.path.join(EXTERNAL_TESTS_OUTPUT, test_suite), + ignore_list=config.get("ignore", []), + is_direct_path=config.get("is_direct_path", False), + manifest=manifest + ) + write_moz_build_file(manifest) + + +if __name__ == "__main__": + main() diff --git a/scripts/run_tests.py b/scripts/run_tests.py index 2e0fb516f..958b09ce4 100644 --- a/scripts/run_tests.py +++ b/scripts/run_tests.py @@ -15,6 +15,8 @@ IGNORE_PREFS_FILE_OUT = os.path.join( 'engine', 'testing', 'mochitest', 'ignorePrefs.json' ) +MOCHITEST_NAME = "mochitests" + def copy_ignore_prefs(): print("Copying ignorePrefs.json from src/zen/tests to engine/testing/mochitest...") @@ -59,7 +61,9 @@ def main(): os.execvp(command[0], command) if path in ("", "all"): - test_dirs = [p for p in Path("zen/tests").iterdir() if p.is_dir()] + test_dirs = [p for p in Path("zen/tests").iterdir() if p.is_dir() and p.name != MOCHITEST_NAME] + mochitest_dirs = [p for p in Path(f"zen/tests/{MOCHITEST_NAME}").iterdir() if p.is_dir()] + test_dirs.extend(mochitest_dirs) test_paths = [str(p) for p in test_dirs] run_mach_with_paths(test_paths) else: diff --git a/src/zen/tests/ignorePrefs.json b/src/zen/tests/ignorePrefs.json index b7244e654..74187ca68 100644 --- a/src/zen/tests/ignorePrefs.json +++ b/src/zen/tests/ignorePrefs.json @@ -10,5 +10,9 @@ "zen.mods.last-update", "zen.view.compact.enable-at-startup", "zen.urlbar.suggestions-learner", - "browser.newtabpage.activity-stream.trendingSearch.defaultSearchEngine" + "browser.newtabpage.activity-stream.trendingSearch.defaultSearchEngine", + + // From the imported safebrowsing tests + "urlclassifier.phishTable", + "urlclassifier.malwareTable" ] diff --git a/src/zen/tests/manifest.toml b/src/zen/tests/manifest.toml new file mode 100644 index 000000000..554198683 --- /dev/null +++ b/src/zen/tests/manifest.toml @@ -0,0 +1,30 @@ + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +[reportbrokensite] +source = "browser/components/reportbrokensite/test/browser" +is_direct_path = true +disable = [ + "browser_addon_data_sent.js" +] + +[reportbrokensite.replace-manifest] +"../../../../../" = "../../../../" + +[safebrowsing] +source = "browser/components/safebrowsing/content/test" +is_direct_path = true + +[shell] +source = "browser/components/shell/test" +is_direct_path = true +disable = [ + "browser_1119088.js", + "browser_setDesktopBackgroundPreview.js", +] + +[tooltiptext] +source = "toolkit/components/tooltiptext" + diff --git a/src/zen/tests/mochitests/moz.build b/src/zen/tests/mochitests/moz.build new file mode 100644 index 000000000..593fada64 --- /dev/null +++ b/src/zen/tests/mochitests/moz.build @@ -0,0 +1,14 @@ + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# This file is autogenerated by scripts/import_external_tests.py +# Do not edit manually. + +BROWSER_CHROME_MANIFESTS += [ + "reportbrokensite/browser.toml", + "safebrowsing/browser.toml", + "shell/browser.toml", + "tooltiptext/browser.toml", +] \ No newline at end of file diff --git a/src/zen/tests/mochitests/reportbrokensite/browser.toml b/src/zen/tests/mochitests/reportbrokensite/browser.toml new file mode 100644 index 000000000..4ae01db0e --- /dev/null +++ b/src/zen/tests/mochitests/reportbrokensite/browser.toml @@ -0,0 +1,57 @@ +[DEFAULT] +tags = "report-broken-site" +support-files = [ + "example_report_page.html", + "head.js", + "sendMoreInfoTestEndpoint.html", +] + +["browser_addon_data_sent.js"] +disabled="Disabled by import_external_tests.py" +support-files = [ "send_more_info.js" ] +skip-if = ["os == 'win' && os_version == '11.26100' && processor == 'x86_64' && opt"] # Bug 1955805 + +["browser_antitracking_data_sent.js"] +support-files = [ "send_more_info.js" ] + +["browser_back_buttons.js"] + +["browser_error_messages.js"] + +["browser_experiment_data_sent.js"] +support-files = [ "send_more_info.js" ] + +["browser_keyboard_navigation.js"] +skip-if = [ + "os == 'linux' && os_version == '24.04' && processor == 'x86_64' && tsan", # Bug 1867132 + "os == 'linux' && os_version == '24.04' && processor == 'x86_64' && asan", # Bug 1867132 + "os == 'linux' && os_version == '24.04' && processor == 'x86_64' && debug", # Bug 1867132 + "os == 'win' && os_version == '11.26100' && processor == 'x86_64' && asan", # Bug 1867132 +] + +["browser_learn_more_link.js"] + +["browser_parent_menuitems.js"] + +["browser_prefers_contrast.js"] + +["browser_reason_dropdown.js"] + +["browser_report_send.js"] +support-files = [ "send.js" ] + +["browser_send_more_info.js"] +support-files = [ + "send_more_info.js", + "../../../../toolkit/components/gfx/content/videotest.mp4", +] + +["browser_tab_key_order.js"] + +["browser_tab_switch_handling.js"] + +["browser_webcompat.com_fallback.js"] +support-files = [ + "send_more_info.js", + "../../../../toolkit/components/gfx/content/videotest.mp4", +] diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_addon_data_sent.js b/src/zen/tests/mochitests/reportbrokensite/browser_addon_data_sent.js new file mode 100644 index 000000000..c7fb557d1 --- /dev/null +++ b/src/zen/tests/mochitests/reportbrokensite/browser_addon_data_sent.js @@ -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(); +}); diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_antitracking_data_sent.js b/src/zen/tests/mochitests/reportbrokensite/browser_antitracking_data_sent.js new file mode 100644 index 000000000..dd0b5ceaf --- /dev/null +++ b/src/zen/tests/mochitests/reportbrokensite/browser_antitracking_data_sent.js @@ -0,0 +1,126 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* Tests to ensure that the right data is sent for + * private windows and when ETP blocks content. + */ + +/* import-globals-from send.js */ +/* import-globals-from send_more_info.js */ + +"use strict"; + +Services.scriptloader.loadSubScript( + getRootDirectory(gTestPath) + "send_more_info.js", + this +); + +add_common_setup(); + +add_task(setupStrictETP); + +function getEtpCategory() { + return Services.prefs.getStringPref( + "browser.contentblocking.category", + "standard" + ); +} + +add_task(async function testSendButton() { + ensureReportBrokenSitePreffedOn(); + ensureReasonOptional(); + + const win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + const blockedPromise = waitForContentBlockingEvent(3, win); + const tab = await openTab(REPORTABLE_PAGE_URL3, win); + await blockedPromise; + + await testSend(tab, AppMenu(win), { + breakageCategory: "adblocker", + description: "another test description", + antitracking: { + blockList: "strict", + blockedOrigins: null, + isPrivateBrowsing: true, + hasTrackingContentBlocked: true, + hasMixedActiveContentBlocked: true, + hasMixedDisplayContentBlocked: true, + btpHasPurgedSite: false, + etpCategory: getEtpCategory(), + }, + frameworks: { + fastclick: true, + marfeel: true, + mobify: true, + }, + }); + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function testSendingMoreInfo() { + ensureReportBrokenSitePreffedOn(); + ensureSendMoreInfoEnabled(); + + const win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + const blockedPromise = waitForContentBlockingEvent(3, win); + const tab = await openTab(REPORTABLE_PAGE_URL3, win); + await blockedPromise; + + await testSendMoreInfo(tab, HelpMenu(win), { + antitracking: { + blockList: "strict", + blockedOrigins: ["https://trackertest.org"], + isPrivateBrowsing: true, + hasTrackingContentBlocked: true, + hasMixedActiveContentBlocked: true, + hasMixedDisplayContentBlocked: true, + btpHasPurgedSite: false, + etpCategory: getEtpCategory(), + }, + frameworks: { fastclick: true, mobify: true, marfeel: true }, + consoleLog: [ + { + level: "error", + log(actual) { + // "Blocked loading mixed display content http://example.com/tests/image/test/mochitest/blue.png" + return ( + Array.isArray(actual) && + actual.length == 1 && + actual[0].includes("blue.png") + ); + }, + pos: "0:1", + uri: REPORTABLE_PAGE_URL3, + }, + { + level: "error", + log(actual) { + // "Blocked loading mixed active content http://tracking.example.org/browser/browser/base/content/test/protectionsUI/benignPage.html", + return ( + Array.isArray(actual) && + actual.length == 1 && + actual[0].includes("benignPage.html") + ); + }, + pos: "0:1", + uri: REPORTABLE_PAGE_URL3, + }, + { + level: "warn", + log(actual) { + // "The resource at https://trackertest.org/ was blocked because content blocking is enabled.", + return ( + Array.isArray(actual) && + actual.length == 1 && + actual[0].includes("trackertest.org") + ); + }, + pos: "0:1", + uri: REPORTABLE_PAGE_URL3, + }, + ], + }); + + await BrowserTestUtils.closeWindow(win); +}); diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_back_buttons.js b/src/zen/tests/mochitests/reportbrokensite/browser_back_buttons.js new file mode 100644 index 000000000..c004442c2 --- /dev/null +++ b/src/zen/tests/mochitests/reportbrokensite/browser_back_buttons.js @@ -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(); + }); +}); diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_error_messages.js b/src/zen/tests/mochitests/reportbrokensite/browser_error_messages.js new file mode 100644 index 000000000..54b93cb2d --- /dev/null +++ b/src/zen/tests/mochitests/reportbrokensite/browser_error_messages.js @@ -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(); + } + }); +}); diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_experiment_data_sent.js b/src/zen/tests/mochitests/reportbrokensite/browser_experiment_data_sent.js new file mode 100644 index 000000000..3cedcd549 --- /dev/null +++ b/src/zen/tests/mochitests/reportbrokensite/browser_experiment_data_sent.js @@ -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(); + } +}); diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_keyboard_navigation.js b/src/zen/tests/mochitests/reportbrokensite/browser_keyboard_navigation.js new file mode 100644 index 000000000..3bf9278e4 --- /dev/null +++ b/src/zen/tests/mochitests/reportbrokensite/browser_keyboard_navigation.js @@ -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(); + } + }); +}); diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_learn_more_link.js b/src/zen/tests/mochitests/reportbrokensite/browser_learn_more_link.js new file mode 100644 index 000000000..4040bef59 --- /dev/null +++ b/src/zen/tests/mochitests/reportbrokensite/browser_learn_more_link.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* Tests to ensure that the reason dropdown is shown or hidden + * based on its pref, and that its optional and required modes affect + * the Send button and report appropriately. + */ + +"use strict"; + +add_common_setup(); + +async function ensureLearnMoreLinkWorks(menu) { + const rbs = await menu.openReportBrokenSite(); + const { win, mainView, learnMoreLink } = rbs; + ok(learnMoreLink, "Found a learn more link"); + + const promises = [ + BrowserTestUtils.waitForEvent(mainView, "ViewHiding"), + BrowserTestUtils.waitForNewTab(win.gBrowser, LEARN_MORE_TEST_URL), + ]; + EventUtils.synthesizeMouseAtCenter(learnMoreLink, {}, win); + const results = await Promise.all(promises); + gBrowser.removeTab(results[1]); +} + +add_task(async function testLearnMoreLink() { + ensureReportBrokenSitePreffedOn(); + await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () { + await ensureLearnMoreLinkWorks(AppMenu()); + await ensureLearnMoreLinkWorks(HelpMenu()); + await ensureLearnMoreLinkWorks(ProtectionsPanel()); + }); + const telemetry = Glean.webcompatreporting.learnMore.testGetValue(); + is(telemetry.length, 3, "Got telemetry"); +}); diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_parent_menuitems.js b/src/zen/tests/mochitests/reportbrokensite/browser_parent_menuitems.js new file mode 100644 index 000000000..36f172789 --- /dev/null +++ b/src/zen/tests/mochitests/reportbrokensite/browser_parent_menuitems.js @@ -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 '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` + ); + } + }); +}); diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_prefers_contrast.js b/src/zen/tests/mochitests/reportbrokensite/browser_prefers_contrast.js new file mode 100644 index 000000000..b184b5145 --- /dev/null +++ b/src/zen/tests/mochitests/reportbrokensite/browser_prefers_contrast.js @@ -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(); + } + ); +}); diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_reason_dropdown.js b/src/zen/tests/mochitests/reportbrokensite/browser_reason_dropdown.js new file mode 100644 index 000000000..365b469a7 --- /dev/null +++ b/src/zen/tests/mochitests/reportbrokensite/browser_reason_dropdown.js @@ -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); +}); diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_report_send.js b/src/zen/tests/mochitests/reportbrokensite/browser_report_send.js new file mode 100644 index 000000000..bf849776d --- /dev/null +++ b/src/zen/tests/mochitests/reportbrokensite/browser_report_send.js @@ -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); +}); diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_send_more_info.js b/src/zen/tests/mochitests/reportbrokensite/browser_send_more_info.js new file mode 100644 index 000000000..9306f5161 --- /dev/null +++ b/src/zen/tests/mochitests/reportbrokensite/browser_send_more_info.js @@ -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); +}); diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_tab_key_order.js b/src/zen/tests/mochitests/reportbrokensite/browser_tab_key_order.js new file mode 100644 index 000000000..ab776fb26 --- /dev/null +++ b/src/zen/tests/mochitests/reportbrokensite/browser_tab_key_order.js @@ -0,0 +1,134 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* Tests of the expected tab key element focus order */ + +"use strict"; + +add_common_setup(); + +requestLongerTimeout(2); + +async function ensureTabOrder(order, win = window) { + const config = { window: win }; + for (let matches of order) { + // We need to tab through all elements in each match array in any order + if (!Array.isArray(matches)) { + matches = [matches]; + } + let matchesLeft = matches.length; + while (matchesLeft--) { + const target = await pressKeyAndGetFocus("VK_TAB", config); + let foundMatch = false; + for (const [i, selector] of matches.entries()) { + foundMatch = selector && target.matches(selector); + if (foundMatch) { + matches[i] = ""; + break; + } + } + ok( + foundMatch, + `Expected [${matches}] next, got id=${target.id}, class=${target.className}, ${target}` + ); + if (!foundMatch) { + return false; + } + } + } + return true; +} + +async function ensureExpectedTabOrder( + expectBackButton, + expectReason, + expectSendMoreInfo +) { + const { activeElement } = window.document; + is( + activeElement?.id, + "report-broken-site-popup-url", + "URL is already focused" + ); + const order = []; + if (expectReason) { + order.push("#report-broken-site-popup-reason"); + } + order.push("#report-broken-site-popup-description"); + order.push("#report-broken-site-popup-blocked-trackers-checkbox"); + if (expectSendMoreInfo) { + order.push("#report-broken-site-popup-send-more-info-link"); + } + // moz-button-groups swap the order of buttons to follow + // platform conventions, so the order of send/cancel will vary. + order.push([ + "#report-broken-site-popup-cancel-button", + "#report-broken-site-popup-send-button", + ]); + if (expectBackButton) { + order.push(".subviewbutton-back"); + } + order.push("#report-broken-site-popup-learn-more-link"); + order.push("#report-broken-site-popup-url"); // check that we've cycled back + return ensureTabOrder(order); +} + +async function testTabOrder(menu) { + ensureReasonDisabled(); + ensureSendMoreInfoDisabled(); + + const { showsBackButton } = menu; + + let rbs = await menu.openReportBrokenSite(); + await ensureExpectedTabOrder(showsBackButton, false, false); + await rbs.close(); + + ensureSendMoreInfoEnabled(); + rbs = await menu.openReportBrokenSite(); + await ensureExpectedTabOrder(showsBackButton, false, true); + await rbs.close(); + + ensureReasonOptional(); + rbs = await menu.openReportBrokenSite(); + await ensureExpectedTabOrder(showsBackButton, true, true); + await rbs.close(); + + ensureReasonRequired(); + rbs = await menu.openReportBrokenSite(); + await ensureExpectedTabOrder(showsBackButton, true, true); + await rbs.close(); + rbs = await menu.openReportBrokenSite(); + rbs.chooseReason("slow"); + await ensureExpectedTabOrder(showsBackButton, true, true); + await rbs.clickCancel(); + + ensureSendMoreInfoDisabled(); + rbs = await menu.openReportBrokenSite(); + await ensureExpectedTabOrder(showsBackButton, true, false); + await rbs.close(); + rbs = await menu.openReportBrokenSite(); + rbs.chooseReason("slow"); + await ensureExpectedTabOrder(showsBackButton, true, false); + await rbs.clickCancel(); + + ensureReasonOptional(); + rbs = await menu.openReportBrokenSite(); + await ensureExpectedTabOrder(showsBackButton, true, false); + await rbs.close(); + + ensureReasonDisabled(); + rbs = await menu.openReportBrokenSite(); + await ensureExpectedTabOrder(showsBackButton, false, false); + await rbs.close(); +} + +add_task(async function testTabOrdering() { + ensureReportBrokenSitePreffedOn(); + ensureSendMoreInfoEnabled(); + + await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () { + await testTabOrder(AppMenu()); + await testTabOrder(ProtectionsPanel()); + await testTabOrder(HelpMenu()); + }); +}); diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_tab_switch_handling.js b/src/zen/tests/mochitests/reportbrokensite/browser_tab_switch_handling.js new file mode 100644 index 000000000..db1190b89 --- /dev/null +++ b/src/zen/tests/mochitests/reportbrokensite/browser_tab_switch_handling.js @@ -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); +}); diff --git a/src/zen/tests/mochitests/reportbrokensite/browser_webcompat.com_fallback.js b/src/zen/tests/mochitests/reportbrokensite/browser_webcompat.com_fallback.js new file mode 100644 index 000000000..4b155bbd9 --- /dev/null +++ b/src/zen/tests/mochitests/reportbrokensite/browser_webcompat.com_fallback.js @@ -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); +}); diff --git a/src/zen/tests/mochitests/reportbrokensite/example_report_page.html b/src/zen/tests/mochitests/reportbrokensite/example_report_page.html new file mode 100644 index 000000000..07602e3fb --- /dev/null +++ b/src/zen/tests/mochitests/reportbrokensite/example_report_page.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + diff --git a/src/zen/tests/mochitests/reportbrokensite/head.js b/src/zen/tests/mochitests/reportbrokensite/head.js new file mode 100644 index 000000000..04357acad --- /dev/null +++ b/src/zen/tests/mochitests/reportbrokensite/head.js @@ -0,0 +1,918 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { CustomizableUITestUtils } = ChromeUtils.importESModule( + "resource://testing-common/CustomizableUITestUtils.sys.mjs" +); + +const { EnterprisePolicyTesting, PoliciesPrefTracker } = + ChromeUtils.importESModule( + "resource://testing-common/EnterprisePolicyTesting.sys.mjs" + ); + +const { UrlClassifierTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/UrlClassifierTestUtils.sys.mjs" +); + +const { ReportBrokenSite } = ChromeUtils.importESModule( + "moz-src:///browser/components/reportbrokensite/ReportBrokenSite.sys.mjs" +); + +const BASE_URL = + "https://example.com/browser/browser/components/reportbrokensite/test/browser/"; + +const REPORTABLE_PAGE_URL = "https://example.com"; + +const REPORTABLE_PAGE_URL2 = REPORTABLE_PAGE_URL.replace(".com", ".org"); + +const REPORTABLE_PAGE_URL3 = `${BASE_URL}example_report_page.html`; + +const SUMO_BASE_URL = Services.urlFormatter.formatURLPref( + "app.support.baseURL" +); +const LEARN_MORE_TEST_URL = `${SUMO_BASE_URL}report-broken-site`; + +const NEW_REPORT_ENDPOINT_TEST_URL = `${BASE_URL}sendMoreInfoTestEndpoint.html`; + +const PREFS = { + DATAREPORTING_ENABLED: "datareporting.healthreport.uploadEnabled", + REPORTER_ENABLED: "ui.new-webcompat-reporter.enabled", + REASON: "ui.new-webcompat-reporter.reason-dropdown", + SEND_MORE_INFO: "ui.new-webcompat-reporter.send-more-info-link", + NEW_REPORT_ENDPOINT: "ui.new-webcompat-reporter.new-report-endpoint", + TOUCH_EVENTS: "dom.w3c_touch_events.enabled", + USE_ACCESSIBILITY_THEME: "ui.useAccessibilityTheme", +}; + +function add_common_setup() { + add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + [PREFS.NEW_REPORT_ENDPOINT, NEW_REPORT_ENDPOINT_TEST_URL], + + // set touch events to auto-detect, as the pref gets set to 1 somewhere + // while tests are running, making hasTouchScreen checks unreliable. + [PREFS.TOUCH_EVENTS, 2], + ], + }); + registerCleanupFunction(function () { + for (const prefName of Object.values(PREFS)) { + Services.prefs.clearUserPref(prefName); + } + Services.telemetry.clearEvents(); + Services.fog.testResetFOG(); + }); + }); +} + +function areObjectsEqual(actual, expected, path = "") { + if (typeof expected == "function") { + try { + const passes = expected(actual); + if (!passes) { + info(`${path} not pass check function: ${actual}`); + } + return passes; + } catch (e) { + info(`${path} threw exception: + got: ${typeof actual}, ${actual} + expected: ${typeof expected}, ${expected} + exception: ${e.message} + ${e.stack}`); + return false; + } + } + + if (typeof actual != typeof expected) { + info(`${path} types do not match: + got: ${typeof actual}, ${actual} + expected: ${typeof expected}, ${expected}`); + return false; + } + if (typeof actual != "object" || actual === null || expected === null) { + if (actual !== expected) { + info(`${path} does not match + got: ${typeof actual}, ${actual} + expected: ${typeof expected}, ${expected}`); + return false; + } + return true; + } + const prefix = path ? `${path}.` : path; + for (const [key, val] of Object.entries(actual)) { + if (!(key in expected)) { + info(`Extra ${prefix}${key}: ${val}`); + return false; + } + } + let result = true; + for (const [key, expectedVal] of Object.entries(expected)) { + if (key in actual) { + if (!areObjectsEqual(actual[key], expectedVal, `${prefix}${key}`)) { + result = false; + } + } else { + info(`Missing ${prefix}${key} (${expectedVal})`); + result = false; + } + } + return result; +} + +function clickAndAwait(toClick, evt, target) { + const menuPromise = BrowserTestUtils.waitForEvent(target, evt); + EventUtils.synthesizeMouseAtCenter(toClick, {}, window); + return menuPromise; +} + +async function openTab(url, win) { + const options = { + gBrowser: + win?.gBrowser || + Services.wm.getMostRecentWindow("navigator:browser").gBrowser, + url, + }; + return BrowserTestUtils.openNewForegroundTab(options); +} + +async function changeTab(tab, url) { + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, url); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); +} + +function closeTab(tab) { + BrowserTestUtils.removeTab(tab); +} + +function switchToWindow(win) { + const promises = [ + BrowserTestUtils.waitForEvent(win, "focus"), + BrowserTestUtils.waitForEvent(win, "activate"), + ]; + win.focus(); + return Promise.all(promises); +} + +function isSelectedTab(win, tab) { + const selectedTab = win.document.querySelector(".tabbrowser-tab[selected]"); + is(selectedTab, tab); +} + +async function setupPolicyEngineWithJson(json, customSchema) { + PoliciesPrefTracker.restoreDefaultValues(); + if (typeof json != "object") { + let filePath = getTestFilePath(json ? json : "non-existing-file.json"); + return EnterprisePolicyTesting.setupPolicyEngineWithJson( + filePath, + customSchema + ); + } + return EnterprisePolicyTesting.setupPolicyEngineWithJson(json, customSchema); +} + +async function ensureReportBrokenSiteDisabledByPolicy() { + await setupPolicyEngineWithJson({ + policies: { + DisableFeedbackCommands: true, + }, + }); +} + +registerCleanupFunction(async function resetPolicies() { + if (Services.policies.status != Ci.nsIEnterprisePolicies.INACTIVE) { + await setupPolicyEngineWithJson(""); + } + EnterprisePolicyTesting.resetRunOnceState(); + PoliciesPrefTracker.restoreDefaultValues(); + PoliciesPrefTracker.stop(); +}); + +function ensureReportBrokenSitePreffedOn() { + Services.prefs.setBoolPref(PREFS.DATAREPORTING_ENABLED, true); + Services.prefs.setBoolPref(PREFS.REPORTER_ENABLED, true); + ensureReasonDisabled(); +} + +function ensureReportBrokenSitePreffedOff() { + Services.prefs.setBoolPref(PREFS.REPORTER_ENABLED, false); +} + +function ensureSendMoreInfoEnabled() { + Services.prefs.setBoolPref(PREFS.SEND_MORE_INFO, true); +} + +function ensureSendMoreInfoDisabled() { + Services.prefs.setBoolPref(PREFS.SEND_MORE_INFO, false); +} + +function ensureReasonDisabled() { + Services.prefs.setIntPref(PREFS.REASON, 0); +} + +function ensureReasonOptional() { + Services.prefs.setIntPref(PREFS.REASON, 1); +} + +function ensureReasonRequired() { + Services.prefs.setIntPref(PREFS.REASON, 2); +} + +function isMenuItemEnabled(menuItem, itemDesc) { + ok(!menuItem.hidden, `${itemDesc} menu item is shown`); + ok(!menuItem.disabled, `${itemDesc} menu item is enabled`); +} + +function isMenuItemHidden(menuItem, itemDesc) { + ok( + !menuItem || menuItem.hidden || !BrowserTestUtils.isVisible(menuItem), + `${itemDesc} menu item is hidden` + ); +} + +function isMenuItemDisabled(menuItem, itemDesc) { + ok(!menuItem.hidden, `${itemDesc} menu item is shown`); + ok(menuItem.disabled, `${itemDesc} menu item is disabled`); +} + +function waitForWebcompatComTab(gBrowser) { + return BrowserTestUtils.waitForNewTab(gBrowser, NEW_REPORT_ENDPOINT_TEST_URL); +} + +class ReportBrokenSiteHelper { + sourceMenu = undefined; + win = undefined; + + constructor(sourceMenu) { + this.sourceMenu = sourceMenu; + this.win = sourceMenu.win; + } + + getViewNode(id) { + return PanelMultiView.getViewNode(this.win.document, id); + } + + get mainView() { + return this.getViewNode("report-broken-site-popup-mainView"); + } + + get sentView() { + return this.getViewNode("report-broken-site-popup-reportSentView"); + } + + get openPanel() { + return this.mainView?.closest("panel"); + } + + get opened() { + return this.openPanel?.hasAttribute("panelopen"); + } + + async click(triggerMenuItem) { + const window = triggerMenuItem.ownerGlobal; + await EventUtils.synthesizeMouseAtCenter(triggerMenuItem, {}, window); + } + + async open(triggerMenuItem) { + const shownPromise = BrowserTestUtils.waitForEvent( + this.mainView, + "ViewShown" + ); + const focusPromise = BrowserTestUtils.waitForEvent(this.URLInput, "focus"); + await this.click(triggerMenuItem); + await shownPromise; + await focusPromise; + await BrowserTestUtils.waitForCondition( + () => this.URLInput.selectionStart === 0 + ); + } + + async #assertClickAndViewChanges(button, view, newView, newFocus) { + ok(view.closest("panel").hasAttribute("panelopen"), "Panel is open"); + ok(BrowserTestUtils.isVisible(button), "Button is visible"); + ok(!button.disabled, "Button is enabled"); + const promises = []; + if (newView) { + if (newView.nodeName == "panel") { + promises.push(BrowserTestUtils.waitForEvent(newView, "popupshown")); + } else { + promises.push(BrowserTestUtils.waitForEvent(newView, "ViewShown")); + } + } else { + promises.push(BrowserTestUtils.waitForEvent(view, "ViewHiding")); + } + if (newFocus) { + promises.push(BrowserTestUtils.waitForEvent(newFocus, "focus")); + } + EventUtils.synthesizeMouseAtCenter(button, {}, this.win); + await Promise.all(promises); + } + + async awaitReportSentViewOpened() { + await Promise.all([ + BrowserTestUtils.waitForEvent(this.sentView, "ViewShown"), + BrowserTestUtils.waitForEvent(this.okayButton, "focus"), + ]); + } + + async clickSend() { + await this.#assertClickAndViewChanges( + this.sendButton, + this.mainView, + this.sentView, + this.okayButton + ); + } + + waitForSendMoreInfoTab() { + return BrowserTestUtils.waitForNewTab( + this.win.gBrowser, + NEW_REPORT_ENDPOINT_TEST_URL + ); + } + + async clickSendMoreInfo() { + const newTabPromise = waitForWebcompatComTab(this.win.gBrowser); + EventUtils.synthesizeMouseAtCenter(this.sendMoreInfoLink, {}, this.win); + const newTab = await newTabPromise; + const receivedData = await SpecialPowers.spawn( + newTab.linkedBrowser, + [], + async function () { + await content.wrappedJSObject.messageArrived; + return content.wrappedJSObject.message; + } + ); + this.win.gBrowser.removeCurrentTab(); + return receivedData; + } + + async clickCancel() { + await this.#assertClickAndViewChanges(this.cancelButton, this.mainView); + } + + async clickOkay() { + await this.#assertClickAndViewChanges(this.okayButton, this.sentView); + } + + async clickBack() { + await this.#assertClickAndViewChanges( + this.backButton, + this.sourceMenu.popup + ); + } + + isBackButtonEnabled() { + ok(BrowserTestUtils.isVisible(this.backButton), "Back button is visible"); + ok(!this.backButton.disabled, "Back button is enabled"); + } + + close() { + if (this.opened) { + this.openPanel?.hidePopup(false); + } + this.sourceMenu?.close(); + } + + // UI element getters + get URLInput() { + return this.getViewNode("report-broken-site-popup-url"); + } + + get URLInvalidMessage() { + return this.getViewNode("report-broken-site-popup-invalid-url-msg"); + } + + get reasonInput() { + return this.getViewNode("report-broken-site-popup-reason"); + } + + get reasonDropdownPopup() { + return this.win.document.getElementById("ContentSelectDropdown").menupopup; + } + + get reasonRequiredMessage() { + return this.getViewNode("report-broken-site-popup-missing-reason-msg"); + } + + get reasonLabelRequired() { + return this.getViewNode("report-broken-site-popup-reason-label"); + } + + get reasonLabelOptional() { + return this.getViewNode("report-broken-site-popup-reason-optional-label"); + } + + get descriptionTextarea() { + return this.getViewNode("report-broken-site-popup-description"); + } + + get learnMoreLink() { + return this.getViewNode("report-broken-site-popup-learn-more-link"); + } + + get sendMoreInfoLink() { + return this.getViewNode("report-broken-site-popup-send-more-info-link"); + } + + get backButton() { + return this.mainView.querySelector(".subviewbutton-back"); + } + + get blockedTrackersCheckbox() { + return this.getViewNode( + "report-broken-site-popup-blocked-trackers-checkbox" + ); + } + + set blockedTrackersCheckbox(checked) { + this.blockedTrackersCheckbox.checked = checked; + } + + get sendButton() { + return this.getViewNode("report-broken-site-popup-send-button"); + } + + get cancelButton() { + return this.getViewNode("report-broken-site-popup-cancel-button"); + } + + get okayButton() { + return this.getViewNode("report-broken-site-popup-okay-button"); + } + + // Test helpers + + #setInput(input, value) { + input.value = value; + input.dispatchEvent( + new UIEvent("input", { bubbles: true, view: this.win }) + ); + } + + setURL(value) { + this.#setInput(this.URLInput, value); + } + + chooseReason(value) { + const item = this.getViewNode(`report-broken-site-popup-reason-${value}`); + this.reasonInput.selectedIndex = item.index; + } + + dismissDropdownPopup() { + const popup = this.reasonDropdownPopup; + const menuPromise = BrowserTestUtils.waitForPopupEvent(popup, "hidden"); + popup.hidePopup(); + return menuPromise; + } + + setDescription(value) { + this.#setInput(this.descriptionTextarea, value); + } + + isURL(expected) { + is(this.URLInput.value, expected); + } + + isURLInvalidMessageShown() { + ok( + BrowserTestUtils.isVisible(this.URLInvalidMessage), + "'Please enter a valid URL' message is shown" + ); + } + + isURLInvalidMessageHidden() { + ok( + !BrowserTestUtils.isVisible(this.URLInvalidMessage), + "'Please enter a valid URL' message is hidden" + ); + } + + isReasonNeededMessageShown() { + ok( + BrowserTestUtils.isVisible(this.reasonRequiredMessage), + "'Please choose a reason' message is shown" + ); + } + + isReasonNeededMessageHidden() { + ok( + !BrowserTestUtils.isVisible(this.reasonRequiredMessage), + "'Please choose a reason' message is hidden" + ); + } + + isSendButtonEnabled() { + ok(BrowserTestUtils.isVisible(this.sendButton), "Send button is visible"); + ok(!this.sendButton.disabled, "Send button is enabled"); + } + + isSendButtonDisabled() { + ok(BrowserTestUtils.isVisible(this.sendButton), "Send button is visible"); + ok(this.sendButton.disabled, "Send button is disabled"); + } + + isSendMoreInfoShown() { + ok( + BrowserTestUtils.isVisible(this.sendMoreInfoLink), + "send more info is shown" + ); + } + + isSendMoreInfoHidden() { + ok( + !BrowserTestUtils.isVisible(this.sendMoreInfoLink), + "send more info is hidden" + ); + } + + isSendMoreInfoShownOrHiddenAppropriately() { + if (Services.prefs.getBoolPref(PREFS.SEND_MORE_INFO)) { + this.isSendMoreInfoShown(); + } else { + this.isSendMoreInfoHidden(); + } + } + + isReasonHidden() { + ok( + !BrowserTestUtils.isVisible(this.reasonInput), + "reason drop-down is hidden" + ); + ok( + !BrowserTestUtils.isVisible(this.reasonLabelOptional), + "optional reason label is hidden" + ); + ok( + !BrowserTestUtils.isVisible(this.reasonLabelRequired), + "required reason label is hidden" + ); + } + + isReasonRequired() { + ok( + BrowserTestUtils.isVisible(this.reasonInput), + "reason drop-down is shown" + ); + ok( + !BrowserTestUtils.isVisible(this.reasonLabelOptional), + "optional reason label is hidden" + ); + ok( + BrowserTestUtils.isVisible(this.reasonLabelRequired), + "required reason label is shown" + ); + } + + isReasonOptional() { + ok( + BrowserTestUtils.isVisible(this.reasonInput), + "reason drop-down is shown" + ); + ok( + BrowserTestUtils.isVisible(this.reasonLabelOptional), + "optional reason label is shown" + ); + ok( + !BrowserTestUtils.isVisible(this.reasonLabelRequired), + "required reason label is hidden" + ); + } + + isReasonShownOrHiddenAppropriately() { + const pref = Services.prefs.getIntPref(PREFS.REASON); + if (pref == 2) { + this.isReasonOptional(); + } else if (pref == 1) { + this.isReasonOptional(); + } else { + this.isReasonHidden(); + } + } + + isDescription(expected) { + return this.descriptionTextarea.value == expected; + } + + isMainViewResetToCurrentTab() { + this.isURL(this.win.gBrowser.selectedBrowser.currentURI.spec); + this.isDescription(""); + this.isReasonShownOrHiddenAppropriately(); + this.isSendMoreInfoShownOrHiddenAppropriately(); + } +} + +class MenuHelper { + menuDescription = undefined; + + win = undefined; + + constructor(win = window) { + this.win = win; + } + + getViewNode(id) { + return PanelMultiView.getViewNode(this.win.document, id); + } + + get showsBackButton() { + return true; + } + + get reportBrokenSite() { + throw new Error("Should be defined in derived class"); + } + + get popup() { + throw new Error("Should be defined in derived class"); + } + + get opened() { + return this.popup?.hasAttribute("panelopen"); + } + + async open() {} + + async close() {} + + isReportBrokenSiteDisabled() { + return isMenuItemDisabled(this.reportBrokenSite, this.menuDescription); + } + + isReportBrokenSiteEnabled() { + return isMenuItemEnabled(this.reportBrokenSite, this.menuDescription); + } + + isReportBrokenSiteHidden() { + return isMenuItemHidden(this.reportBrokenSite, this.menuDescription); + } + + async clickReportBrokenSiteAndAwaitWebCompatTabData() { + const newTabPromise = waitForWebcompatComTab(this.win.gBrowser); + await this.clickReportBrokenSite(); + const newTab = await newTabPromise; + const receivedData = await SpecialPowers.spawn( + newTab.linkedBrowser, + [], + async function () { + await content.wrappedJSObject.messageArrived; + return content.wrappedJSObject.message; + } + ); + + this.win.gBrowser.removeCurrentTab(); + return receivedData; + } + + async clickReportBrokenSite() { + if (!this.opened) { + await this.open(); + } + isMenuItemEnabled(this.reportBrokenSite, this.menuDescription); + const rbs = new ReportBrokenSiteHelper(this); + await rbs.click(this.reportBrokenSite); + return rbs; + } + + async openReportBrokenSite() { + if (!this.opened) { + await this.open(); + } + isMenuItemEnabled(this.reportBrokenSite, this.menuDescription); + const rbs = new ReportBrokenSiteHelper(this); + await rbs.open(this.reportBrokenSite); + return rbs; + } + + async openAndPrefillReportBrokenSite(url = null, description = "") { + let rbs = await this.openReportBrokenSite(); + rbs.isMainViewResetToCurrentTab(); + if (url) { + rbs.setURL(url); + } + if (description) { + rbs.setDescription(description); + } + return rbs; + } +} + +class AppMenuHelper extends MenuHelper { + menuDescription = "AppMenu"; + + get reportBrokenSite() { + return this.getViewNode("appMenu-report-broken-site-button"); + } + + get popup() { + return this.win.document.getElementById("appMenu-popup"); + } + + async open() { + await new CustomizableUITestUtils(this.win).openMainMenu(); + } + + async close() { + if (this.opened) { + await new CustomizableUITestUtils(this.win).hideMainMenu(); + } + } +} + +class HelpMenuHelper extends MenuHelper { + menuDescription = "Help Menu"; + + get showsBackButton() { + return false; + } + + get reportBrokenSite() { + return this.win.document.getElementById("help_reportBrokenSite"); + } + + get popup() { + return this.getViewNode("PanelUI-helpView"); + } + + get helpMenu() { + return this.win.document.getElementById("menu_HelpPopup"); + } + + async openReportBrokenSite() { + // We can't actually open the Help menu properly in testing, so the best + // we can do to open its Report Broken Site panel is to force its DOM to be + // prepared, and then soft-click the Report Broken Site menuitem to open it. + await this.open(); + const shownPromise = BrowserTestUtils.waitForEvent( + this.win, + "ViewShown", + true, + e => e.target.classList.contains("report-broken-site-view") + ); + this.reportBrokenSite.click(); + await shownPromise; + return new ReportBrokenSiteHelper(this); + } + + async clickReportBrokenSite() { + await this.open(); + this.reportBrokenSite.click(); + return new ReportBrokenSiteHelper(this); + } + + async open() { + const { helpMenu } = this; + const promise = BrowserTestUtils.waitForEvent(helpMenu, "popupshown"); + + // This event-faking method was copied from browser_title_case_menus.js. + // We can't actually open the Help menu in testing, but this lets us + // force its DOM to be properly built. + helpMenu.dispatchEvent(new MouseEvent("popupshowing", { bubbles: true })); + helpMenu.dispatchEvent(new MouseEvent("popupshown", { bubbles: true })); + + await promise; + } + + async close() { + const { helpMenu } = this; + const promise = BrowserTestUtils.waitForPopupEvent(helpMenu, "hidden"); + + // (Also copied from browser_title_case_menus.js) + // Just for good measure, we'll fire the popuphiding/popuphidden events + // after we close the menupopups. + helpMenu.dispatchEvent(new MouseEvent("popuphiding", { bubbles: true })); + helpMenu.dispatchEvent(new MouseEvent("popuphidden", { bubbles: true })); + + await promise; + } +} + +class ProtectionsPanelHelper extends MenuHelper { + menuDescription = "Protections Panel"; + + get reportBrokenSite() { + this.win.gProtectionsHandler._initializePopup(); + return this.getViewNode("protections-popup-report-broken-site-button"); + } + + get popup() { + this.win.gProtectionsHandler._initializePopup(); + return this.win.document.getElementById("protections-popup"); + } + + async open() { + const promise = BrowserTestUtils.waitForEvent( + this.win, + "popupshown", + true, + e => e.target.id == "protections-popup" + ); + this.win.gProtectionsHandler.showProtectionsPopup(); + await promise; + } + + async close() { + if (this.opened) { + const popup = this.popup; + const promise = BrowserTestUtils.waitForPopupEvent(popup, "hidden"); + PanelMultiView.hidePopup(popup, false); + await promise; + } + } +} + +function AppMenu(win = window) { + return new AppMenuHelper(win); +} + +function HelpMenu(win = window) { + return new HelpMenuHelper(win); +} + +function ProtectionsPanel(win = window) { + return new ProtectionsPanelHelper(win); +} + +function pressKeyAndAwait(event, key, config = {}) { + const win = config.window || window; + if (!event.then) { + event = BrowserTestUtils.waitForEvent(win, event, config.timeout || 200); + } + EventUtils.synthesizeKey(key, config, win); + return event; +} + +async function pressKeyAndGetFocus(key, config = {}) { + return (await pressKeyAndAwait("focus", key, config)).target; +} + +async function tabTo(match, win = window) { + const config = { window: win }; + const { activeElement } = win.document; + if (activeElement?.matches(match)) { + return activeElement; + } + let initial = await pressKeyAndGetFocus("VK_TAB", config); + let target = initial; + do { + if (target.matches(match)) { + return target; + } + target = await pressKeyAndGetFocus("VK_TAB", config); + } while (target && target !== initial); + return undefined; +} + +function filterFrameworkDetectorFails(ping, expected) { + // the framework detector's frame-script may fail to run in low memory or other + // weird corner-cases, so we ignore the results in that case if they don't match. + if (!areObjectsEqual(ping.frameworks, expected.frameworks)) { + const { fastclick, mobify, marfeel } = ping.frameworks; + if (!fastclick && !mobify && !marfeel) { + console.info("Ignoring failure to get framework data"); + expected.frameworks = ping.frameworks; + } + } +} + +async function setupStrictETP() { + await UrlClassifierTestUtils.addTestTrackers(); + registerCleanupFunction(() => { + UrlClassifierTestUtils.cleanupTestTrackers(); + }); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["security.mixed_content.block_active_content", true], + ["security.mixed_content.block_display_content", true], + ["security.mixed_content.upgrade_display_content", false], + [ + "urlclassifier.trackingTable", + "content-track-digest256,mochitest2-track-simple", + ], + ["browser.contentblocking.category", "strict"], + ], + }); +} + +// copied from browser/base/content/test/protectionsUI/head.js +function waitForContentBlockingEvent(numChanges = 1, win = null) { + if (!win) { + win = window; + } + return new Promise(resolve => { + let n = 0; + let listener = { + onContentBlockingEvent(webProgress, request, event) { + n = n + 1; + info( + `Received onContentBlockingEvent event: ${event} (${n} of ${numChanges})` + ); + if (n >= numChanges) { + win.gBrowser.removeProgressListener(listener); + resolve(n); + } + }, + }; + win.gBrowser.addProgressListener(listener); + }); +} diff --git a/src/zen/tests/mochitests/reportbrokensite/send.js b/src/zen/tests/mochitests/reportbrokensite/send.js new file mode 100644 index 000000000..5083868cf --- /dev/null +++ b/src/zen/tests/mochitests/reportbrokensite/send.js @@ -0,0 +1,355 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* Helper methods for testing sending reports with + * the Report Broken Site feature. + */ + +/* import-globals-from head.js */ + +"use strict"; + +const { Troubleshoot } = ChromeUtils.importESModule( + "resource://gre/modules/Troubleshoot.sys.mjs" +); + +function getSysinfoProperty(propertyName, defaultValue) { + try { + return Services.sysinfo.getProperty(propertyName); + } catch (e) {} + return defaultValue; +} + +function securityStringToArray(str) { + return str ? str.split(";") : null; +} + +function getExpectedGraphicsDevices(snapshot) { + const { graphics } = snapshot; + return [ + graphics.adapterDeviceID, + graphics.adapterVendorID, + graphics.adapterDeviceID2, + graphics.adapterVendorID2, + ] + .filter(i => i) + .sort(); +} + +function compareGraphicsDevices(expected, rawActual) { + const actual = rawActual + .map(({ deviceID, vendorID }) => [deviceID, vendorID]) + .flat() + .filter(i => i) + .sort(); + return areObjectsEqual(actual, expected); +} + +function getExpectedGraphicsDrivers(snapshot) { + const { graphics } = snapshot; + const expected = []; + for (let i = 1; i < 3; ++i) { + const version = graphics[`webgl${i}Version`]; + if (version && version != "-") { + expected.push(graphics[`webgl${i}Renderer`]); + expected.push(version); + } + } + return expected.filter(i => i).sort(); +} + +function compareGraphicsDrivers(expected, rawActual) { + const actual = rawActual + .map(({ renderer, version }) => [renderer, version]) + .flat() + .filter(i => i) + .sort(); + return areObjectsEqual(actual, expected); +} + +function getExpectedGraphicsFeatures(snapshot) { + const expected = {}; + for (let { name, log, status } of snapshot.graphics.featureLog.features) { + for (const item of log?.reverse() ?? []) { + if (item.failureId && item.status == status) { + status = `${status} (${item.message || item.failureId})`; + } + } + expected[name] = status; + } + return expected; +} + +async function getExpectedWebCompatInfo(tab, snapshot, fullAppData = false) { + const gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); + + const { application, graphics, intl, securitySoftware } = snapshot; + + const { fissionAutoStart, memorySizeBytes, updateChannel, userAgent } = + application; + + const app = { + defaultLocales: intl.localeService.available, + defaultUseragentString: userAgent, + fissionEnabled: fissionAutoStart, + }; + if (fullAppData) { + app.applicationName = application.name; + app.osArchitecture = getSysinfoProperty("arch", null); + app.osName = getSysinfoProperty("name", null); + app.osVersion = getSysinfoProperty("version", null); + app.updateChannel = updateChannel; + app.version = application.version; + } + + const hasTouchScreen = graphics.info.ApzTouchInput == 1; + + const { registeredAntiVirus, registeredAntiSpyware, registeredFirewall } = + securitySoftware; + + const browserInfo = { + addons: [], + app, + experiments: [], + graphics: { + devicesJson(actualStr) { + const expected = getExpectedGraphicsDevices(snapshot); + // If undefined is saved to the Glean value here, we'll get the string "undefined" (invalid JSON). + // We should stop using JSON like this in bug 1875185. + if (!actualStr || actualStr == "undefined") { + return !expected.length; + } + return compareGraphicsDevices(expected, JSON.parse(actualStr)); + }, + driversJson(actualStr) { + const expected = getExpectedGraphicsDrivers(snapshot); + // If undefined is saved to the Glean value here, we'll get the string "undefined" (invalid JSON). + // We should stop using JSON like this in bug 1875185. + if (!actualStr || actualStr == "undefined") { + return !expected.length; + } + return compareGraphicsDrivers(expected, JSON.parse(actualStr)); + }, + featuresJson(actualStr) { + const expected = getExpectedGraphicsFeatures(snapshot); + // If undefined is saved to the Glean value here, we'll get the string "undefined" (invalid JSON). + // We should stop using JSON like this in bug 1875185. + if (!actualStr || actualStr == "undefined") { + return !expected.length; + } + return areObjectsEqual(JSON.parse(actualStr), expected); + }, + hasTouchScreen, + monitorsJson(actualStr) { + const expected = gfxInfo.getMonitors(); + // If undefined is saved to the Glean value here, we'll get the string "undefined" (invalid JSON). + // We should stop using JSON like this in bug 1875185. + if (!actualStr || actualStr == "undefined") { + return !expected.length; + } + return areObjectsEqual(JSON.parse(actualStr), expected); + }, + }, + prefs: { + cookieBehavior: Services.prefs.getIntPref( + "network.cookie.cookieBehavior", + -1 + ), + forcedAcceleratedLayers: Services.prefs.getBoolPref( + "layers.acceleration.force-enabled", + false + ), + globalPrivacyControlEnabled: Services.prefs.getBoolPref( + "privacy.globalprivacycontrol.enabled", + false + ), + installtriggerEnabled: Services.prefs.getBoolPref( + "extensions.InstallTrigger.enabled", + false + ), + opaqueResponseBlocking: Services.prefs.getBoolPref( + "browser.opaqueResponseBlocking", + false + ), + resistFingerprintingEnabled: Services.prefs.getBoolPref( + "privacy.resistFingerprinting", + false + ), + softwareWebrender: Services.prefs.getBoolPref( + "gfx.webrender.software", + false + ), + thirdPartyCookieBlockingEnabled: Services.prefs.getBoolPref( + "network.cookie.cookieBehavior.optInPartitioning", + false + ), + thirdPartyCookieBlockingEnabledInPbm: Services.prefs.getBoolPref( + "network.cookie.cookieBehavior.optInPartitioning.pbmode", + false + ), + }, + security: { + antispyware: securityStringToArray(registeredAntiSpyware), + antivirus: securityStringToArray(registeredAntiVirus), + firewall: securityStringToArray(registeredFirewall), + }, + system: { + isTablet: getSysinfoProperty("tablet", false), + memory: Math.round(memorySizeBytes / 1024 / 1024), + }, + }; + + const tabInfo = await tab.linkedBrowser.ownerGlobal.SpecialPowers.spawn( + tab.linkedBrowser, + [], + async function () { + return { + devicePixelRatio: `${content.devicePixelRatio}`, + antitracking: { + blockList: "basic", + blockedOrigins: null, + isPrivateBrowsing: false, + hasTrackingContentBlocked: false, + hasMixedActiveContentBlocked: false, + hasMixedDisplayContentBlocked: false, + btpHasPurgedSite: false, + etpCategory: "standard", + }, + frameworks: { + fastclick: false, + marfeel: false, + mobify: false, + }, + languages: content.navigator.languages, + useragentString: content.navigator.userAgent, + }; + } + ); + + browserInfo.graphics.devicePixelRatio = tabInfo.devicePixelRatio; + delete tabInfo.devicePixelRatio; + + return { browserInfo, tabInfo }; +} + +function extractPingData(branch) { + const data = {}; + for (const [name, value] of Object.entries(branch)) { + data[name] = value.testGetValue(); + } + return data; +} + +function extractBrokenSiteReportFromGleanPing(Glean) { + const ping = extractPingData(Glean.brokenSiteReport); + ping.tabInfo = extractPingData(Glean.brokenSiteReportTabInfo); + ping.tabInfo.antitracking = extractPingData( + Glean.brokenSiteReportTabInfoAntitracking + ); + ping.tabInfo.frameworks = extractPingData( + Glean.brokenSiteReportTabInfoFrameworks + ); + ping.browserInfo = { + addons: Array.from(Glean.brokenSiteReportBrowserInfo.addons.testGetValue()), + app: extractPingData(Glean.brokenSiteReportBrowserInfoApp), + graphics: extractPingData(Glean.brokenSiteReportBrowserInfoGraphics), + experiments: Array.from( + Glean.brokenSiteReportBrowserInfo.experiments.testGetValue() + ), + prefs: extractPingData(Glean.brokenSiteReportBrowserInfoPrefs), + security: extractPingData(Glean.brokenSiteReportBrowserInfoSecurity), + system: extractPingData(Glean.brokenSiteReportBrowserInfoSystem), + }; + return ping; +} + +async function testSend(tab, menu, expectedOverrides = {}) { + const url = expectedOverrides.url ?? menu.win.gBrowser.currentURI.spec; + const description = expectedOverrides.description ?? ""; + const breakageCategory = expectedOverrides.breakageCategory ?? null; + + let rbs = await menu.openAndPrefillReportBrokenSite(url, description); + + const snapshot = await Troubleshoot.snapshot(); + const expected = await getExpectedWebCompatInfo(tab, snapshot); + + expected.url = url; + expected.description = description; + expected.breakageCategory = breakageCategory; + + if (expectedOverrides.addons) { + expected.browserInfo.addons = expectedOverrides.addons; + } + + if (expectedOverrides.experiments) { + expected.browserInfo.experiments = expectedOverrides.experiments; + } + + if (expectedOverrides.antitracking) { + expected.tabInfo.antitracking = expectedOverrides.antitracking; + + if (expectedOverrides.antitracking.blockedOrigins) { + rbs.blockedTrackersCheckbox = true; + } + } + + if (expectedOverrides.frameworks) { + expected.tabInfo.frameworks = expectedOverrides.frameworks; + } + + if (breakageCategory) { + rbs.chooseReason(breakageCategory); + } + + Services.fog.testResetFOG(); + await GleanPings.brokenSiteReport.testSubmission( + () => { + const ping = extractBrokenSiteReportFromGleanPing(Glean); + + // sanity checks + const { browserInfo, tabInfo } = ping; + ok(ping.url?.length, "Got a URL"); + ok( + ["basic", "strict"].includes(tabInfo.antitracking.blockList), + "Got a blockList" + ); + if (rbs.blockedTrackersCheckbox.checked) { + ok( + Array.isArray(tabInfo.antitracking.blockedOrigins), + "Got an array for blockedOrigins" + ); + } else { + ok(!tabInfo.antitracking.blockedOrigins, "No blockedOrigins included"); + } + ok(tabInfo.useragentString?.length, "Got a final UA string"); + ok( + browserInfo.app.defaultUseragentString?.length, + "Got a default UA string" + ); + + filterFrameworkDetectorFails(ping.tabInfo, expected.tabInfo); + + ok(areObjectsEqual(ping, expected), "ping matches expectations"); + }, + () => rbs.clickSend() + ); + + await rbs.clickOkay(); + + const telemetry = Glean.webcompatreporting.send.testGetValue(); + is(telemetry?.length, 1, "Got a 'send' telemetry event"); + is( + telemetry[0].extra.sent_with_blocked_trackers, + String(!!expectedOverrides.antitracking?.blockedOrigins), + "Got correct 'sent_with_blocked_trackers' flag" + ); + + // re-opening the panel, the url and description should be reset + rbs = await menu.openReportBrokenSite(); + rbs.isMainViewResetToCurrentTab(); + ok( + !rbs.blockedTrackersCheckbox.checked, + "blocked trackers checkbox is reset" + ); + rbs.close(); +} diff --git a/src/zen/tests/mochitests/reportbrokensite/sendMoreInfoTestEndpoint.html b/src/zen/tests/mochitests/reportbrokensite/sendMoreInfoTestEndpoint.html new file mode 100644 index 000000000..39b1b3d25 --- /dev/null +++ b/src/zen/tests/mochitests/reportbrokensite/sendMoreInfoTestEndpoint.html @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/src/zen/tests/mochitests/reportbrokensite/send_more_info.js b/src/zen/tests/mochitests/reportbrokensite/send_more_info.js new file mode 100644 index 000000000..f5d28248d --- /dev/null +++ b/src/zen/tests/mochitests/reportbrokensite/send_more_info.js @@ -0,0 +1,307 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* Helper methods for testing the "send more info" link + * of the Report Broken Site feature. + */ + +/* import-globals-from head.js */ +/* import-globals-from send.js */ + +"use strict"; + +Services.scriptloader.loadSubScript( + getRootDirectory(gTestPath) + "send.js", + this +); + +async function reformatExpectedWebCompatInfo(tab, overrides) { + const gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); + const snapshot = await Troubleshoot.snapshot(); + const expected = await getExpectedWebCompatInfo(tab, snapshot, true); + const { browserInfo, tabInfo } = expected; + const { app, graphics, prefs, security } = browserInfo; + const { + applicationName, + defaultUseragentString, + fissionEnabled, + osArchitecture, + osName, + osVersion, + updateChannel, + version, + } = app; + const { devicePixelRatio, hasTouchScreen } = graphics; + const { antitracking, languages, useragentString } = tabInfo; + + const addons = overrides.addons || []; + const experiments = overrides.experiments || []; + const atOverrides = overrides.antitracking; + const blockList = atOverrides?.blockList ?? antitracking.blockList; + const blockedOrigins = + atOverrides?.blockedOrigins ?? antitracking.blockedOrigins ?? []; + const hasMixedActiveContentBlocked = + atOverrides?.hasMixedActiveContentBlocked ?? + antitracking.hasMixedActiveContentBlocked; + const hasMixedDisplayContentBlocked = + atOverrides?.hasMixedDisplayContentBlocked ?? + antitracking.hasMixedDisplayContentBlocked; + const hasTrackingContentBlocked = + atOverrides?.hasTrackingContentBlocked ?? + antitracking.hasTrackingContentBlocked; + const isPrivateBrowsing = + atOverrides?.isPrivateBrowsing ?? antitracking.isPrivateBrowsing; + const btpHasPurgedSite = + atOverrides?.btpHasPurgedSite ?? antitracking.btpHasPurgedSite; + const etpCategory = atOverrides?.etpCategory ?? antitracking.etpCategory; + + const extra_labels = []; + const frameworks = overrides.frameworks ?? { + fastclick: false, + mobify: false, + marfeel: false, + }; + + // ignore the console log unless explicily testing for it. + const consoleLog = overrides.consoleLog ?? (() => true); + + const finalPrefs = {}; + for (const [key, pref] of Object.entries({ + cookieBehavior: "network.cookie.cookieBehavior", + forcedAcceleratedLayers: "layers.acceleration.force-enabled", + globalPrivacyControlEnabled: "privacy.globalprivacycontrol.enabled", + installtriggerEnabled: "extensions.InstallTrigger.enabled", + opaqueResponseBlocking: "browser.opaqueResponseBlocking", + resistFingerprintingEnabled: "privacy.resistFingerprinting", + softwareWebrender: "gfx.webrender.software", + thirdPartyCookieBlockingEnabled: + "network.cookie.cookieBehavior.optInPartitioning", + thirdPartyCookieBlockingEnabledInPbm: + "network.cookie.cookieBehavior.optInPartitioning.pbmode", + })) { + if (key in prefs) { + finalPrefs[pref] = prefs[key]; + } + } + + const reformatted = { + blockList, + details: { + additionalData: { + addons, + applicationName, + blockList, + blockedOrigins, + buildId: snapshot.application.buildID, + devicePixelRatio: parseInt(devicePixelRatio), + experiments, + finalUserAgent: useragentString, + fissionEnabled, + gfxData: { + devices(actual) { + const devices = getExpectedGraphicsDevices(snapshot); + return compareGraphicsDevices(devices, actual); + }, + drivers(actual) { + const drvs = getExpectedGraphicsDrivers(snapshot); + return compareGraphicsDrivers(drvs, actual); + }, + features(actual) { + const features = getExpectedGraphicsFeatures(snapshot); + return areObjectsEqual(actual, features); + }, + hasTouchScreen, + monitors(actual) { + return areObjectsEqual(actual, gfxInfo.getMonitors()); + }, + }, + hasMixedActiveContentBlocked, + hasMixedDisplayContentBlocked, + hasTrackingContentBlocked, + btpHasPurgedSite, + isPB: isPrivateBrowsing, + etpCategory, + languages, + locales: snapshot.intl.localeService.available, + memoryMB: browserInfo.system.memory, + osArchitecture, + osName, + osVersion, + prefs: finalPrefs, + version, + }, + blockList, + channel: updateChannel, + consoleLog, + defaultUserAgent: defaultUseragentString, + frameworks, + hasTouchScreen, + "gfx.webrender.software": prefs.softwareWebrender, + "mixed active content blocked": hasMixedActiveContentBlocked, + "mixed passive content blocked": hasMixedDisplayContentBlocked, + "tracking content blocked": hasTrackingContentBlocked + ? `true (${blockList})` + : "false", + "btp has purged site": btpHasPurgedSite, + }, + extra_labels, + src: "desktop-reporter", + utm_campaign: "report-broken-site", + utm_source: "desktop-reporter", + }; + + const { gfxData } = reformatted.details.additionalData; + for (const optional of [ + "directWriteEnabled", + "directWriteVersion", + "clearTypeParameters", + "targetFrameRate", + ]) { + if (optional in snapshot.graphics) { + gfxData[optional] = snapshot.graphics[optional]; + } + } + + // We only care about this pref on Linux right now on webcompat.com. + if (AppConstants.platform != "linux") { + delete finalPrefs["layers.acceleration.force-enabled"]; + } else { + reformatted.details["layers.acceleration.force-enabled"] = + finalPrefs["layers.acceleration.force-enabled"]; + } + + // Only bother adding the security key if it has any data + if (Object.values(security).filter(e => e).length) { + reformatted.details.additionalData.sec = security; + } + + const expectedCodecs = snapshot.media.codecSupportInfo + .replaceAll(" NONE", "") + .split("\n") + .sort() + .join("\n"); + if (expectedCodecs) { + reformatted.details.additionalData.gfxData.codecSupport = rawActual => { + const actual = Object.entries(rawActual) + .map( + ([ + name, + { hardwareDecode, softwareDecode, hardwareEncode, softwareEncode }, + ]) => + ( + `${name} ` + + `${softwareDecode ? "SWDEC " : ""}` + + `${hardwareDecode ? "HWDEC " : ""}` + + `${softwareEncode ? "SWENC " : ""}` + + `${hardwareEncode ? "HWENC " : ""}` + ).trim() + ) + .sort() + .join("\n"); + return areObjectsEqual(actual, expectedCodecs); + }; + } + + if (blockList != "basic") { + extra_labels.push(`type-tracking-protection-${blockList}`); + } + + if (overrides.expectNoTabDetails) { + delete reformatted.details.frameworks; + delete reformatted.details.consoleLog; + delete reformatted.details["mixed active content blocked"]; + delete reformatted.details["mixed passive content blocked"]; + delete reformatted.details["tracking content blocked"]; + delete reformatted.details["btp has purged site"]; + } else { + const { fastclick, mobify, marfeel } = frameworks; + if (fastclick) { + extra_labels.push("type-fastclick"); + reformatted.details.fastclick = true; + } + if (mobify) { + extra_labels.push("type-mobify"); + reformatted.details.mobify = true; + } + if (marfeel) { + extra_labels.push("type-marfeel"); + reformatted.details.marfeel = true; + } + } + + extra_labels.sort(); + + return reformatted; +} + +async function testSendMoreInfo(tab, menu, expectedOverrides = {}) { + const url = expectedOverrides.url ?? menu.win.gBrowser.currentURI.spec; + const description = expectedOverrides.description ?? ""; + + let rbs = await menu.openAndPrefillReportBrokenSite(url, description); + + const receivedData = await rbs.clickSendMoreInfo(); + await checkWebcompatComPayload( + tab, + url, + description, + expectedOverrides, + receivedData + ); + + // re-opening the panel, the url and description should be reset + rbs = await menu.openReportBrokenSite(); + rbs.isMainViewResetToCurrentTab(); + rbs.close(); +} + +async function testWebcompatComFallback(tab, menu) { + const url = menu.win.gBrowser.currentURI.spec; + const receivedData = + await menu.clickReportBrokenSiteAndAwaitWebCompatTabData(); + await checkWebcompatComPayload(tab, url, "", {}, receivedData); + menu.close(); +} + +async function checkWebcompatComPayload( + tab, + url, + description, + expectedOverrides, + receivedData +) { + const expected = await reformatExpectedWebCompatInfo(tab, expectedOverrides); + expected.url = url; + expected.description = description; + + // sanity checks + const { message } = receivedData; + const { details } = message; + const { additionalData } = details; + ok(message.url?.length, "Got a URL"); + ok(["basic", "strict"].includes(details.blockList), "Got a blockList"); + ok(additionalData.applicationName?.length, "Got an app name"); + ok(additionalData.osArchitecture?.length, "Got an OS arch"); + ok(additionalData.osName?.length, "Got an OS name"); + ok(additionalData.osVersion?.length, "Got an OS version"); + ok(additionalData.version?.length, "Got an app version"); + ok(details.channel?.length, "Got an app channel"); + ok(details.defaultUserAgent?.length, "Got a default UA string"); + ok(additionalData.finalUserAgent?.length, "Got a final UA string"); + + // If we're sending any tab-specific data (which includes console logs), + // check that there is also a valid screenshot. + if ("consoleLog" in details) { + const isScreenshotValid = await new Promise(done => { + var image = new Image(); + image.onload = () => done(image.width > 0); + image.onerror = () => done(false); + image.src = receivedData.screenshot; + }); + ok(isScreenshotValid, "Got a valid screenshot"); + } + + filterFrameworkDetectorFails(message.details, expected.details); + + ok(areObjectsEqual(message, expected), "sent info matches expectations"); +} diff --git a/src/zen/tests/mochitests/safebrowsing/browser.toml b/src/zen/tests/mochitests/safebrowsing/browser.toml new file mode 100644 index 000000000..8745e7efc --- /dev/null +++ b/src/zen/tests/mochitests/safebrowsing/browser.toml @@ -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"] diff --git a/src/zen/tests/mochitests/safebrowsing/browser_bug400731.js b/src/zen/tests/mochitests/safebrowsing/browser_bug400731.js new file mode 100644 index 000000000..e3861bc7a --- /dev/null +++ b/src/zen/tests/mochitests/safebrowsing/browser_bug400731.js @@ -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(); +}); diff --git a/src/zen/tests/mochitests/safebrowsing/browser_bug415846.js b/src/zen/tests/mochitests/safebrowsing/browser_bug415846.js new file mode 100644 index 000000000..8cd2ee36f --- /dev/null +++ b/src/zen/tests/mochitests/safebrowsing/browser_bug415846.js @@ -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" + ); + }); +}); diff --git a/src/zen/tests/mochitests/safebrowsing/browser_mixedcontent_aboutblocked.js b/src/zen/tests/mochitests/safebrowsing/browser_mixedcontent_aboutblocked.js new file mode 100644 index 000000000..5057cef19 --- /dev/null +++ b/src/zen/tests/mochitests/safebrowsing/browser_mixedcontent_aboutblocked.js @@ -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!"); + } + ); +}); diff --git a/src/zen/tests/mochitests/safebrowsing/browser_whitelisted.js b/src/zen/tests/mochitests/safebrowsing/browser_whitelisted.js new file mode 100644 index 000000000..eb217d618 --- /dev/null +++ b/src/zen/tests/mochitests/safebrowsing/browser_whitelisted.js @@ -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); +}); diff --git a/src/zen/tests/mochitests/safebrowsing/empty_file.html b/src/zen/tests/mochitests/safebrowsing/empty_file.html new file mode 100644 index 000000000..0dc101b53 --- /dev/null +++ b/src/zen/tests/mochitests/safebrowsing/empty_file.html @@ -0,0 +1 @@ + diff --git a/src/zen/tests/mochitests/safebrowsing/head.js b/src/zen/tests/mochitests/safebrowsing/head.js new file mode 100644 index 000000000..ffbdb18d1 --- /dev/null +++ b/src/zen/tests/mochitests/safebrowsing/head.js @@ -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(); diff --git a/src/zen/tests/mochitests/shell/browser.toml b/src/zen/tests/mochitests/shell/browser.toml new file mode 100644 index 000000000..6439fa043 --- /dev/null +++ b/src/zen/tests/mochitests/shell/browser.toml @@ -0,0 +1,103 @@ +[DEFAULT] + +["browser_1119088.js"] +disabled="Disabled by import_external_tests.py" +support-files = ["mac_desktop_image.py"] +run-if = ["os == 'mac'"] +tags = "os_integration" +skip-if = ["os == 'mac' && os_version == '14.70' && processor == 'x86_64'"] # Bug 1869703 + +["browser_420786.js"] +run-if = ["os == 'linux'"] + +["browser_633221.js"] +run-if = ["os == 'linux'"] + +["browser_createWindowsShortcut.js"] +run-if = ["os == 'win'"] + +["browser_doesAppNeedPin.js"] + +["browser_headless_screenshot_1.js"] +support-files = [ + "head.js", + "headless.html", +] +skip-if = [ + "os == 'win'", + "ccov", + "tsan", # Bug 1429950, Bug 1583315, Bug 1696109, Bug 1701449 +] +tags = "os_integration" + +["browser_headless_screenshot_2.js"] +support-files = [ + "head.js", + "headless.html", +] +skip-if = [ + "os == 'win'", + "ccov", + "tsan", # Bug 1429950, Bug 1583315, Bug 1696109, Bug 1701449 +] + +["browser_headless_screenshot_3.js"] +support-files = [ + "head.js", + "headless.html", +] +skip-if = [ + "os == 'win'", + "ccov", + "tsan", # Bug 1429950, Bug 1583315, Bug 1696109, Bug 1701449 +] + +["browser_headless_screenshot_4.js"] +support-files = [ + "head.js", + "headless.html", +] +skip-if = [ + "os == 'win'", + "ccov", + "tsan", # Bug 1429950, Bug 1583315, Bug 1696109, Bug 1701449 +] + +["browser_headless_screenshot_cross_origin.js"] +support-files = [ + "head.js", + "headless_cross_origin.html", + "headless_iframe.html", +] +skip-if = [ + "os == 'win'", + "ccov", + "tsan", # Bug 1429950, Bug 1583315, Bug 1696109, Bug 1701449 +] + +["browser_headless_screenshot_redirect.js"] +support-files = [ + "head.js", + "headless.html", + "headless_redirect.html", + "headless_redirect.html^headers^", +] +skip-if = [ + "os == 'win'", + "ccov", + "tsan", # Bug 1429950, Bug 1583315, Bug 1696109, Bug 1701449 +] + +["browser_processAUMID.js"] +run-if = ["os == 'win'"] + +["browser_setDefaultBrowser.js"] +tags = "os_integration" + +["browser_setDefaultPDFHandler.js"] +run-if = ["os == 'win'"] +tags = "os_integration" + +["browser_setDesktopBackgroundPreview.js"] +disabled="Disabled by import_external_tests.py" +tags = "os_integration" diff --git a/src/zen/tests/mochitests/shell/browser_1119088.js b/src/zen/tests/mochitests/shell/browser_1119088.js new file mode 100644 index 000000000..6702769a3 --- /dev/null +++ b/src/zen/tests/mochitests/shell/browser_1119088.js @@ -0,0 +1,173 @@ +// Where we save the desktop background to (~/Pictures). +const NS_OSX_PICTURE_DOCUMENTS_DIR = "Pct"; + +// Paths used to run the CLI command (python script) that is used to +// 1) check the desktop background image matches what we set it to via +// nsIShellService::setDesktopBackground() and +// 2) revert the desktop background image to the OS default + +let kPythonPath = "/usr/bin/python"; +if (AppConstants.isPlatformAndVersionAtLeast("macosx", 23.0)) { + kPythonPath = "/usr/local/bin/python3"; +} + +const kDesktopCheckerScriptPath = + "browser/browser/components/shell/test/mac_desktop_image.py"; +const kDefaultBackgroundImage = + "/System/Library/Desktop Pictures/Solid Colors/Teal.png"; + +ChromeUtils.defineESModuleGetters(this, { + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", +}); + +function getPythonExecutableFile() { + let python = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + info(`Using python at location ${kPythonPath}`); + python.initWithPath(kPythonPath); + return python; +} + +function createProcess() { + return Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess); +} + +// Use a CLI command to set the desktop background to |imagePath|. Returns the +// exit code of the CLI command which reflects whether or not the background +// image was successfully set. Returns 0 on success. +function setDesktopBackgroundCLI(imagePath) { + let setBackgroundProcess = createProcess(); + setBackgroundProcess.init(getPythonExecutableFile()); + let args = [ + kDesktopCheckerScriptPath, + "--verbose", + "--set-background-image", + imagePath, + ]; + setBackgroundProcess.run(true, args, args.length); + return setBackgroundProcess.exitValue; +} + +// Check the desktop background is |imagePath| using a CLI command. +// Returns the exit code of the CLI command which reflects whether or not +// the provided image path matches the path of the current desktop background +// image. A return value of 0 indicates success/match. +function checkDesktopBackgroundCLI(imagePath) { + let checkBackgroundProcess = createProcess(); + checkBackgroundProcess.init(getPythonExecutableFile()); + let args = [ + kDesktopCheckerScriptPath, + "--verbose", + "--check-background-image", + imagePath, + ]; + checkBackgroundProcess.run(true, args, args.length); + return checkBackgroundProcess.exitValue; +} + +// Use the python script to set/check the desktop background is |imagePath| +function setAndCheckDesktopBackgroundCLI(imagePath) { + Assert.ok(FileUtils.File(imagePath).exists(), `${imagePath} exists`); + + let setExitCode = setDesktopBackgroundCLI(imagePath); + Assert.equal(setExitCode, 0, `Setting background via CLI to ${imagePath}`); + + let checkExitCode = checkDesktopBackgroundCLI(imagePath); + Assert.equal(checkExitCode, 0, `Checking background via CLI is ${imagePath}`); +} + +// Restore the automation default background image. i.e., the default used +// in the automated test environment, not the OS default. +function restoreDefaultBackground() { + let defaultBackgroundPath; + defaultBackgroundPath = kDefaultBackgroundImage; + setAndCheckDesktopBackgroundCLI(defaultBackgroundPath); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["test.wait300msAfterTabSwitch", true]], + }); +}); + +/** + * Tests "Set As Desktop Background" platform implementation on macOS. + * + * Sets the desktop background image to the browser logo from the about:logo + * page and verifies it was set successfully. Setting the desktop background + * (which uses the nsIShellService::setDesktopBackground() interface method) + * downloads the image to ~/Pictures using a unique file name and sets the + * desktop background to the downloaded file leaving the download in place. + * After setDesktopBackground() is called, the test uses a python script to + * validate that the current desktop background is in fact set to the + * downloaded logo. + */ +add_task(async function () { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:logo", + }, + async () => { + let dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService( + Ci.nsIDirectoryServiceProvider + ); + let uuidGenerator = Services.uuid; + let shellSvc = Cc["@mozilla.org/browser/shell-service;1"].getService( + Ci.nsIShellService + ); + + // Ensure we are starting with the default background. Log a + // failure if we can not set the background to the default, but + // ignore the case where the background is not already set as that + // that may be due to a previous test failure. + restoreDefaultBackground(); + + // Generate a UUID (with non-alphanumberic characters removed) to build + // up a filename for the desktop background. Use a UUID to distinguish + // between runs so we won't be confused by images that were not properly + // cleaned up after previous runs. + let uuid = uuidGenerator.generateUUID().toString().replace(/\W/g, ""); + + // Set the background image path to be $HOME/Pictures/.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); + } + ); +}); diff --git a/src/zen/tests/mochitests/shell/browser_420786.js b/src/zen/tests/mochitests/shell/browser_420786.js new file mode 100644 index 000000000..b9becb49c --- /dev/null +++ b/src/zen/tests/mochitests/shell/browser_420786.js @@ -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); + } + } + ); +}); diff --git a/src/zen/tests/mochitests/shell/browser_633221.js b/src/zen/tests/mochitests/shell/browser_633221.js new file mode 100644 index 000000000..dbc66e286 --- /dev/null +++ b/src/zen/tests/mochitests/shell/browser_633221.js @@ -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" + ); +} diff --git a/src/zen/tests/mochitests/shell/browser_createWindowsShortcut.js b/src/zen/tests/mochitests/shell/browser_createWindowsShortcut.js new file mode 100644 index 000000000..0b951bde2 --- /dev/null +++ b/src/zen/tests/mochitests/shell/browser_createWindowsShortcut.js @@ -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; + } +} diff --git a/src/zen/tests/mochitests/shell/browser_doesAppNeedPin.js b/src/zen/tests/mochitests/shell/browser_doesAppNeedPin.js new file mode 100644 index 000000000..2a261988a --- /dev/null +++ b/src/zen/tests/mochitests/shell/browser_doesAppNeedPin.js @@ -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" + ); +}); diff --git a/src/zen/tests/mochitests/shell/browser_headless_screenshot_1.js b/src/zen/tests/mochitests/shell/browser_headless_screenshot_1.js new file mode 100644 index 000000000..b5f84f0f6 --- /dev/null +++ b/src/zen/tests/mochitests/shell/browser_headless_screenshot_1.js @@ -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 + ); +}); diff --git a/src/zen/tests/mochitests/shell/browser_headless_screenshot_2.js b/src/zen/tests/mochitests/shell/browser_headless_screenshot_2.js new file mode 100644 index 000000000..871895b45 --- /dev/null +++ b/src/zen/tests/mochitests/shell/browser_headless_screenshot_2.js @@ -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 + ); +}); diff --git a/src/zen/tests/mochitests/shell/browser_headless_screenshot_3.js b/src/zen/tests/mochitests/shell/browser_headless_screenshot_3.js new file mode 100644 index 000000000..b55635652 --- /dev/null +++ b/src/zen/tests/mochitests/shell/browser_headless_screenshot_3.js @@ -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 + ); +}); diff --git a/src/zen/tests/mochitests/shell/browser_headless_screenshot_4.js b/src/zen/tests/mochitests/shell/browser_headless_screenshot_4.js new file mode 100644 index 000000000..4b93ba516 --- /dev/null +++ b/src/zen/tests/mochitests/shell/browser_headless_screenshot_4.js @@ -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 + ); +}); diff --git a/src/zen/tests/mochitests/shell/browser_headless_screenshot_cross_origin.js b/src/zen/tests/mochitests/shell/browser_headless_screenshot_cross_origin.js new file mode 100644 index 000000000..954777358 --- /dev/null +++ b/src/zen/tests/mochitests/shell/browser_headless_screenshot_cross_origin.js @@ -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 + ); +}); diff --git a/src/zen/tests/mochitests/shell/browser_headless_screenshot_redirect.js b/src/zen/tests/mochitests/shell/browser_headless_screenshot_redirect.js new file mode 100644 index 000000000..c50b847b6 --- /dev/null +++ b/src/zen/tests/mochitests/shell/browser_headless_screenshot_redirect.js @@ -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 + ); +}); diff --git a/src/zen/tests/mochitests/shell/browser_processAUMID.js b/src/zen/tests/mochitests/shell/browser_processAUMID.js new file mode 100644 index 000000000..cf98316f8 --- /dev/null +++ b/src/zen/tests/mochitests/shell/browser_processAUMID.js @@ -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" + ); +}); diff --git a/src/zen/tests/mochitests/shell/browser_setDefaultBrowser.js b/src/zen/tests/mochitests/shell/browser_setDefaultBrowser.js new file mode 100644 index 000000000..731bb7f1e --- /dev/null +++ b/src/zen/tests/mochitests/shell/browser_setDefaultBrowser.js @@ -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(); + } +); diff --git a/src/zen/tests/mochitests/shell/browser_setDefaultPDFHandler.js b/src/zen/tests/mochitests/shell/browser_setDefaultPDFHandler.js new file mode 100644 index 000000000..443c8f69f --- /dev/null +++ b/src/zen/tests/mochitests/shell/browser_setDefaultPDFHandler.js @@ -0,0 +1,279 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +ChromeUtils.defineESModuleGetters(this, { + ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", + NimbusTestUtils: "resource://testing-common/NimbusTestUtils.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", +}); + +const setDefaultBrowserUserChoiceStub = sinon.stub(); +const setDefaultExtensionHandlersUserChoiceStub = sinon + .stub() + .callsFake(() => Promise.resolve()); + +const defaultAgentStub = sinon.stub(ShellService, "defaultAgent").value({ + setDefaultBrowserUserChoiceAsync: setDefaultBrowserUserChoiceStub, + setDefaultExtensionHandlersUserChoice: + setDefaultExtensionHandlersUserChoiceStub, +}); + +XPCOMUtils.defineLazyServiceGetter( + this, + "XreDirProvider", + "@mozilla.org/xre/directory-provider;1", + Ci.nsIXREDirProvider +); + +const _userChoiceImpossibleTelemetryResultStub = sinon + .stub(ShellService, "_userChoiceImpossibleTelemetryResult") + .callsFake(() => null); + +// Ensure we don't fall back to a real implementation. +const setDefaultStub = sinon.stub(); +// We'll dynamically update this as needed during the tests. +const queryCurrentDefaultHandlerForStub = sinon.stub(); +const shellStub = sinon.stub(ShellService, "shellService").value({ + setDefaultBrowser: setDefaultStub, + queryCurrentDefaultHandlerFor: queryCurrentDefaultHandlerForStub, +}); + +registerCleanupFunction(() => { + defaultAgentStub.restore(); + _userChoiceImpossibleTelemetryResultStub.restore(); + shellStub.restore(); +}); + +add_task(async function ready() { + await ExperimentAPI.ready(); +}); + +// Everything here is Windows. +Assert.equal(AppConstants.platform, "win", "Platform is Windows"); + +add_task(async function remoteEnableWithPDF() { + let doCleanup = await NimbusTestUtils.enrollWithFeatureConfig( + { + featureId: NimbusFeatures.shellService.featureId, + value: { + setDefaultBrowserUserChoice: true, + setDefaultPDFHandlerOnlyReplaceBrowsers: false, + setDefaultPDFHandler: true, + enabled: true, + }, + }, + { isRollout: true } + ); + + Assert.equal( + NimbusFeatures.shellService.getVariable("setDefaultBrowserUserChoice"), + true + ); + Assert.equal( + NimbusFeatures.shellService.getVariable("setDefaultPDFHandler"), + true + ); + + setDefaultBrowserUserChoiceStub.resetHistory(); + await ShellService.setDefaultBrowser(); + + const aumi = XreDirProvider.getInstallHash(); + Assert.ok(setDefaultBrowserUserChoiceStub.called); + Assert.deepEqual(setDefaultBrowserUserChoiceStub.firstCall.args, [ + aumi, + [".pdf", "FirefoxPDF"], + ]); + + await doCleanup(); +}); + +add_task(async function remoteEnableWithPDF_testOnlyReplaceBrowsers() { + let doCleanup = await NimbusTestUtils.enrollWithFeatureConfig( + { + featureId: NimbusFeatures.shellService.featureId, + value: { + setDefaultBrowserUserChoice: true, + setDefaultPDFHandlerOnlyReplaceBrowsers: true, + setDefaultPDFHandler: true, + enabled: true, + }, + }, + { isRollout: true } + ); + + Assert.equal( + NimbusFeatures.shellService.getVariable("setDefaultBrowserUserChoice"), + true + ); + Assert.equal( + NimbusFeatures.shellService.getVariable("setDefaultPDFHandler"), + true + ); + Assert.equal( + NimbusFeatures.shellService.getVariable( + "setDefaultPDFHandlerOnlyReplaceBrowsers" + ), + true + ); + + const aumi = XreDirProvider.getInstallHash(); + + // We'll take the default from a missing association or a known browser. + for (let progId of ["", "MSEdgePDF"]) { + queryCurrentDefaultHandlerForStub.callsFake(() => progId); + + setDefaultBrowserUserChoiceStub.resetHistory(); + await ShellService.setDefaultBrowser(); + + Assert.ok(setDefaultBrowserUserChoiceStub.called); + Assert.deepEqual( + setDefaultBrowserUserChoiceStub.firstCall.args, + [aumi, [".pdf", "FirefoxPDF"]], + `Will take default from missing association or known browser with ProgID '${progId}'` + ); + } + + // But not from a non-browser. + queryCurrentDefaultHandlerForStub.callsFake(() => "Acrobat.Document.DC"); + + setDefaultBrowserUserChoiceStub.resetHistory(); + await ShellService.setDefaultBrowser(); + + Assert.ok(setDefaultBrowserUserChoiceStub.called); + Assert.deepEqual( + setDefaultBrowserUserChoiceStub.firstCall.args, + [aumi, []], + `Will not take default from non-browser` + ); + + await doCleanup(); +}); + +add_task(async function remoteEnableWithoutPDF() { + let doCleanup = await NimbusTestUtils.enrollWithFeatureConfig( + { + featureId: NimbusFeatures.shellService.featureId, + value: { + setDefaultBrowserUserChoice: true, + setDefaultPDFHandler: false, + enabled: true, + }, + }, + { isRollout: true } + ); + + Assert.equal( + NimbusFeatures.shellService.getVariable("setDefaultBrowserUserChoice"), + true + ); + Assert.equal( + NimbusFeatures.shellService.getVariable("setDefaultPDFHandler"), + false + ); + + setDefaultBrowserUserChoiceStub.resetHistory(); + await ShellService.setDefaultBrowser(); + + const aumi = XreDirProvider.getInstallHash(); + Assert.ok(setDefaultBrowserUserChoiceStub.called); + Assert.deepEqual(setDefaultBrowserUserChoiceStub.firstCall.args, [aumi, []]); + + await doCleanup(); +}); + +add_task(async function remoteDisable() { + let doCleanup = await NimbusTestUtils.enrollWithFeatureConfig( + { + featureId: NimbusFeatures.shellService.featureId, + value: { + setDefaultBrowserUserChoice: false, + setDefaultPDFHandler: true, + enabled: false, + }, + }, + { isRollout: true } + ); + + Assert.equal( + NimbusFeatures.shellService.getVariable("setDefaultBrowserUserChoice"), + false + ); + Assert.equal( + NimbusFeatures.shellService.getVariable("setDefaultPDFHandler"), + true + ); + + setDefaultBrowserUserChoiceStub.resetHistory(); + await ShellService.setDefaultBrowser(); + + Assert.ok(setDefaultBrowserUserChoiceStub.notCalled); + Assert.ok(setDefaultStub.called); + + await doCleanup(); +}); + +add_task(async function test_setAsDefaultPDFHandler_knownBrowser() { + const sandbox = sinon.createSandbox(); + + const aumi = XreDirProvider.getInstallHash(); + const expectedArguments = [aumi, [".pdf", "FirefoxPDF"]]; + + try { + const pdfHandlerResult = { registered: true, knownBrowser: true }; + sandbox + .stub(ShellService, "getDefaultPDFHandler") + .returns(pdfHandlerResult); + + info("Testing setAsDefaultPDFHandler(true) when knownBrowser = true"); + ShellService.setAsDefaultPDFHandler(true); + Assert.ok( + setDefaultExtensionHandlersUserChoiceStub.called, + "Called default browser agent" + ); + Assert.deepEqual( + setDefaultExtensionHandlersUserChoiceStub.firstCall.args, + expectedArguments, + "Called default browser agent with expected arguments" + ); + setDefaultExtensionHandlersUserChoiceStub.resetHistory(); + + info("Testing setAsDefaultPDFHandler(false) when knownBrowser = true"); + ShellService.setAsDefaultPDFHandler(false); + Assert.ok( + setDefaultExtensionHandlersUserChoiceStub.called, + "Called default browser agent" + ); + Assert.deepEqual( + setDefaultExtensionHandlersUserChoiceStub.firstCall.args, + expectedArguments, + "Called default browser agent with expected arguments" + ); + setDefaultExtensionHandlersUserChoiceStub.resetHistory(); + + pdfHandlerResult.knownBrowser = false; + + info("Testing setAsDefaultPDFHandler(true) when knownBrowser = false"); + ShellService.setAsDefaultPDFHandler(true); + Assert.ok( + setDefaultExtensionHandlersUserChoiceStub.notCalled, + "Did not call default browser agent" + ); + setDefaultExtensionHandlersUserChoiceStub.resetHistory(); + + info("Testing setAsDefaultPDFHandler(false) when knownBrowser = false"); + ShellService.setAsDefaultPDFHandler(false); + Assert.ok( + setDefaultExtensionHandlersUserChoiceStub.called, + "Called default browser agent" + ); + Assert.deepEqual( + setDefaultExtensionHandlersUserChoiceStub.firstCall.args, + expectedArguments, + "Called default browser agent with expected arguments" + ); + setDefaultExtensionHandlersUserChoiceStub.resetHistory(); + } finally { + sandbox.restore(); + } +}); diff --git a/src/zen/tests/mochitests/shell/browser_setDesktopBackgroundPreview.js b/src/zen/tests/mochitests/shell/browser_setDesktopBackgroundPreview.js new file mode 100644 index 000000000..b847d0998 --- /dev/null +++ b/src/zen/tests/mochitests/shell/browser_setDesktopBackgroundPreview.js @@ -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; + } + ); +}); diff --git a/src/zen/tests/mochitests/shell/gtest/LimitedAccessFeatureTests.cpp b/src/zen/tests/mochitests/shell/gtest/LimitedAccessFeatureTests.cpp new file mode 100644 index 000000000..ddf3a7548 --- /dev/null +++ b/src/zen/tests/mochitests/shell/gtest/LimitedAccessFeatureTests.cpp @@ -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)); +} diff --git a/src/zen/tests/mochitests/shell/gtest/ShellLinkTests.cpp b/src/zen/tests/mochitests/shell/gtest/ShellLinkTests.cpp new file mode 100644 index 000000000..2cf1b8ed6 --- /dev/null +++ b/src/zen/tests/mochitests/shell/gtest/ShellLinkTests.cpp @@ -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 exe; + nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(exe)); + ASSERT_TRUE(NS_SUCCEEDED(rv)); + + RefPtr 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 exe; + nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(exe)); + ASSERT_TRUE(NS_SUCCEEDED(rv)); + + RefPtr 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); +} diff --git a/src/zen/tests/mochitests/shell/gtest/moz.build b/src/zen/tests/mochitests/shell/gtest/moz.build new file mode 100644 index 000000000..e33786fad --- /dev/null +++ b/src/zen/tests/mochitests/shell/gtest/moz.build @@ -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" diff --git a/src/zen/tests/mochitests/shell/head.js b/src/zen/tests/mochitests/shell/head.js new file mode 100644 index 000000000..a6cebb3d1 --- /dev/null +++ b/src/zen/tests/mochitests/shell/head.js @@ -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); +} diff --git a/src/zen/tests/mochitests/shell/headless.html b/src/zen/tests/mochitests/shell/headless.html new file mode 100644 index 000000000..bbde89507 --- /dev/null +++ b/src/zen/tests/mochitests/shell/headless.html @@ -0,0 +1,6 @@ + + + +Hi + + diff --git a/src/zen/tests/mochitests/shell/headless_cross_origin.html b/src/zen/tests/mochitests/shell/headless_cross_origin.html new file mode 100644 index 000000000..3bb09aa5d --- /dev/null +++ b/src/zen/tests/mochitests/shell/headless_cross_origin.html @@ -0,0 +1,7 @@ + + + + +Hi + + diff --git a/src/zen/tests/mochitests/shell/headless_iframe.html b/src/zen/tests/mochitests/shell/headless_iframe.html new file mode 100644 index 000000000..f318cbe0f --- /dev/null +++ b/src/zen/tests/mochitests/shell/headless_iframe.html @@ -0,0 +1,6 @@ + + + +Hi + + diff --git a/src/zen/tests/mochitests/shell/headless_redirect.html b/src/zen/tests/mochitests/shell/headless_redirect.html new file mode 100644 index 000000000..e69de29bb diff --git a/src/zen/tests/mochitests/shell/headless_redirect.html^headers^ b/src/zen/tests/mochitests/shell/headless_redirect.html^headers^ new file mode 100644 index 000000000..1d7db20ea --- /dev/null +++ b/src/zen/tests/mochitests/shell/headless_redirect.html^headers^ @@ -0,0 +1,2 @@ +HTTP 302 Moved Temporarily +Location: headless.html \ No newline at end of file diff --git a/src/zen/tests/mochitests/shell/mac_desktop_image.py b/src/zen/tests/mochitests/shell/mac_desktop_image.py new file mode 100755 index 000000000..d664a23cb --- /dev/null +++ b/src/zen/tests/mochitests/shell/mac_desktop_image.py @@ -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) diff --git a/src/zen/tests/mochitests/shell/unit/test_macOS_showSecurityPreferences.js b/src/zen/tests/mochitests/shell/unit/test_macOS_showSecurityPreferences.js new file mode 100644 index 000000000..3b9837aac --- /dev/null +++ b/src/zen/tests/mochitests/shell/unit/test_macOS_showSecurityPreferences.js @@ -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"); +}); diff --git a/src/zen/tests/mochitests/shell/unit/xpcshell.toml b/src/zen/tests/mochitests/shell/unit/xpcshell.toml new file mode 100644 index 000000000..445dc9edd --- /dev/null +++ b/src/zen/tests/mochitests/shell/unit/xpcshell.toml @@ -0,0 +1,7 @@ +[DEFAULT] +run-if = ["os != 'android'"] +firefox-appdir = "browser" +tags = "os_integration" + +["test_macOS_showSecurityPreferences.js"] +run-if = ["os == 'mac'"] diff --git a/src/zen/tests/mochitests/tooltiptext/browser.toml b/src/zen/tests/mochitests/tooltiptext/browser.toml new file mode 100644 index 000000000..42aca4a7a --- /dev/null +++ b/src/zen/tests/mochitests/tooltiptext/browser.toml @@ -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"] diff --git a/src/zen/tests/mochitests/tooltiptext/browser_bug329212.js b/src/zen/tests/mochitests/tooltiptext/browser_bug329212.js new file mode 100644 index 000000000..d669e60c2 --- /dev/null +++ b/src/zen/tests/mochitests/tooltiptext/browser_bug329212.js @@ -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); + }); + } + ); +}); diff --git a/src/zen/tests/mochitests/tooltiptext/browser_bug331772_xul_tooltiptext_in_html.js b/src/zen/tests/mochitests/tooltiptext/browser_bug331772_xul_tooltiptext_in_html.js new file mode 100644 index 000000000..61c50e542 --- /dev/null +++ b/src/zen/tests/mochitests/tooltiptext/browser_bug331772_xul_tooltiptext_in_html.js @@ -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"); + }); + } + ); +}); diff --git a/src/zen/tests/mochitests/tooltiptext/browser_bug561623.js b/src/zen/tests/mochitests/tooltiptext/browser_bug561623.js new file mode 100644 index 000000000..93f68d307 --- /dev/null +++ b/src/zen/tests/mochitests/tooltiptext/browser_bug561623.js @@ -0,0 +1,33 @@ +add_task(async function () { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "data:text/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" + ); + }); + } + ); +}); diff --git a/src/zen/tests/mochitests/tooltiptext/browser_bug581947.js b/src/zen/tests/mochitests/tooltiptext/browser_bug581947.js new file mode 100644 index 000000000..097173547 --- /dev/null +++ b/src/zen/tests/mochitests/tooltiptext/browser_bug581947.js @@ -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,
", + }, + 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]); + } + } + ); +}); diff --git a/src/zen/tests/mochitests/tooltiptext/browser_input_file_tooltips.js b/src/zen/tests/mochitests/tooltiptext/browser_input_file_tooltips.js new file mode 100644 index 000000000..2f1385f37 --- /dev/null +++ b/src/zen/tests/mochitests/tooltiptext/browser_input_file_tooltips.js @@ -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; +} diff --git a/src/zen/tests/mochitests/tooltiptext/browser_nac_tooltip.js b/src/zen/tests/mochitests/tooltiptext/browser_nac_tooltip.js new file mode 100644 index 000000000..449c8b9da --- /dev/null +++ b/src/zen/tests/mochitests/tooltiptext/browser_nac_tooltip.js @@ -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,", + }, + 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 = ``; + 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" + ); + } + ); +}); diff --git a/src/zen/tests/mochitests/tooltiptext/browser_shadow_dom_tooltip.js b/src/zen/tests/mochitests/tooltiptext/browser_shadow_dom_tooltip.js new file mode 100644 index 000000000..4ceb918da --- /dev/null +++ b/src/zen/tests/mochitests/tooltiptext/browser_shadow_dom_tooltip.js @@ -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 = + "
shadow
"; + }); + + 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 = "
shadow
"; + }); + + 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 = "
light
"; + doc.body.appendChild(host); + host.setAttribute("style", "position: absolute; top: 0; left: 0;"); + var sr = host.attachShadow({ mode: "closed" }); + sr.innerHTML = + "
"; + }); + + 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); +}); diff --git a/src/zen/tests/mochitests/tooltiptext/title_test.svg b/src/zen/tests/mochitests/tooltiptext/title_test.svg new file mode 100644 index 000000000..80390a3cc --- /dev/null +++ b/src/zen/tests/mochitests/tooltiptext/title_test.svg @@ -0,0 +1,59 @@ + + This is a root SVG element's title + + + + + This is a non-root SVG element title + + + + + + This contains only <title> + + + + This is a title + + + + + This contains only <desc> + This is a desc + + + This contains nothing. + + + This link contains <title> + + This is a title + + + + + + + This text contains <title> + + This is a title + + + + + + This link contains <title> & xlink:title attr. + This is a title + + + + + This link contains xlink:title attr. + + + + This contains nothing. + + diff --git a/src/zen/tests/mochitests/tooltiptext/xul_tooltiptext.xhtml b/src/zen/tests/mochitests/tooltiptext/xul_tooltiptext.xhtml new file mode 100644 index 000000000..8288ffc5f --- /dev/null +++ b/src/zen/tests/mochitests/tooltiptext/xul_tooltiptext.xhtml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/src/zen/tests/moz.build b/src/zen/tests/moz.build index fd6101bce..d857475e7 100644 --- a/src/zen/tests/moz.build +++ b/src/zen/tests/moz.build @@ -15,3 +15,7 @@ BROWSER_CHROME_MANIFESTS += [ "welcome/browser.toml", "workspaces/browser.toml", ] + +DIRS += [ + "mochitests", +]