commit 509ee60b49bcfdb7f85454650e407c7a97b18d85 parent a997f764f303fb5422be58938b2540f0a5ffa35a Author: Cristina Horotan <chorotan@mozilla.com> Date: Thu, 18 Dec 2025 05:39:42 +0200 Revert "Bug 2002127 - Part 20: Restructure Translations Settings Subpage r=hjones" for causing bc failures on browser_MLSuggest_integration.js This reverts commit c3ab936d8de288eb1e055054a4477262b32bbfff. Revert "Bug 2002127 - Part 19: Improve Language-Download States r=translations-reviewers,desktop-theme-reviewers,hjones" This reverts commit e034951ed4167b2109175d39f8f9ca307585c53e. Revert "Bug 2002127 - Part 18: Move moz-selects to header slot r=translations-reviewers,hjones" This reverts commit 53700a63ae0fd67c1b3676905160a966588e1db2. Revert "Bug 2002127 - Part 17: Update Ghost Button Usage r=translations-reviewers,hjones" This reverts commit fe1d8d747881630cf53a62a30a013ccdfc854fc2. Revert "Bug 2002127 - Part 16: Use Button Pattern for NTL r=translations-reviewers,hjones" This reverts commit fe9a7f0ec0e7364f02813905230f8b3808e9b4e9. Revert "Bug 2002127 - Part 15: Use Button Pattern for ATL r=translations-reviewers,fluent-reviewers,bolsson,gregtatum,hjones" This reverts commit 19ce23f408bb2eebf6646004f420aa7cef66d28d. Revert "Bug 2002127 - Part 14: Implement Translations Settings Tests r=translations-reviewers,hjones" This reverts commit add0e3b257912fc6760b8858ce7985f4f30bda06. Revert "Bug 2002127 - Part 13: Implement Download Languages Functionality r=fluent-reviewers,bolsson,hjones" This reverts commit 07f707056f5482261285d99265ef04ccfd460ca7. Revert "Bug 2002127 - Part 12: Implement Never Translate Sites Functionality r=hjones" This reverts commit 4c25cbd6149cb28a68a9135b06d0219a4b8a7327. Revert "Bug 2002127 - Part 11: Implement Never Translate Languages Functionality r=hjones" This reverts commit 74f09dc58c47e98e1d9238fc6010eb5d004609ac. Revert "Bug 2002127 - Part 10: Implement Always Translate Languages Functionality r=hjones" This reverts commit a62db80e52a9b80aa2b150802bdee27be9b88744. Revert "Bug 2002127 - Part 09: Implement Never translate sites default state r=fluent-reviewers,bolsson,hjones" This reverts commit 798f8febaf31bc3e73af37bb402c5467e0e64744. Revert "Bug 2002127 - Part 08: Implement Automatic translation default state r=fluent-reviewers,bolsson,hjones" This reverts commit c98984eee22212b4b12be660b34d47bcc0b05c5c. Revert "Bug 2002127 - Part 07: Implement Speed up translation default state r=fluent-reviewers,bolsson,hjones" This reverts commit 5b48f111a19095bb5d1118530b2f059d32340b99. Revert "Bug 2002127 - Part 06: Implement "More Translation Settings" SubPage r=fluent-reviewers,bolsson,hjones" This reverts commit 71227cdf20ede210b0a14731e2b52c335f063d8f. Revert "Bug 2002127 - Part 05: Implement Top-Level Translations Setting Group r=fluent-reviewers,bolsson,hjones" This reverts commit 25796bd80fdff7e08118cd41f3653923066ee2f4. Revert "Bug 2002127 - Part 04: Fix translations.svg in dark mode r=desktop-theme-reviewers,emilio" This reverts commit e0dcb037e3fd3740676d19b1c0ffef9ee44d2d19. Revert "Bug 2002127 - Part 03: Support icons in subpage headers r=hjones" This reverts commit a6bcf5518e5b32a23faa31828a2e3aeb076fc3d1. Revert "Bug 2002127 - Part 02: Remove unused browser.translations.newSettingsUI code r=desktop-theme-reviewers,translations-reviewers,frontend-codestyle-reviewers,gregtatum,Itiel,hjones" This reverts commit 203bfbfbf2725aeb3c1c05e7880ef868f72fa014. Revert "Bug 2002127 - Part 01: Increase tr8ns default max-run-time in CI r=jmaher" This reverts commit d5b8cf2b29db2f2e1dfe512148490524bf2387b4. Diffstat:
52 files changed, 3366 insertions(+), 7275 deletions(-)
diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js @@ -2375,6 +2375,9 @@ pref("browser.translation.neverForLanguages", ""); // engine https://browser.mt/. pref("browser.translations.enable", true); +// Enable the new Firefox Translations Settings UI Design +pref("browser.translations.newSettingsUI.enable", false); + // Enable Firefox Select translations powered by Bergamot translations // engine https://browser.mt/. pref("browser.translations.select.enable", true); diff --git a/browser/components/preferences/dialogs/jar.mn b/browser/components/preferences/dialogs/jar.mn @@ -42,3 +42,4 @@ browser.jar: content/browser/preferences/dialogs/translationExceptions.xhtml content/browser/preferences/dialogs/translationExceptions.js content/browser/preferences/dialogs/translations.xhtml + content/browser/preferences/dialogs/translations.js diff --git a/browser/components/preferences/dialogs/translations.xhtml b/browser/components/preferences/dialogs/translations.xhtml @@ -29,6 +29,8 @@ <html:link rel="localization" href="browser/translations.ftl" /> </linkset> + <script src="chrome://browser/content/preferences/dialogs/translations.js" /> + <keyset> <key id="key_close" diff --git a/browser/components/preferences/jar.mn b/browser/components/preferences/jar.mn @@ -8,11 +8,11 @@ browser.jar: * content/browser/preferences/preferences.xhtml content/browser/preferences/main.js - content/browser/preferences/translations.js content/browser/preferences/home.js content/browser/preferences/search.js content/browser/preferences/privacy.js content/browser/preferences/containers.js + content/browser/preferences/translations.js content/browser/preferences/sync.js content/browser/preferences/experimental.js content/browser/preferences/moreFromMozilla.js diff --git a/browser/components/preferences/main.inc.xhtml b/browser/components/preferences/main.inc.xhtml @@ -5,7 +5,6 @@ <!-- General panel --> <script src="chrome://browser/content/preferences/main.js"/> -<script src="chrome://browser/content/preferences/translations.js"/> #ifdef MOZ_UPDATER <script src="chrome://browser/content/aboutDialog-appUpdater.js"/> @@ -216,10 +215,7 @@ data-l10n-args='{"localeName": "und"}' preference="intl.regional_prefs.use_os_locales"/> - <!-- This Translations UI manages the prefs for the addon only. - We were planning to remove this once ESR 115 went away, but it seems - that it may live forever. I think it can be safely removed from a current - version of Firefox, though, especially once the Settings Redesign is finalized. --> + <!-- This Firefox Translations UI manages the prefs for the addon only. --> <hbox id="fxtranslationsBox" hidden="true" data-subcategory="fxtranslations"> <description flex="1" control="fxtranslateButton" data-l10n-id="fx-translate-web-pages"/> <button id="fxtranslateButton" @@ -232,9 +228,8 @@ data-l10n-id="check-user-spelling" preference="layout.spellcheckDefault"/> - <!-- This Translations UI is the pre-settings-redesign UI and should be removed - when getting ready for the settings redesign to fully ship. --> - <vbox id="translationsGroup" data-srd-groupid="translations" hidden="true" data-subcategory="translations"> + <!-- Translations --> + <vbox id="translationsGroup" hidden="true" data-subcategory="translations"> <label><html:h2 data-l10n-id="translations-manage-header"/></label> <hbox id="translations-manage-description" align="center"> <description flex="1" data-l10n-id="translations-manage-intro-2"/> @@ -259,9 +254,6 @@ </vbox> </groupbox> -<!-- This Translations UI is compatible with the settings redesign --> -<html:setting-group data-category="paneGeneral" groupid="translations" hidden="true"/> - <!-- Files and Applications --> <hbox id="filesAndApplicationsCategory" class="subcategory" diff --git a/browser/components/preferences/main.js b/browser/components/preferences/main.js @@ -204,9 +204,6 @@ Preferences.addAll([ // Appearance { id: "layout.css.prefers-color-scheme.content-override", type: "int" }, - - // Translations - { id: "browser.translations.automaticallyPopup", type: "bool" }, ]); if (AppConstants.HAVE_SHELL_SERVICE) { @@ -649,116 +646,6 @@ Preferences.addSetting({ Preferences.addSetting({ id: "containersPlaceholder" }); Preferences.addSetting({ - id: "offerTranslations", - pref: "browser.translations.automaticallyPopup", -}); - -function createNeverTranslateSitesDescription() { - const description = document.createElement("span"); - description.dataset.l10nId = - "settings-translations-subpage-never-translate-sites-description"; - - for (const [name, src] of [ - ["translations-icon", "chrome://browser/skin/translations.svg"], - ["settings-icon", "chrome://global/skin/icons/settings.svg"], - ]) { - const icon = document.createElement("img"); - icon.src = src; - - icon.dataset.l10nName = name; - icon.style.verticalAlign = "middle"; - - icon.setAttribute("role", "presentation"); - icon.setAttribute("width", "16"); - icon.setAttribute("height", "16"); - - description.appendChild(icon); - } - - return description; -} - -Preferences.addSetting({ - id: "translationsDownloadLanguagesGroup", -}); - -Preferences.addSetting({ - id: "translationsDownloadLanguagesRow", -}); - -Preferences.addSetting({ - id: "translationsDownloadLanguagesSelect", -}); - -Preferences.addSetting({ - id: "translationsDownloadLanguagesButton", -}); - -Preferences.addSetting({ - id: "translationsDownloadLanguagesNoneRow", -}); - -Preferences.addSetting({ - id: "translationsAlwaysTranslateLanguagesGroup", -}); - -Preferences.addSetting({ - id: "translationsAlwaysTranslateLanguagesRow", -}); - -Preferences.addSetting({ - id: "translationsAlwaysTranslateLanguagesSelect", -}); - -Preferences.addSetting({ - id: "translationsAlwaysTranslateLanguagesNoneRow", -}); - -Preferences.addSetting({ - id: "translationsAlwaysTranslateLanguagesButton", -}); - -Preferences.addSetting({ - id: "translationsNeverTranslateLanguagesNoneRow", -}); - -Preferences.addSetting({ - id: "translationsNeverTranslateLanguagesButton", -}); - -Preferences.addSetting({ - id: "translationsNeverTranslateLanguagesGroup", -}); - -Preferences.addSetting({ - id: "translationsNeverTranslateLanguagesRow", -}); - -Preferences.addSetting({ - id: "translationsNeverTranslateLanguagesSelect", -}); - -Preferences.addSetting({ - id: "translationsNeverTranslateSitesGroup", -}); - -Preferences.addSetting({ - id: "translationsNeverTranslateSitesRow", -}); - -Preferences.addSetting({ - id: "translationsNeverTranslateSitesNoneRow", -}); - -Preferences.addSetting({ - id: "translationsManageButton", - onUserClick(e) { - e.preventDefault(); - gotoPref("paneTranslations"); - }, -}); - -Preferences.addSetting({ id: "data-migration", visible: () => !Services.policies || Services.policies.isAllowed("profileImport"), @@ -2360,24 +2247,6 @@ SettingGroupManager.registerGroups({ }, ], }, - translations: { - inProgress: true, - l10nId: "settings-translations-header", - iconSrc: "chrome://browser/skin/translations.svg", - supportPage: "website-translation", - headingLevel: 2, - items: [ - { - id: "offerTranslations", - l10nId: "settings-translations-offer-to-translate-label", - }, - { - id: "translationsManageButton", - l10nId: "settings-translations-more-settings-button", - control: "moz-box-button", - }, - ], - }, appearance: { l10nId: "web-appearance-group", items: [ @@ -3797,198 +3666,6 @@ SettingGroupManager.registerGroups({ }, ], }, - translationsAutomaticTranslation: { - inProgress: true, - headingLevel: 2, - l10nId: "settings-translations-subpage-automatic-translation-header", - items: [ - { - id: "translationsAlwaysTranslateLanguagesGroup", - control: "moz-box-group", - controlAttrs: { - type: "list", - }, - items: [ - { - id: "translationsAlwaysTranslateLanguagesRow", - l10nId: "settings-translations-subpage-always-translate-header", - control: "moz-box-item", - slot: "header", - controlAttrs: { - class: "box-header-bold", - }, - items: [ - { - id: "translationsAlwaysTranslateLanguagesSelect", - slot: "actions", - control: "moz-select", - options: [ - { - value: "", - l10nId: - "settings-translations-subpage-language-select-option", - }, - ], - }, - { - id: "translationsAlwaysTranslateLanguagesButton", - l10nId: "settings-translations-subpage-language-add-button", - control: "moz-button", - slot: "actions", - controlAttrs: { - type: "icon", - iconsrc: "chrome://global/skin/icons/plus.svg", - }, - }, - ], - }, - { - id: "translationsAlwaysTranslateLanguagesNoneRow", - l10nId: "settings-translations-subpage-no-languages-added", - control: "moz-box-item", - controlAttrs: { - class: "description-deemphasized", - }, - }, - ], - }, - { - id: "translationsNeverTranslateLanguagesGroup", - control: "moz-box-group", - controlAttrs: { - type: "list", - }, - items: [ - { - id: "translationsNeverTranslateLanguagesRow", - l10nId: "settings-translations-subpage-never-translate-header", - control: "moz-box-item", - slot: "header", - controlAttrs: { - class: "box-header-bold", - }, - items: [ - { - id: "translationsNeverTranslateLanguagesSelect", - slot: "actions", - control: "moz-select", - options: [ - { - value: "", - l10nId: - "settings-translations-subpage-language-select-option", - }, - ], - }, - { - id: "translationsNeverTranslateLanguagesButton", - l10nId: "settings-translations-subpage-language-add-button", - control: "moz-button", - slot: "actions", - controlAttrs: { - type: "icon", - iconsrc: "chrome://global/skin/icons/plus.svg", - }, - }, - ], - }, - { - id: "translationsNeverTranslateLanguagesNoneRow", - l10nId: "settings-translations-subpage-no-languages-added", - control: "moz-box-item", - controlAttrs: { - class: "description-deemphasized", - }, - }, - ], - }, - { - id: "translationsNeverTranslateSitesGroup", - control: "moz-box-group", - controlAttrs: { - type: "list", - }, - items: [ - { - id: "translationsNeverTranslateSitesRow", - l10nId: - "settings-translations-subpage-never-translate-sites-header", - control: "moz-box-item", - controlAttrs: { - class: "box-header-bold", - ".description": createNeverTranslateSitesDescription(), - }, - }, - { - id: "translationsNeverTranslateSitesNoneRow", - l10nId: "settings-translations-subpage-no-sites-added", - control: "moz-box-item", - controlAttrs: { - class: "description-deemphasized", - }, - }, - ], - }, - ], - }, - translationsDownloadLanguages: { - inProgress: true, - headingLevel: 2, - l10nId: "settings-translations-subpage-speed-up-translation-header", - items: [ - { - id: "translationsDownloadLanguagesGroup", - control: "moz-box-group", - controlAttrs: { - type: "list", - }, - items: [ - { - id: "translationsDownloadLanguagesRow", - l10nId: "settings-translations-subpage-download-languages-header", - control: "moz-box-item", - slot: "header", - controlAttrs: { - class: "box-header-bold", - }, - items: [ - { - id: "translationsDownloadLanguagesSelect", - slot: "actions", - control: "moz-select", - options: [ - { - value: "", - l10nId: - "settings-translations-subpage-download-languages-select-option", - }, - ], - }, - { - id: "translationsDownloadLanguagesButton", - l10nId: - "settings-translations-subpage-download-languages-button", - control: "moz-button", - slot: "actions", - controlAttrs: { - type: "icon", - iconsrc: "chrome://browser/skin/downloads/downloads.svg", - }, - }, - ], - }, - { - id: "translationsDownloadLanguagesNoneRow", - l10nId: "settings-translations-subpage-no-languages-downloaded", - control: "moz-box-item", - controlAttrs: { - class: "description-deemphasized", - }, - }, - ], - }, - ], - }, }); /** @@ -4106,7 +3783,6 @@ var gMainPane = { initSettingGroup("browsing"); initSettingGroup("zoom"); initSettingGroup("support"); - initSettingGroup("translations"); initSettingGroup("performance"); initSettingGroup("startup"); initSettingGroup("importBrowserData"); @@ -5249,9 +4925,20 @@ var gMainPane = { }, showTranslationsSettings() { - gSubDialog.open( - "chrome://browser/content/preferences/dialogs/translations.xhtml" - ); + if ( + Services.prefs.getBoolPref("browser.translations.newSettingsUI.enable") + ) { + const translationsSettings = document.getElementById( + "translations-settings-page" + ); + translationsSettings.setAttribute("data-hidden-from-search", "false"); + translationsSettings.hidden = false; + gotoPref("translations"); + } else { + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/translations.xhtml" + ); + } }, /** diff --git a/browser/components/preferences/preferences.js b/browser/components/preferences/preferences.js @@ -7,6 +7,7 @@ /* import-globals-from home.js */ /* import-globals-from search.js */ /* import-globals-from containers.js */ +/* import-globals-from translations.js */ /* import-globals-from privacy.js */ /* import-globals-from sync.js */ /* import-globals-from experimental.js */ @@ -295,15 +296,6 @@ const CONFIG_PANES = Object.freeze({ l10nId: "autofill-addresses-manage-addresses-title", groupIds: ["manageAddresses"], }, - translations: { - parent: "general", - l10nId: "settings-translations-subpage-header", - groupIds: [ - "translationsAutomaticTranslation", - "translationsDownloadLanguages", - ], - iconSrc: "chrome://browser/skin/translations.svg", - }, }); var gLastCategory = { category: undefined, subcategory: undefined }; @@ -362,6 +354,9 @@ function init_all() { SettingPaneManager.registerPane(id, config); } + if (Services.prefs.getBoolPref("browser.translations.newSettingsUI.enable")) { + register_module("paneTranslations", gTranslationsPane); + } if (ExperimentAPI.labsEnabled) { // Set hidden based on previous load's hidden value or if Nimbus is // disabled. diff --git a/browser/components/preferences/preferences.xhtml b/browser/components/preferences/preferences.xhtml @@ -26,6 +26,10 @@ /> <link rel="stylesheet" + href="chrome://browser/skin/preferences/translations.css" + /> + <link + rel="stylesheet" href="chrome://browser/content/preferences/dialogs/handlers.css" /> <link @@ -71,6 +75,7 @@ <link rel="localization" href="browser/preferences/siteDataSettings.ftl"/> <link rel="localization" href="browser/sanitize.ftl"/> <link rel="localization" href="browser/translations.ftl"/> + <link rel="localization" href="preview/translations.ftl"/> <link rel="localization" href="preview/enUS-searchFeatures.ftl"/> <link rel="localization" href="security/certificates/certManager.ftl"/> <link rel="localization" href="security/certificates/deviceManager.ftl"/> @@ -251,6 +256,7 @@ #include search.inc.xhtml #include privacy.inc.xhtml #include containers.inc.xhtml +#include translations.inc.xhtml #include sync.inc.xhtml #include experimental.inc.xhtml #include moreFromMozilla.inc.xhtml diff --git a/browser/components/preferences/translations.d.ts b/browser/components/preferences/translations.d.ts @@ -1,35 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/** - * Type definitions for translations.js - */ - -export interface LanguageInfo { - langTag: string; - displayName: string; -} - -export interface SupportedLanguages { - sourceLanguages: LanguageInfo[]; - targetLanguages: LanguageInfo[]; -} - -export interface TranslationsSettingsElements { - alwaysTranslateLanguagesGroup: HTMLElement; - alwaysTranslateLanguagesSelect: HTMLSelectElement; - alwaysTranslateLanguagesButton: HTMLButtonElement; - alwaysTranslateLanguagesNoneRow: HTMLElement; - neverTranslateLanguagesGroup: HTMLElement; - neverTranslateLanguagesSelect: HTMLSelectElement; - neverTranslateLanguagesButton: HTMLButtonElement; - neverTranslateLanguagesNoneRow: HTMLElement; - neverTranslateSitesGroup: HTMLElement; - neverTranslateSitesRow: HTMLElement; - neverTranslateSitesNoneRow: HTMLElement; - downloadLanguagesGroup: HTMLElement; - downloadLanguagesSelect: HTMLSelectElement; - downloadLanguagesButton: HTMLButtonElement; - downloadLanguagesNoneRow: HTMLElement; -} diff --git a/browser/components/preferences/translations.inc.xhtml b/browser/components/preferences/translations.inc.xhtml @@ -0,0 +1,101 @@ +# 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/. + +<script src="chrome://browser/content/preferences/translations.js"/> + +<div id="translations-settings-page" + xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + data-category="paneTranslations" + data-hidden-from-search="true" + data-subpanel="true" + hidden="true"> + + <button id="translations-settings-back-button" class="back-button" + data-l10n-id="translations-settings-back-button"/> + + <h1 id="translations-settings-header" + data-l10n-id="translations-settings-header"/> + + <p id="translations-settings-description" data-l10n-id="translations-settings-description"/> + + <moz-card class="translations-settings-manage-section" + id="translations-settings-always-translate-section"> + <div class="translations-settings-manage-language"> + <h2 id="translations-settings-always-translate" data-l10n-id="translations-settings-always-translate"/> + <xul:menulist id="translations-settings-always-translate-list" + data-l10n-id="translations-settings-add-language-button" + aria-labelledby="translations-settings-always-translate"> + <!-- The list of <menuitem> will be dynamically inserted. --> + <xul:menupopup id="translations-settings-always-translate-popup"/> + </xul:menulist> + </div> + <div id="translations-settings-always-translate-languages-card" class="translations-settings-languages-card" hidden="true"> + <h3 class="translations-settings-language-header" data-l10n-id="translations-settings-language-header"></h3> + <div id="translations-settings-always-translate-language-list" class="translations-settings-language-list" + tabindex="0" role="listbox"> + </div> + </div> + </moz-card> + + <moz-card id="translations-settings-never-translate-section" + class="translations-settings-manage-section"> + <div class="translations-settings-manage-language"> + <h2 id="translations-settings-never-translate" data-l10n-id="translations-settings-never-translate"/> + <xul:menulist id="translations-settings-never-translate-list" + data-l10n-id="translations-settings-add-language-button" + aria-labelledby="translations-settings-never-translate"> + <!-- The list of <menuitem> will be dynamically inserted. --> + <xul:menupopup id="translations-settings-never-translate-popup"/> + </xul:menulist> + </div> + <div id="translations-settings-never-translate-languages-card" class="translations-settings-languages-card" hidden="true"> + <h3 class="translations-settings-language-header" data-l10n-id="translations-settings-language-header"></h3> + <div id="translations-settings-never-translate-language-list" class="translations-settings-language-list" + tabindex="0" role="listbox"> + </div> + </div> + </moz-card> + + <moz-card id="translations-settings-never-sites-section" + class="translations-settings-manage-section"> + <div class="translations-settings-manage-section-info" > + <h2 id="translations-settings-never-sites-header" + data-l10n-id="translations-settings-never-sites-header"/> + <p id="translations-settings-never-sites" + data-l10n-id="translations-settings-never-sites-description"/> + </div> + <div id="translations-settings-never-translate-site-card" class="translations-settings-languages-card" hidden="true"> + <h3 class="translations-settings-language-header" data-l10n-id="translations-settings-language-header"></h3> + <div id="translations-settings-never-translate-site-list" class="translations-settings-language-list" + tabindex="0" role="listbox"> + </div> + </div> + </moz-card> + + <moz-card id="translations-settings-download-section" + class="translations-settings-manage-section"> + <div class="translations-settings-manage-section-info"> + <h2 data-l10n-id="translations-settings-download-languages"/> + <a is="moz-support-link" class="learnMore" + id="download-languages-learn-more" + data-l10n-id="translations-settings-download-languages-link" + support-page="website-translation"/> + </div> + <div class="translations-settings-languages-card"> + <h3 class="translations-settings-language-header" data-l10n-id="translations-settings-language-header"></h3> + <div id="translations-settings-download-language-list" class="translations-settings-language-list" tabindex="0" + role="listbox"> + <div id="translations-settings-download-all-languages-id" class="translations-settings-language" role="option"> + <moz-button class="translations-settings-download-icon translations-settings-manage-downloaded-language-button" type="ghost icon" + data-l10n-id="translations-settings-download-all-button" tabindex="-1"></moz-button> + <!-- The option to "All languages" is added here. + In translations.js the option to download individual languages is + added dynamically based on the supported language list --> + <label id="translations-settings-download-all-languages" data-l10n-id="translations-settings-download-all-languages"></label> + </div> + </div> + </div> + </moz-card> +</div> diff --git a/browser/components/preferences/translations.js b/browser/components/preferences/translations.js @@ -1,2157 +1,1316 @@ /* 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/. */ + * 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/. */ -// @ts-check +/* import-globals-from preferences.js */ -"use strict"; - -/* import-globals-from main.js */ +/** + * @typedef {import("../../../toolkit/components/translations/translations").SupportedLanguages} SupportedLanguages + */ /** - * @import { - * TranslationsSettingsElements, - * SupportedLanguages, - * LanguageInfo - * } from "./translations" + * The permission type to give to Services.perms for Translations. */ +const TRANSLATIONS_PERMISSION = "translations"; -/** @type {string} */ +/** + * The list of BCP-47 language tags that will trigger auto-translate. + */ const ALWAYS_TRANSLATE_LANGS_PREF = "browser.translations.alwaysTranslateLanguages"; -/** @type {string} */ + +/** + * The list of BCP-47 language tags that will prevent auto-translate. + */ const NEVER_TRANSLATE_LANGS_PREF = "browser.translations.neverTranslateLanguages"; -/** @type {string} */ -const TOPIC_TRANSLATIONS_PREF_CHANGED = "translations:pref-changed"; -/** @type {string} */ -const TRANSLATIONS_PERMISSION = "translations"; - -/** @type {string} */ -const ALWAYS_TRANSLATE_LANGUAGE_ITEM_CLASS = - "translations-always-translate-language-item"; -/** @type {string} */ -const ALWAYS_TRANSLATE_LANGUAGE_REMOVE_BUTTON_CLASS = - "translations-always-translate-remove-button"; - -/** @type {string} */ -const NEVER_TRANSLATE_LANGUAGE_ITEM_CLASS = - "translations-never-translate-language-item"; -/** @type {string} */ -const NEVER_TRANSLATE_LANGUAGE_REMOVE_BUTTON_CLASS = - "translations-never-translate-remove-button"; -/** @type {string} */ -const NEVER_TRANSLATE_SITE_ITEM_CLASS = - "translations-never-translate-site-item"; -/** @type {string} */ -const NEVER_TRANSLATE_SITE_REMOVE_BUTTON_CLASS = - "translations-never-translate-site-remove-button"; - -/** @type {string} */ -const DOWNLOAD_LANGUAGE_ITEM_CLASS = "translations-download-language-item"; -/** @type {string} */ -const DOWNLOAD_LANGUAGE_REMOVE_BUTTON_CLASS = - "translations-download-remove-button"; -/** @type {string} */ -const DOWNLOAD_LANGUAGE_RETRY_BUTTON_CLASS = - "translations-download-retry-button"; -/** @type {string} */ -const DOWNLOAD_LANGUAGE_FAILED_CLASS = "translations-download-language-error"; -/** @type {string} */ -const DOWNLOAD_LANGUAGE_DELETE_CONFIRM_BUTTON_CLASS = - "translations-download-delete-confirm-button"; -/** @type {string} */ -const DOWNLOAD_LANGUAGE_DELETE_CANCEL_BUTTON_CLASS = - "translations-download-delete-cancel-button"; -/** @type {string} */ -const DOWNLOAD_LOADING_ICON = "chrome://global/skin/icons/loading.svg"; -/** @type {string} */ -const DOWNLOAD_DELETE_ICON = "chrome://global/skin/icons/delete.svg"; -/** @type {string} */ -const DOWNLOAD_ERROR_ICON = "chrome://global/skin/icons/error.svg"; -/** @type {string} */ -const DOWNLOAD_WARNING_ICON = "chrome://global/skin/icons/warning.svg"; /** - * Dispatches a test-only event when running under automation. - * - * @param {string} name - Event name without the "TranslationsSettingsTest:" prefix. - * @param {object} [detail] - Optional event detail. + * The topic fired to observers when a pref related to Translations changes. */ -function dispatchTestEvent(name, detail) { - if (!globalThis.Cu?.isInAutomation) { - return; - } - const options = detail ? { detail } : undefined; - document.dispatchEvent( - new CustomEvent(`TranslationsSettingsTest:${name}`, options) - ); -} - -const TranslationsSettings = { - /** - * True once initialization has completed. - * - * @type {boolean} - */ - initialized: false, +const TOPIC_TRANSLATIONS_PREF_CHANGED = "translations:pref-changed"; +let gTranslationsPane = { /** - * Promise guarding full initialization to avoid re-entry. + * List of languages set in the Always Translate Preferences * - * @type {Promise<void>|null} + * @type Array<string> */ - initPromise: null, + alwaysTranslateLanguages: [], /** - * Promise cached after the pane/group finish rendering. + * List of languages set in the Never Translate Preferences * - * @type {Promise<void>|null} + * @type Array<string> */ - paneRenderPromise: null, + neverTranslateLanguages: [], /** - * Supported languages fetched from TranslationsParent. + * List of languages set in the Never Translate Site Preferences * - * @type {SupportedLanguages|null} + * @type Array<string> */ - supportedLanguages: null, + neverTranslateSites: [], /** - * Display names for supported languages. + * A mapping from the language tag to the current download phase for that language + * and it's download size. * - * @type {Intl.DisplayNames|null} + * @type {Map<string, {downloadPhase: "downloaded" | "removed" | "loading", size: number}>} */ - languageDisplayNames: null, + downloadPhases: new Map(), /** - * Language metadata used to build labels and selectors. + * Object with details of languages supported by the browser. * - * @type {LanguageInfo[]|null} + * @type {SupportedLanguages} */ - languageList: null, + supportedLanguages: {}, /** - * Download sizes keyed by language tag. + * List of languages names supported along with their tags (BCP 47 locale identifiers). * - * @type {Map<string, number>|null} + * @type Array<{ langTag: string, displayName: string}> */ - languageSizes: null, + supportedLanguageTagsNames: [], /** - * Formatter used for download size labels. - * - * @type {Intl.NumberFormat|null} + * Add Lazy getter for document elements */ - numberFormatter: null, + elements: undefined, - /** - * Current always-translate language tags. - * - * @type {Set<string>} - */ - alwaysTranslateLanguageTags: new Set(), + async init() { + if (!this.elements) { + this._defineLazyElements(document, { + downloadLanguageSection: "translations-settings-download-section", + alwaysTranslateMenuList: "translations-settings-always-translate-list", + neverTranslateMenuList: "translations-settings-never-translate-list", + alwaysTranslateMenuPopup: + "translations-settings-always-translate-popup", + neverTranslateMenuPopup: "translations-settings-never-translate-popup", + downloadLanguageList: "translations-settings-download-language-list", + alwaysTranslateLanguageList: + "translations-settings-always-translate-language-list", + neverTranslateLanguageList: + "translations-settings-never-translate-language-list", + neverTranslateSiteList: + "translations-settings-never-translate-site-list", + translationsSettingsBackButton: "translations-settings-back-button", + translationsSettingsHeader: "translations-settings-header", + translationsSettingsDescription: "translations-settings-description", + translateAlwaysHeader: "translations-settings-always-translate", + translateNeverHeader: "translations-settings-never-translate", + translateNeverSiteHeader: "translations-settings-never-sites-header", + translateNeverSiteDesc: "translations-settings-never-sites", + translateDownloadLanguagesLearnMore: "download-languages-learn-more", + }); + } + this.elements.translationsSettingsBackButton.addEventListener( + "click", + function () { + gotoPref("general"); + } + ); - /** - * Current never-translate language tags. - * - * @type {Set<string>} - */ - neverTranslateLanguageTags: new Set(), + // Keyboard navigation support. + this.elements.alwaysTranslateMenuList.addEventListener("keydown", this); + this.elements.alwaysTranslateMenuPopup.addEventListener( + "popuphidden", + this + ); + this.elements.neverTranslateMenuList.addEventListener("keydown", this); + this.elements.neverTranslateMenuPopup.addEventListener("popuphidden", this); - /** - * Current never-translate site origins. - * - * @type {Set<string>} - */ - neverTranslateSiteOrigins: new Set(), + // Get the settings from the preferences into the translations.js + this.supportedLanguages = await TranslationsParent.getSupportedLanguages(); + this.supportedLanguageTagsNames = TranslationsParent.getLanguageList( + this.supportedLanguages + ); - /** - * Language tags with downloaded translation models. - * - * @type {Set<string>} - */ - downloadedLanguageTags: new Set(), + this.neverTranslateSites = TranslationsParent.listNeverTranslateSites(); - /** - * Language tags currently downloading. - * - * @type {Set<string>} - */ - downloadingLanguageTags: new Set(), + // Deploy observers + Services.obs.addObserver(this, "perm-changed"); + Services.obs.addObserver(this, TOPIC_TRANSLATIONS_PREF_CHANGED); + window.addEventListener("unload", () => this.removeObservers()); + + // Build the HTML elements + this.buildLanguageDropDowns(); + // Keyboard navigation support. + this.elements.alwaysTranslateLanguageList.addEventListener("keydown", this); + this.elements.neverTranslateLanguageList.addEventListener("keydown", this); + this.elements.neverTranslateSiteList.addEventListener("keydown", this); + this.populateLanguageList(ALWAYS_TRANSLATE_LANGS_PREF); + this.populateLanguageList(NEVER_TRANSLATE_LANGS_PREF); + this.populateSiteList(); + + await this.initDownloadInfo(); + this.buildDownloadLanguageList(); + + // The translations settings page takes a long time to initialize + // This event can be used to wait until the initialization is done. + document.dispatchEvent( + new CustomEvent("translationsSettingsInit", { + bubbles: true, + cancelable: true, + }) + ); + }, - /** - * Language tags that failed to download. - * - * @type {Set<string>} - */ - downloadFailedLanguageTags: new Set(), + _defineLazyElements(document, entries) { + this.elements = {}; + for (const [name, elementId] of Object.entries(entries)) { + ChromeUtils.defineLazyGetter(this.elements, name, () => { + const element = document.getElementById(elementId); + if (!element) { + throw new Error(`Could not find "${name}" at "#${elementId}".`); + } + return element; + }); + } + }, /** - * Language tags pending delete confirmation. - * - * @type {Set<string>} + * Populate the Drop down list in <menupopup> with the list of supported languages + * for the user to choose languages to add to Always translate and + * Never translate settings list. */ - downloadPendingDeleteLanguageTags: new Set(), + buildLanguageDropDowns() { + const { sourceLanguages } = this.supportedLanguages; + const { alwaysTranslateMenuPopup, neverTranslateMenuPopup } = this.elements; - /** - * Language tag of the in-progress download, if any. - * - * @type {string|null} - */ - currentDownloadLangTag: null, + for (const { langTag, displayName } of sourceLanguages) { + const alwaysLang = document.createXULElement("menuitem"); + alwaysLang.setAttribute("value", langTag); + alwaysLang.setAttribute("label", displayName); + alwaysTranslateMenuPopup.appendChild(alwaysLang); + const neverLang = document.createXULElement("menuitem"); + neverLang.setAttribute("value", langTag); + neverLang.setAttribute("label", displayName); + neverTranslateMenuPopup.appendChild(neverLang); + } + }, /** - * Cached DOM elements used by the module. + * Initializes the downloadPhases by checking the download status of each language. * - * @type {TranslationsSettingsElements|null} + * @see gTranslationsPane.downloadPhases */ - elements: null, + async initDownloadInfo() { + let downloadCount = 0; + let allDownloadSize = 0; + + this.downloadPhases = new Map(); + for (const language of this.supportedLanguageTagsNames) { + let downloadSize = await TranslationsParent.getLanguageSize( + language.langTag + ); + allDownloadSize += downloadSize; + const hasAllFilesForLanguage = + await TranslationsParent.hasAllFilesForLanguage(language.langTag); + const downloadPhase = hasAllFilesForLanguage ? "downloaded" : "removed"; + this.downloadPhases.set(language.langTag, { + downloadPhase, + size: downloadSize, + }); + downloadCount += downloadPhase === "downloaded" ? 1 : 0; + } + const allDownloadPhase = + downloadCount === this.supportedLanguageTagsNames.length + ? "downloaded" + : "removed"; + this.downloadPhases.set("all", { + downloadPhase: allDownloadPhase, + size: allDownloadSize, + }); + }, /** - * Handles events this object is registered for. - * - * @param {Event} event + * Show a list of languages for the user to be able to download + * and remove language models for local translation. */ - async handleEvent(event) { + buildDownloadLanguageList() { + const { downloadLanguageList } = this.elements; + + function createSizeElement(downloadSize) { + const languageSize = document.createElement("span"); + languageSize.classList.add("translations-settings-download-size"); + const [size, units] = DownloadUtils.convertByteUnits(downloadSize); + + document.l10n.setAttributes( + languageSize, + "translations-settings-download-size", + { + size: size + " " + units, + } + ); + return languageSize; + } + + // The option to download "All languages" is added in xhtml. + // Here the option to download individual languages is dynamically added + // based on the supported language list + const allLangElement = downloadLanguageList.firstElementChild; + let allLangButton = allLangElement.querySelector("moz-button"); + + // The first element is selected by default when keyboard navigation enters this list + downloadLanguageList.setAttribute( + "aria-activedescendant", + allLangElement.id + ); + // Keyboard navigation support. + downloadLanguageList.addEventListener("keydown", this); + allLangButton.addEventListener("click", this); + allLangElement.addEventListener("keydown", this); + + for (const language of this.supportedLanguageTagsNames) { + const downloadSize = this.downloadPhases.get(language.langTag).size; + + const languageSize = createSizeElement(downloadSize); + + const languageLabel = this.createLangLabel( + language.displayName, + language.langTag, + "translations-settings-download-" + language.langTag + ); + + const isDownloaded = + this.downloadPhases.get(language.langTag).downloadPhase === + "downloaded"; + + const mozButton = isDownloaded + ? this.createIconButton( + [ + "translations-settings-remove-icon", + "translations-settings-manage-downloaded-language-button", + ], + "translations-settings-remove-button", + language.displayName + ) + : this.createIconButton( + [ + "translations-settings-download-icon", + "translations-settings-manage-downloaded-language-button", + ], + "translations-settings-download-button", + language.displayName + ); + + const languageElement = this.createLangElement( + [mozButton, languageLabel, languageSize], + "translations-settings-download-" + language.langTag + "-language-id" + ); + downloadLanguageList.appendChild(languageElement); + } + + // Updating "All Language" download button according to the state + if (this.downloadPhases.get("all").downloadPhase === "downloaded") { + this.changeButtonState({ + langButton: allLangButton, + langTag: "all", + langState: "downloaded", + }); + } + + const allDownloadSize = this.downloadPhases.get("all").size; + const languageSize = createSizeElement(allDownloadSize); + + allLangElement.appendChild(languageSize); + }, + + handleEvent(event) { + const eventNode = event.target; + const eventNodeParent = eventNode.parentNode; + const eventNodeClassList = eventNode.classList; + for (const err of document.querySelectorAll( + ".translations-settings-language-error" + )) { + this.removeError(err); + } + switch (event.type) { - case "paneshown": - await this.handlePaneShown( - /** @type {CustomEvent} */ (event).detail?.category - ); + case "keydown": + // Keyboard navigation support. + this.handleKeys(event); break; - case "change": - if (event.target === this.elements?.alwaysTranslateLanguagesSelect) { - this.onAlwaysTranslateLanguageSelectionChanged(); + case "popuphidden": + // Handle Menulist selection through pointing device + if ( + eventNodeParent.id === "translations-settings-always-translate-list" + ) { + this.handleAddAlwaysTranslateLanguage( + event.target.parentNode.getAttribute("value") + ); } else if ( - event.target === this.elements?.neverTranslateLanguagesSelect + eventNodeParent.id === "translations-settings-never-translate-list" ) { - this.onNeverTranslateLanguageSelectionChanged(); - } else if (event.target === this.elements?.downloadLanguagesSelect) { - this.onDownloadSelectionChanged(); + this.handleAddNeverTranslateLanguage( + event.target.parentNode.getAttribute("value") + ); } break; - case "click": { - const target = /** @type {HTMLElement} */ (event.target); - if ( - target === this.elements?.alwaysTranslateLanguagesButton || - target.closest?.("#translationsAlwaysTranslateLanguagesButton") + case "click": + if (eventNodeClassList.contains("translations-settings-site-button")) { + this.handleRemoveNeverTranslateSite(event); + } else if ( + eventNodeClassList.contains( + "translations-settings-language-never-button" + ) ) { - await this.onAlwaysTranslateLanguageChosen( - this.elements?.alwaysTranslateLanguagesSelect?.value ?? "" - ); - break; + this.handleRemoveNeverTranslateLanguage(event); + } else if ( + eventNodeClassList.contains( + "translations-settings-language-always-button" + ) + ) { + this.handleRemoveAlwaysTranslateLanguage(event); + } else if ( + eventNodeClassList.contains( + "translations-settings-manage-downloaded-language-button" + ) + ) { + if ( + eventNodeClassList.contains("translations-settings-download-icon") + ) { + if ( + eventNodeParent.querySelector("label").id === + "translations-settings-download-all-languages" + ) { + this.handleDownloadAllLanguages(event); + } else { + this.handleDownloadLanguage(event); + } + } else if ( + eventNodeClassList.contains("translations-settings-remove-icon") + ) { + if ( + eventNodeParent.querySelector("label").id === + "translations-settings-download-all-languages" + ) { + this.handleRemoveAllDownloadLanguages(event); + } else { + this.handleRemoveDownloadLanguage(event); + } + } } - if ( - target === this.elements?.neverTranslateLanguagesButton || - target.closest?.("#translationsNeverTranslateLanguagesButton") + break; + } + }, + + // Keyboard navigation support. + handleKeys(event) { + switch (event.key) { + case "Enter": + // Handle Menulist selection through keyboard + if (event.target.id === "translations-settings-always-translate-list") { + this.handleAddAlwaysTranslateLanguage( + event.target.getAttribute("value") + ); + } else if ( + event.target.id === "translations-settings-never-translate-list" ) { - await this.onNeverTranslateLanguageChosen( - this.elements?.neverTranslateLanguagesSelect?.value ?? "" + this.handleAddNeverTranslateLanguage( + event.target.getAttribute("value") ); - break; } - + break; + case "ArrowUp": if ( - target === this.elements?.downloadLanguagesButton || - target.closest?.("#translationsDownloadLanguagesButton") + event.target.classList.contains("translations-settings-language-list") ) { - this.onDownloadLanguageButtonClicked(); - break; - } - - const downloadRemoveButton = /** @type {HTMLElement|null} */ ( - target.closest?.(`.${DOWNLOAD_LANGUAGE_REMOVE_BUTTON_CLASS}`) - ); - if (downloadRemoveButton?.dataset.langTag) { - this.onDeleteButtonClicked(downloadRemoveButton.dataset.langTag); - break; - } - - const downloadDeleteConfirmButton = /** @type {HTMLElement|null} */ ( - target.closest?.(`.${DOWNLOAD_LANGUAGE_DELETE_CONFIRM_BUTTON_CLASS}`) - ); - if (downloadDeleteConfirmButton?.dataset.langTag) { - this.confirmDeleteLanguage( - downloadDeleteConfirmButton.dataset.langTag + event.target.children[0].querySelector("moz-button").focus(); + // Update the selected element on the list according to the keyboard navigation by the user + event.target.setAttribute( + "aria-activedescendant", + event.target.children[0].id ); - break; - } - - const downloadDeleteCancelButton = /** @type {HTMLElement|null} */ ( - target.closest?.(`.${DOWNLOAD_LANGUAGE_DELETE_CANCEL_BUTTON_CLASS}`) - ); - if (downloadDeleteCancelButton?.dataset.langTag) { - this.cancelDeleteLanguage(downloadDeleteCancelButton.dataset.langTag); - break; - } - - const downloadRetryButton = /** @type {HTMLElement|null} */ ( - target.closest?.(`.${DOWNLOAD_LANGUAGE_RETRY_BUTTON_CLASS}`) - ); - if (downloadRetryButton?.dataset.langTag) { - this.retryDownloadLanguage(downloadRetryButton.dataset.langTag); - break; + } else if (event.target.tagName === "moz-button") { + if (event.target.parentNode.previousElementSibling) { + event.target.parentNode.previousElementSibling + .querySelector("moz-button") + .focus(); + // Update the selected element on the list according to the keyboard navigation by the user + event.target.parentNode.parentNode.setAttribute( + "aria-activedescendant", + event.target.parentNode.previousElementSibling.id + ); + event.preventDefault(); + } } - - const alwaysRemoveButton = /** @type {HTMLElement|null} */ ( - target.closest?.(`.${ALWAYS_TRANSLATE_LANGUAGE_REMOVE_BUTTON_CLASS}`) - ); - if (alwaysRemoveButton?.dataset.langTag) { - this.removeAlwaysTranslateLanguage( - alwaysRemoveButton.dataset.langTag + break; + case "ArrowDown": + if ( + event.target.classList.contains("translations-settings-language-list") + ) { + event.target.children[0].querySelector("moz-button").focus(); + // Update the selected element on the list according to the keyboard navigation by the user + event.target.setAttribute( + "aria-activedescendant", + event.target.children[0].id ); - break; - } - - const neverRemoveButton = /** @type {HTMLElement|null} */ ( - target.closest?.(`.${NEVER_TRANSLATE_LANGUAGE_REMOVE_BUTTON_CLASS}`) - ); - if (neverRemoveButton?.dataset.langTag) { - this.removeNeverTranslateLanguage(neverRemoveButton.dataset.langTag); - break; - } - - const neverSiteRemoveButton = /** @type {HTMLElement|null} */ ( - target.closest?.(`.${NEVER_TRANSLATE_SITE_REMOVE_BUTTON_CLASS}`) - ); - if (neverSiteRemoveButton?.dataset.origin) { - this.removeNeverTranslateSite(neverSiteRemoveButton.dataset.origin); + } else if (event.target.tagName === "moz-button") { + if (event.target.parentNode.nextElementSibling) { + event.target.parentNode.nextElementSibling + .querySelector("moz-button") + .focus(); + // Update the selected element on the list according to the keyboard navigation by the user + event.target.parentNode.parentNode.setAttribute( + "aria-activedescendant", + event.target.parentNode.nextElementSibling.id + ); + event.preventDefault(); + } } break; - } - case "unload": - this.teardown(); - break; } }, /** - * Observer for translations pref changes. + * Event handler when the user wants to add a language to + * Always translate settings preferences list. * - * @param {any} subject - * @param {string} topic - * @param {string} data + * @param {Event} event */ - observe(subject, topic, data) { - if (topic === TOPIC_TRANSLATIONS_PREF_CHANGED) { - if (data === ALWAYS_TRANSLATE_LANGS_PREF) { - this.refreshAlwaysTranslateLanguages().catch(console.error); - } else if (data === NEVER_TRANSLATE_LANGS_PREF) { - this.refreshNeverTranslateLanguages().catch(console.error); - } - } else if (topic === "perm-changed") { - this.handlePermissionChange(subject, data); - } + async handleAddAlwaysTranslateLanguage(langTag) { + // After a language is selected the menulist button display will be set to the + // selected langauge. After processing the button event the + // data-l10n-id of the menulist button is restored to "Add Language" + + const { alwaysTranslateMenuList } = this.elements; + TranslationsParent.addLangTagToPref(langTag, ALWAYS_TRANSLATE_LANGS_PREF); + await document.l10n.translateElements([alwaysTranslateMenuList]); }, /** - * Runs when the translations sub-pane is shown. + * Event handler when the user wants to add a language to + * Never translate settings preferences list. * - * @param {string} category - * @returns {Promise<void>} + * @param {Event} event */ - async handlePaneShown(category) { - if (category !== "paneTranslations") { - return; - } + async handleAddNeverTranslateLanguage(langTag) { + // After a language is selected the menulist button display will be set to the + // selected langauge. After processing the button event the + // data-l10n-id of the menulist button is restored to "Add Language" - if (this.initPromise) { - await this.initPromise; - await this.refreshAlwaysTranslateLanguages(); - await this.refreshNeverTranslateLanguages(); - this.refreshNeverTranslateSites(); - await this.refreshDownloadedLanguages(); - this.dispatchInitializedTestEvent(); - return; - } + const { neverTranslateMenuList } = this.elements; - if (this.initialized) { - await this.refreshAlwaysTranslateLanguages(); - await this.refreshNeverTranslateLanguages(); - this.refreshNeverTranslateSites(); - await this.refreshDownloadedLanguages(); - this.dispatchInitializedTestEvent(); - return; - } + TranslationsParent.addLangTagToPref(langTag, NEVER_TRANSLATE_LANGS_PREF); + await document.l10n.translateElements([neverTranslateMenuList]); + }, - this.initPromise = this.init(); - await this.initPromise; - this.initPromise = null; + /** + * Finds the langauges added and/or removed in the + * Always/Never translate lists. + * + * @param {Array<string>} currentSet + * @param {Array<string>} newSet + * @returns {object} {Array<string>, Array<string>} + */ + setDifference(currentSet, newSet) { + const added = newSet.filter(lang => !currentSet.includes(lang)); + const removed = currentSet.filter(lang => !newSet.includes(lang)); + return { added, removed }; }, /** - * Ensure the translations pane has finished rendering. + * Builds HTML elements for the Always/Never translate list + * According to the preference setting * - * @returns {Promise<void>} - */ - async ensurePaneRendered() { - if (this.paneRenderPromise) { - await this.paneRenderPromise; - return; - } + * @param {string} pref - name of the preference for which the HTML is built + * NEVER_TRANSLATE_LANGS_PREF / ALWAYS_TRANSLATE_LANGS_PREF + */ + populateLanguageList(pref) { + // languageList: <div> of the Always/Never translate section, which is a list of languages added by the user + // curLangTags: List of Language tag set in the the preference, Always/Never translate to be populated + // otherPref: name of the preference other than "pref" Never/Always + // when a language is added to "pref" remove the same from otherPref(if it exists) + // prefix: "always"/"never" string used to create ids for the language HTML elements for respective lists. + + const { languageList, curLangTags, otherPref, prefix } = + pref === NEVER_TRANSLATE_LANGS_PREF + ? { + languageList: this.elements.neverTranslateLanguageList, + curLangTags: Array.from(this.neverTranslateLanguages), + otherPref: ALWAYS_TRANSLATE_LANGS_PREF, + prefix: "never", + } + : { + languageList: this.elements.alwaysTranslateLanguageList, + curLangTags: Array.from(this.alwaysTranslateLanguages), + otherPref: NEVER_TRANSLATE_LANGS_PREF, + prefix: "always", + }; - /** - * @typedef {HTMLElement & { getUpdateComplete?: () => Promise<void> }} ElementWithUpdateComplete - */ - const pane = /** @type {ElementWithUpdateComplete|null} */ ( - document.querySelector('setting-pane[data-category="paneTranslations"]') - ); - const groups = Array.from( - document.querySelectorAll( - 'setting-group[groupid="translationsAutomaticTranslation"], setting-group[groupid="translationsDownloadLanguages"]' - ) - ); + const updatedLangTags = + pref === NEVER_TRANSLATE_LANGS_PREF + ? Array.from(TranslationsParent.getNeverTranslateLanguages()) + : Array.from(TranslationsParent.getAlwaysTranslateLanguages()); + + const { added, removed } = this.setDifference(curLangTags, updatedLangTags); - const promises = []; - if (pane?.getUpdateComplete) { - promises.push(pane.getUpdateComplete()); + for (const lang of removed) { + this.removeTranslateLanguage(lang, languageList); } - for (const group of groups) { - if (group?.getUpdateComplete) { - promises.push(group.getUpdateComplete()); - } + + // When the preferences is opened for the first time + // the translations settings HTML page is initialized with + // the existing settings by adding all languages from the latest preferences + for (const lang of added) { + this.addTranslateLanguage(lang, languageList, prefix); + // if a language is added to Always translate list, + // remove it from Never translate list and vice-versa + TranslationsParent.removeLangTagFromPref(lang, otherPref); } - if (promises.length) { - this.paneRenderPromise = (async () => { - const results = await Promise.allSettled(promises); - const failure = results.find(result => result.status === "rejected"); - if (failure && failure.reason) { - console.warn("Translations pane render wait failed", failure.reason); - } - })(); - await this.paneRenderPromise; + // Update state for neverTranslateLanguages/alwaysTranslateLanguages + if (pref === NEVER_TRANSLATE_LANGS_PREF) { + this.neverTranslateLanguages = updatedLangTags; + } else { + this.alwaysTranslateLanguages = updatedLangTags; } }, /** - * Initialize the translations settings UI. + * Adds a site to Never translate site list * - * @returns {Promise<void>} + * @param {string} site */ - async init() { - await this.ensurePaneRendered(); - this.cacheElements(); - if ( - !this.elements?.alwaysTranslateLanguagesGroup || - !this.elements?.alwaysTranslateLanguagesSelect || - !this.elements?.alwaysTranslateLanguagesButton || - !this.elements?.alwaysTranslateLanguagesNoneRow || - !this.elements?.neverTranslateLanguagesGroup || - !this.elements?.neverTranslateLanguagesSelect || - !this.elements?.neverTranslateLanguagesButton || - !this.elements?.neverTranslateLanguagesNoneRow || - !this.elements?.neverTranslateSitesGroup || - !this.elements?.downloadLanguagesGroup || - !this.elements?.downloadLanguagesSelect || - !this.elements?.downloadLanguagesButton || - !this.elements?.downloadLanguagesNoneRow - ) { - this.dispatchInitializedTestEvent(); - return; - } + addSite(site) { + const { neverTranslateSiteList } = this.elements; - try { - this.numberFormatter = null; - this.languageDisplayNames = - TranslationsParent.createLanguageDisplayNames(); - this.supportedLanguages = - await TranslationsParent.getSupportedLanguages(); - this.languageList = TranslationsParent.getLanguageList( - this.supportedLanguages - ); - await this.loadLanguageSizes(); - await this.refreshDownloadedLanguages(); - } catch (error) { - console.error("Failed to initialize translations settings UI", error); - this.elements.alwaysTranslateLanguagesSelect.disabled = true; - this.elements.alwaysTranslateLanguagesButton.disabled = true; - this.elements.neverTranslateLanguagesSelect.disabled = true; - this.elements.neverTranslateLanguagesButton.disabled = true; - this.elements.downloadLanguagesSelect.disabled = true; - this.setDownloadLanguageButtonDisabledState(true); - this.dispatchInitializedTestEvent(); - return; - } + // Label and textContent of the added site element is the same + const languageLabel = this.createLangLabel( + site, + site, + "translations-settings-" + site + ); - this.elements.alwaysTranslateLanguagesSelect.disabled = false; - this.elements.alwaysTranslateLanguagesButton.disabled = true; - this.elements.neverTranslateLanguagesSelect.disabled = false; - this.elements.neverTranslateLanguagesButton.disabled = true; - this.elements.downloadLanguagesSelect.disabled = false; - this.resetDownloadSelect(); - this.setDownloadLanguageButtonDisabledState(true); - await this.buildAlwaysTranslateSelectOptions(); - await this.buildNeverTranslateSelectOptions(); - await this.buildDownloadSelectOptions(); - await this.renderDownloadLanguages(); - - this.elements.alwaysTranslateLanguagesSelect.addEventListener( - "change", - this + const mozButton = this.createIconButton( + [ + "translations-settings-remove-icon", + "translations-settings-site-button", + ], + "translations-settings-remove-site-button-2", + site ); - this.elements.alwaysTranslateLanguagesButton.addEventListener( - "click", - this + + // Create unique id using site name + const languageElement = this.createLangElement( + [mozButton, languageLabel], + "translations-settings-" + site + "-id" ); - this.elements.alwaysTranslateLanguagesGroup.addEventListener("click", this); - this.elements.neverTranslateLanguagesSelect.addEventListener( - "change", - this + neverTranslateSiteList.insertBefore( + languageElement, + neverTranslateSiteList.firstElementChild ); - this.elements.neverTranslateLanguagesButton.addEventListener("click", this); - this.elements.neverTranslateLanguagesGroup.addEventListener("click", this); - this.elements.neverTranslateSitesGroup.addEventListener("click", this); - this.elements.downloadLanguagesSelect.addEventListener("change", this); - this.elements.downloadLanguagesGroup.addEventListener("click", this); - this.elements.downloadLanguagesButton.addEventListener("click", this); - Services.obs.addObserver(this, TOPIC_TRANSLATIONS_PREF_CHANGED); - Services.obs.addObserver(this, "perm-changed"); - window.addEventListener("unload", this); - - await this.refreshAlwaysTranslateLanguages(); - await this.refreshNeverTranslateLanguages(); - this.refreshNeverTranslateSites(); - this.initialized = true; - - this.dispatchInitializedTestEvent(); + // The first element is selected by default when keyboard navigation enters this list + neverTranslateSiteList.setAttribute( + "aria-activedescendant", + languageElement.id + ); + if (neverTranslateSiteList.childElementCount) { + neverTranslateSiteList.parentNode.hidden = false; + } }, /** - * Dispatch the test-only Initialized event and mark the document as ready. + * Removes a site from Never translate site list + * + * @param {string} site */ - dispatchInitializedTestEvent() { - dispatchTestEvent("Initialized"); + removeSite(site) { + const { neverTranslateSiteList } = this.elements; + + const langSite = neverTranslateSiteList.querySelector( + `label[value="${site}"]` + ); + + langSite.parentNode.remove(); + if (!neverTranslateSiteList.childElementCount) { + neverTranslateSiteList.parentNode.hidden = true; + } }, /** - * Cache the DOM elements we interact with. + * Builds HTML elements for the Never translate Site list + * According to the permissions setting */ - cacheElements() { - if (this.elements) { - return; - } - - const elements = { - alwaysTranslateLanguagesGroup: /** @type {HTMLElement} */ ( - document.getElementById("translationsAlwaysTranslateLanguagesGroup") - ), - alwaysTranslateLanguagesSelect: /** @type {HTMLSelectElement} */ ( - document.getElementById("translationsAlwaysTranslateLanguagesSelect") - ), - alwaysTranslateLanguagesButton: /** @type {HTMLButtonElement} */ ( - document.getElementById("translationsAlwaysTranslateLanguagesButton") - ), - alwaysTranslateLanguagesNoneRow: /** @type {HTMLElement} */ ( - document.getElementById("translationsAlwaysTranslateLanguagesNoneRow") - ), - neverTranslateLanguagesGroup: /** @type {HTMLElement} */ ( - document.getElementById("translationsNeverTranslateLanguagesGroup") - ), - neverTranslateLanguagesSelect: /** @type {HTMLSelectElement} */ ( - document.getElementById("translationsNeverTranslateLanguagesSelect") - ), - neverTranslateLanguagesButton: /** @type {HTMLButtonElement} */ ( - document.getElementById("translationsNeverTranslateLanguagesButton") - ), - neverTranslateLanguagesNoneRow: /** @type {HTMLElement} */ ( - document.getElementById("translationsNeverTranslateLanguagesNoneRow") - ), - neverTranslateSitesGroup: /** @type {HTMLElement} */ ( - document.getElementById("translationsNeverTranslateSitesGroup") - ), - neverTranslateSitesRow: /** @type {HTMLElement} */ ( - document.getElementById("translationsNeverTranslateSitesRow") - ), - neverTranslateSitesNoneRow: /** @type {HTMLElement} */ ( - document.getElementById("translationsNeverTranslateSitesNoneRow") - ), - downloadLanguagesGroup: /** @type {HTMLElement} */ ( - document.getElementById("translationsDownloadLanguagesGroup") - ), - downloadLanguagesSelect: /** @type {HTMLSelectElement} */ ( - document.getElementById("translationsDownloadLanguagesSelect") - ), - downloadLanguagesButton: /** @type {HTMLButtonElement} */ ( - document.getElementById("translationsDownloadLanguagesButton") - ), - downloadLanguagesNoneRow: /** @type {HTMLElement} */ ( - document.getElementById("translationsDownloadLanguagesNoneRow") - ), - }; - - if ( - !elements.alwaysTranslateLanguagesGroup || - !elements.alwaysTranslateLanguagesSelect || - !elements.alwaysTranslateLanguagesNoneRow || - !elements.neverTranslateLanguagesGroup || - !elements.neverTranslateLanguagesSelect || - !elements.neverTranslateLanguagesNoneRow - ) { - return; + populateSiteList() { + const siteList = TranslationsParent.listNeverTranslateSites(); + for (const site of siteList) { + this.addSite(site); } - - this.elements = elements; + this.neverTranslateSites = siteList; }, /** - * Load the download sizes for all supported languages and cache them. + * Oberver * - * @returns {Promise<void>} + * @param {string} subject Notification specific interface pointer. + * @param {string} topic nsPref:changed/perm-changed + * @param {string} data cleared/changed/added/deleted */ - async loadLanguageSizes() { - if (!this.languageList?.length) { - this.languageSizes = new Map(); - return; - } - - const sizes = await Promise.all( - this.languageList.map(async (/** @type {LanguageInfo} */ { langTag }) => { - try { - return /** @type {[string, number]} */ ([ - langTag, - await TranslationsParent.getLanguageSize(langTag), - ]); - } catch (error) { - console.error(`Failed to get size for ${langTag}`, error); - return /** @type {[string, number]} */ ([langTag, 0]); + observe(subject, topic, data) { + if (topic === "perm-changed") { + if (data === "cleared") { + const { neverTranslateSiteList } = this.elements; + this.neverTranslateSites = []; + for (const elem of neverTranslateSiteList.children) { + elem.remove(); } - }) - ); + if (!neverTranslateSiteList.childElementCount) { + neverTranslateSiteList.parentNode.hidden = true; + } + } else { + const perm = subject.QueryInterface(Ci.nsIPermission); + if (perm.type != TRANSLATIONS_PERMISSION) { + // The updated permission was not for Translations, nothing to do. + return; + } + if (data === "added") { + if (perm.capability != Services.perms.DENY_ACTION) { + // We are only showing data for sites we should never translate. + // If the permission is not DENY_ACTION, we don't care about it here. + return; + } + this.neverTranslateSites = + TranslationsParent.listNeverTranslateSites(); + this.addSite(perm.principal.origin); + } else if (data === "deleted") { + this.neverTranslateSites = + TranslationsParent.listNeverTranslateSites(); + this.removeSite(perm.principal.origin); + } + } + } else if (topic === TOPIC_TRANSLATIONS_PREF_CHANGED) { + switch (data) { + case ALWAYS_TRANSLATE_LANGS_PREF: + case NEVER_TRANSLATE_LANGS_PREF: { + this.populateLanguageList(data); + break; + } + } + } + }, - this.languageSizes = new Map(sizes); + /** + * Removes Observers + */ + removeObservers() { + Services.obs.removeObserver(this, "perm-changed"); + Services.obs.removeObserver(this, TOPIC_TRANSLATIONS_PREF_CHANGED); }, /** - * Format a download size for display. + * Create a div HTML element representing a language. * - * @param {string} langTag - * @returns {string|null} + * @param {Array} langChildren + * @returns {Element} div HTML element */ - formatLanguageSize(langTag) { - const sizeBytes = this.languageSizes?.get(langTag); - if (!sizeBytes && sizeBytes !== 0) { - return null; - } + createLangElement(langChildren, langId) { + const languageElement = document.createElement("div"); + languageElement.classList.add("translations-settings-language"); + // Keyboard navigation support + languageElement.setAttribute("role", "option"); + languageElement.id = langId; + languageElement.addEventListener("keydown", this); - const sizeInMB = sizeBytes / (1024 * 1024); - if (!Number.isFinite(sizeInMB)) { - return null; + for (const child of langChildren) { + languageElement.appendChild(child); } - - return this.getNumberFormatter().format(sizeInMB); + return languageElement; }, /** - * Lazily create and return a number formatter for the app locale. + * Creates a moz-button element as icon * - * @returns {Intl.NumberFormat} + * @param {string} classNames classes added to the moz-button element + * @param {string} buttonFluentID Fluent ID for the aria-label + * @param {string} accessibleName "name" variable value of the aria-label + * @returns {Element} HTML element of type Moz-Button */ - getNumberFormatter() { - if (this.numberFormatter) { - return this.numberFormatter; + createIconButton(classNames, buttonFluentID, accessibleName) { + const mozButton = document.createElement("moz-button"); + + for (const className of classNames) { + mozButton.classList.add(className); } - this.numberFormatter = new Intl.NumberFormat( - Services.locale.appLocaleAsBCP47, - { - minimumFractionDigits: 0, - maximumFractionDigits: 1, - } - ); - return this.numberFormatter; + mozButton.setAttribute("type", "ghost icon"); + // Note: aria-labelledby cannot be used as the id is not available for the shadow DOM element + document.l10n.setAttributes(mozButton, buttonFluentID, { + name: accessibleName, + }); + mozButton.addEventListener("click", this); + // Keyboard navigation support. Do not select the buttons on the list using tab. + // The buttons in the language lists are navigated using arrow buttons + mozButton.setAttribute("tabindex", "-1"); + return mozButton; }, /** - * Build the display label for a download language including its size. + * Adds a language selected by the user to the list of + * Always/Never translate settings list in the HTML. * - * @param {string} langTag - * @returns {Promise<string|null>} + * @param {string} langTag - The BCP-47 language tag for the language + * @param {Element} languageList - HTML element for the list of the languages. + * @param {string} translatePrefix - "never" / "always" prefix depending on the settings section */ - async formatDownloadLabel(langTag) { - const languageLabel = this.formatLanguageLabel(langTag) ?? langTag; - const sizeLabel = this.formatLanguageSize(langTag); - if (!sizeLabel) { - return languageLabel; - } + addTranslateLanguage(langTag, languageList, translatePrefix) { + // While adding the first language, add the Header and language List div + const languageDisplayNames = + TranslationsParent.createLanguageDisplayNames(); + + let languageDisplayName; try { - return await document.l10n.formatValue( - "settings-translations-subpage-download-language-option", - { language: languageLabel, size: sizeLabel } - ); + languageDisplayName = languageDisplayNames.of(langTag); } catch (error) { - console.error("Failed to format download language label", error); - return `${languageLabel} (${sizeLabel})`; + console.warn( + `Failed to retrieve language display name for '${langTag}'.` + ); + return; + } + + const languageLabel = this.createLangLabel( + languageDisplayName, + langTag, + "translations-settings-language-" + translatePrefix + "-" + langTag + ); + + const mozButton = this.createIconButton( + [ + "translations-settings-remove-icon", + "translations-settings-language-" + translatePrefix + "-button", + ], + "translations-settings-remove-language-button-2", + languageDisplayName + ); + + const languageElement = this.createLangElement( + [mozButton, languageLabel], + "translations-settings-language-" + + translatePrefix + + "-" + + langTag + + "-id" + ); + // Add the language after the Language Header + languageList.insertBefore(languageElement, languageList.firstElementChild); + // The first element is selected by default when keyboard navigation enters this list + languageList.setAttribute("aria-activedescendant", languageElement.id); + if (languageList.childElementCount) { + languageList.parentNode.hidden = false; } }, /** - * Populate the select options for download languages with sizes. + * Creates a label HTML element representing + * a language * - * @returns {Promise<void>} + * @param {string} textContent + * @param {string} value + * @param {string} id + * @returns {Element} HTML element of type label + */ + createLangLabel(textContent, value, id) { + const languageLabel = document.createElement("label"); + languageLabel.textContent = textContent; + languageLabel.setAttribute("value", value); + languageLabel.id = id; + return languageLabel; + }, + + /** + * Removes a language currently in the always/never translate language list + * from the DOM. Invoked in response to changes in the relevant preferences. + * + * @param {string} langTag The BCP-47 language tag for the language + * @param {Element} languageList - HTML element for the list of the languages. */ - async buildDownloadSelectOptions() { - const select = this.elements?.downloadLanguagesSelect; - if (!select || !this.supportedLanguages?.sourceLanguages?.length) { - return; + removeTranslateLanguage(langTag, languageList) { + const langElem = languageList.querySelector(`label[value=${langTag}]`); + if (langElem) { + langElem.parentNode.remove(); } - - const placeholder = select.querySelector('moz-option[value=""]'); - for (const option of select.querySelectorAll("moz-option")) { - if (option !== placeholder) { - option.remove(); - } + if (!languageList.childElementCount) { + languageList.parentNode.hidden = true; } - - const sourceLanguages = [...this.supportedLanguages.sourceLanguages] - .filter(({ langTag }) => langTag !== "en") - .sort((lhs, rhs) => - ( - this.formatLanguageLabel(lhs.langTag) ?? lhs.displayName - ).localeCompare( - this.formatLanguageLabel(rhs.langTag) ?? rhs.displayName - ) - ); - for (const { langTag, displayName } of sourceLanguages) { - const option = document.createElement("moz-option"); - option.setAttribute("value", langTag); - const label = - (await this.formatDownloadLabel(langTag)) ?? - this.formatLanguageLabel(langTag) ?? - displayName; - option.setAttribute("label", label); - const sizeLabel = this.formatLanguageSize(langTag) ?? ""; - if (sizeLabel) { - document.l10n.setAttributes( - option, - "settings-translations-subpage-download-language-option", - { - language: this.formatLanguageLabel(langTag) ?? displayName, - size: sizeLabel, - } - ); - } - select.appendChild(option); - } - - this.updateDownloadSelectOptionState(); - this.resetDownloadSelect(); - }, + }, /** - * Disable already-downloaded or downloading languages in the download select. + * Event Handler to remove a language selected by the user from the list of + * Always translate settings list in Preferences. + * + * @param {Event} event */ - updateDownloadSelectOptionState({ preserveSelection = false } = {}) { - const select = this.elements?.downloadLanguagesSelect; - if (!select) { - return; - } - - for (const option of select.querySelectorAll("moz-option")) { - const value = option.getAttribute("value"); - if (!value) { - continue; - } - const isDisabled = - this.downloadedLanguageTags.has(value) || - this.downloadingLanguageTags.has(value); - option.toggleAttribute("disabled", isDisabled); - } - - if (preserveSelection) { - this.updateDownloadLanguageButtonDisabled(); - } else { - this.resetDownloadSelect(); - } - dispatchTestEvent("DownloadedLanguagesSelectOptionsUpdated"); + handleRemoveAlwaysTranslateLanguage(event) { + TranslationsParent.removeLangTagFromPref( + event.target.parentNode.querySelector("label").getAttribute("value"), + ALWAYS_TRANSLATE_LANGS_PREF + ); }, /** - * Handle a selection in the "Always translate languages" dropdown. + * Event Handler to remove a language selected by the user from the list of + * Never translate settings list in Preferences. * - * @param {string} langTag + * @param {Event} event */ - async onAlwaysTranslateLanguageChosen(langTag) { - if (!langTag) { - this.updateAlwaysTranslateAddButtonDisabledState(); - return; - } - - if (this.shouldDisableAlwaysTranslateAddButton()) { - this.updateAlwaysTranslateAddButtonDisabledState(); - return; - } - - TranslationsParent.addLangTagToPref(langTag, ALWAYS_TRANSLATE_LANGS_PREF); + handleRemoveNeverTranslateLanguage(event) { TranslationsParent.removeLangTagFromPref( - langTag, + event.target.parentNode.querySelector("label").getAttribute("value"), NEVER_TRANSLATE_LANGS_PREF ); - await this.resetAlwaysTranslateSelect(); }, /** - * Handle a selection change in the always-translate dropdown. - */ - onAlwaysTranslateLanguageSelectionChanged() { - this.updateAlwaysTranslateAddButtonDisabledState(); - }, - - /** - * Whether the add button for always-translate languages should be disabled. + * Removes the site chosen by the user in the HTML + * from the Never Translate Site Permission * - * @returns {boolean} + * @param {Event} event */ - shouldDisableAlwaysTranslateAddButton() { - const select = this.elements?.alwaysTranslateLanguagesSelect; - if (!select || select.disabled) { - return true; - } - - const langTag = select.value; - if (!langTag) { - return true; - } - - const option = /** @type {HTMLElement|null} */ ( - select.querySelector(`moz-option[value="${langTag}"]`) + handleRemoveNeverTranslateSite(event) { + TranslationsParent.setNeverTranslateSiteByOrigin( + false, + event.target.parentNode.querySelector("label").getAttribute("value") ); - return option?.hasAttribute("disabled") ?? false; }, - /** - * Set the add button enabled state for always-translate languages. + * Record the download phase downloaded/loading/removed for + * given language in the local data. * - * @param {boolean} isDisabled + * @param {string} langTag + * @param {string} downloadPhase */ - setAlwaysTranslateAddButtonDisabledState(isDisabled) { - if (!this.elements?.alwaysTranslateLanguagesButton) { - return; - } - - const wasDisabled = this.elements.alwaysTranslateLanguagesButton.disabled; - this.elements.alwaysTranslateLanguagesButton.disabled = isDisabled; - if (wasDisabled !== isDisabled) { - dispatchTestEvent( - isDisabled - ? "AlwaysTranslateLanguagesAddButtonDisabled" - : "AlwaysTranslateLanguagesAddButtonEnabled" + updateDownloadPhase(langTag, downloadPhase) { + if (!this.downloadPhases.has(langTag)) { + console.error( + `Expected downloadPhases entry for ${langTag}, but found none.` ); + } else { + this.downloadPhases.get(langTag).downloadPhase = downloadPhase; } }, - - /** - * Update the add button enabled state for always-translate languages. - */ - updateAlwaysTranslateAddButtonDisabledState() { - this.setAlwaysTranslateAddButtonDisabledState( - this.shouldDisableAlwaysTranslateAddButton() - ); - }, - /** - * Remove the given language from the always translate list. - * - * @param {string} langTag + * Updates the button icons and its download states for the download language elements + * in the HTML by getting the download status of all languages from the browser records. */ - removeAlwaysTranslateLanguage(langTag) { - TranslationsParent.removeLangTagFromPref( - langTag, - ALWAYS_TRANSLATE_LANGS_PREF - ); - }, - - async resetSelect(select, settingId) { - const setting = Preferences.getSetting?.(settingId); - if (setting) { - setting.value = ""; - } - - if (!select) { - return; - } + async reloadDownloadPhases() { + let downloadCount = 0; + const { downloadLanguageList } = this.elements; - if (select.updateComplete) { - await select.updateComplete; - } + const allLangElem = downloadLanguageList.firstElementChild; + const allLangButton = allLangElem.querySelector("moz-button"); - select.value = ""; - if (select.inputEl) { - select.inputEl.value = ""; + const updatePromises = []; + for (const langElem of downloadLanguageList.querySelectorAll( + ".translations-settings-language:not(:first-child)" + )) { + const langLabel = langElem.querySelector("label"); + const langTag = langLabel.getAttribute("value"); + const langButton = langElem.querySelector("moz-button"); + + updatePromises.push( + TranslationsParent.hasAllFilesForLanguage(langTag).then( + hasAllFilesForLanguage => { + if (hasAllFilesForLanguage) { + downloadCount += 1; + this.changeButtonState({ + langButton, + langTag, + langState: "downloaded", + }); + } else { + this.changeButtonState({ + langButton, + langTag, + langState: "removed", + }); + } + langButton.removeAttribute("disabled"); + } + ) + ); } + await Promise.allSettled(updatePromises); - if (select.updateComplete) { - await select.updateComplete; + const allDownloaded = + downloadCount === this.supportedLanguageTagsNames.length; + if (allDownloaded) { + this.changeButtonState({ + langButton: allLangButton, + langTag: "all", + langState: "downloaded", + }); + } else { + this.changeButtonState({ + langButton: allLangButton, + langTag: "all", + langState: "removed", + }); } }, - /** - * Reset the dropdown back to the placeholder value and underlying setting state. - */ - async resetAlwaysTranslateSelect() { - await this.resetSelect( - this.elements?.alwaysTranslateLanguagesSelect, - "translationsAlwaysTranslateLanguagesSelect" - ); - this.updateAlwaysTranslateAddButtonDisabledState(); + showErrorMessage(parentNode, fluentId, language) { + const errorElement = document.createElement("moz-message-bar"); + errorElement.setAttribute("type", "error"); + document.l10n.setAttributes(errorElement, fluentId, { + name: language, + }); + errorElement.classList.add("translations-settings-language-error"); + parentNode.appendChild(errorElement); }, - /** - * Refresh the rendered list of always-translate languages to match prefs. - */ - async refreshAlwaysTranslateLanguages() { - if (!this.elements?.alwaysTranslateLanguagesGroup) { - return; - } - - const langTags = Array.from( - TranslationsParent.getAlwaysTranslateLanguages?.() ?? [] - ); - - if (this.alwaysTranslateLanguageTags) { - for (const langTag of langTags) { - if (this.alwaysTranslateLanguageTags.has(langTag)) { - continue; - } - TranslationsParent.removeLangTagFromPref( - langTag, - NEVER_TRANSLATE_LANGS_PREF - ); - } - } - - this.alwaysTranslateLanguageTags = new Set(langTags); - - this.renderAlwaysTranslateLanguages(langTags); - await this.updateAlwaysTranslateSelectOptionState(); + removeError(errorNode) { + errorNode?.remove(); }, /** - * Render the current set of always-translate languages into the list UI. + * Event Handler to download a language model selected by the user through HTML * - * @param {string[]} langTags + * @param {Event} event */ - renderAlwaysTranslateLanguages(langTags) { - const { alwaysTranslateLanguagesGroup, alwaysTranslateLanguagesNoneRow } = - this.elements; - - for (const item of alwaysTranslateLanguagesGroup.querySelectorAll( - `.${ALWAYS_TRANSLATE_LANGUAGE_ITEM_CLASS}` - )) { - item.remove(); - } - - const previousEmptyStateVisible = - alwaysTranslateLanguagesNoneRow && - !alwaysTranslateLanguagesNoneRow.hidden; - - if (alwaysTranslateLanguagesNoneRow) { - const hasLanguages = !!langTags.length; - alwaysTranslateLanguagesNoneRow.hidden = hasLanguages; + async handleDownloadLanguage(event) { + let eventButton = event.target; + const langTag = eventButton.parentNode + .querySelector("label") + .getAttribute("value"); - if (hasLanguages && alwaysTranslateLanguagesNoneRow.isConnected) { - alwaysTranslateLanguagesNoneRow.remove(); - } else if ( - !hasLanguages && - !alwaysTranslateLanguagesNoneRow.isConnected - ) { - alwaysTranslateLanguagesGroup.appendChild( - alwaysTranslateLanguagesNoneRow - ); - } - } - - const sortedLangTags = [...langTags].sort((langTagA, langTagB) => { - const labelA = this.formatLanguageLabel(langTagA) ?? langTagA; - const labelB = this.formatLanguageLabel(langTagB) ?? langTagB; - return labelA.localeCompare(labelB); + this.changeButtonState({ + langButton: eventButton, + langTag, + langState: "loading", }); - for (const langTag of sortedLangTags) { - const label = this.formatLanguageLabel(langTag); - if (!label) { - continue; - } + try { + await TranslationsParent.downloadLanguageFiles(langTag); + } catch (error) { + console.error(error); + + const languageDisplayNames = + TranslationsParent.createLanguageDisplayNames(); - const removeButton = document.createElement("moz-button"); - removeButton.setAttribute("slot", "actions-start"); - removeButton.setAttribute("type", "icon"); - removeButton.setAttribute( - "iconsrc", - "chrome://global/skin/icons/delete.svg" + this.showErrorMessage( + eventButton.parentNode, + "translations-settings-language-download-error", + languageDisplayNames.of(langTag) ); - removeButton.classList.add(ALWAYS_TRANSLATE_LANGUAGE_REMOVE_BUTTON_CLASS); - removeButton.dataset.langTag = langTag; - removeButton.setAttribute("aria-label", label); - - const item = document.createElement("moz-box-item"); - item.classList.add(ALWAYS_TRANSLATE_LANGUAGE_ITEM_CLASS); - item.setAttribute("label", label); - item.dataset.langTag = langTag; - item.appendChild(removeButton); - if ( - alwaysTranslateLanguagesNoneRow && - alwaysTranslateLanguagesNoneRow.parentElement === - alwaysTranslateLanguagesGroup - ) { - alwaysTranslateLanguagesGroup.insertBefore( - item, - alwaysTranslateLanguagesNoneRow - ); - } else { - alwaysTranslateLanguagesGroup.appendChild(item); + const hasAllFilesForLanguage = + await TranslationsParent.hasAllFilesForLanguage(langTag); + + if (!hasAllFilesForLanguage) { + this.changeButtonState({ + langButton: eventButton, + langTag, + langState: "removed", + }); + return; } } - - dispatchTestEvent("AlwaysTranslateLanguagesRendered", { - languages: langTags, - count: langTags.length, + this.changeButtonState({ + langButton: eventButton, + langTag, + langState: "downloaded", }); - const currentEmptyStateVisible = - alwaysTranslateLanguagesNoneRow && - !alwaysTranslateLanguagesNoneRow.hidden; - if (previousEmptyStateVisible && !currentEmptyStateVisible) { - dispatchTestEvent("AlwaysTranslateLanguagesEmptyStateHidden"); - } else if (!previousEmptyStateVisible && currentEmptyStateVisible) { - dispatchTestEvent("AlwaysTranslateLanguagesEmptyStateShown"); + // If all languages are downloaded, change "All Languages" to downloaded + const haveRemovedItem = [...this.downloadPhases].some( + ([k, v]) => v.downloadPhase != "downloaded" && k != "all" + ); + if ( + !haveRemovedItem && + this.downloadPhases.get("all").downloadPhase !== "downloaded" + ) { + this.changeButtonState({ + langButton: + this.elements.downloadLanguageList.firstElementChild.querySelector( + "moz-button" + ), + langTag: "all", + langState: "downloaded", + }); } }, /** - * Format a language tag for display using the cached display names. + * Event Handler to remove a language model selected by the user through HTML * - * @param {string} langTag - * @returns {string|null} + * @param {Event} event */ - formatLanguageLabel(langTag) { + async handleRemoveDownloadLanguage(event) { + let eventButton = event.target; + const langTag = eventButton.parentNode + .querySelector("label") + .getAttribute("value"); + + this.changeButtonState({ + langButton: eventButton, + langTag, + langState: "loading", + }); + try { - return this.languageDisplayNames?.of(langTag) ?? null; + await TranslationsParent.deleteLanguageFiles(langTag); } catch (error) { - console.warn(`Failed to format language label for ${langTag}`, error); - return null; - } - }, - - /** - * Populate the select options for the supported source languages. - */ - async buildAlwaysTranslateSelectOptions() { - const select = this.elements?.alwaysTranslateLanguagesSelect; - if (!select || !this.supportedLanguages?.sourceLanguages?.length) { - return; - } + // The download phases are invalidated with the error and must be reloaded. + console.error(error); - const placeholder = select.querySelector('moz-option[value=""]'); - for (const option of select.querySelectorAll("moz-option")) { - if (option !== placeholder) { - option.remove(); - } - } + const languageDisplayNames = + TranslationsParent.createLanguageDisplayNames(); - const sourceLanguages = [...this.supportedLanguages.sourceLanguages].sort( - (lhs, rhs) => - ( - this.formatLanguageLabel(lhs.langTag) ?? lhs.displayName - ).localeCompare( - this.formatLanguageLabel(rhs.langTag) ?? rhs.displayName - ) - ); - for (const { langTag, displayName } of sourceLanguages) { - const option = document.createElement("moz-option"); - option.setAttribute("value", langTag); - option.setAttribute( - "label", - this.formatLanguageLabel(langTag) ?? displayName + this.showErrorMessage( + eventButton.parentNode, + "translations-settings-language-remove-error", + languageDisplayNames.of(langTag) ); - select.appendChild(option); - } - - await this.resetAlwaysTranslateSelect(); - }, - - /** - * Disable already-added languages in the select so they cannot be re-added. - */ - async updateAlwaysTranslateSelectOptionState() { - const select = this.elements?.alwaysTranslateLanguagesSelect; - if (!select) { - return; - } - - for (const option of select.querySelectorAll("moz-option")) { - const value = option.getAttribute("value"); - if (!value) { - continue; + const hasAllFilesForLanguage = + await TranslationsParent.hasAllFilesForLanguage(langTag); + if (hasAllFilesForLanguage) { + this.changeButtonState({ + langButton: eventButton, + langTag, + langState: "downloaded", + }); + return; } - option.disabled = this.alwaysTranslateLanguageTags.has(value); - } - - await this.resetAlwaysTranslateSelect(); - - dispatchTestEvent("AlwaysTranslateLanguagesSelectOptionsUpdated"); - }, - - /** - * Handle a selection in the "Never translate languages" dropdown. - * - * @param {string} langTag - */ - async onNeverTranslateLanguageChosen(langTag) { - if (!langTag) { - this.updateNeverTranslateAddButtonDisabledState(); - return; - } - - if (this.shouldDisableNeverTranslateAddButton()) { - this.updateNeverTranslateAddButtonDisabledState(); - return; } - TranslationsParent.addLangTagToPref(langTag, NEVER_TRANSLATE_LANGS_PREF); - TranslationsParent.removeLangTagFromPref( + this.changeButtonState({ + langButton: eventButton, langTag, - ALWAYS_TRANSLATE_LANGS_PREF - ); - await this.resetNeverTranslateSelect(); - }, + langState: "removed", + }); - /** - * Handle a selection change in the never-translate dropdown. - */ - onNeverTranslateLanguageSelectionChanged() { - this.updateNeverTranslateAddButtonDisabledState(); + // If >=1 languages are removed change "All Languages" state to removed + if (this.downloadPhases.get("all").downloadPhase === "downloaded") { + this.changeButtonState({ + langButton: + this.elements.downloadLanguageList.firstElementChild.querySelector( + "moz-button" + ), + langTag: "all", + langState: "removed", + }); + } }, /** - * Whether the add button for never-translate languages should be disabled. + * Event Handler to download all language models * - * @returns {boolean} + * @param {Event} event */ - shouldDisableNeverTranslateAddButton() { - const select = this.elements?.neverTranslateLanguagesSelect; - if (!select || select.disabled) { - return true; - } + async handleDownloadAllLanguages(event) { + // Disable all buttons and show loading icon + this.disableDownloadButtons(); + let eventButton = event.target; + this.changeButtonState({ + langButton: eventButton, + langTag: "all", + langState: "loading", + }); - const langTag = select.value; - if (!langTag) { - return true; + try { + await TranslationsParent.downloadAllFiles(); + } catch (error) { + console.error(error); + await this.reloadDownloadPhases(); + this.showErrorMessage( + eventButton.parentNode, + "translations-settings-language-download-error", + "all" + ); + return; } - - const option = /** @type {HTMLElement|null} */ ( - select.querySelector(`moz-option[value="${langTag}"]`) - ); - return option?.hasAttribute("disabled") ?? false; + this.changeButtonState({ + langButton: eventButton, + langTag: "all", + langState: "downloaded", + }); + this.updateAllLanguageDownloadButtons("downloaded"); }, /** - * Set the add button enabled state for never-translate languages. + * Event Handler to remove all language models * - * @param {boolean} isDisabled + * @param {Event} event */ - setNeverTranslateAddButtonDisabledState(isDisabled) { - if (!this.elements?.neverTranslateLanguagesButton) { - return; - } + async handleRemoveAllDownloadLanguages(event) { + let eventButton = event.target; + this.disableDownloadButtons(); + this.changeButtonState({ + langButton: eventButton, + langTag: "all", + langState: "loading", + }); - const wasDisabled = this.elements.neverTranslateLanguagesButton.disabled; - this.elements.neverTranslateLanguagesButton.disabled = isDisabled; - if (wasDisabled !== isDisabled) { - dispatchTestEvent( - isDisabled - ? "NeverTranslateLanguagesAddButtonDisabled" - : "NeverTranslateLanguagesAddButtonEnabled" + try { + await TranslationsParent.deleteAllLanguageFiles(); + } catch (error) { + console.error(error); + await this.reloadDownloadPhases(); + this.showErrorMessage( + eventButton.parentNode, + "translations-settings-language-remove-error", + "all" ); + return; } + this.changeButtonState({ + langButton: eventButton, + langTag: "all", + langState: "removed", + }); + this.updateAllLanguageDownloadButtons("removed"); }, /** - * Update the add button enabled state for never-translate languages. - */ - updateNeverTranslateAddButtonDisabledState() { - this.setNeverTranslateAddButtonDisabledState( - this.shouldDisableNeverTranslateAddButton() - ); - }, - - /** - * Remove the given language from the never translate list. - * - * @param {string} langTag - */ - removeNeverTranslateLanguage(langTag) { - TranslationsParent.removeLangTagFromPref( - langTag, - NEVER_TRANSLATE_LANGS_PREF - ); - }, - - /** - * Reset the dropdown back to the placeholder value and underlying setting state. + * Disables the buttons to download/remove inidividual languages + * when "all languages" are downloaded/removed. + * This is done to ensure that no individual languages are downloaded/removed + * when the download/remove operations for "all languages" is progress. */ - async resetNeverTranslateSelect() { - await this.resetSelect( - this.elements?.neverTranslateLanguagesSelect, - "translationsNeverTranslateLanguagesSelect" - ); - this.updateNeverTranslateAddButtonDisabledState(); - }, + disableDownloadButtons() { + const { downloadLanguageList } = this.elements; - /** - * Refresh the rendered list of never-translate languages to match prefs. - */ - async refreshNeverTranslateLanguages() { - if (!this.elements?.neverTranslateLanguagesGroup) { - return; + // Disable all elements except the first one which is "All langauges" + for (const langElem of downloadLanguageList.querySelectorAll( + ".translations-settings-language:not(:first-child)" + )) { + const langButton = langElem.querySelector("moz-button"); + langButton.setAttribute("disabled", "true"); } - - const langTags = Array.from( - TranslationsParent.getNeverTranslateLanguages?.() ?? [] - ); - this.neverTranslateLanguageTags = new Set(langTags); - - this.renderNeverTranslateLanguages(langTags); - await this.updateNeverTranslateSelectOptionState(); }, /** - * Render the current set of never-translate languages into the list UI. + * Changes the state of all individual language buttons as downloaded/removed + * based on the download state of "All Language" status + * changes the icon of individual language buttons: + * from "download" icon to "remove" icon if "All Language" is downloaded. + * from "remove" icon to "download" icon if "All Language" is removed. * - * @param {string[]} langTags + * @param {string} allLanguageDownloadStatus "All Language" status: downloaded/removed */ - renderNeverTranslateLanguages(langTags) { - const { neverTranslateLanguagesGroup, neverTranslateLanguagesNoneRow } = - this.elements; + updateAllLanguageDownloadButtons(allLanguageDownloadStatus) { + const { downloadLanguageList } = this.elements; - for (const item of neverTranslateLanguagesGroup.querySelectorAll( - `.${NEVER_TRANSLATE_LANGUAGE_ITEM_CLASS}` + // Change the state of all individual language buttons except the first one which is "All langauges" + for (const langElem of downloadLanguageList.querySelectorAll( + ".translations-settings-language:not(:first-child)" )) { - item.remove(); - } - - const previousEmptyStateVisible = - neverTranslateLanguagesNoneRow && !neverTranslateLanguagesNoneRow.hidden; - - if (neverTranslateLanguagesNoneRow) { - const hasLanguages = Boolean(langTags.length); - neverTranslateLanguagesNoneRow.hidden = hasLanguages; - - if (hasLanguages && neverTranslateLanguagesNoneRow.isConnected) { - neverTranslateLanguagesNoneRow.remove(); - } else if (!hasLanguages && !neverTranslateLanguagesNoneRow.isConnected) { - neverTranslateLanguagesGroup.appendChild( - neverTranslateLanguagesNoneRow - ); - } - } - - const sortedLangTags = [...langTags].sort((langTagA, langTagB) => { - const labelA = this.formatLanguageLabel(langTagA) ?? langTagA; - const labelB = this.formatLanguageLabel(langTagB) ?? langTagB; - return labelA.localeCompare(labelB); - }); + let langButton = langElem.querySelector("moz-button"); + const langLabel = langElem.querySelector("label"); + const downloadPhase = this.downloadPhases.get( + langLabel.getAttribute("value") + ).downloadPhase; - for (const langTag of sortedLangTags) { - const label = this.formatLanguageLabel(langTag); - if (!label) { - continue; - } + langButton.removeAttribute("disabled"); - const removeButton = document.createElement("moz-button"); - removeButton.setAttribute("slot", "actions-start"); - removeButton.setAttribute("type", "icon"); - removeButton.setAttribute( - "iconsrc", - "chrome://global/skin/icons/delete.svg" - ); - removeButton.classList.add(NEVER_TRANSLATE_LANGUAGE_REMOVE_BUTTON_CLASS); - removeButton.dataset.langTag = langTag; - removeButton.setAttribute("aria-label", label); - - const item = document.createElement("moz-box-item"); - item.classList.add(NEVER_TRANSLATE_LANGUAGE_ITEM_CLASS); - item.setAttribute("label", label); - item.dataset.langTag = langTag; - item.appendChild(removeButton); if ( - neverTranslateLanguagesNoneRow && - neverTranslateLanguagesNoneRow.parentElement === - neverTranslateLanguagesGroup + downloadPhase !== "downloaded" && + allLanguageDownloadStatus === "downloaded" ) { - neverTranslateLanguagesGroup.insertBefore( - item, - neverTranslateLanguagesNoneRow - ); - } else { - neverTranslateLanguagesGroup.appendChild(item); + // In case of "All languages" downloaded + this.changeButtonState({ + langButton, + langTag: langLabel.getAttribute("value"), + langState: "downloaded", + }); + } else if ( + downloadPhase === "downloaded" && + allLanguageDownloadStatus === "removed" + ) { + // In case of "All languages" removed + this.changeButtonState({ + langButton, + langTag: langLabel.getAttribute("value"), + langState: "removed", + }); } } - - dispatchTestEvent("NeverTranslateLanguagesRendered", { - languages: langTags, - count: langTags.length, - }); - - const currentEmptyStateVisible = - neverTranslateLanguagesNoneRow && !neverTranslateLanguagesNoneRow.hidden; - if (previousEmptyStateVisible && !currentEmptyStateVisible) { - dispatchTestEvent("NeverTranslateLanguagesEmptyStateHidden"); - } else if (!previousEmptyStateVisible && currentEmptyStateVisible) { - dispatchTestEvent("NeverTranslateLanguagesEmptyStateShown"); - } }, /** - * Populate the select options for the supported source languages. - */ - async buildNeverTranslateSelectOptions() { - const select = this.elements?.neverTranslateLanguagesSelect; - if (!select || !this.supportedLanguages?.sourceLanguages?.length) { - return; - } - - const placeholder = select.querySelector('moz-option[value=""]'); - for (const option of select.querySelectorAll("moz-option")) { - if (option !== placeholder) { - option.remove(); - } - } - - const sourceLanguages = [...this.supportedLanguages.sourceLanguages].sort( - (lhs, rhs) => - ( - this.formatLanguageLabel(lhs.langTag) ?? lhs.displayName - ).localeCompare( - this.formatLanguageLabel(rhs.langTag) ?? rhs.displayName - ) + * Updates the state of a language download button. + * + * This function changes the button's appearance and behavior based on the current language state + * (e.g., "download", "loading", or "removed"). The button's icon and CSS class are updated to reflect + * the state, and the appropriate event handler is set for downloading or removing the language. + * The aria-label for accessibility is also updated using the Fluent string. + * + * @param {object} options - + * @param {Element} options.langButton - The HTML button element representing the language action (download/remove). + * @param {string} options.langTag - The BCP-47 language tag for the language associated with the button. + * @param {string} options.langState - The current state of the language, which can be "downloaded", "loading", or "removed". + */ + changeButtonState({ langButton, langTag, langState }) { + // Remove any icon by removing it's respective CSS class + langButton.classList.remove( + "translations-settings-download-icon", + "translations-settings-loading-icon", + "translations-settings-remove-icon" ); - for (const { langTag, displayName } of sourceLanguages) { - const option = document.createElement("moz-option"); - option.setAttribute("value", langTag); - option.setAttribute( - "label", - this.formatLanguageLabel(langTag) ?? displayName - ); - select.appendChild(option); - } - - await this.resetNeverTranslateSelect(); - }, - - /** - * Disable already-added languages in the select so they cannot be re-added. - */ - async updateNeverTranslateSelectOptionState() { - const select = this.elements?.neverTranslateLanguagesSelect; - if (!select) { - return; - } - - for (const option of select.querySelectorAll("moz-option")) { - const value = option.getAttribute("value"); - if (!value) { - continue; - } - option.disabled = this.neverTranslateLanguageTags.has(value); - } - - await this.resetNeverTranslateSelect(); - - dispatchTestEvent("NeverTranslateLanguagesSelectOptionsUpdated"); - }, - - /** - * Refresh the rendered list of never-translate sites. - */ - refreshNeverTranslateSites() { - if (!this.elements?.neverTranslateSitesGroup) { - return; - } - - /** @type {string[]} */ - let siteOrigins = []; - try { - siteOrigins = TranslationsParent.listNeverTranslateSites() ?? []; - } catch (error) { - console.error("Failed to list never translate sites", error); - } - - this.neverTranslateSiteOrigins = new Set(siteOrigins); - this.renderNeverTranslateSites(siteOrigins); - }, - - /** - * Render the never-translate sites list. - * - * @param {string[]} siteOrigins - */ - renderNeverTranslateSites(siteOrigins) { - const { neverTranslateSitesGroup, neverTranslateSitesNoneRow } = - this.elements ?? {}; - if (!neverTranslateSitesGroup) { - return; - } - - for (const item of neverTranslateSitesGroup.querySelectorAll( - `.${NEVER_TRANSLATE_SITE_ITEM_CLASS}` - )) { - item.remove(); - } - - const previousEmptyStateVisible = - neverTranslateSitesNoneRow && !neverTranslateSitesNoneRow.hidden; - - if (neverTranslateSitesNoneRow) { - const hasSites = Boolean(siteOrigins.length); - neverTranslateSitesNoneRow.hidden = hasSites; - - if (hasSites && neverTranslateSitesNoneRow.isConnected) { - neverTranslateSitesNoneRow.remove(); - } else if (!hasSites && !neverTranslateSitesNoneRow.isConnected) { - neverTranslateSitesGroup.appendChild(neverTranslateSitesNoneRow); - } - } - - const sortedOrigins = [...siteOrigins].sort((originA, originB) => { - return this.getSiteSortKey(originA).localeCompare( - this.getSiteSortKey(originB) - ); - }); - - for (const origin of sortedOrigins) { - const removeButton = document.createElement("moz-button"); - removeButton.setAttribute("slot", "actions-start"); - removeButton.setAttribute("type", "icon"); - removeButton.setAttribute( - "iconsrc", - "chrome://global/skin/icons/delete.svg" - ); - removeButton.classList.add(NEVER_TRANSLATE_SITE_REMOVE_BUTTON_CLASS); - removeButton.dataset.origin = origin; - removeButton.setAttribute("aria-label", origin); - - const item = document.createElement("moz-box-item"); - item.classList.add(NEVER_TRANSLATE_SITE_ITEM_CLASS); - item.setAttribute("label", origin); - item.dataset.origin = origin; - item.appendChild(removeButton); - if ( - neverTranslateSitesNoneRow && - neverTranslateSitesNoneRow.parentElement === neverTranslateSitesGroup - ) { - neverTranslateSitesGroup.insertBefore(item, neverTranslateSitesNoneRow); - } else { - neverTranslateSitesGroup.appendChild(item); - } - } - - dispatchTestEvent("NeverTranslateSitesRendered", { - sites: siteOrigins, - count: siteOrigins.length, - }); - - const currentEmptyStateVisible = - neverTranslateSitesNoneRow && !neverTranslateSitesNoneRow.hidden; - if (previousEmptyStateVisible && !currentEmptyStateVisible) { - dispatchTestEvent("NeverTranslateSitesEmptyStateHidden"); - } else if (!previousEmptyStateVisible && currentEmptyStateVisible) { - dispatchTestEvent("NeverTranslateSitesEmptyStateShown"); - } - }, - - /** - * Remove a site from the never-translate list. - * - * @param {string} origin - */ - removeNeverTranslateSite(origin) { - if (!origin || !this.neverTranslateSiteOrigins.has(origin)) { - return; - } - - try { - TranslationsParent.setNeverTranslateSiteByOrigin(false, origin); - } catch (error) { - console.error("Failed to remove never translate site", error); - return; - } - - this.refreshNeverTranslateSites(); - }, - - /** - * Create a sort key that ignores protocol differences. - * - * @param {string} origin - * @returns {string} - */ - getSiteSortKey(origin) { - try { - return Services.io.newURI(origin).asciiHostPort; - } catch { - return origin; - } - }, - - /** - * Handle a selection change in the download dropdown. - */ - onDownloadSelectionChanged() { - this.updateDownloadLanguageButtonDisabled(); - }, - - /** - * Whether the download button should be disabled based on selection state. - * - * @returns {boolean} - */ - shouldDisableDownloadLanguageButton() { - const select = this.elements?.downloadLanguagesSelect; - if (!select || this.currentDownloadLangTag) { - return true; - } - - const langTag = select.value; - if (!langTag) { - return true; - } - - const option = /** @type {HTMLElement|null} */ ( - select.querySelector(`moz-option[value="${langTag}"]`) - ); - return option?.hasAttribute("disabled") ?? false; - }, - - /** - * Set the download button state and dispatch test events when it changes. - * - * @param {boolean} isDisabled - */ - setDownloadLanguageButtonDisabledState(isDisabled) { - const button = this.elements?.downloadLanguagesButton; - if (!button) { - return; - } - - const wasDisabled = button.disabled; - button.disabled = isDisabled; - - if (wasDisabled !== isDisabled) { - dispatchTestEvent( - isDisabled - ? "DownloadLanguageButtonDisabled" - : "DownloadLanguageButtonEnabled" - ); - } - }, - - /** - * Update the enabled state of the download button. - */ - updateDownloadLanguageButtonDisabled() { - this.setDownloadLanguageButtonDisabledState( - this.shouldDisableDownloadLanguageButton() - ); - }, - - /** - * Handle a click on the download button. - * - * @returns {Promise<void>} - */ - async onDownloadLanguageButtonClicked() { - const langTag = this.elements?.downloadLanguagesSelect?.value; - if (!langTag || this.currentDownloadLangTag) { - return; - } - - this.downloadPendingDeleteLanguageTags.clear(); - this.downloadFailedLanguageTags.clear(); - this.currentDownloadLangTag = langTag; - this.downloadingLanguageTags.add(langTag); - this.setDownloadControlsDisabled(true); - dispatchTestEvent("DownloadStarted", { langTag }); - await this.renderDownloadLanguages(); - this.updateDownloadSelectOptionState({ preserveSelection: true }); - - let downloadSucceeded = false; - try { - await TranslationsParent.downloadLanguageFiles(langTag); - this.downloadedLanguageTags.add(langTag); - downloadSucceeded = true; - dispatchTestEvent("DownloadCompleted", { langTag }); - } catch (error) { - dispatchTestEvent("DownloadFailed", { langTag }); - console.error("Failed to download language files", error); - this.downloadFailedLanguageTags.add(langTag); - } finally { - this.downloadingLanguageTags.delete(langTag); - this.currentDownloadLangTag = null; - this.setDownloadControlsDisabled(false); - await this.renderDownloadLanguages(); - this.updateDownloadSelectOptionState({ - preserveSelection: !downloadSucceeded, - }); - this.updateDownloadLanguageButtonDisabled(); - } - }, - - /** - * Disable or enable the download controls. - * - * @param {boolean} isDisabled - */ - setDownloadControlsDisabled(isDisabled) { - if (this.elements?.downloadLanguagesSelect) { - this.elements.downloadLanguagesSelect.disabled = isDisabled; - } - this.setDownloadLanguageButtonDisabledState( - isDisabled || this.shouldDisableDownloadLanguageButton() - ); - }, - - /** - * Toggle ghost styling on icon buttons. - * - * @param {HTMLElement|null} button - * @param {boolean} isGhost - */ - setIconButtonGhostState(button, isGhost) { - if (!button) { - return; - } - const type = isGhost ? "icon ghost" : "icon"; - if (button.getAttribute("type") !== type) { - button.setAttribute("type", type); - } - }, - - /** - * Reset the download dropdown back to its placeholder value. - */ - resetDownloadSelect() { - if (this.elements?.downloadLanguagesSelect) { - this.elements.downloadLanguagesSelect.value = ""; - } - const setting = Preferences.getSetting?.( - "translationsDownloadLanguagesSelect" - ); - if (setting) { - setting.value = ""; - } - this.updateDownloadLanguageButtonDisabled(); - }, - - /** - * Refresh download state from disk and update the UI. - * - * @returns {Promise<void>} - */ - async refreshDownloadedLanguages() { - if (!this.languageList?.length) { - return; - } - - this.downloadPendingDeleteLanguageTags.clear(); - const downloaded = await Promise.all( - this.languageList.map(async (/** @type {LanguageInfo} */ { langTag }) => { - try { - const hasFiles = - await TranslationsParent.hasAllFilesForLanguage(langTag); - return /** @type {[string, boolean]} */ ([langTag, hasFiles]); - } catch (error) { - console.error( - `Failed to check download status for ${langTag}`, - error + // Set new icon based on the state of the language model + switch (langState) { + case "downloaded": + // If language is downloaded show 'remove icon' as an option + // for the user to remove the downloaded language model. + langButton.classList.add("translations-settings-remove-icon"); + // The respective aria-label for accessibility is updated with correct Fluent string. + if (langTag === "all") { + document.l10n.setAttributes( + langButton, + "translations-settings-remove-all-button" + ); + } else { + document.l10n.setAttributes( + langButton, + "translations-settings-remove-button", + { + name: document.l10n.getAttributes(langButton).args.name, + } ); - return /** @type {[string, boolean]} */ ([langTag, false]); } - }) - ); - - this.downloadedLanguageTags = new Set( - downloaded.filter(([, isDownloaded]) => isDownloaded).map(([tag]) => tag) - ); - - for (const [langTag, isDownloaded] of downloaded) { - if (isDownloaded) { - this.downloadingLanguageTags.delete(langTag); - this.downloadFailedLanguageTags.delete(langTag); - } else { - this.downloadPendingDeleteLanguageTags.delete(langTag); - } - } - - await this.renderDownloadLanguages(); - this.updateDownloadSelectOptionState(); - this.updateDownloadLanguageButtonDisabled(); - }, - - /** - * Create a delete confirmation item with warning icon and action buttons. - * - * @param {string} langTag - * @param {HTMLElement} item - The moz-box-item element to populate. - * @returns {Promise<void>} - */ - async createDeleteConfirmationItem(langTag, item, disableActions = false) { - const warningButton = document.createElement("moz-button"); - warningButton.setAttribute("slot", "actions-start"); - warningButton.setAttribute("type", "icon"); - warningButton.setAttribute("iconsrc", DOWNLOAD_WARNING_ICON); - warningButton.style.pointerEvents = "none"; - warningButton.style.color = "var(--icon-color-warning)"; - warningButton.classList.add(DOWNLOAD_LANGUAGE_REMOVE_BUTTON_CLASS); - warningButton.dataset.langTag = langTag; - this.setIconButtonGhostState(warningButton, true); - - const sizeLabel = this.formatLanguageSize(langTag) ?? "0"; - const languageLabel = this.formatLanguageLabel(langTag) ?? langTag; - - const confirmContent = document.createElement("div"); - confirmContent.style.cssText = - "display: flex; align-items: center; gap: var(--space-small);"; - - const confirmText = document.createElement("span"); - confirmText.textContent = await document.l10n.formatValue( - "settings-translations-subpage-download-delete-confirm", - { language: languageLabel, size: sizeLabel } - ); - - const buttonGroup = document.createElement("moz-button-group"); - - const deleteButton = document.createElement("moz-button"); - deleteButton.setAttribute("type", "destructive"); - deleteButton.setAttribute("size", "small"); - deleteButton.disabled = disableActions; - document.l10n.setAttributes( - deleteButton, - "settings-translations-subpage-download-delete-button" - ); - deleteButton.classList.add(DOWNLOAD_LANGUAGE_DELETE_CONFIRM_BUTTON_CLASS); - deleteButton.dataset.langTag = langTag; - - const cancelButton = document.createElement("moz-button"); - cancelButton.setAttribute("type", "default"); - cancelButton.setAttribute("size", "small"); - cancelButton.disabled = disableActions; - document.l10n.setAttributes( - cancelButton, - "settings-translations-subpage-download-cancel-button" - ); - cancelButton.classList.add(DOWNLOAD_LANGUAGE_DELETE_CANCEL_BUTTON_CLASS); - cancelButton.dataset.langTag = langTag; - - confirmContent.appendChild(confirmText); - buttonGroup.append(deleteButton, cancelButton); - confirmContent.appendChild(buttonGroup); - - if (!deleteButton.disabled) { - requestAnimationFrame(() => { - requestAnimationFrame(() => { - if (deleteButton.isConnected) { - deleteButton.focus({ focusVisible: true }); - } - }); - }); - } - - item.appendChild(warningButton); - item.appendChild(confirmContent); - }, - - /** - * Create a failed download item with error icon and retry button. - * - * @param {string} langTag - * @param {HTMLElement} item - The moz-box-item element to populate. - * @returns {Promise<void>} - */ - async createFailedDownloadItem(langTag, item, disableActions = false) { - const errorButton = document.createElement("moz-button"); - errorButton.setAttribute("slot", "actions-start"); - errorButton.setAttribute("type", "icon"); - errorButton.setAttribute("iconsrc", DOWNLOAD_ERROR_ICON); - errorButton.style.pointerEvents = "none"; - errorButton.style.color = "var(--text-color-error)"; - errorButton.classList.add(DOWNLOAD_LANGUAGE_REMOVE_BUTTON_CLASS); - errorButton.dataset.langTag = langTag; - this.setIconButtonGhostState(errorButton, true); - - const sizeLabel = this.formatLanguageSize(langTag) ?? "0"; - const languageLabel = this.formatLanguageLabel(langTag) ?? langTag; - - const errorContent = document.createElement("div"); - errorContent.style.cssText = - "display: flex; align-items: center; gap: var(--space-small);"; - - const errorText = document.createElement("span"); - document.l10n.setAttributes( - errorText, - "settings-translations-subpage-download-error", - { language: languageLabel, size: sizeLabel } - ); - - const retryButton = document.createElement("moz-button"); - retryButton.setAttribute("type", "text"); - retryButton.setAttribute("size", "small"); - retryButton.disabled = disableActions; - document.l10n.setAttributes( - retryButton, - "settings-translations-subpage-download-retry-button" - ); - retryButton.classList.add(DOWNLOAD_LANGUAGE_RETRY_BUTTON_CLASS); - retryButton.dataset.langTag = langTag; - - errorContent.appendChild(errorText); - errorContent.appendChild(retryButton); - - if (!retryButton.disabled) { - requestAnimationFrame(() => { - requestAnimationFrame(() => { - if (retryButton.isConnected) { - retryButton.focus({ focusVisible: true }); - } - }); - }); - } - - item.appendChild(errorButton); - item.appendChild(errorContent); - }, - - /** - * Create a download/remove button for downloaded or downloading language items. - * - * @param {string} langTag - * @param {boolean} isDownloading - * @param {HTMLElement} item - The moz-box-item element to populate. - * @param {string} progressLabel - The localized "Downloading..." text. - * @returns {Promise<boolean>} - Returns false if the item should be skipped. - */ - async createDownloadLanguageItem( - langTag, - isDownloading, - item, - progressLabel, - disableActions = false - ) { - const label = await this.formatDownloadLabel(langTag); - if (!label) { - return false; - } - - const removeButton = document.createElement("moz-button"); - removeButton.setAttribute("slot", "actions-start"); - removeButton.setAttribute("type", "icon"); - removeButton.setAttribute( - "iconsrc", - isDownloading ? DOWNLOAD_LOADING_ICON : DOWNLOAD_DELETE_ICON - ); - removeButton.classList.add(DOWNLOAD_LANGUAGE_REMOVE_BUTTON_CLASS); - removeButton.dataset.langTag = langTag; - removeButton.setAttribute("aria-label", label); - if (isDownloading) { - removeButton.style.pointerEvents = "none"; - removeButton.disabled = false; - } else { - removeButton.disabled = disableActions; - } - this.setIconButtonGhostState( - removeButton, - isDownloading || - removeButton.getAttribute("iconsrc") === DOWNLOAD_LOADING_ICON - ); - - item.setAttribute("label", label); - if (isDownloading) { - item.setAttribute("description", progressLabel); - } - - item.appendChild(removeButton); - return true; - }, - - /** - * Render the downloaded (and downloading) languages list. - * - * @returns {Promise<void>} - */ - async renderDownloadLanguages() { - const { downloadLanguagesGroup, downloadLanguagesNoneRow } = - this.elements ?? {}; - if (!downloadLanguagesGroup) { - return; - } - - const isDownloadInProgress = Boolean(this.currentDownloadLangTag); - const previousEmptyStateVisible = - downloadLanguagesNoneRow && !downloadLanguagesNoneRow.hidden; - - for (const item of downloadLanguagesGroup.querySelectorAll( - `.${DOWNLOAD_LANGUAGE_ITEM_CLASS}` - )) { - item.remove(); - } - - const langTags = [ - ...Array.from( - new Set([ - ...Array.from(this.downloadedLanguageTags), - ...Array.from(this.downloadingLanguageTags), - ...Array.from(this.downloadFailedLanguageTags), - ]) - ), - ]; - - if (downloadLanguagesNoneRow) { - const hasLanguages = !!langTags.length; - downloadLanguagesNoneRow.hidden = hasLanguages; - - if (hasLanguages && downloadLanguagesNoneRow.isConnected) { - downloadLanguagesNoneRow.remove(); - } else if (!hasLanguages && !downloadLanguagesNoneRow.isConnected) { - downloadLanguagesGroup.appendChild(downloadLanguagesNoneRow); - } - } - - const currentEmptyStateVisible = - downloadLanguagesNoneRow && !downloadLanguagesNoneRow.hidden; - if (previousEmptyStateVisible && !currentEmptyStateVisible) { - dispatchTestEvent("DownloadedLanguagesEmptyStateHidden"); - } else if (!previousEmptyStateVisible && currentEmptyStateVisible) { - dispatchTestEvent("DownloadedLanguagesEmptyStateShown"); - } - - const sortedLangTags = [...langTags].sort((lhs, rhs) => { - const labelA = this.formatLanguageLabel(lhs) ?? lhs; - const labelB = this.formatLanguageLabel(rhs) ?? rhs; - return labelA.localeCompare(labelB); - }); - - const progressLabel = await document.l10n.formatValue( - "settings-translations-subpage-download-progress" - ); - - for (const langTag of sortedLangTags) { - const isDownloading = this.downloadingLanguageTags.has(langTag); - const isFailed = this.downloadFailedLanguageTags.has(langTag); - const isPendingDelete = - this.downloadPendingDeleteLanguageTags.has(langTag); - - const item = document.createElement("moz-box-item"); - item.classList.add(DOWNLOAD_LANGUAGE_ITEM_CLASS); - item.dataset.langTag = langTag; - - if (isPendingDelete) { - await this.createDeleteConfirmationItem( - langTag, - item, - isDownloadInProgress - ); - } else if (isFailed) { - item.classList.add(DOWNLOAD_LANGUAGE_FAILED_CLASS); - await this.createFailedDownloadItem( - langTag, - item, - isDownloadInProgress - ); - } else { - const shouldAdd = await this.createDownloadLanguageItem( - langTag, - isDownloading, - item, - progressLabel, - isDownloadInProgress - ); - if (!shouldAdd) { - continue; + break; + case "removed": + // If language is removed show 'download icon' as an option + // for the user to download the language model. + langButton.classList.add("translations-settings-download-icon"); + // The respective aria-label for accessibility is updated with correct Fluent string. + if (langTag === "all") { + document.l10n.setAttributes( + langButton, + "translations-settings-download-all-button" + ); + } else { + document.l10n.setAttributes( + langButton, + "translations-settings-download-button", + { + name: document.l10n.getAttributes(langButton).args.name, + } + ); } - } - - if ( - downloadLanguagesNoneRow && - downloadLanguagesNoneRow.parentElement === downloadLanguagesGroup - ) { - downloadLanguagesGroup.insertBefore(item, downloadLanguagesNoneRow); - } else { - downloadLanguagesGroup.appendChild(item); - } - } - - dispatchTestEvent("DownloadedLanguagesRendered", { - languages: sortedLangTags, - count: sortedLangTags.length, - downloading: sortedLangTags.filter(langTag => - this.downloadingLanguageTags.has(langTag) - ), - }); - }, - - /** - * Show delete confirmation UI when delete button is clicked. - * - * @param {string} langTag - * @returns {Promise<void>} - */ - async onDeleteButtonClicked(langTag) { - if (!langTag || !this.downloadedLanguageTags.has(langTag)) { - return; - } - - this.downloadFailedLanguageTags.clear(); - this.downloadPendingDeleteLanguageTags.clear(); - this.downloadPendingDeleteLanguageTags.add(langTag); - await this.renderDownloadLanguages(); - }, - - /** - * Confirm and complete deletion of a language. - * - * @param {string} langTag - * @returns {Promise<void>} - */ - async confirmDeleteLanguage(langTag) { - if (!langTag || !this.downloadPendingDeleteLanguageTags.has(langTag)) { - return; - } - - this.downloadPendingDeleteLanguageTags.delete(langTag); - - try { - await TranslationsParent.deleteLanguageFiles(langTag); - this.downloadedLanguageTags.delete(langTag); - dispatchTestEvent("DownloadDeleted", { langTag }); - } catch (error) { - console.error("Failed to remove downloaded language files", error); - await this.renderDownloadLanguages(); - return; - } - - await this.renderDownloadLanguages(); - this.updateDownloadSelectOptionState(); - this.updateDownloadLanguageButtonDisabled(); - }, - - /** - * Cancel delete confirmation and restore normal state. - * - * @param {string} langTag - * @returns {Promise<void>} - */ - async cancelDeleteLanguage(langTag) { - if (!langTag || !this.downloadPendingDeleteLanguageTags.has(langTag)) { - return; - } - - this.downloadPendingDeleteLanguageTags.delete(langTag); - await this.renderDownloadLanguages(); - }, - - /** - * Retry downloading a failed language. - * - * @param {string} langTag - * @returns {Promise<void>} - */ - async retryDownloadLanguage(langTag) { - if (!langTag || !this.downloadFailedLanguageTags.has(langTag)) { - return; - } - - this.downloadFailedLanguageTags.delete(langTag); - this.currentDownloadLangTag = langTag; - this.downloadingLanguageTags.add(langTag); - this.setDownloadControlsDisabled(true); - dispatchTestEvent("DownloadStarted", { langTag }); - await this.renderDownloadLanguages(); - this.updateDownloadSelectOptionState({ preserveSelection: true }); - - let downloadSucceeded = false; - try { - await TranslationsParent.downloadLanguageFiles(langTag); - this.downloadedLanguageTags.add(langTag); - downloadSucceeded = true; - dispatchTestEvent("DownloadCompleted", { langTag }); - } catch (error) { - console.error("Failed to download language files", error); - this.downloadFailedLanguageTags.add(langTag); - dispatchTestEvent("DownloadFailed", { langTag }); - } finally { - this.downloadingLanguageTags.delete(langTag); - this.currentDownloadLangTag = null; - this.setDownloadControlsDisabled(false); - await this.renderDownloadLanguages(); - this.updateDownloadSelectOptionState({ - preserveSelection: !downloadSucceeded, - }); - this.updateDownloadLanguageButtonDisabled(); - } - }, - - /** - * Handle updates to translations permissions. - * - * @param {nsISupports} subject - * @param {string} data - */ - handlePermissionChange(subject, data) { - if (data === "cleared") { - this.neverTranslateSiteOrigins = new Set(); - this.renderNeverTranslateSites([]); - return; - } - - const perm = subject?.QueryInterface?.(Ci.nsIPermission); - if (perm?.type !== TRANSLATIONS_PERMISSION) { - return; - } - - this.refreshNeverTranslateSites(); - }, - - /** - * Remove observers and listeners added during init. - */ - teardown() { - try { - Services.obs.removeObserver(this, TOPIC_TRANSLATIONS_PREF_CHANGED); - Services.obs.removeObserver(this, "perm-changed"); - } catch (e) { - // Ignore if we were never added. + break; + case "loading": + // While processing the download or remove language model + // show 'loading icon' to the user + langButton.classList.add("translations-settings-loading-icon"); + // The respective aria-label for accessibility is updated with correct Fluent string. + if (langTag === "all") { + document.l10n.setAttributes( + langButton, + "translations-settings-loading-all-button" + ); + } else { + document.l10n.setAttributes( + langButton, + "translations-settings-loading-button", + { + name: document.l10n.getAttributes(langButton).args.name, + } + ); + } + break; } - document.removeEventListener("paneshown", this); - window.removeEventListener("unload", this); - this.elements?.alwaysTranslateLanguagesSelect?.removeEventListener( - "change", - this - ); - this.elements?.alwaysTranslateLanguagesGroup?.removeEventListener( - "click", - this - ); - this.elements?.alwaysTranslateLanguagesButton?.removeEventListener( - "click", - this - ); - this.elements?.neverTranslateLanguagesSelect?.removeEventListener( - "change", - this - ); - this.elements?.neverTranslateLanguagesButton?.removeEventListener( - "click", - this - ); - this.elements?.neverTranslateLanguagesGroup?.removeEventListener( - "click", - this - ); - this.elements?.neverTranslateSitesGroup?.removeEventListener("click", this); - this.elements?.downloadLanguagesSelect?.removeEventListener("change", this); - this.elements?.downloadLanguagesGroup?.removeEventListener("click", this); - this.elements?.downloadLanguagesButton?.removeEventListener("click", this); + this.updateDownloadPhase(langTag, langState); }, }; - -document.addEventListener("paneshown", TranslationsSettings); diff --git a/browser/components/preferences/widgets/setting-pane/setting-pane.mjs b/browser/components/preferences/widgets/setting-pane/setting-pane.mjs @@ -10,7 +10,6 @@ import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; * @property {string} [parent] The pane that links to this one. * @property {string} l10nId Fluent id for the heading/description. * @property {string[]} groupIds What setting groups should be rendered. - * @property {string} [iconSrc] Optional icon shown in the page header. */ export class SettingPane extends MozLitElement { @@ -114,7 +113,6 @@ export class SettingPane extends MozLitElement { return html` <moz-page-header data-l10n-id=${this.config.l10nId} - .iconSrc=${this.config.iconSrc} .backButton=${this.isSubPane} @navigate-back=${this.goBack} ></moz-page-header> diff --git a/browser/components/translations/tests/browser/browser.toml b/browser/components/translations/tests/browser/browser.toml @@ -8,41 +8,23 @@ support-files = [ ["browser_translations_about_preferences_manage_downloaded_languages.js"] -["browser_translations_about_settings_main_page_offer_checkbox.js"] +["browser_translations_about_preferences_settings_always_translate_languages.js"] -["browser_translations_about_settings_subpage_always_translate_langs_a11y.js"] +["browser_translations_about_preferences_settings_download_languages_all_ui.js"] -["browser_translations_about_settings_subpage_always_translate_langs_basic.js"] +["browser_translations_about_preferences_settings_download_languages_error_ui.js"] -["browser_translations_about_settings_subpage_always_translate_langs_modify.js"] +["browser_translations_about_preferences_settings_download_languages_ui.js"] -["browser_translations_about_settings_subpage_always_translate_langs_observe.js"] +["browser_translations_about_preferences_settings_never_translate_languages.js"] -["browser_translations_about_settings_subpage_back_button.js"] +["browser_translations_about_preferences_settings_never_translate_sites.js"] -["browser_translations_about_settings_subpage_default.js"] +["browser_translations_about_preferences_settings_ui.js"] -["browser_translations_about_settings_subpage_download_langs_basic.js"] +["browser_translations_about_preferences_settings_ui_keyboard_a11y.js"] -["browser_translations_about_settings_subpage_download_langs_delete_confirmation.js"] - -["browser_translations_about_settings_subpage_download_langs_errors.js"] - -["browser_translations_about_settings_subpage_download_langs_reset_states.js"] - -["browser_translations_about_settings_subpage_download_langs_sorting.js"] - -["browser_translations_about_settings_subpage_never_translate_langs_a11y.js"] - -["browser_translations_about_settings_subpage_never_translate_langs_basic.js"] - -["browser_translations_about_settings_subpage_never_translate_langs_modify.js"] - -["browser_translations_about_settings_subpage_never_translate_langs_observe.js"] - -["browser_translations_about_settings_subpage_never_translate_sites_basic.js"] - -["browser_translations_about_settings_subpage_never_translate_sites_observe.js"] +["browser_translations_about_preferences_settings_ui_tab.js"] ["browser_translations_e2e_full_page_translate_with_lexical_shortlist.js"] diff --git a/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_always_translate_languages.js b/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_always_translate_languages.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task( + async function test_about_preferences_always_translate_language_settings() { + const { + cleanup, + elements: { settingsButton }, + } = await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [["browser.translations.newSettingsUI.enable", false]], + }); + + info("Ensuring the list of always-translate languages is empty"); + is( + getAlwaysTranslateLanguagesFromPref().length, + 0, + "The list of always-translate languages is empty" + ); + + info("Adding two languages to the alwaysTranslateLanguages pref"); + Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, "fr,de"); + + const dialogWindow = await waitForOpenDialogWindow( + "chrome://browser/content/preferences/dialogs/translations.xhtml", + () => { + click( + settingsButton, + "Opening the about:preferences Translations Settings" + ); + } + ); + let tree = dialogWindow.document.getElementById( + "alwaysTranslateLanguagesTree" + ); + let remove = dialogWindow.document.getElementById( + "removeAlwaysTranslateLanguage" + ); + let removeAll = dialogWindow.document.getElementById( + "removeAllAlwaysTranslateLanguages" + ); + + is( + tree.view.rowCount, + 2, + "The always-translate languages list has 2 items" + ); + ok(remove.disabled, "The 'Remove Language' button is disabled"); + ok(!removeAll.disabled, "The 'Remove All Languages' button is enabled"); + + info("Selecting the first always-translate language."); + tree.view.selection.select(0); + ok(!remove.disabled, "The 'Remove Language' button is enabled"); + + click(remove, "Clicking the remove-language button"); + is( + tree.view.rowCount, + 1, + "The always-translate languages list now contains 1 item" + ); + is( + getAlwaysTranslateLanguagesFromPref().length, + 1, + "One language tag in the pref" + ); + + info("Removing all languages from the alwaysTranslateLanguages pref"); + Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, ""); + is(tree.view.rowCount, 0, "The always-translate languages list is empty"); + ok(remove.disabled, "The 'Remove Language' button is disabled"); + ok(removeAll.disabled, "The 'Remove All Languages' button is disabled"); + + info("Adding more languages to the alwaysTranslateLanguages pref"); + Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, "fr,en,es"); + is( + tree.view.rowCount, + 3, + "The always-translate languages list has 3 items" + ); + ok(remove.disabled, "The 'Remove Language' button is disabled"); + ok(!removeAll.disabled, "The 'Remove All Languages' button is enabled"); + + click(removeAll, "Clicking the remove-all languages button"); + is(tree.view.rowCount, 0, "The always-translate languages list is empty"); + ok(remove.disabled, "The 'Remove Language' button is disabled"); + ok(removeAll.disabled, "The 'Remove All Languages' button is disabled"); + is( + getAlwaysTranslateLanguagesFromPref().length, + 0, + "There are no languages in the alwaysTranslateLanguages pref" + ); + + await waitForCloseDialogWindow(dialogWindow); + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_download_languages_all_ui.js b/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_download_languages_all_ui.js @@ -0,0 +1,159 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_translations_settings_download_languages_all() { + const { + cleanup, + remoteClients, + elements: { settingsButton }, + } = await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [["browser.translations.newSettingsUI.enable", true]], + }); + + const frenchModels = languageModelNames([ + { fromLang: "fr", toLang: "en" }, + { fromLang: "en", toLang: "fr" }, + ]); + + const spanishModels = languageModelNames([ + { fromLang: "es", toLang: "en" }, + { fromLang: "en", toLang: "es" }, + ]); + + const ukrainianModels = languageModelNames([ + { fromLang: "uk", toLang: "en" }, + { fromLang: "en", toLang: "uk" }, + ]); + + assertVisibility({ + message: "Expect paneGeneral elements to be visible.", + visible: { settingsButton }, + }); + + info( + "Open translations settings page by clicking on translations settings button." + ); + const { downloadLanguageList } = + await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane( + settingsButton + ); + + info( + "Install each language French, Spanish and Ukrainian and check if All language state changes to 'all language downloaded' by changing the all language button icon to 'remove icon'" + ); + + info("Download French language model."); + let langFr = Array.from(downloadLanguageList.querySelectorAll("label")).find( + el => el.getAttribute("value") === "fr" + ); + + let clickButton = BrowserTestUtils.waitForEvent( + langFr.parentNode.querySelector("moz-button"), + "click" + ); + langFr.parentNode.querySelector("moz-button").click(); + await clickButton; + + Assert.deepEqual( + await remoteClients.translationModels.resolvePendingDownloads( + frenchModels.length + ), + frenchModels, + "French models were downloaded." + ); + + await TranslationsSettingsTestUtils.downaloadButtonClick( + langFr, + "translations-settings-remove-icon", + "Delete icon is visible for French language hence downloaded" + ); + + info("Download Spanish language model."); + + let langEs = Array.from(downloadLanguageList.querySelectorAll("label")).find( + el => el.getAttribute("value") === "es" + ); + + clickButton = BrowserTestUtils.waitForEvent( + langEs.parentNode.querySelector("moz-button"), + "click" + ); + langEs.parentNode.querySelector("moz-button").click(); + await clickButton; + + Assert.deepEqual( + await remoteClients.translationModels.resolvePendingDownloads( + spanishModels.length + ), + spanishModels, + "Spanish models were downloaded." + ); + + await TranslationsSettingsTestUtils.downaloadButtonClick( + langEs, + "translations-settings-remove-icon", + "Delete icon is visible for Spanish language hence downloaded" + ); + + info("Download Ukrainian language model."); + + let langUk = Array.from(downloadLanguageList.querySelectorAll("label")).find( + el => el.getAttribute("value") === "uk" + ); + + clickButton = BrowserTestUtils.waitForEvent( + langUk.parentNode.querySelector("moz-button"), + "click" + ); + langUk.parentNode.querySelector("moz-button").click(); + await clickButton; + + Assert.deepEqual( + await remoteClients.translationModels.resolvePendingDownloads( + ukrainianModels.length + ), + ukrainianModels, + "Ukrainian models were downloaded." + ); + + await TranslationsSettingsTestUtils.downaloadButtonClick( + langUk, + "translations-settings-remove-icon", + "Delete icon is visible for Ukranian language hence downloaded." + ); + + // Download "All languages" is the first child + let langAll = downloadLanguageList.children[0]; + + ok( + langAll + .querySelector("moz-button") + .classList.contains("translations-settings-remove-icon"), + "Delete icon is visible for All Languages after all individual language models were downloaded." + ); + + info( + "Remove one language ensure that All Languages change state changes to 'removed' to indicate that all languages are not downloaded." + ); + + info("Remove Spanish language model."); + langEs.parentNode.querySelector("moz-button").click(); + await clickButton; + + await TranslationsSettingsTestUtils.downaloadButtonClick( + langEs, + "translations-settings-download-icon", + "Download icon is visible for Spanish language hence removed" + ); + + ok( + langAll + .querySelector("moz-button") + .classList.contains("translations-settings-download-icon"), + "Download icon is visible for all languages i.e. all languages are not downloaded since one language, Spanish was removed." + ); + + await cleanup(); +}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_download_languages_error_ui.js b/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_download_languages_error_ui.js @@ -0,0 +1,173 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task( + async function test_translations_settings_download_languages_error_handling() { + const { + cleanup, + remoteClients, + elements: { settingsButton }, + } = await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [["browser.translations.newSettingsUI.enable", true]], + }); + + const frenchModels = languageModelNames([ + { fromLang: "fr", toLang: "en" }, + { fromLang: "en", toLang: "fr" }, + ]); + + const spanishModels = languageModelNames([ + { fromLang: "es", toLang: "en" }, + { fromLang: "en", toLang: "es" }, + ]); + + const allModels = languageModelNames(LANGUAGE_PAIRS); + + assertVisibility({ + message: "Expect paneGeneral elements to be visible.", + visible: { settingsButton }, + }); + + info( + "Open translations settings page by clicking on translations settings button." + ); + const { downloadLanguageList } = + await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane( + settingsButton + ); + + info("Test French language model for download error"); + + let langFr = Array.from( + downloadLanguageList.querySelectorAll("label") + ).find(el => el.getAttribute("value") === "fr"); + + let clickButton = BrowserTestUtils.waitForEvent( + langFr.parentNode.querySelector("moz-button"), + "click" + ); + langFr.parentNode.querySelector("moz-button").click(); + await clickButton; + + await captureTranslationsError(() => + remoteClients.translationModels.rejectPendingDownloads( + frenchModels.length + ) + ); + + const errorElement = gBrowser.selectedBrowser.contentDocument.querySelector( + ".translations-settings-language-error" + ); + + assertVisibility({ + message: "Moz-message-bar with error message is visible", + visible: { errorElement }, + }); + is( + document.l10n.getAttributes(errorElement).id, + "translations-settings-language-download-error", + "Error message correctly shows download error" + ); + is( + document.l10n.getAttributes(errorElement).args.name, + "French", + "Error message correctly shows download error for French language" + ); + + await TranslationsSettingsTestUtils.downaloadButtonClick( + langFr, + "translations-settings-download-icon", + "Download icon is visible on French button" + ); + + remoteClients.translationsWasm.assertNoNewDownloads(); + + info("Download Spanish language model successfully."); + + let langEs = Array.from( + downloadLanguageList.querySelectorAll("label") + ).find(el => el.getAttribute("value") === "es"); + + clickButton = BrowserTestUtils.waitForEvent( + langEs.parentNode.querySelector("moz-button"), + "click" + ); + langEs.parentNode.querySelector("moz-button").click(); + await clickButton; + + const errorElementEs = + gBrowser.selectedBrowser.contentDocument.querySelector( + ".translations-settings-language-error" + ); + + ok( + !errorElementEs, + "Previous error is remove when new action occured, i.e. click download Spanish button" + ); + + Assert.deepEqual( + await remoteClients.translationModels.resolvePendingDownloads( + spanishModels.length + ), + spanishModels, + "Spanish models were downloaded." + ); + + await TranslationsSettingsTestUtils.downaloadButtonClick( + langEs, + "translations-settings-remove-icon", + "Delete icon is visible for Spanish language hence downloaded" + ); + + info("Test All language models download error"); + // Download "All languages" is the first child + let langAll = downloadLanguageList.children[0]; + + let clickButtonAll = BrowserTestUtils.waitForEvent( + langAll.querySelector("moz-button"), + "click" + ); + langAll.querySelector("moz-button").click(); + await clickButtonAll; + + await captureTranslationsError(() => + remoteClients.translationModels.rejectPendingDownloads(allModels.length) + ); + + await captureTranslationsError(() => + remoteClients.translationsWasm.rejectPendingDownloads(allModels.length) + ); + + remoteClients.translationsWasm.assertNoNewDownloads(); + + await TranslationsSettingsTestUtils.downaloadButtonClick( + langAll, + "translations-settings-download-icon", + "Download icon is visible for 'all languages'" + ); + + const errorElementAll = + gBrowser.selectedBrowser.contentDocument.querySelector( + ".translations-settings-language-error" + ); + + assertVisibility({ + message: "Moz-message-bar with error message is visible", + visible: { errorElementAll }, + }); + is( + document.l10n.getAttributes(errorElementAll).id, + "translations-settings-language-download-error", + "Error message correctly shows download error" + ); + is( + document.l10n.getAttributes(errorElementAll).args.name, + "all", + "Error message correctly shows download error for all language" + ); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_download_languages_ui.js b/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_download_languages_ui.js @@ -0,0 +1,117 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_translations_settings_download_languages() { + await testWithAndWithoutLexicalShortlist(async lexicalShortlistPrefs => { + const { + cleanup, + remoteClients, + elements: { settingsButton }, + } = await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [ + ["browser.translations.newSettingsUI.enable", true], + ...lexicalShortlistPrefs, + ], + }); + + assertVisibility({ + message: "Expect paneGeneral elements to be visible.", + visible: { settingsButton }, + }); + + info( + "Open translations settings page by clicking on translations settings button." + ); + const { downloadLanguageList } = + await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane( + settingsButton + ); + + info("Test French language model install and uninstall function."); + + let langFr = Array.from( + downloadLanguageList.querySelectorAll("label") + ).find(el => el.getAttribute("value") === "fr"); + + let clickButton = BrowserTestUtils.waitForEvent( + langFr.parentNode.querySelector("moz-button"), + "click" + ); + langFr.parentNode.querySelector("moz-button").click(); + await clickButton; + + const frenchModels = languageModelNames([ + { fromLang: "fr", toLang: "en" }, + { fromLang: "en", toLang: "fr" }, + ]); + + Assert.deepEqual( + await remoteClients.translationModels.resolvePendingDownloads( + frenchModels.length + ), + frenchModels, + "French models were downloaded." + ); + + await TranslationsSettingsTestUtils.downaloadButtonClick( + langFr, + "translations-settings-remove-icon", + "Delete icon is visible on French button." + ); + + langFr.parentNode.querySelector("moz-button").click(); + await clickButton; + + await TranslationsSettingsTestUtils.downaloadButtonClick( + langFr, + "translations-settings-download-icon", + "Download icon is visible on French Button." + ); + + info("Test 'All language' models install and uninstall function"); + + // Download "All languages" is the first child + let langAll = downloadLanguageList.children[0]; + + let clickButtonAll = BrowserTestUtils.waitForEvent( + langAll.querySelector("moz-button"), + "click" + ); + langAll.querySelector("moz-button").click(); + await clickButtonAll; + + const allModels = languageModelNames(LANGUAGE_PAIRS); + + Assert.deepEqual( + await remoteClients.translationModels.resolvePendingDownloads( + allModels.length + ), + allModels, + "All models were downloaded." + ); + Assert.deepEqual( + await remoteClients.translationsWasm.resolvePendingDownloads(1), + ["bergamot-translator"], + "Wasm was downloaded." + ); + + await TranslationsSettingsTestUtils.downaloadButtonClick( + langAll, + "translations-settings-remove-icon", + "Delete icon is visible on 'All languages' button" + ); + + langAll.querySelector("moz-button").click(); + await clickButton; + + await TranslationsSettingsTestUtils.downaloadButtonClick( + langAll, + "translations-settings-download-icon", + "Download icon is visible on 'All Language' button." + ); + + await cleanup(); + }); +}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_never_translate_languages.js b/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_never_translate_languages.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task( + async function test_about_preferences_never_translate_language_settings() { + const { + cleanup, + elements: { settingsButton }, + } = await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [["browser.translations.newSettingsUI.enable", false]], + }); + + info("Ensuring the list of never-translate languages is empty"); + is( + getNeverTranslateLanguagesFromPref().length, + 0, + "The list of never-translate languages is empty" + ); + + info("Adding two languages to the neverTranslateLanguages pref"); + Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, "fr,de"); + + const dialogWindow = await waitForOpenDialogWindow( + "chrome://browser/content/preferences/dialogs/translations.xhtml", + () => { + click( + settingsButton, + "Opening the about:preferences Translations Settings" + ); + } + ); + let tree = dialogWindow.document.getElementById( + "neverTranslateLanguagesTree" + ); + let remove = dialogWindow.document.getElementById( + "removeNeverTranslateLanguage" + ); + let removeAll = dialogWindow.document.getElementById( + "removeAllNeverTranslateLanguages" + ); + + is(tree.view.rowCount, 2, "The never-translate languages list has 2 items"); + ok(remove.disabled, "The 'Remove Language' button is disabled"); + ok(!removeAll.disabled, "The 'Remove All Languages' button is enabled"); + + info("Selecting the first never-translate language."); + tree.view.selection.select(0); + ok(!remove.disabled, "The 'Remove Language' button is enabled"); + + click(remove, "Clicking the remove-language button"); + is( + tree.view.rowCount, + 1, + "The never-translate languages list now contains 1 item" + ); + is( + getNeverTranslateLanguagesFromPref().length, + 1, + "One language tag in the pref" + ); + + info("Removing all languages from the neverTranslateLanguages pref"); + Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, ""); + is(tree.view.rowCount, 0, "The never-translate languages list is empty"); + ok(remove.disabled, "The 'Remove Language' button is disabled"); + ok(removeAll.disabled, "The 'Remove All Languages' button is disabled"); + + info("Adding more languages to the neverTranslateLanguages pref"); + Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, "fr,en,es"); + is(tree.view.rowCount, 3, "The never-translate languages list has 3 items"); + ok(remove.disabled, "The 'Remove Language' button is disabled"); + ok(!removeAll.disabled, "The 'Remove All Languages' button is enabled"); + + click(removeAll, "Clicking the remove-all languages button"); + is(tree.view.rowCount, 0, "The never-translate languages list is empty"); + ok(remove.disabled, "The 'Remove Language' button is disabled"); + ok(removeAll.disabled, "The 'Remove All Languages' button is disabled"); + is( + getNeverTranslateLanguagesFromPref().length, + 0, + "There are no languages in the neverTranslateLanguages pref" + ); + + await waitForCloseDialogWindow(dialogWindow); + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_never_translate_sites.js b/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_never_translate_sites.js @@ -0,0 +1,124 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +add_task(async function test_about_preferences_never_translate_site_settings() { + const { + cleanup, + elements: { settingsButton }, + } = await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [["browser.translations.newSettingsUI.enable", false]], + permissionsUrls: [ + "https://example.com", + "https://example.org", + "https://example.net", + ], + }); + + info("Ensuring the list of never-translate sites is empty"); + is( + getNeverTranslateSitesFromPerms().length, + 0, + "The list of never-translate sites is empty" + ); + + info("Adding two sites to the neverTranslateSites perms"); + PermissionTestUtils.add( + "https://example.com", + TRANSLATIONS_PERMISSION, + Services.perms.DENY_ACTION + ); + PermissionTestUtils.add( + "https://example.org", + TRANSLATIONS_PERMISSION, + Services.perms.DENY_ACTION + ); + PermissionTestUtils.add( + "https://example.net", + TRANSLATIONS_PERMISSION, + Services.perms.DENY_ACTION + ); + + const dialogWindow = await waitForOpenDialogWindow( + "chrome://browser/content/preferences/dialogs/translations.xhtml", + () => { + click( + settingsButton, + "Opening the about:preferences Translations Settings" + ); + } + ); + let tree = dialogWindow.document.getElementById("neverTranslateSitesTree"); + let remove = dialogWindow.document.getElementById("removeNeverTranslateSite"); + let removeAll = dialogWindow.document.getElementById( + "removeAllNeverTranslateSites" + ); + + is(tree.view.rowCount, 3, "The never-translate sites list has 2 items"); + ok(remove.disabled, "The 'Remove Site' button is disabled"); + ok(!removeAll.disabled, "The 'Remove All Sites' button is enabled"); + + info("Selecting the first never-translate site."); + tree.view.selection.select(0); + ok(!remove.disabled, "The 'Remove Site' button is enabled"); + + click(remove, "Clicking the remove-site button"); + is( + tree.view.rowCount, + 2, + "The never-translate sites list now contains 2 items" + ); + is( + getNeverTranslateSitesFromPerms().length, + 2, + "There are 2 sites with permissions" + ); + + info("Removing all sites from the neverTranslateSites perms"); + PermissionTestUtils.remove("https://example.com", TRANSLATIONS_PERMISSION); + PermissionTestUtils.remove("https://example.org", TRANSLATIONS_PERMISSION); + PermissionTestUtils.remove("https://example.net", TRANSLATIONS_PERMISSION); + + is(tree.view.rowCount, 0, "The never-translate sites list is empty"); + ok(remove.disabled, "The 'Remove Site' button is disabled"); + ok(removeAll.disabled, "The 'Remove All Sites' button is disabled"); + + info("Adding more sites to the neverTranslateSites perms"); + PermissionTestUtils.add( + "https://example.org", + TRANSLATIONS_PERMISSION, + Services.perms.DENY_ACTION + ); + PermissionTestUtils.add( + "https://example.com", + TRANSLATIONS_PERMISSION, + Services.perms.DENY_ACTION + ); + PermissionTestUtils.add( + "https://example.net", + TRANSLATIONS_PERMISSION, + Services.perms.DENY_ACTION + ); + + is(tree.view.rowCount, 3, "The never-translate sites list has 3 items"); + ok(remove.disabled, "The 'Remove Site' button is disabled"); + ok(!removeAll.disabled, "The 'Remove All Sites' button is enabled"); + + click(removeAll, "Clicking the remove-all sites button"); + is(tree.view.rowCount, 0, "The never-translate sites list is empty"); + ok(remove.disabled, "The 'Remove Site' button is disabled"); + ok(removeAll.disabled, "The 'Remove All Sites' button is disabled"); + is( + getNeverTranslateSitesFromPerms().length, + 0, + "There are no sites in the neverTranslateSites perms" + ); + + await waitForCloseDialogWindow(dialogWindow); + await cleanup(); +}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_ui.js b/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_ui.js @@ -0,0 +1,464 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_translations_settings_pane_elements() { + const { + cleanup, + elements: { settingsButton }, + } = await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [["browser.translations.newSettingsUI.enable", true]], + }); + + assertVisibility({ + message: "Expect paneGeneral elements to be visible.", + visible: { settingsButton }, + }); + + info( + "Open translations settings page by clicking on translations settings button." + ); + const { + translationsSettingsBackButton, + translationsSettingsHeader, + translationsSettingsDescription, + translateAlwaysHeader, + translateNeverHeader, + alwaysTranslateMenuList, + neverTranslateMenuList, + translateNeverSiteHeader, + translateNeverSiteDesc, + downloadLanguageSection, + translateDownloadLanguagesLearnMore, + } = + await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane( + settingsButton + ); + + const translateDownloadLanguagesHeader = + downloadLanguageSection.querySelector("h2"); + assertVisibility({ + message: "Expect paneTranslations elements to be visible.", + visible: { + translationsSettingsBackButton, + translationsSettingsHeader, + translationsSettingsDescription, + translateAlwaysHeader, + translateNeverHeader, + alwaysTranslateMenuList, + neverTranslateMenuList, + translateNeverSiteHeader, + translateNeverSiteDesc, + translateDownloadLanguagesLearnMore, + }, + hidden: { + settingsButton, + }, + }); + + info( + "In translations settings page, click on back button to go back to main preferences page." + ); + const paneEvent = BrowserTestUtils.waitForEvent( + document, + "paneshown", + false, + event => event.detail.category === "paneGeneral" + ); + + click(translationsSettingsBackButton); + await paneEvent; + + assertVisibility({ + message: "Expect paneGeneral elements to be visible.", + visible: { + settingsButton, + }, + hidden: { + translationsSettingsBackButton, + translationsSettingsHeader, + translationsSettingsDescription, + translateAlwaysHeader, + translateNeverHeader, + alwaysTranslateMenuList, + neverTranslateMenuList, + translateNeverSiteHeader, + translateNeverSiteDesc, + translateDownloadLanguagesHeader, + translateDownloadLanguagesLearnMore, + }, + }); + await cleanup(); +}); + +add_task(async function test_translations_settings_always_translate() { + const { + cleanup, + elements: { settingsButton }, + } = await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [["browser.translations.newSettingsUI.enable", true]], + }); + + assertVisibility({ + message: "Expect paneGeneral elements to be visible.", + visible: { settingsButton }, + }); + + info( + "Open translations settings page by clicking on translations settings button." + ); + const { + alwaysTranslateMenuList, + alwaysTranslateLanguageList, + alwaysTranslateMenuPopup, + } = + await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane( + settingsButton + ); + + info("Testing the Always translate langauge settings"); + await testLanguageList( + alwaysTranslateLanguageList, + alwaysTranslateMenuList, + alwaysTranslateMenuPopup, + ALWAYS_TRANSLATE_LANGS_PREF, + "Always" + ); + await testLanguageListWithPref( + alwaysTranslateLanguageList, + ALWAYS_TRANSLATE_LANGS_PREF, + "Always" + ); + + await cleanup(); +}); + +add_task(async function test_translations_settings_never_translate() { + const { + cleanup, + elements: { settingsButton }, + } = await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [["browser.translations.newSettingsUI.enable", true]], + }); + + info( + "Open translations settings page by clicking on translations settings button." + ); + + assertVisibility({ + message: "Expect paneGeneral elements to be visible.", + visible: { settingsButton }, + }); + + const { + neverTranslateMenuList, + neverTranslateLanguageList, + neverTranslateMenuPopup, + } = + await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane( + settingsButton + ); + + info("Testing the Never translate langauge settings"); + await testLanguageList( + neverTranslateLanguageList, + neverTranslateMenuList, + neverTranslateMenuPopup, + NEVER_TRANSLATE_LANGS_PREF, + "Never" + ); + await testLanguageListWithPref( + neverTranslateLanguageList, + NEVER_TRANSLATE_LANGS_PREF, + "Never" + ); + await cleanup(); +}); +function getLangsFromPref(pref) { + let rawLangs = Services.prefs.getCharPref(pref); + if (!rawLangs) { + return []; + } + let langArr = rawLangs.split(","); + return langArr; +} + +async function testLanguageList( + languageList, + menuList, + menuPopup, + pref, + sectionName +) { + info("Ensure the Always/Never list is empty initially."); + + is( + languageList.childElementCount, + 0, + `Language list empty in ${sectionName} Translate list` + ); + + const menuItems = menuPopup.children; + + info( + "Click each language on the menulist to add it into the Always/Never list." + ); + for (const menuItem of menuItems) { + menuList.open = true; + + let clickMenu = BrowserTestUtils.waitForEvent( + menuList.querySelector("menupopup"), + "popuphidden" + ); + click(menuItem); + menuList.querySelector("menupopup").hidePopup(); + await clickMenu; + + /** + * Languages are always added on the top, so check the firstChild + * for newly added languages. + * the firstChild.querySelector("label").innerText is the language display name + * which is compared with the menulist display name that is selected + */ + let langElem = languageList.firstElementChild; + const displayName = getIntlDisplayName(menuItem.value); + is( + langElem.querySelector("label").innerText, + displayName, + `Language list has element ${displayName}` + ); + + const langTag = langElem.querySelector("label").getAttribute("value"); + ok( + getLangsFromPref(pref).includes(langTag), + `Perferences contains ${langTag}` + ); + } + /** The test cases has 4 languages, so check if 4 languages are added to the list */ + let langNum = languageList.childElementCount; + is(langNum, 4, "Number of languages added is 4"); + + info( + "Remove each language from the Always/Never list that we added initially." + ); + for (let i = 0; i < langNum; i++) { + // Delete the first language in the list + let langElem = languageList.children[0]; + let langName = langElem.querySelector("label").innerText; + const langTag = langElem.querySelector("label").getAttribute("value"); + let langButton = langElem.querySelector("moz-button"); + let clickButton = BrowserTestUtils.waitForEvent(langButton, "click"); + langButton.click(); + await clickButton; + + ok( + !getLangsFromPref(pref).includes(langTag), + `Perferences does not contain ${langTag}` + ); + + if (i < langNum) { + is( + languageList.childElementCount, + langNum - i - 1, + `${langName} removed from ${sectionName} Translate` + ); + } + } +} + +async function testLanguageListWithPref(languageList, pref, sectionName) { + const langs = [ + "fr", + "de", + "en", + "es", + "fr,de", + "fr,en", + "fr,es", + "de,en", + "de,en,es", + "es,fr,en", + "en,es,fr,de", + ]; + + info("Ensure the Always/Never list is empty initially."); + + is( + languageList.childElementCount, + 0, + `Language list is empty in ${sectionName} Translate list` + ); + + info( + "Add languages to the Always/Never list in translations setting by setting the ALWAYS_TRANSLATE_LANGS_PREF/NEVER_TRANSLATE_LANGS_PREF." + ); + + for (const langOptions of langs) { + Services.prefs.setCharPref(pref, langOptions); + + /** + * Languages are always added on the top, so check the firstChild + * for newly added languages. + * the firstChild.querySelector("label").innerText is the language display name + * which is compared with the menulist display name that is selected + */ + + const langsAdded = langOptions.split(","); + is( + languageList.childElementCount, + langsAdded.length, + `Language list has ${langsAdded.length} elements ` + ); + + let langsAddedHtml = Array.from(languageList.querySelectorAll("label")); + + for (const lang of langsAdded) { + const langFind = langsAddedHtml + .find(el => el.getAttribute("value") === lang) + .getAttribute("value"); + is(langFind, lang, `Language list has element ${lang}`); + } + } + + Services.prefs.setCharPref(pref, ""); + is( + languageList.childElementCount, + 0, + `All removed from ${sectionName} Translate` + ); +} + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +add_task(async function test_translations_settings_never_translate_site() { + const { + cleanup, + elements: { settingsButton }, + } = await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [["browser.translations.newSettingsUI.enable", true]], + }); + + info( + "Open translations settings page by clicking on translations settings button." + ); + const { neverTranslateSiteList } = + await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane( + settingsButton + ); + + info("Ensuring the list of never-translate sites is empty"); + is( + getNeverTranslateSitesFromPerms().length, + 0, + "The list of never-translate sites is empty" + ); + + is( + neverTranslateSiteList.childElementCount, + 0, + "The never-translate sites html list is empty" + ); + + info("Adding sites to the neverTranslateSites perms"); + await PermissionTestUtils.add( + "https://example.com", + TRANSLATIONS_PERMISSION, + Services.perms.DENY_ACTION + ); + await PermissionTestUtils.add( + "https://example.org", + TRANSLATIONS_PERMISSION, + Services.perms.DENY_ACTION + ); + await PermissionTestUtils.add( + "https://example.net", + TRANSLATIONS_PERMISSION, + Services.perms.DENY_ACTION + ); + + is( + getNeverTranslateSitesFromPerms().length, + 3, + "The list of never-translate sites has 3 elements" + ); + + is( + neverTranslateSiteList.childElementCount, + 3, + "The never-translate sites html list has 3 elements" + ); + + const permissionsUrls = [ + "https://example.com", + "https://example.org", + "https://example.net", + ]; + + info( + "Ensure that the Never translate sites in permissions settings are reflected in Never translate sites section of translations settings page" + ); + + const siteNum = neverTranslateSiteList.children.length; + for (let i = siteNum; i > 0; i--) { + is( + neverTranslateSiteList.children[i - 1].querySelector("label").textContent, + permissionsUrls[permissionsUrls.length - i], + `Never translate URL ${ + permissionsUrls[permissionsUrls.length - i] + } is added` + ); + } + + info( + "Delete each site by clicking the button in Never translate sites section of translations settings page and check if it is removed in the Never translate sites in permissions settings" + ); + for (let i = 0; i < siteNum; i++) { + // Delete the first site in the list + let siteElem = neverTranslateSiteList.children[0]; + // Delete the first language in the list + let siteName = siteElem.querySelector("label").innerText; + let siteButton = siteElem.querySelector("moz-button"); + + ok( + neverTranslateSiteList.querySelector(`label[value="${siteName}"]`), + `Site ${siteName} present in the Never transalate site list` + ); + + ok( + getNeverTranslateSitesFromPerms().find(p => p.origin === siteName), + `Site ${siteName} present in the Never transalate site permissions list` + ); + + let clickButton = BrowserTestUtils.waitForEvent(siteButton, "click"); + siteButton.click(); + await clickButton; + + ok( + !neverTranslateSiteList.querySelector(`label[value="${siteName}"]`), + `Site ${siteName} removed successfully from the Never transalate site list` + ); + + ok( + !getNeverTranslateSitesFromPerms().find(p => p.origin === siteName), + `Site ${siteName} removed from successfully from the Never transalate site permissions list` + ); + + if (i < siteNum) { + is( + neverTranslateSiteList.childElementCount, + siteNum - i - 1, + `${siteName} removed from Never Translate Site` + ); + } + const siteLen = siteNum - i - 1; + is( + getNeverTranslateSitesFromPerms().length, + siteLen, + `There are ${siteLen} site in Never translate site` + ); + } + await cleanup(); +}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_ui_keyboard_a11y.js b/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_ui_keyboard_a11y.js @@ -0,0 +1,635 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +add_task(async function test_translations_settings_keyboard_a11y() { + const { + cleanup, + elements: { settingsButton }, + } = await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [["browser.translations.newSettingsUI.enable", true]], + }); + + info( + "Open translations settings page by clicking on translations settings button." + ); + assertVisibility({ + message: "Expect paneGeneral elements to be visible.", + visible: { settingsButton }, + }); + + const { + translationsSettingsBackButton, + alwaysTranslateMenuList, + neverTranslateMenuList, + translateDownloadLanguagesLearnMore, + downloadLanguageList, + } = + await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane( + settingsButton + ); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Press the Tab key to focus the first page element, the back button"); + + EventUtils.synthesizeKey("KEY_Tab"); + is( + document.activeElement.id, + translationsSettingsBackButton.id, + "Key is focused on back button" + ); + + info( + "Press the Tab key to focus the next page element, the Always Translate Menulist button" + ); + + EventUtils.synthesizeKey("KEY_Tab"); + is( + document.activeElement.id, + alwaysTranslateMenuList.id, + "Key is focused on Always Translate Menulist button" + ); + + info( + "Press the Tab key to focus the next page element, the Never Translate Menulist button" + ); + EventUtils.synthesizeKey("KEY_Tab"); + is( + document.activeElement.id, + neverTranslateMenuList.id, + "Key is focused on Never Translate Menulist button" + ); + + info( + "Press the Tab key to focus the next page element, the Download Languages' Learn More link" + ); + EventUtils.synthesizeKey("KEY_Tab"); + is( + document.activeElement.id, + translateDownloadLanguagesLearnMore.id, + "Key is focused on Download Languages' Learn More link" + ); + + info( + "Press the Tab key to focus the next page element, the Download Languages list section" + ); + EventUtils.synthesizeKey("KEY_Tab"); + is( + document.activeElement.id, + downloadLanguageList.id, + "Key is focused on Download Languages list section" + ); + + await cleanup(); +}); + +add_task(async function test_translations_settings_keyboard_download_a11y() { + const { + cleanup, + elements: { settingsButton }, + } = await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [["browser.translations.newSettingsUI.enable", true]], + }); + + info( + "Open translations settings page by clicking on translations settings button." + ); + assertVisibility({ + message: "Expect paneGeneral elements to be visible.", + visible: { settingsButton }, + }); + + const { + translationsSettingsBackButton, + alwaysTranslateMenuList, + neverTranslateMenuList, + translateDownloadLanguagesLearnMore, + downloadLanguageList, + } = + await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane( + settingsButton + ); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Press the Tab key to focus the first page element, the back button"); + + EventUtils.synthesizeKey("KEY_Tab"); + is( + document.activeElement.id, + translationsSettingsBackButton.id, + "Key is focused on back button" + ); + + info( + "Press the Tab key to focus the next page element, the Always Translate Menulist button" + ); + EventUtils.synthesizeKey("KEY_Tab"); + is( + document.activeElement.id, + alwaysTranslateMenuList.id, + "Key is focused on Always Translate Menulist button" + ); + + info( + "Press the Tab key to focus the next page element, the Never Translate Menulist button" + ); + EventUtils.synthesizeKey("KEY_Tab"); + is( + document.activeElement.id, + neverTranslateMenuList.id, + "Key is focused on Never Translate Menulist button" + ); + + info( + "Press the Tab key to focus the next page element, the Download Languages' Learn More link" + ); + EventUtils.synthesizeKey("KEY_Tab"); + is( + document.activeElement.id, + translateDownloadLanguagesLearnMore.id, + "Key is focused on Download Languages' Learn More link" + ); + + info( + "Press the Tab key to focus the next page element, the Download Languages list section" + ); + EventUtils.synthesizeKey("KEY_Tab"); + is( + document.activeElement.id, + downloadLanguageList.id, + "Key is focused on Download Languages list section" + ); + + info( + "Press the Arrow Down key to focus the first language element in the Download List Section" + ); + + for (let element of downloadLanguageList.children) { + info( + "Press the Arrow Down key to focus the next language element in the Download List Section" + ); + EventUtils.synthesizeKey("KEY_ArrowDown"); + is( + document.activeElement.parentNode.id, + element.id, + "Key is focused on the language " + + element.querySelector("label").textContent + + " within the language list" + ); + } + + is( + document.activeElement.parentNode.id, + downloadLanguageList.lastElementChild.id, + "Key is focused on the last language " + + downloadLanguageList.lastElementChild.querySelector("label").textContent + + " within the language list" + ); + + info( + "Press the Arrow up key to focus the previous language element in the Download List Section" + ); + for (let i = downloadLanguageList.children.length - 2; i >= 0; i--) { + info( + "Press the Arrow up key to focus the previous language element in the Download List Section" + ); + EventUtils.synthesizeKey("KEY_ArrowUp"); + is( + document.activeElement.parentNode.id, + downloadLanguageList.children[i].id, + "Key is focused on the language " + + downloadLanguageList.children[i].querySelector("label").textContent + + " within the language list" + ); + } + + is( + document.activeElement.parentNode.id, + downloadLanguageList.firstElementChild.id, + "Key is focused on the first language within the language list" + ); + + await cleanup(); +}); + +add_task( + async function test_translations_settings_keyboard_never_translate_site_a11y() { + const { + cleanup, + elements: { settingsButton }, + } = await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [["browser.translations.newSettingsUI.enable", true]], + }); + + info( + "Open translations settings page by clicking on translations settings button." + ); + assertVisibility({ + message: "Expect paneGeneral elements to be visible.", + visible: { settingsButton }, + }); + + const { + translationsSettingsBackButton, + alwaysTranslateMenuList, + neverTranslateMenuList, + neverTranslateSiteList, + translateDownloadLanguagesLearnMore, + } = + await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane( + settingsButton + ); + + is( + neverTranslateSiteList.childElementCount, + 0, + "The never-translate sites html list is empty" + ); + + info("Adding sites to the neverTranslateSites perms"); + await PermissionTestUtils.add( + "https://example.com", + TRANSLATIONS_PERMISSION, + Services.perms.DENY_ACTION + ); + await PermissionTestUtils.add( + "https://example.org", + TRANSLATIONS_PERMISSION, + Services.perms.DENY_ACTION + ); + await PermissionTestUtils.add( + "https://example.net", + TRANSLATIONS_PERMISSION, + Services.perms.DENY_ACTION + ); + + is( + getNeverTranslateSitesFromPerms().length, + 3, + "The list of never-translate sites has 3 elements" + ); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Press the Tab key to focus the first page element, the back button"); + EventUtils.synthesizeKey("KEY_Tab"); + is( + document.activeElement.id, + translationsSettingsBackButton.id, + "Key is focused on back button" + ); + + info( + "Press the Tab key to focus the next page element, the Always Translate Menulist button" + ); + EventUtils.synthesizeKey("KEY_Tab"); + is( + document.activeElement.id, + alwaysTranslateMenuList.id, + "Key is focused on Always Translate Menulist button" + ); + + info( + "Press the Tab key to focus the next page element, the Never Translate Menulist button" + ); + EventUtils.synthesizeKey("KEY_Tab"); + is( + document.activeElement.id, + neverTranslateMenuList.id, + "Key focus is now Never Translate List Menu button" + ); + + info( + "Press the Tab key to focus the next page element, the Never Translate Site List section" + ); + EventUtils.synthesizeKey("KEY_Tab"); + is( + document.activeElement.id, + neverTranslateSiteList.id, + "Key focus is now Never Translate Site List" + ); + info( + "Press the Arrow Down key to focus the first site element in the Never Translate Site List" + ); + for (const site of neverTranslateSiteList.children) { + info( + "Press the Arrow Down key to focus the next site element in the Never Translate Site List" + ); + EventUtils.synthesizeKey("KEY_ArrowDown"); + is( + document.activeElement.parentNode.id, + site.id, + "Key focus is now Never Translate Site list element " + + site.querySelector("label").textContent + ); + } + is( + document.activeElement.parentNode.id, + neverTranslateSiteList.lastElementChild.id, + "Key is focused on the last site " + + neverTranslateSiteList.lastElementChild.querySelector("label") + .textContent + + " within the site list" + ); + + info( + "Press the Arrow up key to focus the previous site element in the Never Translate Site List" + ); + for (let i = neverTranslateSiteList.children.length - 2; i >= 0; i--) { + info( + "Press the Arrow up key to focus the previous site element in the Never Translate Site List" + ); + EventUtils.synthesizeKey("KEY_ArrowUp"); + is( + document.activeElement.parentNode.id, + neverTranslateSiteList.children[i].id, + "Key is focused on the site " + + neverTranslateSiteList.children[i].querySelector("label") + .textContent + + " within the site list" + ); + } + + info( + "Press the Tab key to focus the next page element, the Download Languages' Learn More link" + ); + EventUtils.synthesizeKey("KEY_Tab"); + is( + document.activeElement.id, + translateDownloadLanguagesLearnMore.id, + "Key is focused on Download Languages' Learn More link" + ); + + await cleanup(); + } +); + +add_task( + async function test_translations_settings_keyboard_never_translate_a11y() { + const { + cleanup, + elements: { settingsButton }, + } = await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [["browser.translations.newSettingsUI.enable", true]], + }); + + info( + "Open translations settings page by clicking on translations settings button." + ); + assertVisibility({ + message: "Expect paneGeneral elements to be visible.", + visible: { settingsButton }, + }); + + const { + translationsSettingsBackButton, + alwaysTranslateMenuList, + neverTranslateMenuList, + neverTranslateLanguageList, + neverTranslateMenuPopup, + translateDownloadLanguagesLearnMore, + } = + await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane( + settingsButton + ); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Press the Tab key to focus the first page element, the back button"); + + EventUtils.synthesizeKey("KEY_Tab"); + is( + document.activeElement.id, + translationsSettingsBackButton.id, + "Key is focused on back button" + ); + + info( + "Press the Tab key to focus the next page element, the Always Translate Menulist button" + ); + EventUtils.synthesizeKey("KEY_Tab"); + is( + document.activeElement.id, + alwaysTranslateMenuList.id, + "Key is focused on Always Translate Menulist button" + ); + + info( + "Press the Tab key to focus the next page element, the Never Translate Menulist button." + ); + EventUtils.synthesizeKey("KEY_Tab"); + is( + document.activeElement.id, + neverTranslateMenuList.id, + "Key is focused on Never Translate Menulist button" + ); + + info("Press the Arrow Down key to focus on the first list element."); + for (const menuItem of neverTranslateMenuPopup.children) { + if (AppConstants.platform === "macosx") { + info("Opening the menu popup."); + const popupPromise = BrowserTestUtils.waitForEvent( + neverTranslateMenuPopup, + "popupshown" + ); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await popupPromise; + } + + EventUtils.synthesizeKey("KEY_ArrowDown"); + + if (AppConstants.platform === "macosx") { + info("Closing the menu popup."); + const popupPromise = BrowserTestUtils.waitForEvent( + neverTranslateMenuPopup, + "popuphidden" + ); + EventUtils.synthesizeKey("KEY_Enter"); + await popupPromise; + } else { + const { promise, resolve } = Promise.withResolvers(); + requestAnimationFrame(() => { + requestAnimationFrame(resolve); + }); + + EventUtils.synthesizeKey("KEY_Enter"); + await promise; + } + + is( + neverTranslateLanguageList.firstElementChild.querySelector("label") + .textContent, + menuItem.textContent, + menuItem.textContent + "is added to never translate language" + ); + } + + info( + "Press the Tab key to focus the next page element, the Never Translate list" + ); + EventUtils.synthesizeKey("KEY_Tab"); + is( + neverTranslateLanguageList.id, + document.activeElement.id, + "Key is focused on Always Translate list." + ); + + info("Press the Arrow Down key to focus on the first list element."); + + for (const lang of neverTranslateLanguageList.children) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + is( + document.activeElement.parentNode.id, + lang.id, + "Key is focused on " + + lang.querySelector("label").textContent + + " element of Never Translate list." + ); + } + + info( + "Press the Tab key to focus the next page element, the Download Languages' Learn More link" + ); + EventUtils.synthesizeKey("KEY_Tab"); + is( + document.activeElement.id, + translateDownloadLanguagesLearnMore.id, + "Key is focused on Download Languages' Learn More link" + ); + + await cleanup(); + } +); + +add_task( + async function test_translations_settings_keyboard_always_translate_a11y() { + const { + cleanup, + elements: { settingsButton }, + } = await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [["browser.translations.newSettingsUI.enable", true]], + }); + + info( + "Open translations settings page by clicking on translations settings button." + ); + assertVisibility({ + message: "Expect paneGeneral elements to be visible.", + visible: { settingsButton }, + }); + + const { + translationsSettingsBackButton, + alwaysTranslateMenuList, + neverTranslateMenuList, + alwaysTranslateLanguageList, + alwaysTranslateMenuPopup, + } = + await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane( + settingsButton + ); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Press the Tab key to focus the first page element, the back button"); + + EventUtils.synthesizeKey("KEY_Tab"); + is( + document.activeElement.id, + translationsSettingsBackButton.id, + "Key is focused on back button" + ); + + info( + "Press the Tab key to focus the next page element, the Always Translate Menulist button" + ); + EventUtils.synthesizeKey("KEY_Tab"); + is( + document.activeElement.id, + alwaysTranslateMenuList.id, + "Key is focused on Always Translate Menulist button" + ); + + info("Press the Arrow Down key to focus on the first list element."); + for (const menuItem of alwaysTranslateMenuPopup.children) { + if (AppConstants.platform === "macosx") { + info("Opening the menu popup."); + const popupPromise = BrowserTestUtils.waitForEvent( + alwaysTranslateMenuPopup, + "popupshown" + ); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await popupPromise; + } + + EventUtils.synthesizeKey("KEY_ArrowDown"); + + if (AppConstants.platform === "macosx") { + info("Closing the menu popup."); + const popupPromise = BrowserTestUtils.waitForEvent( + alwaysTranslateMenuPopup, + "popuphidden" + ); + EventUtils.synthesizeKey("KEY_Enter"); + await popupPromise; + } else { + const { promise, resolve } = Promise.withResolvers(); + requestAnimationFrame(() => { + requestAnimationFrame(resolve); + }); + + EventUtils.synthesizeKey("KEY_Enter"); + await promise; + } + + is( + alwaysTranslateLanguageList.firstElementChild.querySelector("label") + .textContent, + menuItem.textContent, + menuItem.textContent + "is added to always translate language" + ); + } + + info( + "Press the Tab key to focus the next page element, the Always Translate list" + ); + EventUtils.synthesizeKey("KEY_Tab"); + is( + alwaysTranslateLanguageList.id, + document.activeElement.id, + "Key is focused on Always Translate list." + ); + + info("Press the Arrow Down key to focus on the first list element."); + + for (const lang of alwaysTranslateLanguageList.children) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + is( + document.activeElement.parentNode.id, + lang.id, + "Key is focused on " + + lang.querySelector("label").textContent + + " element of Always Translate list." + ); + } + + info( + "Press the Tab key to focus the next page element, the Never Translate list" + ); + EventUtils.synthesizeKey("KEY_Tab"); + is( + document.activeElement.id, + neverTranslateMenuList.id, + "Key focus is now Never Translate List Menu button" + ); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_ui_tab.js b/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_ui_tab.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task( + async function test_translations_settings_about_preferences_translations_tab() { + const { cleanup } = await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [["browser.translations.newSettingsUI.enable", true]], + }); + + info( + 'Open translations settings directly with URL "about:preferences#translations" to ensure that the translations settings elements are visible. This proves that the attribute data-subpanel="true" in the translations settings elements is working' + ); + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:preferences#translations", + true // waitForLoad + ); + + const translationsPane = + content.window.gCategoryModules.get("paneTranslations"); + const { + translationsSettingsBackButton, + translationsSettingsHeader, + translationsSettingsDescription, + translateAlwaysHeader, + translateNeverHeader, + alwaysTranslateMenuList, + neverTranslateMenuList, + translateNeverSiteHeader, + translateNeverSiteDesc, + translateDownloadLanguagesLearnMore, + } = translationsPane.elements; + + assertVisibility({ + message: "Expect paneTranslations elements to be visible.", + visible: { + translationsSettingsBackButton, + translationsSettingsHeader, + translationsSettingsDescription, + translateAlwaysHeader, + translateNeverHeader, + alwaysTranslateMenuList, + neverTranslateMenuList, + translateNeverSiteHeader, + translateNeverSiteDesc, + translateDownloadLanguagesLearnMore, + }, + }); + + BrowserTestUtils.removeTab(tab); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_main_page_offer_checkbox.js b/browser/components/translations/tests/browser/browser_translations_about_settings_main_page_offer_checkbox.js @@ -1,163 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - https://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -/** - * Tests that the "Always offer to translate" checkbox in about:preferences - * properly updates the browser.translations.automaticallyPopup preference - * when toggled by the user. - */ -add_task(async function test_offer_translations_checkbox_toggle() { - const PREF_NAME = "browser.translations.automaticallyPopup"; - - const { cleanup } = await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [ - ["browser.settings-redesign.enabled", true], - [PREF_NAME, true], - ], - }); - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Waiting for the offerTranslations checkbox to be available"); - const checkbox = await waitForCondition( - () => document.getElementById("offerTranslations"), - "Waiting for offerTranslations checkbox to be visible" - ); - - await ensureVisibility({ - message: "offerTranslations checkbox should be visible", - visible: { checkbox }, - }); - - info("Scrolling checkbox into view"); - checkbox.scrollIntoView({ behavior: "instant", block: "center" }); - - info("Verifying initial state matches the preference"); - let prefValue = Services.prefs.getBoolPref(PREF_NAME); - is(prefValue, true, "Initial pref value should be true"); - - const checkboxInput = - checkbox.tagName === "INPUT" - ? checkbox - : checkbox.querySelector('input[type="checkbox"]'); - - if (checkboxInput) { - is( - checkboxInput.checked, - prefValue, - "Checkbox checked state should match preference value" - ); - } else { - is( - checkbox.checked, - prefValue, - "Checkbox checked state should match preference value" - ); - } - - info("Clicking checkbox to toggle it OFF (true → false)"); - let prefChanged = TestUtils.waitForPrefChange(PREF_NAME); - click(checkbox, "Toggling offerTranslations checkbox to false"); - await prefChanged; - - prefValue = Services.prefs.getBoolPref(PREF_NAME); - is(prefValue, false, "Pref should now be false after clicking"); - - if (checkboxInput) { - is(checkboxInput.checked, false, "Checkbox should be unchecked"); - } else { - is(checkbox.checked, false, "Checkbox should be unchecked"); - } - - info("Clicking checkbox to toggle it back ON (false → true)"); - prefChanged = TestUtils.waitForPrefChange(PREF_NAME); - click(checkbox, "Toggling offerTranslations checkbox to true"); - await prefChanged; - - prefValue = Services.prefs.getBoolPref(PREF_NAME); - is(prefValue, true, "Pref should now be true after clicking again"); - - if (checkboxInput) { - is(checkboxInput.checked, true, "Checkbox should be checked"); - } else { - is(checkbox.checked, true, "Checkbox should be checked"); - } - - await cleanup(); -}); - -/** - * Tests that changing the browser.translations.automaticallyPopup preference - * directly updates the checkbox state in the UI automatically. - */ -add_task(async function test_offer_translations_pref_updates_checkbox() { - const PREF_NAME = "browser.translations.automaticallyPopup"; - - const { cleanup } = await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [ - ["browser.settings-redesign.enabled", true], - [PREF_NAME, true], - ], - }); - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Waiting for the offerTranslations checkbox to be available"); - const checkbox = await waitForCondition( - () => document.getElementById("offerTranslations"), - "Waiting for offerTranslations checkbox to be visible" - ); - - await ensureVisibility({ - message: "offerTranslations checkbox should be visible", - visible: { checkbox }, - }); - - info("Scrolling checkbox into view"); - checkbox.scrollIntoView({ behavior: "instant", block: "center" }); - - const checkboxInput = - checkbox.tagName === "INPUT" - ? checkbox - : checkbox.querySelector('input[type="checkbox"]'); - - const getCheckedState = () => - checkboxInput ? checkboxInput.checked : checkbox.checked; - - info("Verifying initial state is checked (pref is true)"); - is(getCheckedState(), true, "Checkbox should initially be checked"); - - info("Setting pref to false directly"); - Services.prefs.setBoolPref(PREF_NAME, false); - - info("Waiting for checkbox to update to unchecked state"); - await waitForCondition( - () => getCheckedState() === false, - "Waiting for checkbox to become unchecked after pref change" - ); - - is( - getCheckedState(), - false, - "Checkbox should be unchecked after pref set to false" - ); - - info("Setting pref to true directly"); - Services.prefs.setBoolPref(PREF_NAME, true); - - info("Waiting for checkbox to update to checked state"); - await waitForCondition( - () => getCheckedState() === true, - "Waiting for checkbox to become checked after pref change" - ); - - is( - getCheckedState(), - true, - "Checkbox should be checked after pref set to true" - ); - - await cleanup(); -}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_always_translate_langs_a11y.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_always_translate_langs_a11y.js @@ -1,175 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - https://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -/** - * Tests keyboard activation (Enter key) of remove buttons in the always-translate language list. - */ -add_task(async function test_always_translate_languages_keyboard_activation() { - const { cleanup, translationsSettingsTestUtils } = - await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [ - ["browser.settings-redesign.enabled", true], - [ALWAYS_TRANSLATE_LANGS_PREF, "es,fr,uk"], - ], - }); - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Navigating to translations subpage"); - const manageButton = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton" - ); - - info("Scrolling button into view"); - manageButton.scrollIntoView({ behavior: "instant", block: "center" }); - - info("Clicking manage button and waiting for initialization"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [[TranslationsSettingsTestUtils.Events.Initialized]], - }, - async () => { - click( - manageButton, - "Clicking manage button to open translations subpage" - ); - } - ); - - info("Verifying three language items are rendered"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ - languages: ["es", "fr", "uk"], - count: 3, - }); - - info("Getting first remove button"); - const firstRemoveButton = document.querySelector( - ".translations-always-translate-remove-button" - ); - ok(firstRemoveButton, "First remove button should exist"); - - info("Testing keyboard activation with Enter key"); - firstRemoveButton.focus(); - is( - document.activeElement, - firstRemoveButton, - "Remove button should be focused" - ); - - info("Pressing Enter to remove first language"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.AlwaysTranslateLanguagesRendered], - ], - }, - async () => { - const prefChanged = TestUtils.waitForPrefChange( - ALWAYS_TRANSLATE_LANGS_PREF - ); - const enterEvent = new KeyboardEvent("keydown", { - key: "Enter", - bubbles: true, - }); - firstRemoveButton.dispatchEvent(enterEvent); - click(firstRemoveButton, "Activating remove via Enter key"); - await prefChanged; - } - ); - - info("Verifying language was removed"); - const langs = getAlwaysTranslateLanguagesFromPref(); - is(langs.length, 2, "Should have 2 languages after removal"); - - info("Verifying UI updated to show 2 items"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ - count: 2, - }); - - await cleanup(); -}); - -/** - * Tests accessibility features including ARIA labels and keyboard-only operation. - */ -add_task(async function test_always_translate_languages_accessibility() { - const { cleanup, translationsSettingsTestUtils } = - await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [ - ["browser.settings-redesign.enabled", true], - [ALWAYS_TRANSLATE_LANGS_PREF, "es,fr"], - ], - }); - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Navigating to translations subpage"); - const manageButton = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton" - ); - - info("Scrolling button into view"); - manageButton.scrollIntoView({ behavior: "instant", block: "center" }); - - info("Clicking manage button and waiting for initialization"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [[TranslationsSettingsTestUtils.Events.Initialized]], - }, - async () => { - click( - manageButton, - "Clicking manage button to open translations subpage" - ); - } - ); - - info("Verifying two language items are rendered"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ - languages: ["es", "fr"], - count: 2, - }); - - info("Getting language items and remove buttons"); - const items = document.querySelectorAll( - ".translations-always-translate-language-item" - ); - is(items.length, 2, "Should have 2 language items"); - - info("Verifying remove buttons have accessible labels"); - const removeButtons = document.querySelectorAll( - ".translations-always-translate-remove-button" - ); - is(removeButtons.length, 2, "Should have 2 remove buttons"); - - for (const button of removeButtons) { - const ariaLabel = - button.getAttribute("aria-label") || - button.ariaLabel || - button.getAttribute("data-l10n-id"); - ok( - ariaLabel, - "Remove button should have aria-label or data-l10n-id for accessibility" - ); - if (typeof ariaLabel === "string") { - Assert.greater( - ariaLabel.length, - 0, - "Remove button accessibility label should not be empty" - ); - } - } - - info("Verifying language items have proper semantic structure"); - for (const item of items) { - const label = item.getAttribute("label"); - ok(label, "Each language item should have a label attribute"); - Assert.greater(label.length, 0, "Label should not be empty"); - } - - await cleanup(); -}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_always_translate_langs_basic.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_always_translate_langs_basic.js @@ -1,160 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - https://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -/** - * Tests that pre-populated languages load correctly when opening the subpage. - */ -add_task(async function test_always_translate_languages_prepopulated() { - const { cleanup, translationsSettingsTestUtils } = - await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [ - ["browser.settings-redesign.enabled", true], - [ALWAYS_TRANSLATE_LANGS_PREF, "uk,es,fr"], - ], - }); - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Navigating to translations subpage"); - const manageButton = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton" - ); - - info("Scrolling button into view"); - manageButton.scrollIntoView({ behavior: "instant", block: "center" }); - - info("Clicking manage button and waiting for initialization"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [[TranslationsSettingsTestUtils.Events.Initialized]], - }, - async () => { - click( - manageButton, - "Clicking manage button to open translations subpage" - ); - } - ); - - info("Verifying three language items are displayed"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ - languages: ["es", "fr", "uk"], - count: 3, - }); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguagesOrder({ - languages: ["fr", "es", "uk"], - }); - - info("Verifying empty state is not visible"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguagesEmptyState({ - visible: false, - }); - - await cleanup(); -}); - -/** - * Tests that the dropdown state is managed correctly - already-added languages - * should be disabled in the dropdown. - */ -add_task(async function test_always_translate_languages_dropdown_state() { - const { cleanup, translationsSettingsTestUtils } = - await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [ - ["browser.settings-redesign.enabled", true], - [ALWAYS_TRANSLATE_LANGS_PREF, "es,fr"], - ], - }); - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Navigating to translations subpage"); - const manageButton = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton" - ); - - info("Scrolling button into view"); - manageButton.scrollIntoView({ behavior: "instant", block: "center" }); - - info("Clicking manage button and waiting for initialization"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [[TranslationsSettingsTestUtils.Events.Initialized]], - }, - async () => { - click( - manageButton, - "Clicking manage button to open translations subpage" - ); - } - ); - - info("Verifying two language items are rendered"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ - languages: ["es", "fr"], - count: 2, - }); - - info("Getting the dropdown element"); - const dropdown = - translationsSettingsTestUtils.getAlwaysTranslateLanguagesSelect(); - - info("Verifying Spanish and French options are disabled"); - const spanishOption = dropdown.querySelector('moz-option[value="es"]'); - const frenchOption = dropdown.querySelector('moz-option[value="fr"]'); - const ukrainianOption = dropdown.querySelector('moz-option[value="uk"]'); - - ok(spanishOption, "Spanish option should exist in dropdown"); - ok(frenchOption, "French option should exist in dropdown"); - ok(ukrainianOption, "Ukrainian option should exist in dropdown"); - - ok( - spanishOption.disabled, - "Spanish option should be disabled (already added)" - ); - ok(frenchOption.disabled, "French option should be disabled (already added)"); - ok(!ukrainianOption.disabled, "Ukrainian option should not be disabled"); - - info("Adding Ukrainian via dropdown"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.AlwaysTranslateLanguagesRendered], - [ - TranslationsSettingsTestUtils.Events - .AlwaysTranslateLanguagesSelectOptionsUpdated, - ], - ], - }, - async () => { - const prefChanged = TestUtils.waitForPrefChange( - ALWAYS_TRANSLATE_LANGS_PREF - ); - await translationsSettingsTestUtils.addAlwaysTranslateLanguage("uk"); - await prefChanged; - } - ); - - info("Verifying Ukrainian was added"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ - languages: ["es", "fr", "uk"], - count: 3, - }); - - info("Verifying dropdown updated and Ukrainian is now disabled"); - ok( - ukrainianOption.disabled, - "Ukrainian option should now be disabled (just added)" - ); - - await dropdown.updateComplete; - - info("Verifying dropdown resets to placeholder"); - is(dropdown.value, "", "Dropdown should reset to empty value"); - - await cleanup(); -}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_always_translate_langs_modify.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_always_translate_langs_modify.js @@ -1,424 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - https://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -/** - * Tests basic adding and removing of languages in the Always Translate Languages - * section, including empty state transitions. - */ -add_task(async function test_always_translate_languages_add_and_remove() { - const { cleanup, translationsSettingsTestUtils } = - await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [ - ["browser.settings-redesign.enabled", true], - [ALWAYS_TRANSLATE_LANGS_PREF, ""], - ], - }); - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Navigating to translations subpage"); - const manageButton = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton" - ); - - info("Scrolling button into view"); - manageButton.scrollIntoView({ behavior: "instant", block: "center" }); - - info("Clicking manage button and waiting for initialization"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [[TranslationsSettingsTestUtils.Events.Initialized]], - }, - async () => { - click( - manageButton, - "Clicking manage button to open translations subpage" - ); - } - ); - - info("Verifying empty state is visible initially"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguagesEmptyState({ - visible: true, - }); - - info("Adding Spanish (es) via dropdown"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.AlwaysTranslateLanguagesRendered], - [ - TranslationsSettingsTestUtils.Events - .AlwaysTranslateLanguagesEmptyStateHidden, - ], - ], - }, - async () => { - const prefChanged = TestUtils.waitForPrefChange( - ALWAYS_TRANSLATE_LANGS_PREF - ); - await translationsSettingsTestUtils.addAlwaysTranslateLanguage("es"); - await prefChanged; - } - ); - - info("Verifying Spanish was added"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ - languages: ["es"], - count: 1, - }); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguagesEmptyState({ - visible: false, - }); - - info("Adding French (fr) via dropdown"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.AlwaysTranslateLanguagesRendered], - ], - }, - async () => { - const prefChanged = TestUtils.waitForPrefChange( - ALWAYS_TRANSLATE_LANGS_PREF - ); - await translationsSettingsTestUtils.addAlwaysTranslateLanguage("fr"); - await prefChanged; - } - ); - - info("Verifying French was added"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ - languages: ["es", "fr"], - count: 2, - }); - - info("Adding Ukrainian (uk) via dropdown"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.AlwaysTranslateLanguagesRendered], - ], - }, - async () => { - const prefChanged = TestUtils.waitForPrefChange( - ALWAYS_TRANSLATE_LANGS_PREF - ); - await translationsSettingsTestUtils.addAlwaysTranslateLanguage("uk"); - await prefChanged; - } - ); - - info("Verifying Ukrainian was added"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ - languages: ["es", "fr", "uk"], - count: 3, - }); - - info("Removing middle item (French)"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.AlwaysTranslateLanguagesRendered], - ], - }, - async () => { - const prefChanged = TestUtils.waitForPrefChange( - ALWAYS_TRANSLATE_LANGS_PREF - ); - await translationsSettingsTestUtils.removeAlwaysTranslateLanguage("fr"); - await prefChanged; - } - ); - - info("Verifying French was removed"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ - languages: ["es", "uk"], - count: 2, - }); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguagesEmptyState({ - visible: false, - }); - - info("Removing Ukrainian"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.AlwaysTranslateLanguagesRendered], - ], - }, - async () => { - const prefChanged = TestUtils.waitForPrefChange( - ALWAYS_TRANSLATE_LANGS_PREF - ); - await translationsSettingsTestUtils.removeAlwaysTranslateLanguage("uk"); - await prefChanged; - } - ); - - info("Verifying Ukrainian was removed"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ - languages: ["es"], - count: 1, - }); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguagesEmptyState({ - visible: false, - }); - - info("Removing Spanish (last language)"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.AlwaysTranslateLanguagesRendered], - [ - TranslationsSettingsTestUtils.Events - .AlwaysTranslateLanguagesEmptyStateShown, - ], - ], - }, - async () => { - const prefChanged = TestUtils.waitForPrefChange( - ALWAYS_TRANSLATE_LANGS_PREF - ); - await translationsSettingsTestUtils.removeAlwaysTranslateLanguage("es"); - await prefChanged; - } - ); - - info("Verifying all languages removed and empty state reappears"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ - languages: [], - count: 0, - }); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguagesEmptyState({ - visible: true, - }); - - await cleanup(); -}); - -/** - * Tests that invalid language tags don't break the UI and valid languages - * are still rendered correctly. - */ -add_task(async function test_always_translate_languages_invalid_tags() { - const { cleanup, translationsSettingsTestUtils } = - await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [ - ["browser.settings-redesign.enabled", true], - [ALWAYS_TRANSLATE_LANGS_PREF, "es,fr,uk"], - ], - }); - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Navigating to translations subpage"); - const manageButton = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton" - ); - - info("Scrolling button into view"); - manageButton.scrollIntoView({ behavior: "instant", block: "center" }); - - info("Clicking manage button and waiting for initialization"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [[TranslationsSettingsTestUtils.Events.Initialized]], - }, - async () => { - click( - manageButton, - "Clicking manage button to open translations subpage" - ); - } - ); - - info("Verifying three valid languages are displayed"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ - languages: ["es", "fr", "uk"], - count: 3, - }); - - info("Testing UI doesn't break when pref has mixed valid/invalid tags"); - info("Adding invalid tags via pref change"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.AlwaysTranslateLanguagesRendered], - ], - }, - async () => { - Services.prefs.setCharPref( - ALWAYS_TRANSLATE_LANGS_PREF, - "es,INVALID,fr,uk" - ); - } - ); - - info("Verifying UI still works and valid languages remain accessible"); - ok( - document.querySelector('[data-lang-tag="es"]'), - "Spanish item should still exist" - ); - ok( - document.querySelector('[data-lang-tag="fr"]'), - "French item should still exist" - ); - ok( - document.querySelector('[data-lang-tag="uk"]'), - "Ukrainian item should still exist" - ); - - await cleanup(); -}); - -/** - * Tests that adding a language to always-translate automatically removes it - * from the never-translate list ("stealing" behavior). - */ -add_task(async function test_always_translate_languages_stealing() { - const { cleanup, translationsSettingsTestUtils } = - await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [ - ["browser.settings-redesign.enabled", true], - [NEVER_TRANSLATE_LANGS_PREF, "es,fr,uk"], - [ALWAYS_TRANSLATE_LANGS_PREF, ""], - ], - }); - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Navigating to translations subpage"); - const manageButton = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton" - ); - - info("Scrolling button into view"); - manageButton.scrollIntoView({ behavior: "instant", block: "center" }); - - info("Clicking manage button and waiting for initialization"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [[TranslationsSettingsTestUtils.Events.Initialized]], - }, - async () => { - click( - manageButton, - "Clicking manage button to open translations subpage" - ); - } - ); - - info("Verifying always-translate section is empty"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguagesEmptyState({ - visible: true, - }); - - info("Verifying never-translate has three languages"); - let neverLangs = getNeverTranslateLanguagesFromPref(); - is(neverLangs.length, 3, "Should have 3 never-translate languages"); - ok( - neverLangs.includes("es") && - neverLangs.includes("fr") && - neverLangs.includes("uk"), - "Never-translate should include es, fr, uk" - ); - - info("Adding Spanish to always-translate via UI"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.AlwaysTranslateLanguagesRendered], - [ - TranslationsSettingsTestUtils.Events - .AlwaysTranslateLanguagesEmptyStateHidden, - ], - ], - }, - async () => { - const alwaysPrefChanged = TestUtils.waitForPrefChange( - ALWAYS_TRANSLATE_LANGS_PREF - ); - await translationsSettingsTestUtils.addAlwaysTranslateLanguage("es"); - await alwaysPrefChanged; - } - ); - - info("Verifying Spanish appears in always-translate list"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ - languages: ["es"], - count: 1, - }); - - info("Verifying Spanish was removed from never-translate pref"); - neverLangs = getNeverTranslateLanguagesFromPref(); - is(neverLangs.length, 2, "Should have 2 never-translate languages"); - ok(!neverLangs.includes("es"), "Never-translate should not include Spanish"); - ok( - neverLangs.includes("fr") && neverLangs.includes("uk"), - "Never-translate should still include French and Ukrainian" - ); - - info("Adding French to always-translate via UI"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.AlwaysTranslateLanguagesRendered], - ], - }, - async () => { - const alwaysPrefChanged = TestUtils.waitForPrefChange( - ALWAYS_TRANSLATE_LANGS_PREF - ); - await translationsSettingsTestUtils.addAlwaysTranslateLanguage("fr"); - await alwaysPrefChanged; - } - ); - - info("Verifying both Spanish and French in always-translate list"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ - languages: ["es", "fr"], - count: 2, - }); - - info("Verifying only Ukrainian remains in never-translate pref"); - neverLangs = getNeverTranslateLanguagesFromPref(); - is(neverLangs.length, 1, "Should have 1 never-translate language"); - ok( - neverLangs.includes("uk"), - "Never-translate should only include Ukrainian" - ); - - info("Adding Ukrainian to always-translate via UI"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.AlwaysTranslateLanguagesRendered], - ], - }, - async () => { - const alwaysPrefChanged = TestUtils.waitForPrefChange( - ALWAYS_TRANSLATE_LANGS_PREF - ); - await translationsSettingsTestUtils.addAlwaysTranslateLanguage("uk"); - await alwaysPrefChanged; - } - ); - - info("Verifying all three languages now in always-translate"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ - languages: ["es", "fr", "uk"], - count: 3, - }); - - info("Verifying never-translate is empty after stealing"); - neverLangs = getNeverTranslateLanguagesFromPref(); - is(neverLangs.length, 0, "Never-translate should be empty after stealing"); - - await cleanup(); -}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_always_translate_langs_observe.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_always_translate_langs_observe.js @@ -1,245 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - https://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -/** - * Tests that the UI reactively updates when prefs are changed externally - * while the subpage is open. - */ -add_task(async function test_always_translate_languages_observe_pref_changes() { - const { cleanup, translationsSettingsTestUtils } = - await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [ - ["browser.settings-redesign.enabled", true], - [ALWAYS_TRANSLATE_LANGS_PREF, ""], - ], - }); - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Navigating to translations subpage"); - const manageButton = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton" - ); - - info("Scrolling button into view"); - manageButton.scrollIntoView({ behavior: "instant", block: "center" }); - - info("Clicking manage button and waiting for initialization"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [[TranslationsSettingsTestUtils.Events.Initialized]], - }, - async () => { - click( - manageButton, - "Clicking manage button to open translations subpage" - ); - } - ); - - info("Verifying empty state initially visible"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguagesEmptyState({ - visible: true, - }); - - info("Adding Spanish (es) via pref directly"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.AlwaysTranslateLanguagesRendered], - [ - TranslationsSettingsTestUtils.Events - .AlwaysTranslateLanguagesEmptyStateHidden, - ], - ], - }, - async () => { - Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, "es"); - } - ); - - info("Verifying Spanish was added"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ - languages: ["es"], - count: 1, - }); - - info("Adding more languages via pref (es,fr,uk)"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.AlwaysTranslateLanguagesRendered], - ], - }, - async () => { - Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, "es,fr,uk"); - } - ); - - info("Verifying all three languages are displayed"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ - languages: ["es", "fr", "uk"], - count: 3, - }); - - info("Removing French via pref (es,uk)"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.AlwaysTranslateLanguagesRendered], - ], - }, - async () => { - Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, "es,uk"); - } - ); - - info("Verifying French was removed"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ - languages: ["es", "uk"], - count: 2, - }); - - info("Clearing all languages via pref"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.AlwaysTranslateLanguagesRendered], - [ - TranslationsSettingsTestUtils.Events - .AlwaysTranslateLanguagesEmptyStateShown, - ], - ], - }, - async () => { - Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, ""); - } - ); - - info("Verifying all languages removed and empty state returns"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ - languages: [], - count: 0, - }); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguagesEmptyState({ - visible: true, - }); - - await cleanup(); -}); - -/** - * Tests that both UI lists update correctly when simulating a stealing scenario - * by manually adding a language to one pref and removing it from the other. - */ -add_task(async function test_always_translate_languages_simulated_stealing() { - const { cleanup, translationsSettingsTestUtils } = - await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [ - ["browser.settings-redesign.enabled", true], - [NEVER_TRANSLATE_LANGS_PREF, "es,fr"], - [ALWAYS_TRANSLATE_LANGS_PREF, ""], - ], - }); - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Navigating to translations subpage"); - const manageButton = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton" - ); - - info("Scrolling button into view"); - manageButton.scrollIntoView({ behavior: "instant", block: "center" }); - - info("Clicking manage button and waiting for initialization"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [[TranslationsSettingsTestUtils.Events.Initialized]], - }, - async () => { - click( - manageButton, - "Clicking manage button to open translations subpage" - ); - } - ); - - info("Verifying always-translate section is empty"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguagesEmptyState({ - visible: true, - }); - - info( - "Simulating stealing by adding Spanish to always-translate and removing from never-translate" - ); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.NeverTranslateLanguagesRendered], - [TranslationsSettingsTestUtils.Events.AlwaysTranslateLanguagesRendered], - [ - TranslationsSettingsTestUtils.Events - .AlwaysTranslateLanguagesEmptyStateHidden, - ], - ], - }, - async () => { - Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, "es"); - Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, "fr"); - } - ); - - info("Verifying Spanish appears in always-translate UI"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ - languages: ["es"], - count: 1, - }); - - info("Verifying never-translate UI updated to show only French"); - await translationsSettingsTestUtils.assertNeverTranslateLanguages({ - languages: ["fr"], - count: 1, - }); - - info( - "Simulating stealing French by adding to always-translate and removing from never-translate" - ); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.NeverTranslateLanguagesRendered], - [ - TranslationsSettingsTestUtils.Events - .NeverTranslateLanguagesEmptyStateShown, - ], - [TranslationsSettingsTestUtils.Events.AlwaysTranslateLanguagesRendered], - ], - }, - async () => { - Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, "es,fr"); - Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, ""); - } - ); - - info("Verifying both Spanish and French in always-translate UI"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ - languages: ["es", "fr"], - count: 2, - }); - - info("Verifying never-translate UI shows empty state"); - await translationsSettingsTestUtils.assertNeverTranslateLanguages({ - languages: [], - count: 0, - }); - await translationsSettingsTestUtils.assertNeverTranslateLanguagesEmptyState({ - visible: true, - }); - - await cleanup(); -}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_back_button.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_back_button.js @@ -1,41 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - https://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -/** - * Tests that the translations subpage back button returns to the main page. - */ -add_task(async function test_translations_subpage_back_button() { - const { cleanup, translationsSettingsTestUtils } = - await TranslationsSettingsTestUtils.openTranslationsSettingsSubpage(); - - const document = gBrowser.selectedBrowser.contentDocument; - const pane = translationsSettingsTestUtils.getTranslationsPane(); - - ok(pane, "Translations setting pane should exist"); - ok( - translationsSettingsTestUtils.getBackButton(), - "Translations subpage should include a back button" - ); - - await translationsSettingsTestUtils.clickBackButton(); - - is( - document.location.hash, - "#general", - "Hash should return to the General pane after clicking back" - ); - ok(pane?.hidden, "Translations pane should hide after navigating back"); - - const manageButton = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton on main page" - ); - ok( - BrowserTestUtils.isVisible(manageButton), - "Main page translations section should be visible" - ); - - await cleanup(); -}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_default.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_default.js @@ -1,104 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - https://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -/** - * Tests that the "More translation settings" button in about:preferences - * properly navigates to the translations subpage and that all default-state - * elements are visible. - */ -add_task( - async function test_translations_subpage_button_and_default_elements() { - const { cleanup } = await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [["browser.settings-redesign.enabled", true]], - }); - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Waiting for the translationsManageButton to be available"); - const button = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton to be visible" - ); - - await ensureVisibility({ - message: "translationsManageButton should be visible", - visible: { button }, - }); - - info("Scrolling button into view"); - button.scrollIntoView({ behavior: "instant", block: "center" }); - - info("Clicking button to navigate to translations subpage"); - click(button, "Navigating to translations subpage"); - - info("Waiting for default-state element in Always Translate section"); - const alwaysTranslateDefaultRow = await waitForCondition( - () => - document.getElementById("translationsAlwaysTranslateLanguagesNoneRow"), - "Waiting for translationsAlwaysTranslateLanguagesNoneRow to be visible" - ); - - await ensureVisibility({ - message: "Always Translate default row should be visible", - visible: { alwaysTranslateDefaultRow }, - }); - is( - alwaysTranslateDefaultRow.getAttribute("data-l10n-id"), - "settings-translations-subpage-no-languages-added", - "Always Translate default row has correct l10n ID" - ); - - info("Waiting for default-state element in Never Translate section"); - const neverTranslateDefaultRow = await waitForCondition( - () => - document.getElementById("translationsNeverTranslateLanguagesNoneRow"), - "Waiting for translationsNeverTranslateLanguagesNoneRow to be visible" - ); - - await ensureVisibility({ - message: "Never Translate default row should be visible", - visible: { neverTranslateDefaultRow }, - }); - is( - neverTranslateDefaultRow.getAttribute("data-l10n-id"), - "settings-translations-subpage-no-languages-added", - "Never Translate default row has correct l10n ID" - ); - - info("Waiting for default-state element in Never Translate Sites section"); - const neverTranslateSitesDefaultRow = await waitForCondition( - () => document.getElementById("translationsNeverTranslateSitesNoneRow"), - "Waiting for translationsNeverTranslateSitesNoneRow to be visible" - ); - - await ensureVisibility({ - message: "Never Translate Sites default row should be visible", - visible: { neverTranslateSitesDefaultRow }, - }); - is( - neverTranslateSitesDefaultRow.getAttribute("data-l10n-id"), - "settings-translations-subpage-no-sites-added", - "Never Translate Sites default row has correct l10n ID" - ); - - info("Waiting for default-state element in Download Languages section"); - const downloadLanguagesDefaultRow = await waitForCondition( - () => document.getElementById("translationsDownloadLanguagesNoneRow"), - "Waiting for translationsDownloadLanguagesNoneRow to be visible" - ); - - await ensureVisibility({ - message: "Download Languages default row should be visible", - visible: { downloadLanguagesDefaultRow }, - }); - is( - downloadLanguagesDefaultRow.getAttribute("data-l10n-id"), - "settings-translations-subpage-no-languages-downloaded", - "Download Languages default row has correct l10n ID" - ); - - await cleanup(); - } -); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_download_langs_basic.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_download_langs_basic.js @@ -1,206 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - https://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -requestLongerTimeout(2); - -add_task(async function test_download_languages_basic_flow() { - const { cleanup, remoteClients, translationsSettingsTestUtils } = - await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [["browser.settings-redesign.enabled", true]], - }); - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Waiting for translationsManageButton"); - const manageButton = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton" - ); - - manageButton.scrollIntoView({ behavior: "instant", block: "center" }); - - info("Opening translations subpage"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [[TranslationsSettingsTestUtils.Events.Initialized]], - }, - async () => { - click(manageButton, "Open translations subpage"); - } - ); - - const downloadSelect = - translationsSettingsTestUtils.getDownloadedLanguagesSelect(); - const downloadButton = - translationsSettingsTestUtils.getDownloadLanguageButton(); - - ok(downloadSelect, "Download languages select should exist"); - ok(downloadButton, "Download languages button should exist"); - - await translationsSettingsTestUtils.assertDownloadedLanguagesEmptyState({ - visible: true, - }); - ok( - downloadButton.disabled, - "Download button disabled before selecting a language" - ); - - const frenchOption = downloadSelect.querySelector('moz-option[value="fr"]'); - ok(frenchOption, "French option should be available"); - ok(!frenchOption.disabled, "French option should start enabled"); - - info("Select French to enable download button"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.DownloadLanguageButtonEnabled], - ], - }, - async () => { - await translationsSettingsTestUtils.selectDownloadLanguage("fr"); - } - ); - ok(!downloadButton.disabled, "Download button enabled after selecting"); - - const expectedModelDownloads = languageModelNames([ - { fromLang: "fr", toLang: "en" }, - { fromLang: "en", toLang: "fr" }, - ]); - - info("Download French language models"); - const downloadButtonDisabled = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadLanguageButtonDisabled - ); - const downloadStarted = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadStarted, - { expectedDetail: { langTag: "fr" } } - ); - const renderDownloading = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, - { expectedDetail: { languages: ["fr"], count: 1, downloading: ["fr"] } } - ); - const optionsDuringDownload = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadedLanguagesSelectOptionsUpdated - ); - - await click(downloadButton, "Start French download"); - await Promise.all([ - downloadButtonDisabled, - downloadStarted, - renderDownloading, - optionsDuringDownload, - ]); - - const spinnerButton = - translationsSettingsTestUtils.getDownloadRemoveButton("fr"); - ok(spinnerButton, "Spinner button should be present while downloading"); - is( - spinnerButton.getAttribute("type"), - "icon ghost", - "Spinner button should use ghost styling while downloading" - ); - - const downloadCompleted = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadCompleted, - { expectedDetail: { langTag: "fr" } } - ); - const renderDownloaded = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, - { expectedDetail: { languages: ["fr"], count: 1, downloading: [] } } - ); - const optionsAfterDownload = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadedLanguagesSelectOptionsUpdated - ); - Assert.deepEqual( - await remoteClients.translationModels.resolvePendingDownloads( - expectedModelDownloads.length - ), - expectedModelDownloads, - "French models were downloaded." - ); - await Promise.all([ - downloadCompleted, - renderDownloaded, - optionsAfterDownload, - ]); - - await translationsSettingsTestUtils.assertDownloadedLanguages({ - languages: ["fr"], - downloading: [], - count: 1, - }); - const removeButton = - translationsSettingsTestUtils.getDownloadRemoveButton("fr"); - ok(removeButton, "Delete icon should be present after download completes"); - is( - removeButton.getAttribute("type"), - "icon", - "Delete icon should not use ghost styling after download completes" - ); - ok(frenchOption.disabled, "French option disabled after download"); - - info("Open delete confirmation then cancel"); - await translationsSettingsTestUtils.openDownloadDeleteConfirmation("fr"); - const warningButton = - translationsSettingsTestUtils.getDownloadWarningButton("fr"); - ok(warningButton, "Warning icon should be shown during delete confirmation"); - ok( - warningButton.getAttribute("iconsrc")?.includes("warning"), - "Warning icon should use warning asset" - ); - is( - warningButton.getAttribute("type"), - "icon ghost", - "Warning icon should use ghost styling during delete confirmation" - ); - const cancelRender = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, - { expectedDetail: { languages: ["fr"], count: 1, downloading: [] } } - ); - await translationsSettingsTestUtils.cancelDownloadDelete("fr"); - await cancelRender; - - await translationsSettingsTestUtils.assertDownloadedLanguages({ - languages: ["fr"], - downloading: [], - count: 1, - }); - const deleteIcon = - translationsSettingsTestUtils.getDownloadRemoveButton("fr"); - ok(deleteIcon, "Delete icon should return after cancel"); - ok( - deleteIcon.getAttribute("iconsrc")?.includes("delete"), - "Delete icon should use delete asset" - ); - is( - deleteIcon.getAttribute("type"), - "icon", - "Delete icon should not use ghost styling after canceling delete" - ); - - info("Confirm deletion after second attempt"); - const deleted = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadDeleted, - { expectedDetail: { langTag: "fr" } } - ); - const renderAfterDelete = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, - { expectedDetail: { languages: [], count: 0, downloading: [] } } - ); - const optionsAfterDelete = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadedLanguagesSelectOptionsUpdated - ); - - await translationsSettingsTestUtils.openDownloadDeleteConfirmation("fr"); - await translationsSettingsTestUtils.confirmDownloadDelete("fr"); - await Promise.all([deleted, renderAfterDelete, optionsAfterDelete]); - - await translationsSettingsTestUtils.assertDownloadedLanguagesEmptyState({ - visible: true, - }); - ok(!frenchOption.disabled, "French option re-enabled after removal"); - - await cleanup(); -}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_download_langs_delete_confirmation.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_download_langs_delete_confirmation.js @@ -1,180 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - https://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -add_task( - async function test_delete_confirmation_replaced_by_new_confirmation() { - const { cleanup, remoteClients, translationsSettingsTestUtils } = - await TranslationsSettingsTestUtils.openTranslationsSettingsSubpage(); - - await translationsSettingsTestUtils.assertDownloadedLanguagesEmptyState({ - visible: true, - }); - - info("Download French"); - await translationsSettingsTestUtils.downloadLanguage({ - langTag: "fr", - remoteClients, - inProgressLanguages: ["fr"], - finalLanguages: ["fr"], - }); - - info("Download Spanish"); - await translationsSettingsTestUtils.downloadLanguage({ - langTag: "es", - remoteClients, - inProgressLanguages: ["fr", "es"], - finalLanguages: ["fr", "es"], - }); - - await translationsSettingsTestUtils.openDownloadDeleteConfirmation("fr"); - ok( - translationsSettingsTestUtils.getDownloadDeleteConfirmButton("fr"), - "French delete confirmation should be shown" - ); - - await translationsSettingsTestUtils.openDownloadDeleteConfirmation("es"); - ok( - translationsSettingsTestUtils.getDownloadDeleteConfirmButton("es"), - "Spanish delete confirmation should be shown" - ); - ok( - !translationsSettingsTestUtils.getDownloadDeleteConfirmButton("fr"), - "French delete confirmation should close when Spanish opens" - ); - ok( - translationsSettingsTestUtils.getDownloadRemoveButton("fr"), - "French delete icon should return after switching confirmations" - ); - - await cleanup(); - } -); - -add_task(async function test_delete_confirmation_closes_when_download_starts() { - const { cleanup, remoteClients, translationsSettingsTestUtils } = - await TranslationsSettingsTestUtils.openTranslationsSettingsSubpage(); - - await translationsSettingsTestUtils.downloadLanguage({ - langTag: "fr", - remoteClients, - inProgressLanguages: ["fr"], - finalLanguages: ["fr"], - }); - - await translationsSettingsTestUtils.openDownloadDeleteConfirmation("fr"); - ok( - translationsSettingsTestUtils.getDownloadDeleteConfirmButton("fr"), - "French delete confirmation should be open before starting new download" - ); - - const renderInProgress = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, - { - expectedDetail: { - languages: ["fr", "es"], - count: 2, - downloading: ["es"], - }, - } - ); - const optionsUpdated = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadedLanguagesSelectOptionsUpdated - ); - const started = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadStarted, - { expectedDetail: { langTag: "es" } } - ); - - await translationsSettingsTestUtils.selectDownloadLanguage("es"); - await click( - translationsSettingsTestUtils.getDownloadLanguageButton(), - "Start Spanish download" - ); - await Promise.all([renderInProgress, optionsUpdated, started]); - - ok( - !translationsSettingsTestUtils.getDownloadDeleteConfirmButton("fr"), - "Delete confirmation should close when download begins" - ); - const frenchRemoveButton = - translationsSettingsTestUtils.getDownloadRemoveButton("fr"); - ok( - frenchRemoveButton?.disabled, - "Other delete buttons should be disabled during download" - ); - - const completed = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadCompleted, - { expectedDetail: { langTag: "es" } } - ); - const renderComplete = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, - { - expectedDetail: { - languages: ["fr", "es"], - count: 2, - downloading: [], - }, - } - ); - const optionsAfter = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadedLanguagesSelectOptionsUpdated - ); - - await remoteClients.translationModels.resolvePendingDownloads( - TranslationsSettingsTestUtils.getLanguageModelNames("es").length - ); - await Promise.all([completed, renderComplete, optionsAfter]); - - await cleanup(); -}); - -add_task( - async function test_failed_download_closes_when_delete_confirmation_opens() { - const { cleanup, remoteClients, translationsSettingsTestUtils } = - await TranslationsSettingsTestUtils.openTranslationsSettingsSubpage(); - - await translationsSettingsTestUtils.downloadLanguage({ - langTag: "es", - remoteClients, - inProgressLanguages: ["es"], - finalLanguages: ["es"], - }); - - info("Start French download expecting failure"); - await translationsSettingsTestUtils.startDownloadFailure({ - langTag: "fr", - remoteClients, - inProgressLanguages: ["fr", "es"], - failedLanguages: ["fr", "es"], - }); - - ok( - translationsSettingsTestUtils.getDownloadErrorButton("fr"), - "French error should be visible before opening delete confirmation" - ); - - await translationsSettingsTestUtils.openDownloadDeleteConfirmation("es"); - ok( - translationsSettingsTestUtils.getDownloadDeleteConfirmButton("es"), - "Spanish delete confirmation should be shown" - ); - ok( - !translationsSettingsTestUtils.getDownloadErrorButton("fr"), - "French failure state should close when delete confirmation opens" - ); - ok( - !translationsSettingsTestUtils.getDownloadRetryButton("fr"), - "Retry button should close with error state" - ); - await translationsSettingsTestUtils.assertDownloadedLanguages({ - languages: ["es"], - downloading: [], - count: 1, - }); - - await cleanup(); - } -); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_download_langs_errors.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_download_langs_errors.js @@ -1,204 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - https://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -requestLongerTimeout(2); - -add_task( - async function test_download_error_retry_via_selector_and_main_button() { - const { cleanup, remoteClients, translationsSettingsTestUtils } = - await TranslationsSettingsTestUtils.openTranslationsSettingsSubpage(); - - const document = gBrowser.selectedBrowser.contentDocument; - const downloadButton = - translationsSettingsTestUtils.getDownloadLanguageButton(); - - await translationsSettingsTestUtils.assertDownloadedLanguagesEmptyState({ - visible: true, - }); - - await translationsSettingsTestUtils.startDownloadFailure({ - langTag: "fr", - remoteClients, - }); - - const errorButton = - translationsSettingsTestUtils.getDownloadErrorButton("fr"); - const retryButton = - translationsSettingsTestUtils.getDownloadRetryButton("fr"); - ok(errorButton, "Error icon should be visible"); - ok(retryButton, "Retry button should be visible"); - is( - errorButton.getAttribute("type"), - "icon ghost", - "Error icon should use ghost styling" - ); - const errorMessage = getByL10nId( - "settings-translations-subpage-download-error", - document - ); - ok(errorMessage, "Error message should be shown"); - is( - translationsSettingsTestUtils.getSelectedDownloadLanguage(), - "fr", - "French should stay selected after failed download" - ); - ok( - !downloadButton.disabled, - "Download button should stay enabled after failed download" - ); - - info("Retry French download via main button"); - await translationsSettingsTestUtils.selectDownloadLanguage("fr"); - await click(downloadButton, "Retry French download via main button"); - - const modelNames = - TranslationsSettingsTestUtils.getLanguageModelNames("fr"); - await remoteClients.translationModels.resolvePendingDownloads( - modelNames.length - ); - - info("Waiting for French retry to finish"); - await waitForCondition( - () => - translationsSettingsTestUtils.getDownloadRemoveButton("fr") && - !translationsSettingsTestUtils.getDownloadRetryButton("fr"), - "Waiting for French download to succeed after retry" - ); - - await translationsSettingsTestUtils.assertDownloadedLanguages({ - languages: ["fr"], - downloading: [], - count: 1, - }); - is( - translationsSettingsTestUtils.getSelectedDownloadLanguage(), - "", - "Download selection should reset after successful retry" - ); - await translationsSettingsTestUtils.assertDownloadedLanguagesEmptyState({ - visible: false, - }); - - await cleanup(); - } -); - -add_task(async function test_download_error_retry_via_retry_button() { - const { cleanup, remoteClients, translationsSettingsTestUtils } = - await TranslationsSettingsTestUtils.openTranslationsSettingsSubpage(); - - const document = gBrowser.selectedBrowser.contentDocument; - const downloadButton = - translationsSettingsTestUtils.getDownloadLanguageButton(); - - await translationsSettingsTestUtils.assertDownloadedLanguagesEmptyState({ - visible: true, - }); - - await translationsSettingsTestUtils.startDownloadFailure({ - langTag: "es", - remoteClients, - }); - - const errorButton = - translationsSettingsTestUtils.getDownloadErrorButton("es"); - const retryButton = - translationsSettingsTestUtils.getDownloadRetryButton("es"); - ok(errorButton, "Error icon should be visible"); - ok(retryButton, "Retry button should be visible"); - is( - errorButton.getAttribute("type"), - "icon ghost", - "Error icon should use ghost styling" - ); - const errorMessage = getByL10nId( - "settings-translations-subpage-download-error", - document - ); - ok(errorMessage, "Error message should be shown"); - const errorRow = - await translationsSettingsTestUtils.waitForDownloadedLanguageItem("es"); - ok( - errorRow.classList.contains("translations-download-language-error"), - "Error row should have the error class" - ); - is( - translationsSettingsTestUtils.getSelectedDownloadLanguage(), - "es", - "Spanish should stay selected after failed download" - ); - ok( - !downloadButton.disabled, - "Download button should stay enabled after failed download" - ); - - info("Retry Spanish download via inline retry button"); - await translationsSettingsTestUtils.clickDownloadRetry("es"); - - const modelNames = TranslationsSettingsTestUtils.getLanguageModelNames("es"); - await remoteClients.translationModels.resolvePendingDownloads( - modelNames.length - ); - - info("Waiting for Spanish retry to finish"); - await waitForCondition( - () => - translationsSettingsTestUtils.getDownloadRemoveButton("es") && - !translationsSettingsTestUtils.getDownloadRetryButton("es"), - "Waiting for Spanish download to succeed after retry button" - ); - - await translationsSettingsTestUtils.assertDownloadedLanguages({ - languages: ["es"], - downloading: [], - count: 1, - }); - is( - translationsSettingsTestUtils.getSelectedDownloadLanguage(), - "", - "Download selection should reset after retry success" - ); - await translationsSettingsTestUtils.assertDownloadedLanguagesEmptyState({ - visible: false, - }); - - await cleanup(); -}); - -add_task(async function test_download_delete_cancel_restores_state() { - const { cleanup, remoteClients, translationsSettingsTestUtils } = - await TranslationsSettingsTestUtils.openTranslationsSettingsSubpage(); - - const downloadButton = - translationsSettingsTestUtils.getDownloadLanguageButton(); - - await translationsSettingsTestUtils.selectDownloadLanguage("uk"); - info("Download Ukrainian"); - await click(downloadButton, "Download Ukrainian"); - - const modelNames = TranslationsSettingsTestUtils.getLanguageModelNames("uk"); - await remoteClients.translationModels.resolvePendingDownloads( - modelNames.length - ); - info("Waiting for Ukrainian download to complete"); - await waitForCondition( - () => - translationsSettingsTestUtils.getDownloadRemoveButton("uk") && - !translationsSettingsTestUtils.getDownloadRetryButton("uk"), - "Waiting for Ukrainian download to complete" - ); - - await translationsSettingsTestUtils.openDownloadDeleteConfirmation("uk"); - info("Cancel delete for Ukrainian"); - await translationsSettingsTestUtils.cancelDownloadDelete("uk"); - - await translationsSettingsTestUtils.assertDownloadedLanguages({ - languages: ["uk"], - downloading: [], - count: 1, - }); - - await cleanup(); -}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_download_langs_reset_states.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_download_langs_reset_states.js @@ -1,152 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - https://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -add_task(async function test_other_actions_disable_during_active_download() { - const { cleanup, remoteClients, translationsSettingsTestUtils } = - await TranslationsSettingsTestUtils.openTranslationsSettingsSubpage(); - - await translationsSettingsTestUtils.downloadLanguage({ - langTag: "fr", - remoteClients, - inProgressLanguages: ["fr"], - finalLanguages: ["fr"], - }); - - const started = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadStarted, - { expectedDetail: { langTag: "es" } } - ); - const renderInProgress = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, - { - expectedDetail: { - languages: ["fr", "es"], - count: 2, - downloading: ["es"], - }, - } - ); - const optionsUpdated = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadedLanguagesSelectOptionsUpdated - ); - - await translationsSettingsTestUtils.selectDownloadLanguage("es"); - await click( - translationsSettingsTestUtils.getDownloadLanguageButton(), - "Start Spanish download while French exists" - ); - await Promise.all([started, renderInProgress, optionsUpdated]); - - ok( - translationsSettingsTestUtils.getDownloadLanguageButton().disabled, - "Download button should be disabled during active download" - ); - ok( - translationsSettingsTestUtils.getDownloadedLanguagesSelect().disabled, - "Download select should be disabled during active download" - ); - - const frenchRemoveButton = - translationsSettingsTestUtils.getDownloadRemoveButton("fr"); - ok( - frenchRemoveButton?.disabled, - "Other delete buttons should stay disabled during download" - ); - ok( - !translationsSettingsTestUtils.getDownloadDeleteConfirmButton("fr"), - "Delete confirmation should not open while download in progress" - ); - - const completed = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadCompleted, - { expectedDetail: { langTag: "es" } } - ); - const renderComplete = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, - { - expectedDetail: { - languages: ["fr", "es"], - count: 2, - downloading: [], - }, - } - ); - const optionsAfter = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadedLanguagesSelectOptionsUpdated - ); - - await remoteClients.translationModels.resolvePendingDownloads( - TranslationsSettingsTestUtils.getLanguageModelNames("es").length - ); - await Promise.all([completed, renderComplete, optionsAfter]); - - await cleanup(); -}); - -add_task(async function test_states_reset_after_reload() { - const { cleanup, remoteClients, translationsSettingsTestUtils } = - await TranslationsSettingsTestUtils.openTranslationsSettingsSubpage(); - - await translationsSettingsTestUtils.downloadLanguage({ - langTag: "es", - remoteClients, - inProgressLanguages: ["es"], - finalLanguages: ["es"], - }); - - info("Trigger French download failure before reloading"); - await translationsSettingsTestUtils.startDownloadFailure({ - langTag: "fr", - remoteClients, - inProgressLanguages: ["fr", "es"], - failedLanguages: ["fr", "es"], - }); - - ok( - translationsSettingsTestUtils.getDownloadErrorButton("fr"), - "French error should be visible before opening delete confirmation" - ); - - await translationsSettingsTestUtils.openDownloadDeleteConfirmation("es"); - ok( - translationsSettingsTestUtils.getDownloadDeleteConfirmButton("es"), - "Spanish delete confirmation should be open before reload" - ); - ok( - !translationsSettingsTestUtils.getDownloadErrorButton("fr"), - "French error should close when another delete confirmation opens" - ); - - info("Reload about:preferences"); - await loadNewPage(gBrowser.selectedBrowser, "about:preferences"); - - const reloadedTestUtils = new TranslationsSettingsTestUtils( - gBrowser.selectedBrowser.contentDocument - ); - - await reloadedTestUtils.openTranslationsSubpageFromDocument(); - await reloadedTestUtils.assertDownloadedLanguagesEmptyState({ - visible: true, - }); - is( - reloadedTestUtils.getSelectedDownloadLanguage(), - "", - "Download selection should reset after reload" - ); - ok( - !reloadedTestUtils.getDownloadDeleteConfirmButton("es"), - "Delete confirmation should reset after reload" - ); - ok( - !reloadedTestUtils.getDownloadErrorButton("fr"), - "Failed download state should reset after reload" - ); - ok( - !reloadedTestUtils.getDownloadRetryButton("fr"), - "Retry button should not persist after reload" - ); - - await cleanup(); -}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_download_langs_sorting.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_download_langs_sorting.js @@ -1,133 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - https://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -add_task(async function test_download_languages_sorting_and_batch_resolution() { - const { cleanup, remoteClients, translationsSettingsTestUtils } = - await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [["browser.settings-redesign.enabled", true]], - }); - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Waiting for translationsManageButton"); - const manageButton = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton" - ); - manageButton.scrollIntoView({ behavior: "instant", block: "center" }); - - const initialDownloadsRendered = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, - { - expectedDetail: { languages: [], count: 0, downloading: [] }, - } - ); - - info("Opening translations subpage"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [[TranslationsSettingsTestUtils.Events.Initialized]], - }, - async () => { - click(manageButton, "Open translations subpage"); - } - ); - await initialDownloadsRendered; - - const downloadButton = - translationsSettingsTestUtils.getDownloadLanguageButton(); - - info("Verify empty state before downloads"); - await translationsSettingsTestUtils.assertDownloadedLanguagesEmptyState({ - visible: true, - }); - - async function downloadAndResolve(langTag, inProgressLangs, finalOrder) { - await translationsSettingsTestUtils.selectDownloadLanguage(langTag); - - const started = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadStarted, - { expectedDetail: { langTag } } - ); - const renderInProgress = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, - { - expectedDetail: { - languages: inProgressLangs, - count: inProgressLangs.length, - downloading: [langTag], - }, - } - ); - const optionsUpdated = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events - .DownloadedLanguagesSelectOptionsUpdated - ); - - const completed = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadCompleted, - { expectedDetail: { langTag } } - ); - const renderComplete = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, - { - expectedDetail: { - languages: finalOrder, - count: finalOrder.length, - downloading: [], - }, - } - ); - const optionsUpdatedAfter = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events - .DownloadedLanguagesSelectOptionsUpdated - ); - - await click(downloadButton, `Start ${langTag} download`); - await Promise.all([started, renderInProgress, optionsUpdated]); - - const modelNames = languageModelNames([ - { fromLang: langTag, toLang: "en" }, - { fromLang: "en", toLang: langTag }, - ]); - await remoteClients.translationModels.resolvePendingDownloads( - modelNames.length - ); - - await Promise.all([completed, renderComplete, optionsUpdatedAfter]); - } - - info("Download French first"); - await downloadAndResolve("fr", ["fr"], ["fr"]); - await translationsSettingsTestUtils.assertDownloadedLanguagesEmptyState({ - visible: false, - }); - await translationsSettingsTestUtils.assertDownloadedLanguagesOrder({ - languages: ["fr"], - }); - - info("Download Ukrainian second"); - await downloadAndResolve("uk", ["fr", "uk"], ["fr", "uk"]); - await translationsSettingsTestUtils.assertDownloadedLanguagesOrder({ - languages: ["fr", "uk"], - }); - - info("Download Spanish; expect it to sort between French and Ukrainian"); - await downloadAndResolve("es", ["fr", "es", "uk"], ["fr", "es", "uk"]); - - await translationsSettingsTestUtils.assertDownloadedLanguages({ - languages: ["es", "fr", "uk"], - downloading: [], - count: 3, - }); - await translationsSettingsTestUtils.assertDownloadedLanguagesOrder({ - languages: ["fr", "es", "uk"], - }); - await translationsSettingsTestUtils.assertDownloadedLanguagesEmptyState({ - visible: false, - }); - - await cleanup(); -}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_never_translate_langs_a11y.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_never_translate_langs_a11y.js @@ -1,175 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - https://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -/** - * Tests keyboard activation (Enter key) of remove buttons in the never-translate language list. - */ -add_task(async function test_never_translate_languages_keyboard_activation() { - const { cleanup, translationsSettingsTestUtils } = - await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [ - ["browser.settings-redesign.enabled", true], - [NEVER_TRANSLATE_LANGS_PREF, "es,fr,uk"], - ], - }); - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Navigating to translations subpage"); - const manageButton = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton" - ); - - info("Scrolling button into view"); - manageButton.scrollIntoView({ behavior: "instant", block: "center" }); - - info("Clicking manage button and waiting for initialization"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [[TranslationsSettingsTestUtils.Events.Initialized]], - }, - async () => { - click( - manageButton, - "Clicking manage button to open translations subpage" - ); - } - ); - - info("Verifying three language items are rendered"); - await translationsSettingsTestUtils.assertNeverTranslateLanguages({ - languages: ["es", "fr", "uk"], - count: 3, - }); - - info("Getting first remove button"); - const firstRemoveButton = document.querySelector( - ".translations-never-translate-remove-button" - ); - ok(firstRemoveButton, "First remove button should exist"); - - info("Testing keyboard activation with Enter key"); - firstRemoveButton.focus(); - is( - document.activeElement, - firstRemoveButton, - "Remove button should be focused" - ); - - info("Pressing Enter to remove first language"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.NeverTranslateLanguagesRendered], - ], - }, - async () => { - const prefChanged = TestUtils.waitForPrefChange( - NEVER_TRANSLATE_LANGS_PREF - ); - const enterEvent = new KeyboardEvent("keydown", { - key: "Enter", - bubbles: true, - }); - firstRemoveButton.dispatchEvent(enterEvent); - click(firstRemoveButton, "Activating remove via Enter key"); - await prefChanged; - } - ); - - info("Verifying language was removed"); - const langs = getNeverTranslateLanguagesFromPref(); - is(langs.length, 2, "Should have 2 languages after removal"); - - info("Verifying UI updated to show 2 items"); - await translationsSettingsTestUtils.assertNeverTranslateLanguages({ - count: 2, - }); - - await cleanup(); -}); - -/** - * Tests accessibility features including ARIA labels and keyboard-only operation. - */ -add_task(async function test_never_translate_languages_accessibility() { - const { cleanup, translationsSettingsTestUtils } = - await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [ - ["browser.settings-redesign.enabled", true], - [NEVER_TRANSLATE_LANGS_PREF, "es,fr"], - ], - }); - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Navigating to translations subpage"); - const manageButton = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton" - ); - - info("Scrolling button into view"); - manageButton.scrollIntoView({ behavior: "instant", block: "center" }); - - info("Clicking manage button and waiting for initialization"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [[TranslationsSettingsTestUtils.Events.Initialized]], - }, - async () => { - click( - manageButton, - "Clicking manage button to open translations subpage" - ); - } - ); - - info("Verifying two language items are rendered"); - await translationsSettingsTestUtils.assertNeverTranslateLanguages({ - languages: ["es", "fr"], - count: 2, - }); - - info("Getting language items and remove buttons"); - const items = document.querySelectorAll( - ".translations-never-translate-language-item" - ); - is(items.length, 2, "Should have 2 language items"); - - info("Verifying remove buttons have accessible labels"); - const removeButtons = document.querySelectorAll( - ".translations-never-translate-remove-button" - ); - is(removeButtons.length, 2, "Should have 2 remove buttons"); - - for (const button of removeButtons) { - const ariaLabel = - button.getAttribute("aria-label") || - button.ariaLabel || - button.getAttribute("data-l10n-id"); - ok( - ariaLabel, - "Remove button should have aria-label or data-l10n-id for accessibility" - ); - if (typeof ariaLabel === "string") { - Assert.greater( - ariaLabel.length, - 0, - "Remove button accessibility label should not be empty" - ); - } - } - - info("Verifying language items have proper semantic structure"); - for (const item of items) { - const label = item.getAttribute("label"); - ok(label, "Each language item should have a label attribute"); - Assert.greater(label.length, 0, "Label should not be empty"); - } - - await cleanup(); -}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_never_translate_langs_basic.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_never_translate_langs_basic.js @@ -1,160 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - https://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -/** - * Tests that pre-populated languages load correctly when opening the subpage. - */ -add_task(async function test_never_translate_languages_prepopulated() { - const { cleanup, translationsSettingsTestUtils } = - await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [ - ["browser.settings-redesign.enabled", true], - [NEVER_TRANSLATE_LANGS_PREF, "uk,es,fr"], - ], - }); - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Navigating to translations subpage"); - const manageButton = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton" - ); - - info("Scrolling button into view"); - manageButton.scrollIntoView({ behavior: "instant", block: "center" }); - - info("Clicking manage button and waiting for initialization"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [[TranslationsSettingsTestUtils.Events.Initialized]], - }, - async () => { - click( - manageButton, - "Clicking manage button to open translations subpage" - ); - } - ); - - info("Verifying three language items are displayed"); - await translationsSettingsTestUtils.assertNeverTranslateLanguages({ - languages: ["es", "fr", "uk"], - count: 3, - }); - await translationsSettingsTestUtils.assertNeverTranslateLanguagesOrder({ - languages: ["fr", "es", "uk"], - }); - - info("Verifying empty state is not visible"); - await translationsSettingsTestUtils.assertNeverTranslateLanguagesEmptyState({ - visible: false, - }); - - await cleanup(); -}); - -/** - * Tests that the dropdown state is managed correctly - already-added languages - * should be disabled in the dropdown. - */ -add_task(async function test_never_translate_languages_dropdown_state() { - const { cleanup, translationsSettingsTestUtils } = - await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [ - ["browser.settings-redesign.enabled", true], - [NEVER_TRANSLATE_LANGS_PREF, "es,fr"], - ], - }); - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Navigating to translations subpage"); - const manageButton = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton" - ); - - info("Scrolling button into view"); - manageButton.scrollIntoView({ behavior: "instant", block: "center" }); - - info("Clicking manage button and waiting for initialization"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [[TranslationsSettingsTestUtils.Events.Initialized]], - }, - async () => { - click( - manageButton, - "Clicking manage button to open translations subpage" - ); - } - ); - - info("Verifying two language items are rendered"); - await translationsSettingsTestUtils.assertNeverTranslateLanguages({ - languages: ["es", "fr"], - count: 2, - }); - - info("Getting the dropdown element"); - const dropdown = - translationsSettingsTestUtils.getNeverTranslateLanguagesSelect(); - - info("Verifying Spanish and French options are disabled"); - const spanishOption = dropdown.querySelector('moz-option[value="es"]'); - const frenchOption = dropdown.querySelector('moz-option[value="fr"]'); - const ukrainianOption = dropdown.querySelector('moz-option[value="uk"]'); - - ok(spanishOption, "Spanish option should exist in dropdown"); - ok(frenchOption, "French option should exist in dropdown"); - ok(ukrainianOption, "Ukrainian option should exist in dropdown"); - - ok( - spanishOption.disabled, - "Spanish option should be disabled (already added)" - ); - ok(frenchOption.disabled, "French option should be disabled (already added)"); - ok(!ukrainianOption.disabled, "Ukrainian option should not be disabled"); - - info("Adding Ukrainian via dropdown"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.NeverTranslateLanguagesRendered], - [ - TranslationsSettingsTestUtils.Events - .NeverTranslateLanguagesSelectOptionsUpdated, - ], - ], - }, - async () => { - const prefChanged = TestUtils.waitForPrefChange( - NEVER_TRANSLATE_LANGS_PREF - ); - await translationsSettingsTestUtils.addNeverTranslateLanguage("uk"); - await prefChanged; - } - ); - - info("Verifying Ukrainian was added"); - await translationsSettingsTestUtils.assertNeverTranslateLanguages({ - languages: ["es", "fr", "uk"], - count: 3, - }); - - info("Verifying dropdown updated and Ukrainian is now disabled"); - ok( - ukrainianOption.disabled, - "Ukrainian option should now be disabled (just added)" - ); - - await dropdown.updateComplete; - - info("Verifying dropdown resets to placeholder"); - is(dropdown.value, "", "Dropdown should reset to empty value"); - - await cleanup(); -}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_never_translate_langs_modify.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_never_translate_langs_modify.js @@ -1,427 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - https://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -/** - * Tests basic adding and removing of languages in the Never Translate Languages - * section, including empty state transitions. - */ -add_task(async function test_never_translate_languages_add_and_remove() { - const { cleanup, translationsSettingsTestUtils } = - await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [ - ["browser.settings-redesign.enabled", true], - [NEVER_TRANSLATE_LANGS_PREF, ""], - ], - }); - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Navigating to translations subpage"); - const manageButton = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton" - ); - - info("Scrolling button into view"); - manageButton.scrollIntoView({ behavior: "instant", block: "center" }); - - info("Clicking manage button and waiting for initialization"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [[TranslationsSettingsTestUtils.Events.Initialized]], - }, - async () => { - click( - manageButton, - "Clicking manage button to open translations subpage" - ); - } - ); - - info("Verifying empty state is visible initially"); - await translationsSettingsTestUtils.assertNeverTranslateLanguagesEmptyState({ - visible: true, - }); - - info("Adding Spanish (es) via dropdown"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.NeverTranslateLanguagesRendered], - [ - TranslationsSettingsTestUtils.Events - .NeverTranslateLanguagesEmptyStateHidden, - ], - ], - }, - async () => { - const prefChanged = TestUtils.waitForPrefChange( - NEVER_TRANSLATE_LANGS_PREF - ); - await translationsSettingsTestUtils.addNeverTranslateLanguage("es"); - await prefChanged; - } - ); - - info("Verifying Spanish was added"); - await translationsSettingsTestUtils.assertNeverTranslateLanguages({ - languages: ["es"], - count: 1, - }); - await translationsSettingsTestUtils.assertNeverTranslateLanguagesEmptyState({ - visible: false, - }); - - info("Adding French (fr) via dropdown"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.NeverTranslateLanguagesRendered], - ], - }, - async () => { - const prefChanged = TestUtils.waitForPrefChange( - NEVER_TRANSLATE_LANGS_PREF - ); - await translationsSettingsTestUtils.addNeverTranslateLanguage("fr"); - await prefChanged; - } - ); - - info("Verifying French was added"); - await translationsSettingsTestUtils.assertNeverTranslateLanguages({ - languages: ["es", "fr"], - count: 2, - }); - - info("Adding Ukrainian (uk) via dropdown"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.NeverTranslateLanguagesRendered], - ], - }, - async () => { - const prefChanged = TestUtils.waitForPrefChange( - NEVER_TRANSLATE_LANGS_PREF - ); - await translationsSettingsTestUtils.addNeverTranslateLanguage("uk"); - await prefChanged; - } - ); - - info("Verifying Ukrainian was added"); - await translationsSettingsTestUtils.assertNeverTranslateLanguages({ - languages: ["es", "fr", "uk"], - count: 3, - }); - - info("Removing middle item (French)"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.NeverTranslateLanguagesRendered], - ], - }, - async () => { - const prefChanged = TestUtils.waitForPrefChange( - NEVER_TRANSLATE_LANGS_PREF - ); - await translationsSettingsTestUtils.removeNeverTranslateLanguage("fr"); - await prefChanged; - } - ); - - info("Verifying French was removed"); - await translationsSettingsTestUtils.assertNeverTranslateLanguages({ - languages: ["es", "uk"], - count: 2, - }); - await translationsSettingsTestUtils.assertNeverTranslateLanguagesEmptyState({ - visible: false, - }); - - info("Removing Ukrainian"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.NeverTranslateLanguagesRendered], - ], - }, - async () => { - const prefChanged = TestUtils.waitForPrefChange( - NEVER_TRANSLATE_LANGS_PREF - ); - await translationsSettingsTestUtils.removeNeverTranslateLanguage("uk"); - await prefChanged; - } - ); - - info("Verifying Ukrainian was removed"); - await translationsSettingsTestUtils.assertNeverTranslateLanguages({ - languages: ["es"], - count: 1, - }); - await translationsSettingsTestUtils.assertNeverTranslateLanguagesEmptyState({ - visible: false, - }); - - info("Removing Spanish (last language)"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.NeverTranslateLanguagesRendered], - [ - TranslationsSettingsTestUtils.Events - .NeverTranslateLanguagesEmptyStateShown, - ], - ], - }, - async () => { - const prefChanged = TestUtils.waitForPrefChange( - NEVER_TRANSLATE_LANGS_PREF - ); - await translationsSettingsTestUtils.removeNeverTranslateLanguage("es"); - await prefChanged; - } - ); - - info("Verifying all languages removed and empty state reappears"); - await translationsSettingsTestUtils.assertNeverTranslateLanguages({ - languages: [], - count: 0, - }); - await translationsSettingsTestUtils.assertNeverTranslateLanguagesEmptyState({ - visible: true, - }); - - await cleanup(); -}); - -/** - * Tests that invalid language tags don't break the UI and valid languages - * are still rendered correctly. - */ -add_task(async function test_never_translate_languages_invalid_tags() { - const { cleanup, translationsSettingsTestUtils } = - await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [ - ["browser.settings-redesign.enabled", true], - [NEVER_TRANSLATE_LANGS_PREF, "es,fr,uk"], - ], - }); - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Navigating to translations subpage"); - const manageButton = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton" - ); - - info("Scrolling button into view"); - manageButton.scrollIntoView({ behavior: "instant", block: "center" }); - - info("Clicking manage button and waiting for initialization"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [[TranslationsSettingsTestUtils.Events.Initialized]], - }, - async () => { - click( - manageButton, - "Clicking manage button to open translations subpage" - ); - } - ); - - info("Verifying three valid languages are displayed"); - await translationsSettingsTestUtils.assertNeverTranslateLanguages({ - languages: ["es", "fr", "uk"], - count: 3, - }); - - info("Testing UI doesn't break when pref has mixed valid/invalid tags"); - info("Adding invalid tags via pref change"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.NeverTranslateLanguagesRendered], - ], - }, - async () => { - Services.prefs.setCharPref( - NEVER_TRANSLATE_LANGS_PREF, - "es,INVALID,fr,uk" - ); - } - ); - - info("Verifying UI still works and valid languages remain accessible"); - ok( - document.querySelector('[data-lang-tag="es"]'), - "Spanish item should still exist" - ); - ok( - document.querySelector('[data-lang-tag="fr"]'), - "French item should still exist" - ); - ok( - document.querySelector('[data-lang-tag="uk"]'), - "Ukrainian item should still exist" - ); - - await cleanup(); -}); - -/** - * Tests that adding a language to never-translate automatically removes it - * from the always-translate list ("stealing" behavior). - */ -add_task(async function test_never_translate_languages_stealing() { - const { cleanup, translationsSettingsTestUtils } = - await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [ - ["browser.settings-redesign.enabled", true], - [ALWAYS_TRANSLATE_LANGS_PREF, "es,fr,uk"], - [NEVER_TRANSLATE_LANGS_PREF, ""], - ], - }); - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Navigating to translations subpage"); - const manageButton = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton" - ); - - info("Scrolling button into view"); - manageButton.scrollIntoView({ behavior: "instant", block: "center" }); - - info("Clicking manage button and waiting for initialization"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [[TranslationsSettingsTestUtils.Events.Initialized]], - }, - async () => { - click( - manageButton, - "Clicking manage button to open translations subpage" - ); - } - ); - - info("Verifying never-translate section is empty"); - await translationsSettingsTestUtils.assertNeverTranslateLanguagesEmptyState({ - visible: true, - }); - - info("Verifying always-translate has three languages"); - let alwaysLangs = getAlwaysTranslateLanguagesFromPref(); - is(alwaysLangs.length, 3, "Should have 3 always-translate languages"); - ok( - alwaysLangs.includes("es") && - alwaysLangs.includes("fr") && - alwaysLangs.includes("uk"), - "Always-translate should include es, fr, uk" - ); - - info("Adding Spanish to never-translate via UI"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.NeverTranslateLanguagesRendered], - [ - TranslationsSettingsTestUtils.Events - .NeverTranslateLanguagesEmptyStateHidden, - ], - ], - }, - async () => { - const neverPrefChanged = TestUtils.waitForPrefChange( - NEVER_TRANSLATE_LANGS_PREF - ); - await translationsSettingsTestUtils.addNeverTranslateLanguage("es"); - await neverPrefChanged; - } - ); - - info("Verifying Spanish appears in never-translate list"); - await translationsSettingsTestUtils.assertNeverTranslateLanguages({ - languages: ["es"], - count: 1, - }); - - info("Verifying Spanish was removed from always-translate pref"); - alwaysLangs = getAlwaysTranslateLanguagesFromPref(); - is(alwaysLangs.length, 2, "Should have 2 always-translate languages"); - ok( - !alwaysLangs.includes("es"), - "Always-translate should not include Spanish" - ); - ok( - alwaysLangs.includes("fr") && alwaysLangs.includes("uk"), - "Always-translate should still include French and Ukrainian" - ); - - info("Adding French to never-translate via UI"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.NeverTranslateLanguagesRendered], - ], - }, - async () => { - const neverPrefChanged = TestUtils.waitForPrefChange( - NEVER_TRANSLATE_LANGS_PREF - ); - await translationsSettingsTestUtils.addNeverTranslateLanguage("fr"); - await neverPrefChanged; - } - ); - - info("Verifying both Spanish and French in never-translate list"); - await translationsSettingsTestUtils.assertNeverTranslateLanguages({ - languages: ["es", "fr"], - count: 2, - }); - - info("Verifying only Ukrainian remains in always-translate pref"); - alwaysLangs = getAlwaysTranslateLanguagesFromPref(); - is(alwaysLangs.length, 1, "Should have 1 always-translate language"); - ok( - alwaysLangs.includes("uk"), - "Always-translate should only include Ukrainian" - ); - - info("Verifying stealing also works via UI for Ukrainian"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.NeverTranslateLanguagesRendered], - ], - }, - async () => { - const neverPrefChanged = TestUtils.waitForPrefChange( - NEVER_TRANSLATE_LANGS_PREF - ); - await translationsSettingsTestUtils.addNeverTranslateLanguage("uk"); - await neverPrefChanged; - } - ); - - info("Verifying all three languages now in never-translate"); - await translationsSettingsTestUtils.assertNeverTranslateLanguages({ - languages: ["es", "fr", "uk"], - count: 3, - }); - - info("Verifying always-translate is empty after stealing"); - alwaysLangs = getAlwaysTranslateLanguagesFromPref(); - is(alwaysLangs.length, 0, "Always-translate should be empty after stealing"); - - await cleanup(); -}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_never_translate_langs_observe.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_never_translate_langs_observe.js @@ -1,245 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - https://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -/** - * Tests that the UI reactively updates when prefs are changed externally - * while the subpage is open. - */ -add_task(async function test_never_translate_languages_observe_pref_changes() { - const { cleanup, translationsSettingsTestUtils } = - await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [ - ["browser.settings-redesign.enabled", true], - [NEVER_TRANSLATE_LANGS_PREF, ""], - ], - }); - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Navigating to translations subpage"); - const manageButton = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton" - ); - - info("Scrolling button into view"); - manageButton.scrollIntoView({ behavior: "instant", block: "center" }); - - info("Clicking manage button and waiting for initialization"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [[TranslationsSettingsTestUtils.Events.Initialized]], - }, - async () => { - click( - manageButton, - "Clicking manage button to open translations subpage" - ); - } - ); - - info("Verifying empty state initially visible"); - await translationsSettingsTestUtils.assertNeverTranslateLanguagesEmptyState({ - visible: true, - }); - - info("Adding Spanish (es) via pref directly"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.NeverTranslateLanguagesRendered], - [ - TranslationsSettingsTestUtils.Events - .NeverTranslateLanguagesEmptyStateHidden, - ], - ], - }, - async () => { - Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, "es"); - } - ); - - info("Verifying Spanish was added"); - await translationsSettingsTestUtils.assertNeverTranslateLanguages({ - languages: ["es"], - count: 1, - }); - - info("Adding more languages via pref (es,fr,uk)"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.NeverTranslateLanguagesRendered], - ], - }, - async () => { - Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, "es,fr,uk"); - } - ); - - info("Verifying all three languages are displayed"); - await translationsSettingsTestUtils.assertNeverTranslateLanguages({ - languages: ["es", "fr", "uk"], - count: 3, - }); - - info("Removing French via pref (es,uk)"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.NeverTranslateLanguagesRendered], - ], - }, - async () => { - Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, "es,uk"); - } - ); - - info("Verifying French was removed"); - await translationsSettingsTestUtils.assertNeverTranslateLanguages({ - languages: ["es", "uk"], - count: 2, - }); - - info("Clearing all languages via pref"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.NeverTranslateLanguagesRendered], - [ - TranslationsSettingsTestUtils.Events - .NeverTranslateLanguagesEmptyStateShown, - ], - ], - }, - async () => { - Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, ""); - } - ); - - info("Verifying all languages removed and empty state returns"); - await translationsSettingsTestUtils.assertNeverTranslateLanguages({ - languages: [], - count: 0, - }); - await translationsSettingsTestUtils.assertNeverTranslateLanguagesEmptyState({ - visible: true, - }); - - await cleanup(); -}); - -/** - * Tests that both UI lists update correctly when simulating a stealing scenario - * by manually adding a language to one pref and removing it from the other. - */ -add_task(async function test_never_translate_languages_simulated_stealing() { - const { cleanup, translationsSettingsTestUtils } = - await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [ - ["browser.settings-redesign.enabled", true], - [ALWAYS_TRANSLATE_LANGS_PREF, "es,fr"], - [NEVER_TRANSLATE_LANGS_PREF, ""], - ], - }); - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Navigating to translations subpage"); - const manageButton = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton" - ); - - info("Scrolling button into view"); - manageButton.scrollIntoView({ behavior: "instant", block: "center" }); - - info("Clicking manage button and waiting for initialization"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [[TranslationsSettingsTestUtils.Events.Initialized]], - }, - async () => { - click( - manageButton, - "Clicking manage button to open translations subpage" - ); - } - ); - - info("Verifying never-translate section is empty"); - await translationsSettingsTestUtils.assertNeverTranslateLanguagesEmptyState({ - visible: true, - }); - - info( - "Simulating stealing by adding Spanish to never-translate and removing from always-translate" - ); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.NeverTranslateLanguagesRendered], - [ - TranslationsSettingsTestUtils.Events - .NeverTranslateLanguagesEmptyStateHidden, - ], - [TranslationsSettingsTestUtils.Events.AlwaysTranslateLanguagesRendered], - ], - }, - async () => { - Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, "es"); - Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, "fr"); - } - ); - - info("Verifying Spanish appears in never-translate UI"); - await translationsSettingsTestUtils.assertNeverTranslateLanguages({ - languages: ["es"], - count: 1, - }); - - info("Verifying always-translate UI updated to show only French"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ - languages: ["fr"], - count: 1, - }); - - info( - "Simulating stealing French by adding to never-translate and removing from always-translate" - ); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [TranslationsSettingsTestUtils.Events.NeverTranslateLanguagesRendered], - [TranslationsSettingsTestUtils.Events.AlwaysTranslateLanguagesRendered], - [ - TranslationsSettingsTestUtils.Events - .AlwaysTranslateLanguagesEmptyStateShown, - ], - ], - }, - async () => { - Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, "es,fr"); - Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, ""); - } - ); - - info("Verifying both Spanish and French in never-translate UI"); - await translationsSettingsTestUtils.assertNeverTranslateLanguages({ - languages: ["es", "fr"], - count: 2, - }); - - info("Verifying always-translate UI shows empty state"); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ - languages: [], - count: 0, - }); - await translationsSettingsTestUtils.assertAlwaysTranslateLanguagesEmptyState({ - visible: true, - }); - - await cleanup(); -}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_never_translate_sites_basic.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_never_translate_sites_basic.js @@ -1,265 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - https://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -/** - * Tests that never-translate site permissions load when opening the settings subpage. - */ -add_task(async function test_never_translate_sites_prepopulated() { - const exampleComOrigin = new URL(ENGLISH_PAGE_URL).origin; - const exampleOrgOrigin = new URL(SPANISH_PAGE_URL_DOT_ORG).origin; - const exampleNetOrigin = new URL("https://example.net").origin; - const siteOrigins = [exampleOrgOrigin, exampleComOrigin, exampleNetOrigin]; - const expectedSites = TranslationsSettingsTestUtils.sortOrigins(siteOrigins); - - const { cleanup, translationsSettingsTestUtils } = - await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [["browser.settings-redesign.enabled", true]], - }); - - info("Adding never-translate site permissions before opening settings"); - for (const origin of siteOrigins) { - TranslationsParent.setNeverTranslateSiteByOrigin(true, origin); - } - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Navigating to the translations settings subpage"); - const manageButton = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton" - ); - - info("Scrolling manage button into view"); - manageButton.scrollIntoView({ behavior: "instant", block: "center" }); - - info( - "Opening settings subpage and waiting for never-translate sites to render" - ); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [ - TranslationsSettingsTestUtils.Events.NeverTranslateSitesRendered, - { sites: expectedSites, count: expectedSites.length }, - ], - [ - TranslationsSettingsTestUtils.Events - .NeverTranslateSitesEmptyStateHidden, - ], - [TranslationsSettingsTestUtils.Events.Initialized], - ], - }, - async () => { - click(manageButton, "Opening translations settings subpage"); - } - ); - - info("Verifying pre-populated never-translate sites are displayed"); - await translationsSettingsTestUtils.assertNeverTranslateSites({ - sites: expectedSites, - count: expectedSites.length, - }); - - info( - "Verifying never-translate sites empty state is hidden when sites exist" - ); - await translationsSettingsTestUtils.assertNeverTranslateSitesEmptyState({ - visible: false, - }); - - await cleanup(); -}); - -/** - * Tests deleting never-translate sites and showing the empty-state row. - */ -add_task(async function test_never_translate_sites_delete_and_empty_state() { - const exampleComOrigin = new URL(ENGLISH_PAGE_URL).origin; - const exampleOrgOrigin = new URL(SPANISH_PAGE_URL_DOT_ORG).origin; - const initialSites = [exampleComOrigin, exampleOrgOrigin]; - const expectedInitialSites = - TranslationsSettingsTestUtils.sortOrigins(initialSites); - - const { cleanup, translationsSettingsTestUtils } = - await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [["browser.settings-redesign.enabled", true]], - }); - - info("Adding never-translate site permissions before opening settings"); - for (const origin of initialSites) { - TranslationsParent.setNeverTranslateSiteByOrigin(true, origin); - } - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Navigating to the translations settings subpage"); - const manageButton = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton" - ); - - info("Scrolling manage button into view"); - manageButton.scrollIntoView({ behavior: "instant", block: "center" }); - - info( - "Opening settings subpage and waiting for never-translate sites to render" - ); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [ - TranslationsSettingsTestUtils.Events.NeverTranslateSitesRendered, - { - sites: expectedInitialSites, - count: expectedInitialSites.length, - }, - ], - [ - TranslationsSettingsTestUtils.Events - .NeverTranslateSitesEmptyStateHidden, - ], - [TranslationsSettingsTestUtils.Events.Initialized], - ], - }, - async () => { - click(manageButton, "Opening translations settings subpage"); - } - ); - - info("Verifying both never-translate sites are shown"); - await translationsSettingsTestUtils.assertNeverTranslateSites({ - sites: expectedInitialSites, - count: expectedInitialSites.length, - }); - - const removedOrigin = expectedInitialSites[0]; - const remainingOrigin = expectedInitialSites[1]; - - info(`Removing never-translate site ${removedOrigin} via delete button`); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [ - TranslationsSettingsTestUtils.Events.NeverTranslateSitesRendered, - { sites: [remainingOrigin], count: 1 }, - ], - ], - }, - async () => { - await translationsSettingsTestUtils.removeNeverTranslateSite( - removedOrigin - ); - } - ); - - info("Verifying single site remains after deletion"); - await translationsSettingsTestUtils.assertNeverTranslateSites({ - sites: [remainingOrigin], - count: 1, - }); - await translationsSettingsTestUtils.assertNeverTranslateSitesEmptyState({ - visible: false, - }); - - info(`Removing final never-translate site ${remainingOrigin}`); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [ - TranslationsSettingsTestUtils.Events.NeverTranslateSitesRendered, - { sites: [], count: 0 }, - ], - [ - TranslationsSettingsTestUtils.Events - .NeverTranslateSitesEmptyStateShown, - ], - ], - }, - async () => { - await translationsSettingsTestUtils.removeNeverTranslateSite( - remainingOrigin - ); - } - ); - - info("Verifying empty state row appears after last deletion"); - await translationsSettingsTestUtils.assertNeverTranslateSites({ - sites: [], - count: 0, - }); - await translationsSettingsTestUtils.assertNeverTranslateSitesEmptyState({ - visible: true, - }); - - await cleanup(); -}); - -/** - * Tests that never-translate sites are sorted by domain ignoring the scheme. - */ -add_task(async function test_never_translate_sites_sorted_ignoring_scheme() { - const httpEsOrigin = new URL("https://es.wikipedia.org").origin; - const httpsEnOrigin = new URL("https://en.wikipedia.org").origin; - const httpsJaOrigin = new URL("https://ja.wikipedia.org").origin; - const siteOrigins = [httpsJaOrigin, httpEsOrigin, httpsEnOrigin]; - const expectedSites = TranslationsSettingsTestUtils.sortOrigins(siteOrigins); - - const { cleanup, translationsSettingsTestUtils } = - await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [["browser.settings-redesign.enabled", true]], - }); - - info("Adding never-translate site permissions before opening settings"); - for (const origin of siteOrigins) { - TranslationsParent.setNeverTranslateSiteByOrigin(true, origin); - } - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Navigating to the translations settings subpage"); - const manageButton = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton" - ); - - info("Scrolling manage button into view"); - manageButton.scrollIntoView({ behavior: "instant", block: "center" }); - - info( - "Opening settings subpage and waiting for never-translate sites to render" - ); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [ - TranslationsSettingsTestUtils.Events.NeverTranslateSitesRendered, - { sites: expectedSites, count: expectedSites.length }, - ], - [ - TranslationsSettingsTestUtils.Events - .NeverTranslateSitesEmptyStateHidden, - ], - [TranslationsSettingsTestUtils.Events.Initialized], - ], - }, - async () => { - click(manageButton, "Opening translations settings subpage"); - } - ); - - info("Verifying never-translate sites are sorted by domain ignoring scheme"); - await translationsSettingsTestUtils.assertNeverTranslateSites({ - sites: expectedSites, - count: expectedSites.length, - }); - await translationsSettingsTestUtils.assertNeverTranslateSitesOrder({ - sites: expectedSites, - }); - await translationsSettingsTestUtils.assertNeverTranslateSitesEmptyState({ - visible: false, - }); - - await cleanup(); -}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_never_translate_sites_observe.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_never_translate_sites_observe.js @@ -1,116 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - https://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -/** - * Tests that adding never-translate site permissions while the settings subpage is open - * updates the UI via the permissions observer. - */ -add_task( - async function test_never_translate_sites_observe_permission_changes() { - const exampleComOrigin = new URL(ENGLISH_PAGE_URL).origin; - const exampleOrgOrigin = new URL(SPANISH_PAGE_URL_DOT_ORG).origin; - - const { cleanup, translationsSettingsTestUtils } = - await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [["browser.settings-redesign.enabled", true]], - }); - - const document = gBrowser.selectedBrowser.contentDocument; - - info("Navigating to the translations settings subpage"); - const manageButton = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton" - ); - - info("Scrolling manage button into view"); - manageButton.scrollIntoView({ behavior: "instant", block: "center" }); - - info("Opening settings subpage and waiting for initial empty render"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [ - TranslationsSettingsTestUtils.Events.NeverTranslateSitesRendered, - { sites: [], count: 0 }, - ], - [TranslationsSettingsTestUtils.Events.Initialized], - ], - }, - async () => { - click(manageButton, "Opening translations settings subpage"); - } - ); - - info("Verifying never-translate sites empty state is visible initially"); - await translationsSettingsTestUtils.assertNeverTranslateSitesEmptyState({ - visible: true, - }); - - info("Adding example.com to never-translate sites while settings are open"); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [ - TranslationsSettingsTestUtils.Events.NeverTranslateSitesRendered, - { sites: [exampleComOrigin], count: 1 }, - ], - [ - TranslationsSettingsTestUtils.Events - .NeverTranslateSitesEmptyStateHidden, - ], - ], - }, - async () => { - TranslationsParent.setNeverTranslateSiteByOrigin( - true, - exampleComOrigin - ); - } - ); - - info("Verifying example.com is rendered in the never-translate sites list"); - await translationsSettingsTestUtils.assertNeverTranslateSites({ - sites: [exampleComOrigin], - count: 1, - }); - await translationsSettingsTestUtils.assertNeverTranslateSitesEmptyState({ - visible: false, - }); - - info("Adding example.org to never-translate sites while settings are open"); - const expectedSites = TranslationsSettingsTestUtils.sortOrigins([ - exampleComOrigin, - exampleOrgOrigin, - ]); - await translationsSettingsTestUtils.assertEvents( - { - expected: [ - [ - TranslationsSettingsTestUtils.Events.NeverTranslateSitesRendered, - { sites: expectedSites, count: expectedSites.length }, - ], - ], - }, - async () => { - TranslationsParent.setNeverTranslateSiteByOrigin( - true, - exampleOrgOrigin - ); - } - ); - - info("Verifying both sites are displayed after observer update"); - await translationsSettingsTestUtils.assertNeverTranslateSites({ - sites: expectedSites, - count: expectedSites.length, - }); - await translationsSettingsTestUtils.assertNeverTranslateSitesEmptyState({ - visible: false, - }); - - await cleanup(); - } -); diff --git a/browser/components/translations/tests/browser/head.js b/browser/components/translations/tests/browser/head.js @@ -4205,3 +4205,68 @@ class SelectTranslationsTestUtils { ); } } + +class TranslationsSettingsTestUtils { + /** + * Opens the Translation Settings page by clicking the settings button sent in the argument. + * + * @param {HTMLElement} settingsButton + * @returns {Element} + */ + static async openAboutPreferencesTranslationsSettingsPane(settingsButton) { + const document = gBrowser.selectedBrowser.contentDocument; + + const translationsPane = + content.window.gCategoryModules.get("paneTranslations"); + const promise = BrowserTestUtils.waitForEvent( + document, + "paneshown", + false, + event => event.detail.category === "paneTranslations" + ); + + click(settingsButton, "Click settings button"); + await promise; + + return translationsPane.elements; + } + + /** + * Utility function to handle the click event for a `moz-button` element that controls + * the Download/Remove Language functionality. + * + * The button's icon reflects the current state of the language (downloaded, loading, or removed), + * which is represented by a corresponding CSS class. + * + * When this button is clicked for any language, the function waits for the button's state and icon + * to update. It then checks whether the button's state and icon match the expected state as defined + * by the test case, and logs the respective message provided by the test case. + * + * @param {Element} langButton - The `moz-button` element representing the download/remove button. + * @param {string} buttonIcon - The expected CSS class representing the button's state/icon (e.g., download, loading, or remove icon). + * @param {string} logMsg - A custom log message provided by the test case indicating the expected result. + */ + + static async downaloadButtonClick(langButton, buttonIcon, logMsg) { + if ( + !langButton.parentNode + .querySelector("moz-button") + .classList.contains(buttonIcon) + ) { + await BrowserTestUtils.waitForMutationCondition( + langButton.parentNode.querySelector("moz-button"), + { attributes: true, attributeFilter: ["class"] }, + () => + langButton.parentNode + .querySelector("moz-button") + .classList.contains(buttonIcon) + ); + } + ok( + langButton.parentNode + .querySelector("moz-button") + .classList.contains(buttonIcon), + logMsg + ); + } +} diff --git a/browser/locales-preview/translations.ftl b/browser/locales-preview/translations.ftl @@ -0,0 +1,66 @@ +# 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/. + +## Translation Settings section + +translations-settings-back-button = + .aria-label = Back to Settings +translations-settings-header = More Translation Settings + +translations-settings-description = Set your language and site translation preferences and manage languages downloaded for offline translation. +translations-settings-add-language-button = + .label = Add language +translations-settings-always-translate = Always translate these languages +translations-settings-never-translate = Never translate these languages +translations-settings-never-sites-header = Never translate these sites +translations-settings-never-sites-description = To add to this list, visit a site and select “Never translate this site” from the translation menu. + +## Section to download language models to enable offline translation. + +translations-settings-download-languages = Download languages +translations-settings-download-all-languages = All languages +translations-settings-download-languages-link = Learn more about downloading languages +# Variables: +# $size (number) - The size of the download in megabites +translations-settings-download-size = ({ $size }) +translations-settings-language-header = Language + +# Variables: +# $name (string) - The language to be downloaded +translations-settings-language-download-error = + .heading = Download Error + .message = Could not download { $name } language. Please try again. + +# Variables: +# $name (string) - The language to be downloaded +translations-settings-language-remove-error = + .heading = Remove Error + .message = Could not remove { $name } language. Please try again. + +# Variables: +# $name (string) - The display name of the language that is to be downloaded +translations-settings-download-button = + .aria-label = Download { $name } +# Variables: +# $name (string) - The display name of the language that is to be removed +translations-settings-remove-button = + .aria-label = Remove { $name } +# Variables: +# $name (string) - The display name of the language that is loading +translations-settings-loading-button = + .aria-label = Loading { $name } +translations-settings-download-all-button = + .aria-label = Download all languages +translations-settings-remove-all-button = + .aria-label = Remove all languages +translations-settings-loading-all-button = + .aria-label = Loading all languages +# Variables: +# $name (string) - The display name of the language that is Always/Never translated +translations-settings-remove-language-button-2 = + .aria-label = Remove { $name } +# Variables: +# $name (string) - The site address that is Never to be translated +translations-settings-remove-site-button-2 = + .aria-label = Remove { $name } diff --git a/browser/locales/en-US/browser/preferences/preferences.ftl b/browser/locales/en-US/browser/preferences/preferences.ftl @@ -421,94 +421,6 @@ translate-exceptions = .label = Exceptions… .accesskey = x -settings-translations-header = - .label = Translations - .aria-label = Translations - .description = Translate pages or selected text. To protect your privacy, translations stay on your device. - -settings-translations-offer-to-translate-label = - .label = Offer full page translation - -settings-translations-more-settings-button = - .label = More translation settings - .description = Set preferences for languages, websites, and offline translation. - -settings-translations-subpage-header = - .heading = More translation settings - -settings-translations-subpage-speed-up-translation-header = - .label = Speed up translation - .description = Download complete languages for faster translations and to translate offline. - -settings-translations-subpage-automatic-translation-header = - .label = Automatic translation - -settings-translations-subpage-always-translate-header = - .label = Always translate these languages - -settings-translations-subpage-never-translate-header = - .label = Never translate these languages - -settings-translations-subpage-never-translate-sites-header = - .label = Never translate these sites - -# The icon placeholders show the translations button and the settings gear in the urlbar panel. -settings-translations-subpage-never-translate-sites-description = - To add a site, open the <img data-l10n-name="translations-icon"/> translation panel, select <img data-l10n-name="settings-icon"/> translation settings, then choose “Never translate this site” - -settings-translations-subpage-language-select-option = - .label = Add language - -settings-translations-subpage-language-add-button = - .aria-label = Add language - .title = Add language - -settings-translations-subpage-download-languages-header = - .label = Download languages - -settings-translations-subpage-download-languages-select-option = - .label = Select language - -settings-translations-subpage-download-languages-button = - .aria-label = Download language - .title = Download language - -# Variables: -# $language (string) - Localized name of the language to download. -# $size (string) - Download size in megabytes, formatted for the locale. -settings-translations-subpage-download-language-option = { $language } ({ $size }MB) - .label = { $language } ({ $size }MB) - -settings-translations-subpage-no-languages-downloaded = - .label = No languages downloaded - -settings-translations-subpage-no-languages-added = - .label = No languages added - -settings-translations-subpage-download-progress = Download in progress… - -# Variables: -# $language (string) - The localized display name of the language. -# $size (string) - The download size of the language in megabytes. -settings-translations-subpage-download-error = Couldn’t download { $language } ({ $size }MB) - -settings-translations-subpage-download-retry-button = - .label = Try again - -# Variables: -# $language (string) - The localized display name of the language. -# $size (string) - The download size of the language in megabytes. -settings-translations-subpage-download-delete-confirm = Delete { $language } ({ $size }MB)? - -settings-translations-subpage-download-delete-button = - .label = Delete - -settings-translations-subpage-download-cancel-button = - .label = Cancel - -settings-translations-subpage-no-sites-added = - .label = No sites added - # Variables: # $localeName (string) - Localized name of the locale to be used. use-system-locale = diff --git a/browser/locales/jar.mn b/browser/locales/jar.mn @@ -13,6 +13,7 @@ preview/enUS-searchFeatures.ftl (../components/urlbar/content/enUS-searchFeatures.ftl) preview/onboarding.ftl (../components/aboutwelcome/content/onboarding.ftl) preview/genai.ftl (../components/genai/content/genai.ftl) + preview/translations.ftl (../locales-preview/translations.ftl) preview/credentialChooser.ftl (../../toolkit/components/credentialmanagement/credentialChooser.ftl) browser (%browser/**/*.ftl) preview/aiWindow.ftl (../locales-preview/aiWindow.ftl) diff --git a/browser/themes/shared/icons/translations.svg b/browser/themes/shared/icons/translations.svg @@ -1,6 +1,10 @@ <!-- This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -<svg width="17" height="16" xmlns="http://www.w3.org/2000/svg" fill="context-fill light-dark(black, white)" fill-opacity="context-fill-opacity"> - <path fill-rule="nonzero" d="M15.37 15H17l-3.63-8.54a.75.75 0 0 0-.69-.46h-.82c-.3 0-.58.18-.7.46L9.32 10.8l-.01-.01a10.8 10.8 0 0 1-3.27-2.2 12.38 12.38 0 0 0 2.54-4.18L9.08 3H10V1.5H5.75V0h-1.5v1.5H0V3h7.5l-.33.91c-.47 1.31-1.2 2.52-2.13 3.56-.7-.9-1.25-1.9-1.63-2.97H1.8l.18.48a12.43 12.43 0 0 0 1.97 3.56c-.9.75-1.89 1.35-2.96 1.78v1.58a12.3 12.3 0 0 0 3.96-2.26 12.31 12.31 0 0 0 3.77 2.54L7.53 15h1.64l1.06-2.5h4.08l1.06 2.5Zm-4.5-4 1.4-3.3 1.4 3.3h-2.8Z"/> +<svg width="17" height="16" xmlns="http://www.w3.org/2000/svg"> + <path + d="M15.37 15H17l-3.63-8.54a.75.75 0 0 0-.69-.46h-.82c-.3 0-.58.18-.7.46L9.32 10.8l-.01-.01a10.8 10.8 0 0 1-3.27-2.2 12.38 12.38 0 0 0 2.54-4.18L9.08 3H10V1.5H5.75V0h-1.5v1.5H0V3h7.5l-.33.91c-.47 1.31-1.2 2.52-2.13 3.56-.7-.9-1.25-1.9-1.63-2.97H1.8l.18.48a12.43 12.43 0 0 0 1.97 3.56c-.9.75-1.89 1.35-2.96 1.78v1.58a12.3 12.3 0 0 0 3.96-2.26 12.31 12.31 0 0 0 3.77 2.54L7.53 15h1.64l1.06-2.5h4.08l1.06 2.5Zm-4.5-4 1.4-3.3 1.4 3.3h-2.8Z" + fill="context-fill" + fill-opacity="context-fill-opacity" + fill-rule="nonzero"/> </svg> diff --git a/browser/themes/shared/jar.inc.mn b/browser/themes/shared/jar.inc.mn @@ -144,6 +144,7 @@ skin/classic/browser/preferences/category-search.svg (../shared/preferences/category-search.svg) skin/classic/browser/preferences/category-sync.svg (../shared/preferences/category-sync.svg) skin/classic/browser/preferences/containers.css (../shared/preferences/containers.css) + skin/classic/browser/preferences/translations.css (../shared/preferences/translations.css) skin/classic/browser/preferences/containers-dialog.css (../shared/preferences/containers-dialog.css) skin/classic/browser/preferences/dialog.css (../shared/preferences/dialog.css) skin/classic/browser/preferences/fxaPairDevice.css (../shared/preferences/fxaPairDevice.css) diff --git a/browser/themes/shared/preferences/preferences.css b/browser/themes/shared/preferences/preferences.css @@ -1450,10 +1450,6 @@ setting-group[groupid="home"] { margin: var(--space-large) 0; } -.translations-download-language-error { - background-color: var(--background-color-critical); -} - .hidden-category { display: none; } @@ -1555,7 +1551,3 @@ setting-group[groupid="home"] { #payments-list-header { --box-label-font-weight: var(--heading-font-weight); } - -.box-header-bold { - --box-label-font-weight: var(--heading-font-weight); -} diff --git a/browser/themes/shared/preferences/translations.css b/browser/themes/shared/preferences/translations.css @@ -0,0 +1,86 @@ +/* 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/. */ + +#translations-settings-header { + margin-top: var(--space-xlarge); + margin-bottom: calc(2 * var(--space-small)); +} + +.translations-settings-manage-section { + margin-top: var(--space-xlarge); +} + +.translations-settings-manage-language { + margin: 0 calc(2 * var(--space-small)); + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.translations-settings-manage-section-info { + display: flex; + flex-direction: column; + h2, + p, + a { + display: block; + margin: var(--space-small) calc(2 * var(--space-small)); + } + a { + display: block; + } +} + +.translations-settings-languages-card { + flex-direction: column; + max-height: calc(14 * var(--space-xlarge)); + padding-inline: calc(2 * var(--space-small)); + + &[hidden] { + display: none; + } + &:not([hidden]) { + display: flex; + } +} + +.translations-settings-language-header { + margin: calc(2 * var(--space-small)) 0; + font-size: var(--font-size-root); + font-weight: var(--heading-font-weight); +} + +.translations-settings-language-list { + overflow: auto; +} + +.translations-settings-language { + display: flex; + flex-wrap: wrap; + align-items: center; + padding: var(--space-small) 0; + border-top: 1px solid var(--border-color); + label { + margin: 0 calc(2 * var(--space-small)); + } +} +.translations-settings-language-error { + display: inline-block; + flex: 0 1 100%; +} +.translations-settings-download-icon[type~="icon"]::part(button) { + background-image: url(chrome://browser/skin/downloads/downloads.svg); +} + +.translations-settings-remove-icon[type~="icon"]::part(button) { + background-image: url(chrome://global/skin/icons/delete.svg); +} + +.translations-settings-loading-icon[type~="icon"]::part(button) { + background-image: url(chrome://global/skin/icons/loading.svg); +} + +.translations-settings-download-size { + color: var(--text-color-deemphasized); +} diff --git a/stylelint-rollouts.config.js b/stylelint-rollouts.config.js @@ -168,6 +168,7 @@ module.exports = [ "browser/themes/shared/preferences/privacy.css", "browser/themes/shared/preferences/search.css", "browser/themes/shared/preferences/siteDataSettings.css", + "browser/themes/shared/preferences/translations.css", "browser/themes/shared/privatebrowsing/aboutPrivateBrowsing.css", "browser/themes/shared/search/searchbar.css", "browser/themes/shared/sidebar.css", diff --git a/taskcluster/kinds/mochitest/kind.yml b/taskcluster/kinds/mochitest/kind.yml @@ -536,7 +536,8 @@ tasks: .*-asan.*: 1800 .*-tsan.*: 2400 .*-ccov.*: 2400 - default: 1500 + win.*/debug: 1800 + default: 1200 mozharness: mochitest-flavor: browser instance-size: default diff --git a/toolkit/components/translations/actors/TranslationsParent.sys.mjs b/toolkit/components/translations/actors/TranslationsParent.sys.mjs @@ -2813,7 +2813,6 @@ export class TranslationsParent extends JSWindowActorParent { language, /* includePivotRecords */ true )) { - await chaosMode(1 / 6); const download = () => { lazy.console.log("Downloading record", record.name, record.id); return client.attachments.download(record); @@ -4627,9 +4626,7 @@ async function downloadManager(queue) { const newRetriesLeft = retriesLeft - 1; - // Skip retries in automation to avoid slow test timeouts, - // especially when running in chaos mode when things take longer. - if (retriesLeft > 0 && !Cu.isInAutomation) { + if (retriesLeft > 0) { lazy.console.log( `Queueing another attempt. ${newRetriesLeft} attempts left.` ); @@ -4661,9 +4658,6 @@ async function downloadManager(queue) { } // Wait for any active downloads to complete. - if (!pendingDownloadAttempts.size) { - break; - } await Promise.race(pendingDownloadAttempts); } diff --git a/toolkit/components/translations/tests/browser/shared-head.js b/toolkit/components/translations/tests/browser/shared-head.js @@ -353,1210 +353,6 @@ function upperCaseNode(node) { } /** - * Test utility class for translations settings UI tests. - * Provides methods for interacting with and asserting the state of - * the translations settings page in about:preferences. - */ -class TranslationsSettingsTestUtils { - /** - * @param {Document} document - The settings document - */ - constructor(document) { - this.document = document; - } - - async openTranslationsSubpageFromDocument() { - const manageButton = await waitForCondition( - () => this.document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton" - ); - manageButton.scrollIntoView({ behavior: "instant", block: "center" }); - - await this.assertEvents( - { - expected: [[TranslationsSettingsTestUtils.Events.Initialized]], - }, - async () => { - click(manageButton, "Open translations subpage"); - } - ); - } - - /** - * Opens the translations settings subpage and returns helpers. - * - * @param {Array} [lexicalShortlistPrefs] - * @returns {Promise<{cleanup: Function, remoteClients: object, translationsSettingsTestUtils: TranslationsSettingsTestUtils}>} - */ - static async openTranslationsSettingsSubpage(lexicalShortlistPrefs = []) { - const { cleanup, remoteClients, translationsSettingsTestUtils } = - await setupAboutPreferences(LANGUAGE_PAIRS, { - prefs: [ - ["browser.settings-redesign.enabled", true], - ...lexicalShortlistPrefs, - ], - }); - - const document = gBrowser.selectedBrowser.contentDocument; - const manageButton = await waitForCondition( - () => document.getElementById("translationsManageButton"), - "Waiting for translationsManageButton" - ); - manageButton.scrollIntoView({ behavior: "instant", block: "center" }); - - await translationsSettingsTestUtils.assertEvents( - { - expected: [[TranslationsSettingsTestUtils.Events.Initialized]], - }, - async () => { - click(manageButton, "Open translations subpage"); - } - ); - - return { cleanup, remoteClients, translationsSettingsTestUtils }; - } - - static getLanguageModelNames(langTag) { - return languageModelNames([ - { fromLang: langTag, toLang: "en" }, - { fromLang: "en", toLang: langTag }, - ]); - } - - /** - * Returns origins sorted alphabetically while ignoring schemes. - * - * @param {string[]} origins - * @returns {string[]} - */ - static sortOrigins(origins) { - const stripScheme = origin => - origin.replace(/^[a-z][a-z0-9+.-]*:\/\//i, ""); - return [...origins].sort((originA, originB) => - stripScheme(originA).localeCompare(stripScheme(originB)) - ); - } - - /** - * Static Events class for event name constants. - */ - static Events = class Events { - static AlwaysTranslateLanguagesRendered = - "TranslationsSettingsTest:AlwaysTranslateLanguagesRendered"; - static NeverTranslateLanguagesRendered = - "TranslationsSettingsTest:NeverTranslateLanguagesRendered"; - static NeverTranslateSitesRendered = - "TranslationsSettingsTest:NeverTranslateSitesRendered"; - static DownloadedLanguagesRendered = - "TranslationsSettingsTest:DownloadedLanguagesRendered"; - - static AlwaysTranslateLanguagesEmptyStateShown = - "TranslationsSettingsTest:AlwaysTranslateLanguagesEmptyStateShown"; - static AlwaysTranslateLanguagesEmptyStateHidden = - "TranslationsSettingsTest:AlwaysTranslateLanguagesEmptyStateHidden"; - static NeverTranslateLanguagesEmptyStateShown = - "TranslationsSettingsTest:NeverTranslateLanguagesEmptyStateShown"; - static NeverTranslateLanguagesEmptyStateHidden = - "TranslationsSettingsTest:NeverTranslateLanguagesEmptyStateHidden"; - static NeverTranslateSitesEmptyStateShown = - "TranslationsSettingsTest:NeverTranslateSitesEmptyStateShown"; - static NeverTranslateSitesEmptyStateHidden = - "TranslationsSettingsTest:NeverTranslateSitesEmptyStateHidden"; - static DownloadedLanguagesEmptyStateShown = - "TranslationsSettingsTest:DownloadedLanguagesEmptyStateShown"; - static DownloadedLanguagesEmptyStateHidden = - "TranslationsSettingsTest:DownloadedLanguagesEmptyStateHidden"; - static AlwaysTranslateLanguagesAddButtonEnabled = - "TranslationsSettingsTest:AlwaysTranslateLanguagesAddButtonEnabled"; - static AlwaysTranslateLanguagesAddButtonDisabled = - "TranslationsSettingsTest:AlwaysTranslateLanguagesAddButtonDisabled"; - - static AlwaysTranslateLanguagesSelectOptionsUpdated = - "TranslationsSettingsTest:AlwaysTranslateLanguagesSelectOptionsUpdated"; - static NeverTranslateLanguagesSelectOptionsUpdated = - "TranslationsSettingsTest:NeverTranslateLanguagesSelectOptionsUpdated"; - static DownloadedLanguagesSelectOptionsUpdated = - "TranslationsSettingsTest:DownloadedLanguagesSelectOptionsUpdated"; - static NeverTranslateLanguagesAddButtonEnabled = - "TranslationsSettingsTest:NeverTranslateLanguagesAddButtonEnabled"; - static NeverTranslateLanguagesAddButtonDisabled = - "TranslationsSettingsTest:NeverTranslateLanguagesAddButtonDisabled"; - - static DownloadStarted = "TranslationsSettingsTest:DownloadStarted"; - static DownloadProgress = "TranslationsSettingsTest:DownloadProgress"; - static DownloadCompleted = "TranslationsSettingsTest:DownloadCompleted"; - static DownloadFailed = "TranslationsSettingsTest:DownloadFailed"; - static DownloadDeleted = "TranslationsSettingsTest:DownloadDeleted"; - - static Initialized = "TranslationsSettingsTest:Initialized"; - static InitializationFailed = - "TranslationsSettingsTest:InitializationFailed"; - - static DownloadLanguageButtonEnabled = - "TranslationsSettingsTest:DownloadLanguageButtonEnabled"; - static DownloadLanguageButtonDisabled = - "TranslationsSettingsTest:DownloadLanguageButtonDisabled"; - }; - - /** - * Waits for a translations settings event to be dispatched. - * - * @param {string} eventName - The event name to wait for - * @param {object} options - * @param {object} [options.expectedDetail] - Expected detail properties - * @returns {Promise<CustomEvent>} - */ - async waitForEvent(eventName, options = {}) { - const { expectedDetail } = options; - - return BrowserTestUtils.waitForEvent( - this.document, - eventName, - false, - event => { - if (expectedDetail) { - for (const key of Object.keys(expectedDetail)) { - const actual = event.detail?.[key]; - const expected = expectedDetail[key]; - if (JSON.stringify(actual) !== JSON.stringify(expected)) { - return false; - } - } - } - return true; - } - ); - } - - /** - * Asserts that specific events occur (or don't occur) during an action. - * - * @param {object} assertions - * @param {Array<[string, object?]>} assertions.expected - Events that must occur - * @param {Array<string>} [assertions.unexpected] - Events that must not occur - * @param {number} [assertions.timeout=10000] - Timeout in milliseconds - * @param {Function} callback - The action to perform - * @returns {Promise<void>} - */ - async assertEvents( - { expected = [], unexpected = [], timeout = 10000 }, - callback - ) { - const firedEvents = []; - const unexpectedEventsFired = []; - - const handlers = new Map(); - - const isInitializedFlagSet = - this.document?.defaultView?.wrappedJSObject?.TranslationsSettings - ?.initialized; - - const preseedEventsIfAlreadySatisfied = () => { - for (const [eventName] of expected) { - if ( - eventName === TranslationsSettingsTestUtils.Events.Initialized && - isInitializedFlagSet && - !firedEvents.some(([name]) => name === eventName) - ) { - firedEvents.push([eventName, null]); - } - } - }; - - preseedEventsIfAlreadySatisfied(); - - const maybeAddSyntheticInitializationEvent = () => { - if ( - expected.some( - ([name]) => name === TranslationsSettingsTestUtils.Events.Initialized - ) && - !firedEvents.some( - ([name]) => name === TranslationsSettingsTestUtils.Events.Initialized - ) && - this.document?.defaultView?.wrappedJSObject?.TranslationsSettings - ?.initialized - ) { - firedEvents.push([ - TranslationsSettingsTestUtils.Events.Initialized, - null, - ]); - } - }; - - for (const [eventName] of expected) { - const handler = event => { - firedEvents.push([eventName, event.detail]); - }; - handlers.set(eventName, handler); - this.document.addEventListener(eventName, handler); - } - - for (const eventName of unexpected) { - const handler = event => { - unexpectedEventsFired.push([eventName, event.detail]); - }; - handlers.set(eventName, handler); - this.document.addEventListener(eventName, handler); - } - - try { - await callback(); - - maybeAddSyntheticInitializationEvent(); - preseedEventsIfAlreadySatisfied(); - - const interval = 100; - const maxTries = Math.ceil(timeout / interval); - const expectedEventNames = expected.map(([name]) => name).join(", "); - try { - await TestUtils.waitForCondition( - () => { - maybeAddSyntheticInitializationEvent(); - return firedEvents.length >= expected.length; - }, - `Waiting for ${expected.length} expected event(s): ${expectedEventNames}`, - interval, - maxTries - ); - } catch (error) { - throw new Error( - error?.message ?? - error ?? - `Timed out waiting for expected event(s): ${expectedEventNames}` - ); - } - - for (let i = 0; i < expected.length; i++) { - const [expectedEventName, expectedDetail] = expected[i]; - const [firedEventName, firedDetail] = firedEvents[i] || []; - - is( - firedEventName, - expectedEventName, - `Expected event ${i}: ${expectedEventName}` - ); - - if (expectedDetail) { - for (const key of Object.keys(expectedDetail)) { - Assert.deepEqual( - firedDetail?.[key], - expectedDetail[key], - `Event ${expectedEventName} detail.${key} matches` - ); - } - } - } - - const unexpectedNames = unexpectedEventsFired - .map(([name]) => name) - .join(", "); - is( - unexpectedEventsFired.length, - 0, - `No unexpected events should fire. Fired: ${unexpectedNames}` - ); - } finally { - for (const [eventName, handler] of handlers.entries()) { - this.document.removeEventListener(eventName, handler); - } - } - } - - /** - * Gets the translations setting pane element. - * - * @returns {HTMLElement|null} - */ - getTranslationsPane() { - return this.document.querySelector( - 'setting-pane[data-category="paneTranslations"]' - ); - } - - /** - * Gets the translations subpage back button element. - * - * @returns {HTMLElement|null} - */ - getBackButton() { - return this.getTranslationsPane()?.pageHeaderEl?.backButtonEl ?? null; - } - - /** - * Clicks the translations subpage back button and waits for the main pane. - * - * @returns {Promise<void>} - */ - async clickBackButton() { - const pane = this.getTranslationsPane(); - if (!pane) { - throw new Error("Translations pane not found"); - } - - if (pane.getUpdateComplete) { - await pane.getUpdateComplete(); - } - - const backButton = pane.pageHeaderEl?.backButtonEl; - if (!backButton) { - throw new Error("Translations back button not found"); - } - - const paneShown = BrowserTestUtils.waitForEvent( - this.document, - "paneshown", - event => event.detail?.category === "paneGeneral" - ); - - await click(backButton, "Navigate back to main settings"); - await paneShown; - - await TestUtils.waitForCondition( - () => pane.hidden, - "Waiting for translations pane to hide" - ); - } - - /** - * Gets the always-translate languages select element. - * - * @returns {HTMLSelectElement|null} - */ - getAlwaysTranslateLanguagesSelect() { - return this.document.getElementById( - "translationsAlwaysTranslateLanguagesSelect" - ); - } - - /** - * Gets the always-translate languages add button. - * - * @returns {HTMLButtonElement|null} - */ - getAlwaysTranslateLanguagesAddButton() { - return this.document.getElementById( - "translationsAlwaysTranslateLanguagesButton" - ); - } - - /** - * Gets the never-translate languages select element. - * - * @returns {HTMLSelectElement|null} - */ - getNeverTranslateLanguagesSelect() { - return this.document.getElementById( - "translationsNeverTranslateLanguagesSelect" - ); - } - - /** - * Gets the never-translate languages add button. - * - * @returns {HTMLButtonElement|null} - */ - getNeverTranslateLanguagesAddButton() { - return this.document.getElementById( - "translationsNeverTranslateLanguagesButton" - ); - } - - /** - * Gets the download languages select element. - * - * @returns {HTMLSelectElement|null} - */ - getDownloadedLanguagesSelect() { - return this.document.getElementById("translationsDownloadLanguagesSelect"); - } - - getSelectedDownloadLanguage() { - return this.getDownloadedLanguagesSelect()?.value ?? ""; - } - - /** - * Gets the download button element. - * - * @returns {HTMLButtonElement|null} - */ - getDownloadLanguageButton() { - return this.document.getElementById("translationsDownloadLanguagesButton"); - } - - /** - * Gets the download languages group element. - * - * @returns {HTMLElement|null} - */ - getDownloadedLanguagesGroup() { - return this.document.getElementById("translationsDownloadLanguagesGroup"); - } - - /** - * Selects a language in the download dropdown. - * - * @param {string} langTag - */ - async selectDownloadLanguage(langTag) { - const dropdown = this.getDownloadedLanguagesSelect(); - dropdown.value = langTag; - dropdown.dispatchEvent(new Event("change", { bubbles: true })); - } - - async downloadLanguage({ - langTag, - remoteClients, - inProgressLanguages, - finalLanguages, - }) { - await this.selectDownloadLanguage(langTag); - - const started = this.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadStarted, - { expectedDetail: { langTag } } - ); - const renderInProgress = this.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, - { - expectedDetail: { - languages: inProgressLanguages, - count: inProgressLanguages.length, - downloading: [langTag], - }, - } - ); - const optionsUpdated = this.waitForEvent( - TranslationsSettingsTestUtils.Events - .DownloadedLanguagesSelectOptionsUpdated - ); - - await click(this.getDownloadLanguageButton(), `Start ${langTag} download`); - await Promise.all([started, renderInProgress, optionsUpdated]); - - const completed = this.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadCompleted, - { expectedDetail: { langTag } } - ); - const renderComplete = this.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, - { - expectedDetail: { - languages: finalLanguages, - count: finalLanguages.length, - downloading: [], - }, - } - ); - const optionsAfter = this.waitForEvent( - TranslationsSettingsTestUtils.Events - .DownloadedLanguagesSelectOptionsUpdated - ); - - await remoteClients.translationModels.resolvePendingDownloads( - TranslationsSettingsTestUtils.getLanguageModelNames(langTag).length - ); - await Promise.all([completed, renderComplete, optionsAfter]); - } - - /** - * Starts a download expected to fail and waits for the failure state. - * - * @param {object} options - * @param {string} options.langTag - * @param {object} options.remoteClients - * @param {string[]} [options.inProgressLanguages] - * @param {string[]} [options.failedLanguages] - */ - async startDownloadFailure({ - langTag, - remoteClients, - inProgressLanguages = [langTag], - failedLanguages = [langTag], - }) { - await this.selectDownloadLanguage(langTag); - - const started = this.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadStarted, - { expectedDetail: { langTag } } - ); - const renderInProgress = this.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, - { - expectedDetail: { - languages: inProgressLanguages, - count: inProgressLanguages.length, - downloading: [langTag], - }, - } - ); - const optionsUpdated = this.waitForEvent( - TranslationsSettingsTestUtils.Events - .DownloadedLanguagesSelectOptionsUpdated - ); - - await click( - this.getDownloadLanguageButton(), - `Start ${langTag} download (expect failure)` - ); - await Promise.all([started, renderInProgress, optionsUpdated]); - - const spinnerButton = this.getDownloadRemoveButton(langTag); - ok(spinnerButton, "Spinner button should be visible while downloading"); - is( - spinnerButton.getAttribute("type"), - "icon ghost", - "Spinner button should use ghost styling while downloading" - ); - - const failed = this.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadFailed, - { expectedDetail: { langTag } } - ); - const renderFailed = this.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, - { - expectedDetail: { - languages: failedLanguages, - count: failedLanguages.length, - downloading: [], - }, - } - ); - const optionsAfterFail = this.waitForEvent( - TranslationsSettingsTestUtils.Events - .DownloadedLanguagesSelectOptionsUpdated - ); - - const modelNames = - TranslationsSettingsTestUtils.getLanguageModelNames(langTag); - await remoteClients.translationModels.waitForPendingDownloads( - modelNames.length - ); - await remoteClients.translationModels.rejectPendingDownloads( - modelNames.length - ); - await Promise.all([failed, renderFailed, optionsAfterFail]); - } - - /** - * Waits for a language to appear in the download languages list. - * - * @param {string} langTag - * @returns {Promise<Element>} - */ - async waitForDownloadedLanguageItem(langTag) { - return waitForCondition( - () => - this.document.querySelector( - `.translations-download-language-item[data-lang-tag="${langTag}"]` - ), - `Waiting for downloaded language item: ${langTag}` - ); - } - - /** - * Asserts the current state of the downloaded languages list. - * - * @param {object} expected - * @param {string[]} [expected.languages] - Expected language tags - * @param {string[]} [expected.downloading] - Expected language tags that are downloading - * @param {number} [expected.count] - Expected count of languages - * @returns {Promise<void>} - */ - async assertDownloadedLanguages({ languages, downloading, count }) { - const items = this.document.querySelectorAll( - ".translations-download-language-item" - ); - - if (count !== undefined) { - is(items.length, count, `Should have ${count} downloaded language(s)`); - } - - const langTags = Array.from(items).map(item => item.dataset.langTag); - - if (languages) { - Assert.deepEqual( - langTags.sort(), - [...languages].sort(), - "Downloaded languages match" - ); - } - - if (downloading) { - const downloadingLangs = Array.from(items) - .filter(item => - item - .querySelector(".translations-download-remove-button") - ?.hasAttribute("disabled") - ) - .map(item => item.dataset.langTag); - Assert.deepEqual( - downloadingLangs.sort(), - [...downloading].sort(), - "Downloading languages match" - ); - } - } - - /** - * Asserts the current order of the downloaded languages list. - * - * @param {object} expected - * @param {string[]} expected.languages - Expected language tags in order - * @returns {Promise<void>} - */ - async assertDownloadedLanguagesOrder({ languages }) { - const items = this.document.querySelectorAll( - ".translations-download-language-item" - ); - const actualLanguages = Array.from(items).map(item => item.dataset.langTag); - Assert.deepEqual( - actualLanguages, - languages, - "Downloaded languages order matches" - ); - } - - /** - * Asserts the visibility state of the downloaded languages empty state. - * - * @param {object} expected - * @param {boolean} expected.visible - Whether empty state should be visible - * @returns {Promise<void>} - */ - async assertDownloadedLanguagesEmptyState({ visible }) { - const emptyRow = this.document.getElementById( - "translationsDownloadLanguagesNoneRow" - ); - if (visible) { - ok( - emptyRow && !emptyRow.hidden, - "Downloaded languages empty state should be visible" - ); - } else { - ok( - !emptyRow || emptyRow.hidden, - "Downloaded languages empty state should be hidden" - ); - } - } - - /** - * Removes a language from the downloaded languages list. - * - * @param {string} langTag - * @returns {Promise<void>} - */ - async removeDownloadedLanguage(langTag) { - const removeButton = await waitForCondition( - () => this.getDownloadDeleteIconButton(langTag), - `Waiting for download delete icon button for ${langTag}` - ); - removeButton.click(); - await waitForCondition( - () => this.getDownloadDeleteConfirmButton(langTag), - `Waiting for delete confirmation for ${langTag}` - ); - } - - getDownloadRemoveButton(langTag) { - return this.document.querySelector( - `[data-lang-tag="${langTag}"].translations-download-remove-button` - ); - } - - getDownloadDeleteConfirmButton(langTag) { - return this.document.querySelector( - `[data-lang-tag="${langTag}"].translations-download-delete-confirm-button` - ); - } - - getDownloadDeleteCancelButton(langTag) { - return this.document.querySelector( - `[data-lang-tag="${langTag}"].translations-download-delete-cancel-button` - ); - } - - getDownloadRetryButton(langTag) { - return this.document.querySelector( - `[data-lang-tag="${langTag}"].translations-download-retry-button` - ); - } - - getDownloadErrorButton(langTag) { - return this.document.querySelector( - `[data-lang-tag="${langTag}"].translations-download-remove-button[iconsrc*="error"]` - ); - } - - getDownloadWarningButton(langTag) { - return this.document.querySelector( - `[data-lang-tag="${langTag}"].translations-download-remove-button[iconsrc*="warning"]` - ); - } - - getDownloadDeleteIconButton(langTag) { - return this.document.querySelector( - `[data-lang-tag="${langTag}"].translations-download-remove-button[iconsrc*="delete"]` - ); - } - - async openDownloadDeleteConfirmation(langTag) { - const removeButton = await waitForCondition( - () => this.getDownloadDeleteIconButton(langTag), - `Waiting for download delete icon button for ${langTag}` - ); - removeButton.click(); - await waitForCondition( - () => this.getDownloadDeleteConfirmButton(langTag), - `Waiting for delete confirmation for ${langTag}` - ); - } - - async cancelDownloadDelete(langTag) { - const cancelButton = await waitForCondition( - () => this.getDownloadDeleteCancelButton(langTag), - `Waiting for delete cancel button for ${langTag}` - ); - cancelButton.click(); - await waitForCondition( - () => this.getDownloadDeleteIconButton(langTag), - `Waiting for delete icon button to return for ${langTag}` - ); - } - - async confirmDownloadDelete(langTag) { - const confirmButton = await waitForCondition( - () => this.getDownloadDeleteConfirmButton(langTag), - `Waiting for delete confirm button for ${langTag}` - ); - const deleted = this.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadDeleted, - { expectedDetail: { langTag } } - ); - confirmButton.click(); - await deleted; - } - - async clickDownloadRetry(langTag) { - const retryButton = await waitForCondition( - () => this.getDownloadRetryButton(langTag), - `Waiting for retry button for ${langTag}` - ); - const started = this.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadStarted, - { expectedDetail: { langTag } } - ); - retryButton.click(); - await started; - } - - /** - * Adds a language to the always-translate list. - * - * @param {string} langTag - * @returns {Promise<void>} - */ - async addAlwaysTranslateLanguage(langTag) { - const dropdown = this.getAlwaysTranslateLanguagesSelect(); - dropdown.value = langTag; - dropdown.dispatchEvent(new Event("change", { bubbles: true })); - - const addButton = await waitForCondition( - () => this.getAlwaysTranslateLanguagesAddButton(), - "Waiting for always-translate add button" - ); - if (addButton.disabled) { - const addButtonEnabled = this.waitForEvent( - TranslationsSettingsTestUtils.Events - .AlwaysTranslateLanguagesAddButtonEnabled - ); - await addButtonEnabled; - } - addButton.click(); - - const addedLanguage = this.waitForAlwaysTranslateLanguageItem(langTag); - const addButtonDisabledPromise = addButton.disabled - ? Promise.resolve() - : this.waitForEvent( - TranslationsSettingsTestUtils.Events - .AlwaysTranslateLanguagesAddButtonDisabled - ); - await Promise.all([addedLanguage, addButtonDisabledPromise]); - } - - /** - * Removes a language from the always-translate list. - * - * @param {string} langTag - * @returns {Promise<void>} - */ - async removeAlwaysTranslateLanguage(langTag) { - const removeButton = this.document.querySelector( - `[data-lang-tag="${langTag}"].translations-always-translate-remove-button` - ); - if (!removeButton) { - throw new Error(`Remove button not found for language: ${langTag}`); - } - const rendered = this.waitForEvent( - TranslationsSettingsTestUtils.Events.AlwaysTranslateLanguagesRendered - ); - removeButton.click(); - await rendered; - } - - /** - * Waits for a language to appear in the always-translate languages list. - * - * @param {string} langTag - * @returns {Promise<Element>} - */ - async waitForAlwaysTranslateLanguageItem(langTag) { - return TestUtils.waitForCondition( - () => - this.document.querySelector( - `[data-lang-tag="${langTag}"].translations-always-translate-language-item` - ), - `Waiting for always-translate language item: ${langTag}` - ); - } - - /** - * Asserts the current state of the always-translate languages list. - * - * @param {object} expected - * @param {string[]} [expected.languages] - Expected language tags - * @param {number} [expected.count] - Expected count of languages - * @returns {Promise<void>} - */ - async assertAlwaysTranslateLanguages({ languages, count }) { - const items = this.document.querySelectorAll( - ".translations-always-translate-language-item" - ); - - if (count !== undefined) { - is( - items.length, - count, - `Should have ${count} always-translate language(s)` - ); - } - - if (languages) { - const actualLanguages = Array.from(items).map( - item => item.dataset.langTag - ); - Assert.deepEqual( - actualLanguages.sort(), - [...languages].sort(), - "Always-translate languages match" - ); - } - } - - /** - * Asserts the current order of the always-translate languages list. - * - * @param {object} expected - * @param {string[]} expected.languages - Expected language tags in order - * @returns {Promise<void>} - */ - async assertAlwaysTranslateLanguagesOrder({ languages }) { - const items = this.document.querySelectorAll( - ".translations-always-translate-language-item" - ); - const actualLanguages = Array.from(items).map(item => item.dataset.langTag); - Assert.deepEqual( - actualLanguages, - languages, - "Always-translate languages order matches" - ); - } - - /** - * Asserts the visibility state of the always-translate languages empty state. - * - * @param {object} expected - * @param {boolean} expected.visible - Whether empty state should be visible - * @returns {Promise<void>} - */ - async assertAlwaysTranslateLanguagesEmptyState({ visible }) { - const emptyRow = this.document.getElementById( - "translationsAlwaysTranslateLanguagesNoneRow" - ); - if (visible) { - ok( - emptyRow && !emptyRow.hidden, - "Always-translate languages empty state should be visible" - ); - } else { - ok( - !emptyRow || emptyRow.hidden, - "Always-translate languages empty state should be hidden" - ); - } - } - - /** - * Adds a language to the never-translate list. - * - * @param {string} langTag - * @returns {Promise<void>} - */ - async addNeverTranslateLanguage(langTag) { - const dropdown = this.getNeverTranslateLanguagesSelect(); - dropdown.value = langTag; - dropdown.dispatchEvent(new Event("change", { bubbles: true })); - - const addButton = await waitForCondition( - () => this.getNeverTranslateLanguagesAddButton(), - "Waiting for never-translate add button" - ); - if (addButton.disabled) { - const addButtonEnabled = this.waitForEvent( - TranslationsSettingsTestUtils.Events - .NeverTranslateLanguagesAddButtonEnabled - ); - await addButtonEnabled; - } - addButton.click(); - - const addedLanguage = this.waitForNeverTranslateLanguageItem(langTag); - const addButtonDisabledPromise = addButton.disabled - ? Promise.resolve() - : this.waitForEvent( - TranslationsSettingsTestUtils.Events - .NeverTranslateLanguagesAddButtonDisabled - ); - await Promise.all([addedLanguage, addButtonDisabledPromise]); - } - - /** - * Removes a language from the never-translate list. - * - * @param {string} langTag - * @returns {Promise<void>} - */ - async removeNeverTranslateLanguage(langTag) { - const removeButton = this.document.querySelector( - `[data-lang-tag="${langTag}"].translations-never-translate-remove-button` - ); - if (!removeButton) { - throw new Error(`Remove button not found for language: ${langTag}`); - } - const rendered = this.waitForEvent( - TranslationsSettingsTestUtils.Events.NeverTranslateLanguagesRendered - ); - removeButton.click(); - await rendered; - } - - /** - * Waits for a language to appear in the never-translate languages list. - * - * @param {string} langTag - * @returns {Promise<Element>} - */ - async waitForNeverTranslateLanguageItem(langTag) { - return TestUtils.waitForCondition( - () => - this.document.querySelector( - `[data-lang-tag="${langTag}"].translations-never-translate-language-item` - ), - `Waiting for never-translate language item: ${langTag}` - ); - } - - /** - * Asserts the current state of the never-translate languages list. - * - * @param {object} expected - * @param {string[]} [expected.languages] - Expected language tags - * @param {number} [expected.count] - Expected count of languages - * @returns {Promise<void>} - */ - async assertNeverTranslateLanguages({ languages, count }) { - const items = this.document.querySelectorAll( - ".translations-never-translate-language-item" - ); - - if (count !== undefined) { - is( - items.length, - count, - `Should have ${count} never-translate language(s)` - ); - } - - if (languages) { - const actualLanguages = Array.from(items).map( - item => item.dataset.langTag - ); - Assert.deepEqual( - actualLanguages.sort(), - [...languages].sort(), - "Never-translate languages match" - ); - } - } - - /** - * Asserts the current order of the never-translate languages list. - * - * @param {object} expected - * @param {string[]} expected.languages - Expected language tags in order - * @returns {Promise<void>} - */ - async assertNeverTranslateLanguagesOrder({ languages }) { - const items = this.document.querySelectorAll( - ".translations-never-translate-language-item" - ); - const actualLanguages = Array.from(items).map(item => item.dataset.langTag); - Assert.deepEqual( - actualLanguages, - languages, - "Never-translate languages order matches" - ); - } - - /** - * Asserts the visibility state of the never-translate languages empty state. - * - * @param {object} expected - * @param {boolean} expected.visible - Whether empty state should be visible - * @returns {Promise<void>} - */ - async assertNeverTranslateLanguagesEmptyState({ visible }) { - const emptyRow = this.document.getElementById( - "translationsNeverTranslateLanguagesNoneRow" - ); - if (visible) { - ok( - emptyRow && !emptyRow.hidden, - "Never-translate languages empty state should be visible" - ); - } else { - ok( - !emptyRow || emptyRow.hidden, - "Never-translate languages empty state should be hidden" - ); - } - } - - /** - * Gets the never-translate sites list element. - * - * @returns {HTMLElement|null} - */ - getNeverTranslateSitesGroup() { - return this.document.getElementById("translationsNeverTranslateSitesGroup"); - } - - /** - * Waits for a site to appear in the never-translate sites list. - * - * @param {string} origin - * @returns {Promise<Element>} - */ - async waitForNeverTranslateSiteItem(origin) { - return waitForCondition( - () => - this.document.querySelector( - `[data-origin="${origin}"].translations-never-translate-site-item` - ), - `Waiting for never-translate site item: ${origin}` - ); - } - - /** - * Removes a site from the never-translate list. - * - * @param {string} origin - * @returns {Promise<void>} - */ - async removeNeverTranslateSite(origin) { - const removeButton = await waitForCondition( - () => - this.document.querySelector( - `[data-origin="${origin}"].translations-never-translate-site-remove-button` - ), - `Waiting for remove button for ${origin}` - ); - const rendered = this.waitForEvent( - TranslationsSettingsTestUtils.Events.NeverTranslateSitesRendered - ); - removeButton.click(); - await rendered; - } - - /** - * Asserts the current state of the never-translate sites list. - * - * @param {object} expected - * @param {string[]} [expected.sites] - Expected site origins - * @param {number} [expected.count] - Expected count of sites - * @returns {Promise<void>} - */ - async assertNeverTranslateSites({ sites, count }) { - const items = this.document.querySelectorAll( - ".translations-never-translate-site-item" - ); - - if (count !== undefined) { - is(items.length, count, `Should have ${count} never-translate site(s)`); - } - - if (sites) { - const actualSites = Array.from(items).map(item => item.dataset.origin); - Assert.deepEqual( - actualSites.sort(), - [...sites].sort(), - "Never-translate sites match" - ); - } - } - - /** - * Asserts the current order of the never-translate sites list. - * - * @param {object} expected - * @param {string[]} expected.sites - Expected site origins in order - * @returns {Promise<void>} - */ - async assertNeverTranslateSitesOrder({ sites }) { - const items = this.document.querySelectorAll( - ".translations-never-translate-site-item" - ); - const actualSites = Array.from(items).map(item => item.dataset.origin); - Assert.deepEqual(actualSites, sites, "Never-translate sites order matches"); - } - - /** - * Asserts the visibility state of the never-translate sites empty state. - * - * @param {object} expected - * @param {boolean} expected.visible - Whether empty state should be visible - * @returns {Promise<void>} - */ - async assertNeverTranslateSitesEmptyState({ visible }) { - const emptyRow = this.document.getElementById( - "translationsNeverTranslateSitesNoneRow" - ); - if (visible) { - ok( - emptyRow && !emptyRow.hidden, - "Never-translate sites empty state should be visible" - ); - } else { - ok( - !emptyRow || emptyRow.hidden, - "Never-translate sites empty state should be hidden" - ); - } - } -} - -/** * Recursively transforms all child nodes to have diacriticized text. This is useful * to spot multiple translations. * @@ -2965,23 +1761,6 @@ function createAttachmentMock( autoDownloadFromRemoteSettings ) { const pendingDownloads = []; - const pendingDownloadsWaiters = []; - - function notifyPendingDownload() { - const ready = pendingDownloadsWaiters.filter( - waiter => pendingDownloads.length >= waiter.count - ); - for (const waiter of ready) { - waiter.resolve(); - } - for (const waiter of ready) { - const index = pendingDownloadsWaiters.indexOf(waiter); - if (index !== -1) { - pendingDownloadsWaiters.splice(index, 1); - } - } - } - client.attachments.download = record => new Promise((resolve, reject) => { console.log("Download requested:", client.collectionName, record.name); @@ -2994,7 +1773,6 @@ function createAttachmentMock( resolve({ buffer }); } else { pendingDownloads.push({ record, resolve, reject }); - notifyPendingDownload(); } }); @@ -3012,47 +1790,11 @@ function createAttachmentMock( `Intentionally rejecting ${expectedDownloadCount} mocked downloads for "${client.collectionName}"` ); - const names = []; - const waitTick = () => new Promise(resolve => setTimeout(resolve, 0)); - - const rejectNext = () => { - const download = pendingDownloads.shift(); - if (!download) { - return false; - } - console.log(`Handling download:`, client.collectionName); - download.reject(new Error("Intentionally rejecting downloads.")); - names.push(download.record.name); - return true; - }; - - // Wait for the expected downloads to start arriving and reject them as they do. - while (names.length < expectedDownloadCount) { - await waitForPendingDownloads(names.length + 1); - while (names.length < expectedDownloadCount && rejectNext()) { - // Keep rejecting until we reach the expected count. - } - } - - // Drain any retries until the queue stays empty for a short idle window. - let idleTicks = 0; - const idleWindow = 20; - while (idleTicks < idleWindow) { - await waitTick(); - if (rejectNext()) { - idleTicks = 0; - } else { - idleTicks++; - } - } - - if (pendingDownloads.length) { - throw new Error( - `An unexpected download was found, only expected ${expectedDownloadCount} downloads` - ); - } - - return names.sort((a, b) => a.localeCompare(b)); + // Add 1 to account for the original attempt. + const attempts = TranslationsParent.MAX_DOWNLOAD_RETRIES + 1; + return downloadHandler(expectedDownloadCount * attempts, download => + download.reject(new Error("Intentionally rejecting downloads.")) + ); } async function downloadHandler(expectedDownloadCount, action) { @@ -3093,22 +1835,12 @@ function createAttachmentMock( ); } - function waitForPendingDownloads(expectedCount) { - if (pendingDownloads.length >= expectedCount) { - return Promise.resolve(); - } - return new Promise(resolve => { - pendingDownloadsWaiters.push({ count: expectedCount, resolve }); - }); - } - return { client, pendingDownloads, resolvePendingDownloads, rejectPendingDownloads, assertNoNewDownloads, - waitForPendingDownloads, }; } @@ -3149,6 +1881,15 @@ function createRecordsForLanguagePair(fromLang, toLang, splitVocab = false) { : [{ fileType: "vocab", name: `vocab.${lang}.spm` }]), ]; + const attachment = { + hash: `${crypto.randomUUID()}`, + size: `123`, + filename: `model.${lang}.intgemm.alphas.bin`, + location: `main-workspace/translations-models/${crypto.randomUUID()}.bin`, + mimetype: "application/octet-stream", + isDownloaded: false, + }; + const expectedLength = splitVocab ? RECORDS_PER_LANGUAGE_PAIR_SPLIT_VOCAB : RECORDS_PER_LANGUAGE_PAIR_SHARED_VOCAB; @@ -3160,15 +1901,6 @@ function createRecordsForLanguagePair(fromLang, toLang, splitVocab = false) { ); for (const { fileType, name } of models) { - const attachment = { - hash: `${crypto.randomUUID()}`, - size: "123", - filename: name, - location: `main-workspace/translations-models/${crypto.randomUUID()}.bin`, - mimetype: "application/octet-stream", - isDownloaded: false, - }; - records.push({ id: crypto.randomUUID(), name, @@ -3178,7 +1910,7 @@ function createRecordsForLanguagePair(fromLang, toLang, splitVocab = false) { version: TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX + ".0", last_modified: Date.now(), schema: Date.now(), - attachment: JSON.parse(JSON.stringify(attachment)), // Making a deep copy + attachment: JSON.parse(JSON.stringify(attachment)), // Making a deep copy. }); } return records; @@ -3541,6 +2273,14 @@ async function setupAboutPreferences( true // waitForLoad ); + let initTranslationsEvent; + if (Services.prefs.getBoolPref("browser.translations.newSettingsUI.enable")) { + initTranslationsEvent = BrowserTestUtils.waitForEvent( + document, + "translationsSettingsInit" + ); + } + const { remoteClients, removeMocks } = await createAndMockRemoteSettings({ languagePairs, }); @@ -3549,10 +2289,9 @@ async function setupAboutPreferences( const elements = await selectAboutPreferencesElements(); - const document = gBrowser.selectedBrowser.contentDocument; - const translationsSettingsTestUtils = new TranslationsSettingsTestUtils( - document - ); + if (Services.prefs.getBoolPref("browser.translations.newSettingsUI.enable")) { + await initTranslationsEvent; + } async function cleanup() { Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, ""); @@ -3571,7 +2310,6 @@ async function setupAboutPreferences( cleanup, remoteClients, elements, - translationsSettingsTestUtils, }; }