summaryrefslogtreecommitdiffstats
path: root/browser/components/translations
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/components/translations/TranslationsTelemetry.sys.mjs19
-rw-r--r--browser/components/translations/content/translationsPanel.inc.xhtml149
-rw-r--r--browser/components/translations/content/translationsPanel.js1132
-rw-r--r--browser/components/translations/jar.mn7
-rw-r--r--browser/components/translations/metrics.yaml30
-rw-r--r--browser/components/translations/moz.build10
-rw-r--r--browser/components/translations/tests/browser/browser.ini19
-rw-r--r--browser/components/translations/tests/browser/browser_manage_languages.js195
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_always_translate_language.js413
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_basics.js94
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_beta_langs.js66
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_button.js80
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_cancel.js32
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_gear.js42
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_never_translate_language.js343
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_never_translate_site.js432
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_retry.js80
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_switch_languages.js97
-rw-r--r--browser/components/translations/tests/browser/browser_translations_telemetry_open_panel.js73
-rw-r--r--browser/components/translations/tests/browser/browser_translations_telemetry_translation_failure.js195
-rw-r--r--browser/components/translations/tests/browser/browser_translations_telemetry_translation_request.js173
-rw-r--r--browser/components/translations/tests/browser/head.js348
22 files changed, 4029 insertions, 0 deletions
diff --git a/browser/components/translations/TranslationsTelemetry.sys.mjs b/browser/components/translations/TranslationsTelemetry.sys.mjs
new file mode 100644
index 0000000000..7b6bdfb830
--- /dev/null
+++ b/browser/components/translations/TranslationsTelemetry.sys.mjs
@@ -0,0 +1,19 @@
+/* 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/. */
+
+/**
+ * Telemetry functions for Translations desktop UI
+ */
+export class TranslationsTelemetry {
+ /**
+ * Records a telemetry event when the translations panel is opened.
+ *
+ * @param {boolean} openedFromAppMenu
+ */
+ static onOpenPanel(openedFromAppMenu) {
+ Glean.translationsPanel.open.record({
+ opened_from: openedFromAppMenu ? "appMenu" : "translationsButton",
+ });
+ }
+}
diff --git a/browser/components/translations/content/translationsPanel.inc.xhtml b/browser/components/translations/content/translationsPanel.inc.xhtml
new file mode 100644
index 0000000000..df08469822
--- /dev/null
+++ b/browser/components/translations/content/translationsPanel.inc.xhtml
@@ -0,0 +1,149 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html:template id="template-translations-panel">
+<panel id="translations-panel"
+ class="panel-no-padding translations-panel"
+ type="arrow"
+ role="alertdialog"
+ noautofocus="true"
+ aria-labelledby="translations-panel-main-header-label"
+ orient="vertical">
+ <panelmultiview id="translations-panel-multiview"
+ mainViewId="translations-panel-view-default">
+ <panelview id="translations-panel-view-default"
+ class="PanelUI-subView translations-panel-view"
+ role="document"
+ showheader="true">
+ <hbox class="panel-header translations-panel-header">
+ <html:h1>
+ <html:span id="translations-panel-header"></html:span>
+ </html:h1>
+ <toolbarbutton id="translations-panel-settings" class="panel-info-button"
+ data-l10n-id="translations-panel-settings-button"
+ closemenu="none"
+ oncommand="TranslationsPanel.openSettingsPopup(this)">
+ <image class="translations-panel-gear-icon" />
+ <menupopup>
+ <menuitem class="always-translate-language-menuitem"
+ data-l10n-id="translations-panel-settings-always-translate-unknown-language"
+ type="checkbox"
+ checked="false"
+ autocheck="false"
+ oncommand="TranslationsPanel.onAlwaysTranslateLanguage()"/>
+ <menuitem class="never-translate-language-menuitem"
+ data-l10n-id="translations-panel-settings-never-translate-unknown-language"
+ type="checkbox"
+ checked="false"
+ autocheck="false"
+ oncommand="TranslationsPanel.onNeverTranslateLanguage()"/>
+ <menuitem class="never-translate-site-menuitem"
+ data-l10n-id="translations-panel-settings-never-translate-site"
+ type="checkbox"
+ checked="false"
+ autocheck="false"
+ oncommand="TranslationsPanel.onNeverTranslateSite()"/>
+ <menuseparator/>
+ <menuitem data-l10n-id="translations-panel-settings-manage-languages"
+ oncommand="TranslationsPanel.openManageLanguages()"/>
+ </menupopup>
+ </toolbarbutton>
+ </hbox>
+
+ <vbox class="translations-panel-content">
+ <vbox id="translations-panel-lang-selection">
+ <label data-l10n-id="translations-panel-from-label"></label>
+ <menulist id="translations-panel-from"
+ flex="1"
+ value="detect"
+ size="large"
+ oncommand="TranslationsPanel.onChangeLanguages(event)">
+ <menupopup id="translations-panel-from-menupopup"
+ class="translations-panel-language-menupopup-from">
+ <menuitem data-l10n-id="translations-panel-choose-language" value=""></menuitem>
+ <!-- The list of <menuitem> will be dynamically inserted. -->
+ </menupopup>
+ </menulist>
+
+ <label data-l10n-id="translations-panel-to-label"></label>
+ <menulist id="translations-panel-to"
+ flex="1"
+ value="detect"
+ size="large"
+ oncommand="TranslationsPanel.onChangeLanguages(event)">
+ <menupopup id="translations-panel-to-menupopup"
+ class="translations-panel-language-menupopup-to">
+ <menuitem data-l10n-id="translations-panel-choose-language" value=""></menuitem>
+ <!-- The list of <menuitem> will be dynamically inserted. -->
+ </menupopup>
+ </menulist>
+ </vbox>
+
+ <vbox id="translations-panel-error">
+ <hbox class="translations-panel-error-header">
+ <image class="translations-panel-error-icon translations-panel-error-header-icon" />
+ <description id="translations-panel-error-message"></description>
+ </hbox>
+ <hbox id="translations-panel-error-message-hint"></hbox>
+ <hbox pack="end">
+ <button id="translations-panel-translate-hint-action" />
+ </hbox>
+ </vbox>
+ </vbox>
+
+ <hbox class="panel-footer translations-panel-footer">
+ <button id="translations-panel-restore-button"
+ class="subviewbutton panel-subview-footer-button"
+ oncommand="TranslationsPanel.onRestore(event);"
+ data-l10n-id="translations-panel-restore-button"
+ tabindex="0">
+ </button>
+ <button id="translations-panel-not-now"
+ class="subviewbutton panel-subview-footer-button"
+ oncommand="TranslationsPanel.onCancel(event);"
+ data-l10n-id="translations-panel-translate-cancel"
+ tabindex="0">
+ </button>
+ <button id="translations-panel-translate"
+ class="subviewbutton panel-subview-footer-button"
+ oncommand="TranslationsPanel.onTranslate(event);"
+ data-l10n-id="translations-panel-translate-button"
+ default="true"
+ tabindex="0">
+ </button>
+ </hbox>
+ </panelview>
+
+ <panelview id="translations-panel-view-unsupported-language"
+ class="PanelUI-subView translations-panel-view"
+ role="document"
+ showheader="true">
+ <hbox class="panel-header translations-panel-header">
+ <image class="translations-panel-error-icon" />
+ <html:h1>
+ <html:span data-l10n-id="translations-panel-error-unsupported"></html:span>
+ </html:h1>
+ </hbox>
+
+ <vbox class="translations-panel-content">
+ <description id="translations-panel-error-unsupported-hint"></description>
+ </vbox>
+
+ <hbox class="panel-footer translations-panel-footer">
+ <button class="subviewbutton panel-subview-footer-button"
+ oncommand="TranslationsPanel.onChangeSourceLanguage(event);"
+ data-l10n-id="translations-panel-error-change-button"
+ tabindex="0">
+ </button>
+ <button class="subviewbutton panel-subview-footer-button"
+ oncommand="TranslationsPanel.onCancel(event);"
+ data-l10n-id="translations-panel-error-dismiss-button"
+ default="true"
+ tabindex="0">
+ </button>
+ </hbox>
+ </panelview>
+ </panelmultiview>
+</panel>
+</html:template>
diff --git a/browser/components/translations/content/translationsPanel.js b/browser/components/translations/content/translationsPanel.js
new file mode 100644
index 0000000000..3d7d5be5fc
--- /dev/null
+++ b/browser/components/translations/content/translationsPanel.js
@@ -0,0 +1,1132 @@
+/* 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/. */
+
+/* eslint-env mozilla/browser-window */
+
+/* eslint-disable jsdoc/valid-types */
+/**
+ * @typedef {import("../../../../toolkit/components/translations/translations").LangTags} LangTags
+ */
+/* eslint-enable jsdoc/valid-types */
+
+ChromeUtils.defineESModuleGetters(this, {
+ TranslationsTelemetry:
+ "chrome://browser/content/translations/TranslationsTelemetry.sys.mjs",
+});
+
+/**
+ * The set of actions that can occur from interaction with the
+ * translations panel.
+ */
+const PageAction = Object.freeze({
+ NO_CHANGE: "NO_CHANGE",
+ HIDE_BUTTON: "HIDE_BUTTON",
+ RESTORE_PAGE: "RESTORE_PAGE",
+ TRANSLATE_PAGE: "TRANSLATE_PAGE",
+});
+
+/**
+ * A mechanism for determining the next relevant page action
+ * based on the current translated state of the page and the state
+ * of the persistent options in the translations panel settings.
+ */
+class CheckboxStateMachine {
+ /**
+ * Whether or not translations is active on the page.
+ *
+ * @type {boolean}
+ */
+ #translationsActive = false;
+
+ /**
+ * Whether the always-translate-language menuitem is checked
+ * in the translations panel settings menu.
+ *
+ * @type {boolean}
+ */
+ #alwaysTranslateLanguage = false;
+
+ /**
+ * Whether the never-translate-language menuitem is checked
+ * in the translations panel settings menu.
+ *
+ * @type {boolean}
+ */
+ #neverTranslateLanguage = false;
+
+ /**
+ * Whether the never-translate-site menuitem is checked
+ * in the translations panel settings menu.
+ *
+ * @type {boolean}
+ */
+ #neverTranslateSite = false;
+
+ /**
+ * @param {boolean} translationsActive
+ * @param {boolean} alwaysTranslateLanguage
+ * @param {boolean} neverTranslateLanguage
+ * @param {boolean} neverTranslateSite
+ */
+ constructor(
+ translationsActive,
+ alwaysTranslateLanguage,
+ neverTranslateLanguage,
+ neverTranslateSite
+ ) {
+ this.#translationsActive = translationsActive;
+ this.#alwaysTranslateLanguage = alwaysTranslateLanguage;
+ this.#neverTranslateLanguage = neverTranslateLanguage;
+ this.#neverTranslateSite = neverTranslateSite;
+ }
+
+ /**
+ * Accepts four integers that are either 0 or 1 and returns
+ * a single, unique number for each possible combination of
+ * values.
+ *
+ * @param {number} translationsActive
+ * @param {number} alwaysTranslateLanguage
+ * @param {number} neverTranslateLanguage
+ * @param {number} neverTranslateSite
+ *
+ * @returns {number} - An integer representation of the state
+ */
+ static #computeState(
+ translationsActive,
+ alwaysTranslateLanguage,
+ neverTranslateLanguage,
+ neverTranslateSite
+ ) {
+ return (
+ (translationsActive << 3) |
+ (alwaysTranslateLanguage << 2) |
+ (neverTranslateLanguage << 1) |
+ neverTranslateSite
+ );
+ }
+
+ /**
+ * Returns the current state of the data members as a single number.
+ *
+ * @returns {number} - An integer representation of the state
+ */
+ #state() {
+ return CheckboxStateMachine.#computeState(
+ Number(this.#translationsActive),
+ Number(this.#alwaysTranslateLanguage),
+ Number(this.#neverTranslateLanguage),
+ Number(this.#neverTranslateSite)
+ );
+ }
+
+ /**
+ * Returns the next page action to take when the always-translate-language
+ * menuitem is toggled in the translations panel settings menu.
+ *
+ * @returns {PageAction}
+ */
+ onAlwaysTranslateLanguage() {
+ switch (this.#state()) {
+ case CheckboxStateMachine.#computeState(1, 1, 0, 1):
+ case CheckboxStateMachine.#computeState(1, 1, 0, 0): {
+ return PageAction.RESTORE_PAGE;
+ }
+ case CheckboxStateMachine.#computeState(0, 0, 1, 0):
+ case CheckboxStateMachine.#computeState(0, 0, 0, 0): {
+ return PageAction.TRANSLATE_PAGE;
+ }
+ }
+ return PageAction.NO_CHANGE;
+ }
+
+ /**
+ * Returns the next page action to take when the never-translate-language
+ * menuitem is toggled in the translations panel settings menu.
+ *
+ * @returns {PageAction}
+ */
+ onNeverTranslateLanguage() {
+ switch (this.#state()) {
+ case CheckboxStateMachine.#computeState(1, 1, 0, 1):
+ case CheckboxStateMachine.#computeState(1, 1, 0, 0):
+ case CheckboxStateMachine.#computeState(1, 0, 0, 1):
+ case CheckboxStateMachine.#computeState(1, 0, 0, 0): {
+ return PageAction.RESTORE_PAGE;
+ }
+ case CheckboxStateMachine.#computeState(0, 1, 0, 0):
+ case CheckboxStateMachine.#computeState(0, 0, 0, 0): {
+ return PageAction.HIDE_BUTTON;
+ }
+ }
+ return PageAction.NO_CHANGE;
+ }
+
+ /**
+ * Returns the next page action to take when the never-translate-site
+ * menuitem is toggled in the translations panel settings menu.
+ *
+ * @returns {PageAction}
+ */
+ onNeverTranslateSite() {
+ switch (this.#state()) {
+ case CheckboxStateMachine.#computeState(1, 1, 0, 0):
+ case CheckboxStateMachine.#computeState(1, 0, 1, 0):
+ case CheckboxStateMachine.#computeState(1, 0, 0, 0): {
+ return PageAction.RESTORE_PAGE;
+ }
+ case CheckboxStateMachine.#computeState(0, 1, 0, 0):
+ case CheckboxStateMachine.#computeState(0, 0, 0, 0): {
+ return PageAction.HIDE_BUTTON;
+ }
+ case CheckboxStateMachine.#computeState(0, 1, 0, 1): {
+ return PageAction.TRANSLATE_PAGE;
+ }
+ }
+ return PageAction.NO_CHANGE;
+ }
+}
+
+/**
+ * This singleton class controls the Translations popup panel.
+ *
+ * This component is a `/browser` component, and the actor is a `/toolkit` actor, so care
+ * must be taken to keep the presentation (this component) from the state management
+ * (the Translations actor). This class reacts to state changes coming from the
+ * Translations actor.
+ */
+var TranslationsPanel = new (class {
+ /** @type {Console?} */
+ #console;
+
+ /**
+ * The cached detected languages for both the document and the user.
+ *
+ * @type {null | LangTags}
+ */
+ detectedLanguages = null;
+
+ /**
+ * Lazily get a console instance.
+ *
+ * @returns {Console}
+ */
+ get console() {
+ if (!this.#console) {
+ this.#console = console.createInstance({
+ maxLogLevelPref: "browser.translations.logLevel",
+ prefix: "Translations",
+ });
+ }
+ return this.#console;
+ }
+
+ /**
+ * Where the lazy elements are stored.
+ *
+ * @type {Record<string, Element>?}
+ */
+ #lazyElements;
+
+ /**
+ * Lazily creates the dom elements, and lazily selects them.
+ *
+ * @returns {Record<string, Element>}
+ */
+ get elements() {
+ if (!this.#lazyElements) {
+ // Lazily turn the template into a DOM element.
+ /** @type {HTMLTemplateElement} */
+ const wrapper = document.getElementById("template-translations-panel");
+ const panel = wrapper.content.firstElementChild;
+ wrapper.replaceWith(wrapper.content);
+
+ const settingsButton = document.getElementById(
+ "translations-panel-settings"
+ );
+ // Clone the settings toolbarbutton across all the views.
+ for (const header of panel.querySelectorAll(".panel-header")) {
+ if (header.contains(settingsButton)) {
+ continue;
+ }
+ const settingsButtonClone = settingsButton.cloneNode(true);
+ settingsButtonClone.removeAttribute("id");
+ header.appendChild(settingsButtonClone);
+ }
+
+ // Lazily select the elements.
+ this.#lazyElements = {
+ panel,
+ settingsButton,
+ // The rest of the elements are set by the getter below.
+ };
+
+ /**
+ * Define a getter on #lazyElements that gets the element by an id
+ * or class name.
+ */
+ const getter = (name, discriminator) => {
+ let element;
+ Object.defineProperty(this.#lazyElements, name, {
+ get: () => {
+ if (!element) {
+ if (discriminator[0] === ".") {
+ // Lookup by class
+ element = document.querySelector(discriminator);
+ } else {
+ // Lookup by id
+ element = document.getElementById(discriminator);
+ }
+ }
+ if (!element) {
+ throw new Error(
+ `Could not find "${name}" at "#${discriminator}".`
+ );
+ }
+ return element;
+ },
+ });
+ };
+
+ // Getters by id
+ getter("appMenuButton", "PanelUI-menu-button");
+ getter("button", "translations-button");
+ getter("buttonLocale", "translations-button-locale");
+ getter("buttonCircleArrows", "translations-button-circle-arrows");
+ getter("defaultTranslate", "translations-panel-translate");
+ getter("error", "translations-panel-error");
+ getter("errorMessage", "translations-panel-error-message");
+ getter("errorMessageHint", "translations-panel-error-message-hint");
+ getter("errorHintAction", "translations-panel-translate-hint-action");
+ getter("fromMenuList", "translations-panel-from");
+ getter("header", "translations-panel-header");
+ getter("langSelection", "translations-panel-lang-selection");
+ getter("multiview", "translations-panel-multiview");
+ getter("notNowButton", "translations-panel-not-now");
+ getter("restoreButton", "translations-panel-restore-button");
+ getter("toMenuList", "translations-panel-to");
+ getter("unsupportedHint", "translations-panel-error-unsupported-hint");
+
+ // Getters by class
+ getter(
+ "alwaysTranslateLanguageMenuItem",
+ ".always-translate-language-menuitem"
+ );
+ getter(
+ "neverTranslateLanguageMenuItem",
+ ".never-translate-language-menuitem"
+ );
+ getter("neverTranslateSiteMenuItem", ".never-translate-site-menuitem");
+ }
+
+ return this.#lazyElements;
+ }
+
+ /**
+ * Cache the last command used for error hints so that it can be later removed.
+ */
+ #lastHintCommand = null;
+
+ /**
+ * @param {object} options
+ * @param {string} options.message - l10n id
+ * @param {string} options.hint - l10n id
+ * @param {string} options.actionText - l10n id
+ * @param {Function} options.actionCommand - The action to perform.
+ */
+ #showError({
+ message,
+ hint,
+ actionText: hintCommandText,
+ actionCommand: hintCommand,
+ }) {
+ const { error, errorMessage, errorMessageHint, errorHintAction } =
+ this.elements;
+ error.hidden = false;
+ document.l10n.setAttributes(errorMessage, message);
+
+ if (hint) {
+ errorMessageHint.hidden = false;
+ document.l10n.setAttributes(errorMessageHint, hint);
+ } else {
+ errorMessageHint.hidden = true;
+ }
+
+ if (hintCommand && hintCommandText) {
+ errorHintAction.removeEventListener("command", this.#lastHintCommand);
+ this.#lastHintCommand = hintCommand;
+ errorHintAction.addEventListener("command", hintCommand);
+ errorHintAction.hidden = false;
+ document.l10n.setAttributes(errorHintAction, hintCommandText);
+ } else {
+ errorHintAction.hidden = true;
+ }
+ }
+
+ /**
+ * @returns {TranslationsParent}
+ */
+ #getTranslationsActor() {
+ const actor =
+ gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor(
+ "Translations"
+ );
+
+ if (!actor) {
+ throw new Error("Unable to get the TranslationsParent");
+ }
+ return actor;
+ }
+
+ /**
+ * Fetches the language tags for the document and the user and caches the results
+ * Use `#getCachedDetectedLanguages` when the lang tags do not need to be re-fetched.
+ * This requires a bit of work to do, so prefer the cached version when possible.
+ *
+ * @returns {Promise<LangTags>}
+ */
+ async #fetchDetectedLanguages() {
+ this.detectedLanguages =
+ await this.#getTranslationsActor().getLangTagsForTranslation();
+ return this.detectedLanguages;
+ }
+
+ /**
+ * If the detected language tags have been retrieved previously, return the cached
+ * version. Otherwise do a fresh lookup of the document's language tag.
+ *
+ * @returns {Promise<LangTags>}
+ */
+ async #getCachedDetectedLanguages() {
+ if (!this.detectedLanguages) {
+ return this.#fetchDetectedLanguages();
+ }
+ return this.detectedLanguages;
+ }
+
+ /**
+ * @type {"initialized" | "error" | "uninitialized"}
+ */
+ #langListsPhase = "uninitialized";
+
+ /**
+ * Builds the <menulist> of languages for both the "from" and "to". This can be
+ * called every time the popup is shown, as it will retry when there is an error
+ * (such as a network error) or be a noop if it's already initialized.
+ *
+ * TODO(Bug 1813796) This needs to be updated when the supported languages change
+ * via RemoteSettings.
+ */
+ async #ensureLangListsBuilt() {
+ switch (this.#langListsPhase) {
+ case "initialized":
+ // This has already been initialized.
+ return;
+ case "error":
+ // Attempt to re-initialize.
+ this.#langListsPhase = "uninitialized";
+ break;
+ case "uninitialized":
+ // Ready to initialize.
+ break;
+ default:
+ this.console.error("Unknown langList phase", this.#langListsPhase);
+ }
+
+ try {
+ /** @type {SupportedLanguages} */
+ const { languagePairs, fromLanguages, toLanguages } =
+ await this.#getTranslationsActor().getSupportedLanguages();
+
+ // Verify that we are in a proper state.
+ if (languagePairs.length === 0) {
+ throw new Error("No translation languages were retrieved.");
+ }
+
+ const { panel } = this.elements;
+ const fromPopups = panel.querySelectorAll(
+ ".translations-panel-language-menupopup-from"
+ );
+ const toPopups = panel.querySelectorAll(
+ ".translations-panel-language-menupopup-to"
+ );
+
+ for (const popup of fromPopups) {
+ for (const { langTag, isBeta, displayName } of fromLanguages) {
+ const fromMenuItem = document.createXULElement("menuitem");
+ fromMenuItem.setAttribute("value", langTag);
+ if (isBeta) {
+ document.l10n.setAttributes(
+ fromMenuItem,
+ "translations-panel-displayname-beta",
+ { language: displayName }
+ );
+ } else {
+ fromMenuItem.setAttribute("label", displayName);
+ }
+ popup.appendChild(fromMenuItem);
+ }
+ }
+
+ for (const popup of toPopups) {
+ for (const { langTag, isBeta, displayName } of toLanguages) {
+ const toMenuItem = document.createXULElement("menuitem");
+ toMenuItem.setAttribute("value", langTag);
+ if (isBeta) {
+ document.l10n.setAttributes(
+ toMenuItem,
+ "translations-panel-displayname-beta",
+ { language: displayName }
+ );
+ } else {
+ toMenuItem.setAttribute("label", displayName);
+ }
+ popup.appendChild(toMenuItem);
+ }
+ }
+
+ this.#langListsPhase = "initialized";
+ } catch (error) {
+ this.console.error(error);
+ this.#langListsPhase = "error";
+ }
+ }
+
+ /**
+ * Show the default view of choosing a source and target language.
+ *
+ * @param {boolean} force - Force the page to show translation options.
+ */
+ async #showDefaultView(force = false) {
+ const {
+ fromMenuList,
+ multiview,
+ panel,
+ error,
+ toMenuList,
+ defaultTranslate,
+ langSelection,
+ } = this.elements;
+
+ if (this.#langListsPhase === "error") {
+ // There was an error, display it in the view rather than the language
+ // dropdowns.
+ const { restoreButton, notNowButton, header, errorHintAction } =
+ this.elements;
+
+ this.#showError({
+ message: "translations-panel-error-load-languages",
+ hint: "translations-panel-error-load-languages-hint",
+ actionText: "translations-panel-error-load-languages-hint-button",
+ actionCommand: () => this.#reloadLangList(),
+ });
+
+ document.l10n.setAttributes(header, "translations-panel-header");
+ defaultTranslate.disabled = true;
+ restoreButton.hidden = true;
+ notNowButton.hidden = false;
+ langSelection.hidden = true;
+ errorHintAction.disabled = false;
+ return;
+ }
+
+ // Remove any old selected values synchronously before asking for new ones.
+ fromMenuList.value = "";
+ error.hidden = true;
+ langSelection.hidden = false;
+
+ /** @type {null | LangTags} */
+ const langTags = await this.#fetchDetectedLanguages();
+ if (langTags?.isDocLangTagSupported || force) {
+ // Show the default view with the language selection
+ const { header, restoreButton, notNowButton } = this.elements;
+ document.l10n.setAttributes(header, "translations-panel-header");
+
+ if (langTags?.isDocLangTagSupported) {
+ fromMenuList.value = langTags?.docLangTag ?? "";
+ } else {
+ fromMenuList.value = "";
+ }
+ toMenuList.value = langTags?.userLangTag ?? "";
+
+ this.onChangeLanguages();
+
+ restoreButton.hidden = true;
+ notNowButton.hidden = false;
+ multiview.setAttribute("mainViewId", "translations-panel-view-default");
+ } else {
+ // Show the "unsupported language" view.
+ const { unsupportedHint } = this.elements;
+ multiview.setAttribute(
+ "mainViewId",
+ "translations-panel-view-unsupported-language"
+ );
+ let language;
+ if (langTags?.docLangTag) {
+ const displayNames = new Intl.DisplayNames(undefined, {
+ type: "language",
+ fallback: "none",
+ });
+ language = displayNames.of(langTags.docLangTag);
+ }
+ if (language) {
+ document.l10n.setAttributes(
+ unsupportedHint,
+ "translations-panel-error-unsupported-hint-known",
+ { language }
+ );
+ } else {
+ document.l10n.setAttributes(
+ unsupportedHint,
+ "translations-panel-error-unsupported-hint-unknown"
+ );
+ }
+ }
+
+ // Focus the "from" language, as it is the only field not set.
+ panel.addEventListener(
+ "ViewShown",
+ () => {
+ if (!fromMenuList.value) {
+ fromMenuList.focus();
+ }
+ if (!toMenuList.value) {
+ toMenuList.focus();
+ }
+ },
+ { once: true }
+ );
+ }
+
+ /**
+ * Updates the checked states of the settings menu checkboxes that
+ * pertain to languages.
+ */
+ async #updateSettingsMenuLanguageCheckboxStates() {
+ const { docLangTag, isDocLangTagSupported } =
+ await this.#getCachedDetectedLanguages();
+
+ const { panel } = this.elements;
+ const alwaysTranslateMenuItems = panel.querySelectorAll(
+ ".always-translate-language-menuitem"
+ );
+ const neverTranslateMenuItems = panel.querySelectorAll(
+ ".never-translate-language-menuitem"
+ );
+
+ if (
+ !docLangTag ||
+ !isDocLangTagSupported ||
+ docLangTag === new Intl.Locale(Services.locale.appLocaleAsBCP47).language
+ ) {
+ for (const menuitem of alwaysTranslateMenuItems) {
+ menuitem.disabled = true;
+ }
+ for (const menuitem of neverTranslateMenuItems) {
+ menuitem.disabled = true;
+ }
+ return;
+ }
+
+ const alwaysTranslateLanguage =
+ TranslationsParent.shouldAlwaysTranslateLanguage(docLangTag);
+ const neverTranslateLanguage =
+ TranslationsParent.shouldNeverTranslateLanguage(docLangTag);
+
+ for (const menuitem of alwaysTranslateMenuItems) {
+ menuitem.setAttribute(
+ "checked",
+ alwaysTranslateLanguage ? "true" : "false"
+ );
+ menuitem.disabled = false;
+ }
+ for (const menuitem of neverTranslateMenuItems) {
+ menuitem.setAttribute(
+ "checked",
+ neverTranslateLanguage ? "true" : "false"
+ );
+ menuitem.disabled = false;
+ }
+ }
+
+ /**
+ * Updates the checked states of the settings menu checkboxes that
+ * pertain to site permissions.
+ */
+ async #updateSettingsMenuSiteCheckboxStates() {
+ const { panel } = this.elements;
+ const neverTranslateSiteMenuItems = panel.querySelectorAll(
+ ".never-translate-site-menuitem"
+ );
+ const neverTranslateSite =
+ await this.#getTranslationsActor().shouldNeverTranslateSite();
+
+ for (const menuitem of neverTranslateSiteMenuItems) {
+ menuitem.setAttribute("checked", neverTranslateSite ? "true" : "false");
+ }
+ }
+
+ /**
+ * Populates the language-related settings menuitems by adding the
+ * localized display name of the document's detected language tag.
+ */
+ async #populateSettingsMenuItems() {
+ const { docLangTag } = await this.#getCachedDetectedLanguages();
+
+ const { panel } = this.elements;
+
+ const alwaysTranslateMenuItems = panel.querySelectorAll(
+ ".always-translate-language-menuitem"
+ );
+ const neverTranslateMenuItems = panel.querySelectorAll(
+ ".never-translate-language-menuitem"
+ );
+
+ /** @type {string | undefined} */
+ let docLangDisplayName;
+ if (docLangTag) {
+ const displayNames = new Services.intl.DisplayNames(undefined, {
+ type: "language",
+ fallback: "none",
+ });
+ // The display name will still be empty if the docLangTag is not known.
+ docLangDisplayName = displayNames.of(docLangTag);
+ }
+
+ for (const menuitem of alwaysTranslateMenuItems) {
+ if (docLangDisplayName) {
+ document.l10n.setAttributes(
+ menuitem,
+ "translations-panel-settings-always-translate-language",
+ { language: docLangDisplayName }
+ );
+ } else {
+ document.l10n.setAttributes(
+ menuitem,
+ "translations-panel-settings-always-translate-unknown-language"
+ );
+ }
+ }
+
+ for (const menuitem of neverTranslateMenuItems) {
+ if (docLangDisplayName) {
+ document.l10n.setAttributes(
+ menuitem,
+ "translations-panel-settings-never-translate-language",
+ { language: docLangDisplayName }
+ );
+ } else {
+ document.l10n.setAttributes(
+ menuitem,
+ "translations-panel-settings-never-translate-unknown-language"
+ );
+ }
+ }
+
+ await Promise.all([
+ this.#updateSettingsMenuLanguageCheckboxStates(),
+ this.#updateSettingsMenuSiteCheckboxStates(),
+ ]);
+ }
+
+ /**
+ * Configures the panel for the user to reset the page after it has been translated.
+ *
+ * @param {TranslationPair} translationPair
+ */
+ async #showRevisitView({ fromLanguage, toLanguage }) {
+ const { header, fromMenuList, toMenuList, restoreButton, notNowButton } =
+ this.elements;
+
+ fromMenuList.value = fromLanguage;
+ toMenuList.value = toLanguage;
+ this.onChangeLanguages();
+
+ restoreButton.hidden = false;
+ notNowButton.hidden = true;
+
+ const displayNames = new Services.intl.DisplayNames(undefined, {
+ type: "language",
+ });
+
+ document.l10n.setAttributes(header, "translations-panel-revisit-header", {
+ fromLanguage: displayNames.of(fromLanguage),
+ toLanguage: displayNames.of(toLanguage),
+ });
+ }
+
+ /**
+ * Handle the disable logic for when the menulist is changed for the "Translate to"
+ * on the "revisit" subview.
+ */
+ onChangeRevisitTo() {
+ const { revisitTranslate, revisitMenuList } = this.elements;
+ revisitTranslate.disabled = !revisitMenuList.value;
+ }
+
+ /**
+ * When changing the "dual" view's language, handle cases where the translate button
+ * should be disabled.
+ */
+ onChangeLanguages() {
+ const { defaultTranslate, toMenuList, fromMenuList } = this.elements;
+ const { requestedTranslationPair } =
+ this.#getTranslationsActor().languageState;
+ defaultTranslate.disabled =
+ // The translation languages are the same, don't allow this translation.
+ toMenuList.value === fromMenuList.value ||
+ // No "to" language was provided.
+ !toMenuList.value ||
+ // No "from" language was provided.
+ !fromMenuList.value ||
+ // The is the requested translation pair.
+ (requestedTranslationPair &&
+ requestedTranslationPair.fromLanguage === fromMenuList.value &&
+ requestedTranslationPair.toLanguage === toMenuList.value);
+ }
+
+ /**
+ * When a language is not supported and the menu is manually invoked, an error message
+ * is shown. This method switches the panel back to the language selection view.
+ * Note that this bypasses the showSubView method since the main view doesn't support
+ * a subview.
+ */
+ async onChangeSourceLanguage(event) {
+ const { panel } = this.elements;
+ panel.addEventListener("popuphidden", async () => {}, { once: true });
+ PanelMultiView.hidePopup(panel);
+
+ await this.#showDefaultView(true /* force this view to be shown */);
+
+ PanelMultiView.openPopup(panel, this.elements.appMenuButton, {
+ position: "bottomright topright",
+ triggeringEvent: event,
+ }).catch(error => this.console.error(error));
+ }
+
+ async #reloadLangList() {
+ try {
+ await this.#ensureLangListsBuilt();
+ await this.#showDefaultView();
+ } catch (error) {
+ this.elements.errorHintAction.disabled = false;
+ }
+ }
+
+ /**
+ * Opens the TranslationsPanel.
+ *
+ * @param {Event} event
+ */
+ async open(event) {
+ const { panel, button } = this.elements;
+
+ await this.#ensureLangListsBuilt();
+
+ const { requestedTranslationPair } =
+ this.#getTranslationsActor().languageState;
+
+ if (requestedTranslationPair) {
+ await this.#showRevisitView(requestedTranslationPair).catch(error => {
+ this.console.error(error);
+ });
+ } else {
+ await this.#showDefaultView().catch(error => {
+ this.console.error(error);
+ });
+ }
+
+ this.#populateSettingsMenuItems();
+
+ const [targetButton, openedFromAppMenu] = button.contains(event.target)
+ ? [button, false]
+ : [this.elements.appMenuButton, true];
+
+ panel.addEventListener(
+ "ViewShown",
+ () => TranslationsTelemetry.onOpenPanel(openedFromAppMenu),
+ { once: true }
+ );
+
+ PanelMultiView.openPopup(panel, targetButton, {
+ position: "bottomright topright",
+ triggerEvent: event,
+ }).catch(error => this.console.error(error));
+ }
+
+ /**
+ * Removes the translations button.
+ */
+ #hideTranslationsButton() {
+ const { button, buttonLocale, buttonCircleArrows } = this.elements;
+ button.hidden = true;
+ buttonLocale.hidden = true;
+ buttonCircleArrows.hidden = true;
+ button.removeAttribute("translationsactive");
+ }
+
+ /**
+ * Returns true if translations is currently active, otherwise false.
+ *
+ * @returns {boolean}
+ */
+ #isTranslationsActive() {
+ const { requestedTranslationPair } =
+ this.#getTranslationsActor().languageState;
+ return requestedTranslationPair !== null;
+ }
+
+ /**
+ * Handle the translation button being clicked when there are two language options.
+ */
+ async onTranslate() {
+ PanelMultiView.hidePopup(this.elements.panel);
+
+ const actor = this.#getTranslationsActor();
+ actor.translate(
+ this.elements.fromMenuList.value,
+ this.elements.toMenuList.value
+ );
+ }
+
+ onCancel() {
+ PanelMultiView.hidePopup(this.elements.panel);
+ }
+
+ /**
+ * A handler for opening the settings context menu.
+ */
+ openSettingsPopup(button) {
+ this.#updateSettingsMenuLanguageCheckboxStates();
+ this.#updateSettingsMenuSiteCheckboxStates();
+ const popup = button.querySelector("menupopup");
+ popup.openPopup(button);
+ }
+
+ /**
+ * Creates a new CheckboxStateMachine based on the current translated
+ * state of the page and the state of the persistent options in the
+ * translations panel settings.
+ *
+ * @returns {CheckboxStateMachine}
+ */
+ createCheckboxStateMachine() {
+ const {
+ alwaysTranslateLanguageMenuItem,
+ neverTranslateLanguageMenuItem,
+ neverTranslateSiteMenuItem,
+ } = this.elements;
+
+ const alwaysTranslateLanguage =
+ alwaysTranslateLanguageMenuItem.getAttribute("checked") === "true";
+ const neverTranslateLanguage =
+ neverTranslateLanguageMenuItem.getAttribute("checked") === "true";
+ const neverTranslateSite =
+ neverTranslateSiteMenuItem.getAttribute("checked") === "true";
+
+ return new CheckboxStateMachine(
+ this.#isTranslationsActive(),
+ alwaysTranslateLanguage,
+ neverTranslateLanguage,
+ neverTranslateSite
+ );
+ }
+
+ /**
+ * Redirect the user to about:preferences
+ */
+ openManageLanguages() {
+ const window =
+ gBrowser.selectedBrowser.browsingContext.top.embedderElement.ownerGlobal;
+ window.openTrustedLinkIn("about:preferences#general-translations", "tab");
+ }
+
+ /**
+ * Performs the given page action.
+ *
+ * @param {PageAction} pageAction
+ */
+ async #doPageAction(pageAction) {
+ switch (pageAction) {
+ case PageAction.NO_CHANGE: {
+ break;
+ }
+ case PageAction.HIDE_BUTTON: {
+ this.#hideTranslationsButton();
+ break;
+ }
+ case PageAction.RESTORE_PAGE: {
+ await this.onRestore();
+ break;
+ }
+ case PageAction.TRANSLATE_PAGE: {
+ await this.onTranslate();
+ break;
+ }
+ }
+ }
+
+ /**
+ * Updates the always-translate-language menuitem prefs and checked state.
+ * If auto-translate is currently active for the doc language, deactivates it.
+ * If auto-translate is currently inactive for the doc language, activates it.
+ */
+ async onAlwaysTranslateLanguage() {
+ const { docLangTag } = await this.#getCachedDetectedLanguages();
+ if (!docLangTag) {
+ throw new Error("Expected to have a document language tag.");
+ }
+ const pageAction =
+ this.createCheckboxStateMachine().onAlwaysTranslateLanguage();
+ TranslationsParent.toggleAlwaysTranslateLanguagePref(docLangTag);
+ this.#updateSettingsMenuLanguageCheckboxStates();
+ await this.#doPageAction(pageAction);
+ }
+
+ /**
+ * Updates the never-translate-language menuitem prefs and checked state.
+ * If never-translate is currently active for the doc language, deactivates it.
+ * If never-translate is currently inactive for the doc language, activates it.
+ */
+ async onNeverTranslateLanguage() {
+ const { docLangTag } = await this.#getCachedDetectedLanguages();
+ if (!docLangTag) {
+ throw new Error("Expected to have a document language tag.");
+ }
+ const pageAction =
+ this.createCheckboxStateMachine().onNeverTranslateLanguage();
+ TranslationsParent.toggleNeverTranslateLanguagePref(docLangTag);
+ this.#updateSettingsMenuLanguageCheckboxStates();
+ await this.#doPageAction(pageAction);
+ }
+
+ /**
+ * Updates the never-translate-site menuitem permissions and checked state.
+ * If never-translate is currently active for the site, deactivates it.
+ * If never-translate is currently inactive for the site, activates it.
+ */
+ async onNeverTranslateSite() {
+ const pageAction = this.createCheckboxStateMachine().onNeverTranslateSite();
+ await this.#getTranslationsActor().toggleNeverTranslateSitePermissions();
+ this.#updateSettingsMenuSiteCheckboxStates();
+ await this.#doPageAction(pageAction);
+ }
+
+ /**
+ * Handle the restore button being clicked.
+ */
+ async onRestore() {
+ const { panel } = this.elements;
+ PanelMultiView.hidePopup(panel);
+ const { docLangTag } = await this.#getCachedDetectedLanguages();
+ if (!docLangTag) {
+ throw new Error("Expected to have a document language tag.");
+ }
+
+ this.#getTranslationsActor().restorePage(docLangTag);
+ }
+
+ /**
+ * Set the state of the translations button in the URL bar.
+ *
+ * @param {CustomEvent} event
+ */
+ handleEvent = async event => {
+ switch (event.type) {
+ case "TranslationsParent:LanguageState":
+ const {
+ detectedLanguages,
+ requestedTranslationPair,
+ error,
+ isEngineReady,
+ } = event.detail;
+
+ const { panel, button, buttonLocale, buttonCircleArrows } =
+ this.elements;
+
+ const hasSupportedLanguage =
+ detectedLanguages?.docLangTag &&
+ detectedLanguages?.userLangTag &&
+ detectedLanguages?.isDocLangTagSupported;
+
+ if (detectedLanguages) {
+ // Ensure the cached detected languages are up to date, for instance whenever
+ // the user switches tabs.
+ TranslationsPanel.detectedLanguages = detectedLanguages;
+ }
+
+ /**
+ * Defer this check to the end of the `if` statement since it requires work.
+ */
+ const shouldNeverTranslate = async () => {
+ return Boolean(
+ TranslationsParent.shouldNeverTranslateLanguage(
+ detectedLanguages?.docLangTag
+ ) ||
+ // The site is present in the never-translate list.
+ (await this.#getTranslationsActor().shouldNeverTranslateSite())
+ );
+ };
+
+ if (
+ // We've already requested to translate this page, so always show the icon.
+ requestedTranslationPair ||
+ // There was an error translating, so always show the icon. This can happen
+ // when a user manually invokes the translation and we wouldn't normally show
+ // the icon.
+ error ||
+ // Finally check that this is a supported language that we should translate.
+ (hasSupportedLanguage && !(await shouldNeverTranslate()))
+ ) {
+ button.hidden = false;
+ if (requestedTranslationPair) {
+ // The translation is active, update the urlbar button.
+ button.setAttribute("translationsactive", true);
+ if (isEngineReady) {
+ // Show the locale of the page in the button.
+ buttonLocale.hidden = false;
+ buttonCircleArrows.hidden = true;
+ buttonLocale.innerText = requestedTranslationPair.toLanguage;
+ } else {
+ // Show the spinning circle arrows to indicate that the engine is
+ // still loading.
+ buttonCircleArrows.hidden = false;
+ buttonLocale.hidden = true;
+ }
+ } else {
+ // The translation is not active, update the urlbar button.
+ button.removeAttribute("translationsactive");
+ buttonLocale.hidden = true;
+ buttonCircleArrows.hidden = true;
+ }
+ } else {
+ this.#hideTranslationsButton();
+ }
+
+ switch (error) {
+ case null:
+ this.elements.error.hidden = true;
+ break;
+ case "engine-load-failure":
+ this.elements.error.hidden = false;
+ this.#showError({
+ message: "translations-panel-error-translating",
+ });
+ const targetButton = button.hidden
+ ? this.elements.appMenuButton
+ : button;
+
+ // Re-open the menu on an error.
+ PanelMultiView.openPopup(panel, targetButton, {
+ position: "bottomright topright",
+ }).catch(panelError => this.console.error(panelError));
+
+ break;
+ default:
+ console.error("Unknown translation error", error);
+ }
+ break;
+ }
+ };
+})();
diff --git a/browser/components/translations/jar.mn b/browser/components/translations/jar.mn
new file mode 100644
index 0000000000..63920beb19
--- /dev/null
+++ b/browser/components/translations/jar.mn
@@ -0,0 +1,7 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+browser.jar:
+ content/browser/translations/translationsPanel.js (content/translationsPanel.js)
+ content/browser/translations/TranslationsTelemetry.sys.mjs (TranslationsTelemetry.sys.mjs)
diff --git a/browser/components/translations/metrics.yaml b/browser/components/translations/metrics.yaml
new file mode 100644
index 0000000000..6042fa2332
--- /dev/null
+++ b/browser/components/translations/metrics.yaml
@@ -0,0 +1,30 @@
+# This Source Code Form is subject to the terms of the Mozilla Public↩
+# License, v. 2.0. If a copy of the MPL was not distributed with this↩
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.↩
+
+# Adding a new metric? We have docs for that!↩
+# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html↩
+
+---
+$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
+$tags:
+ - 'Firefox :: Translation'
+
+translations.panel:
+ open:
+ type: event
+ description: >
+ Triggers when the translations panel is opened.
+ extra_keys:
+ opened_from:
+ type: string
+ description: The method by which the translations panel was opened.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1835502
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1835502#c7
+ data_sensitivity:
+ - interaction
+ notification_emails:
+ - translations-telemetry-alerts@mozilla.com
+ expires: 122
diff --git a/browser/components/translations/moz.build b/browser/components/translations/moz.build
new file mode 100644
index 0000000000..4fd489745f
--- /dev/null
+++ b/browser/components/translations/moz.build
@@ -0,0 +1,10 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Firefox", "Translation")
+
+BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.ini"]
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/browser/components/translations/tests/browser/browser.ini b/browser/components/translations/tests/browser/browser.ini
new file mode 100644
index 0000000000..fd67597972
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser.ini
@@ -0,0 +1,19 @@
+[DEFAULT]
+support-files =
+ head.js
+ !/toolkit/components/translations/tests/browser/shared-head.js
+ !/toolkit/components/translations/tests/browser/translations-test.mjs
+[browser_manage_languages.js]
+[browser_translations_panel_always_translate_language.js]
+[browser_translations_panel_basics.js]
+[browser_translations_panel_beta_langs.js]
+[browser_translations_panel_button.js]
+[browser_translations_panel_cancel.js]
+[browser_translations_panel_gear.js]
+[browser_translations_panel_never_translate_language.js]
+[browser_translations_panel_never_translate_site.js]
+[browser_translations_panel_retry.js]
+[browser_translations_panel_switch_languages.js]
+[browser_translations_telemetry_open_panel.js]
+[browser_translations_telemetry_translation_failure.js]
+[browser_translations_telemetry_translation_request.js]
diff --git a/browser/components/translations/tests/browser/browser_manage_languages.js b/browser/components/translations/tests/browser/browser_manage_languages.js
new file mode 100644
index 0000000000..651029ad86
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_manage_languages.js
@@ -0,0 +1,195 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const frenchModels = [
+ "lex.50.50.enfr.s2t.bin",
+ "lex.50.50.fren.s2t.bin",
+ "model.enfr.intgemm.alphas.bin",
+ "model.fren.intgemm.alphas.bin",
+ "vocab.enfr.spm",
+ "vocab.fren.spm",
+];
+
+add_task(async function test_about_preferences_manage_languages() {
+ const {
+ cleanup,
+ remoteClients,
+ elements: {
+ downloadAllLabel,
+ downloadAll,
+ deleteAll,
+ frenchLabel,
+ frenchDownload,
+ frenchDelete,
+ spanishLabel,
+ spanishDownload,
+ spanishDelete,
+ },
+ } = await setupAboutPreferences([
+ { fromLang: "en", toLang: "fr" },
+ { fromLang: "fr", toLang: "en" },
+ { fromLang: "en", toLang: "es" },
+ { fromLang: "es", toLang: "en" },
+ ]);
+
+ is(
+ downloadAllLabel.getAttribute("data-l10n-id"),
+ "translations-manage-all-language",
+ "The first row is all of the languages."
+ );
+ is(frenchLabel.textContent, "French", "There is a French row.");
+ is(spanishLabel.textContent, "Spanish", "There is a Spanish row.");
+
+ await assertVisibility({
+ message: "Everything starts out as available to download",
+ visible: { downloadAll, frenchDownload, spanishDownload },
+ hidden: { deleteAll, frenchDelete, spanishDelete },
+ });
+
+ click(frenchDownload, "Downloading French");
+
+ Assert.deepEqual(
+ await remoteClients.translationModels.resolvePendingDownloads(
+ frenchModels.length
+ ),
+ frenchModels,
+ "French models were downloaded."
+ );
+
+ await assertVisibility({
+ message: "French can now be deleted, and delete all is available.",
+ visible: { downloadAll, deleteAll, frenchDelete, spanishDownload },
+ hidden: { frenchDownload, spanishDelete },
+ });
+
+ click(frenchDelete, "Deleting French");
+
+ await assertVisibility({
+ message: "Everything can be downloaded.",
+ visible: { downloadAll, frenchDownload, spanishDownload },
+ hidden: { deleteAll, frenchDelete, spanishDelete },
+ });
+
+ click(downloadAll, "Downloading all languages.");
+
+ const allModels = [
+ "lex.50.50.enes.s2t.bin",
+ "lex.50.50.enfr.s2t.bin",
+ "lex.50.50.esen.s2t.bin",
+ "lex.50.50.fren.s2t.bin",
+ "model.enes.intgemm.alphas.bin",
+ "model.enfr.intgemm.alphas.bin",
+ "model.esen.intgemm.alphas.bin",
+ "model.fren.intgemm.alphas.bin",
+ "vocab.enes.spm",
+ "vocab.enfr.spm",
+ "vocab.esen.spm",
+ "vocab.fren.spm",
+ ];
+ Assert.deepEqual(
+ await remoteClients.translationModels.resolvePendingDownloads(
+ allModels.length
+ ),
+ allModels,
+ "All models were downloaded."
+ );
+ Assert.deepEqual(
+ await remoteClients.languageIdModels.resolvePendingDownloads(1),
+ ["lid.176.ftz"],
+ "Language ID model was downloaded."
+ );
+ Assert.deepEqual(
+ await remoteClients.translationsWasm.resolvePendingDownloads(2),
+ ["bergamot-translator", "fasttext-wasm"],
+ "Wasm was downloaded."
+ );
+
+ await assertVisibility({
+ message: "Everything can be deleted.",
+ visible: { deleteAll, frenchDelete, spanishDelete },
+ hidden: { downloadAll, frenchDownload, spanishDownload },
+ });
+
+ click(deleteAll, "Deleting all languages.");
+
+ await assertVisibility({
+ message: "Everything can be downloaded again",
+ visible: { downloadAll, frenchDownload, spanishDownload },
+ hidden: { deleteAll, frenchDelete, spanishDelete },
+ });
+
+ click(frenchDownload, "Downloading French.");
+ click(spanishDownload, "Downloading Spanish.");
+
+ Assert.deepEqual(
+ await remoteClients.translationModels.resolvePendingDownloads(
+ allModels.length
+ ),
+ allModels,
+ "All models were downloaded again."
+ );
+
+ remoteClients.translationsWasm.assertNoNewDownloads();
+ remoteClients.languageIdModels.assertNoNewDownloads();
+
+ await assertVisibility({
+ message: "Everything is downloaded again.",
+ visible: { deleteAll, frenchDelete, spanishDelete },
+ hidden: { downloadAll, frenchDownload, spanishDownload },
+ });
+
+ return cleanup();
+});
+
+add_task(async function test_about_preferences_download_reject() {
+ const {
+ cleanup,
+ remoteClients,
+ elements: { document, frenchDownload },
+ } = await setupAboutPreferences([
+ { fromLang: "en", toLang: "fr" },
+ { fromLang: "fr", toLang: "en" },
+ { fromLang: "en", toLang: "es" },
+ { fromLang: "es", toLang: "en" },
+ ]);
+
+ click(frenchDownload, "Downloading French");
+
+ is(
+ maybeGetByL10nId("translations-manage-error-download", document),
+ null,
+ "No error messages are present."
+ );
+
+ const errors = await captureTranslationsError(() =>
+ remoteClients.translationModels.rejectPendingDownloads(frenchModels.length)
+ );
+
+ ok(
+ !!errors.length,
+ `The errors for download should have been reported, found ${errors.length} errors`
+ );
+ for (const { error } of errors) {
+ is(
+ error?.message,
+ "Failed to download file.",
+ "The error reported was a download error."
+ );
+ }
+
+ await TestUtils.waitForCondition(
+ () => maybeGetByL10nId("translations-manage-error-download", document),
+ "The error message is now visible."
+ );
+
+ click(frenchDownload, "Attempting to download French again", document);
+ is(
+ maybeGetByL10nId("translations-manage-error-download", document),
+ null,
+ "The error message is hidden again."
+ );
+
+ return cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_always_translate_language.js b/browser/components/translations/tests/browser/browser_translations_panel_always_translate_language.js
new file mode 100644
index 0000000000..4bc2c7dc15
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_always_translate_language.js
@@ -0,0 +1,413 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the effect of toggling the always-translate-language menuitem.
+ * Checking the box on an untranslated page should immediately translate the page.
+ * Unchecking the box on a translated page should immediately restore the page.
+ */
+add_task(async function test_toggle_always_translate_language_menuitem() {
+ const { cleanup, resolveDownloads, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.alwaysTranslateLanguages", "pl,fr"]],
+ });
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The translations button is visible."
+ );
+
+ info(
+ 'The document language "es" is not in the alwaysTranslateLanguages pref, ' +
+ "so the page should be untranslated, in its original form"
+ );
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ info(
+ "Simulate clicking always-translate-language in the settings menu, " +
+ "adding the document language to the alwaysTranslateLanguages pref"
+ );
+ await openSettingsMenu();
+
+ await assertIsAlwaysTranslateLanguage("es", false);
+ await toggleAlwaysTranslateLanguage();
+ await assertIsAlwaysTranslateLanguage("es", true);
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: true, locale: false, icon: true },
+ "The icon presents the loading indicator."
+ );
+
+ await resolveDownloads(1);
+
+ const { locale } = await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: true, icon: true },
+ "The icon presents the locale."
+ );
+
+ is(locale.innerText, "en", "The English language tag is shown.");
+
+ info(
+ "The page should now be automatically translated because the document language " +
+ "should be added to the always-translate pref"
+ );
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is translated automatically",
+ getH1,
+ "DON QUIJOTE DE LA MANCHA [es to en, html]"
+ );
+ });
+
+ info("Navigate to a different Spanish page");
+ await navigate(SPANISH_PAGE_URL_DOT_ORG);
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: true, locale: false, icon: true },
+ "The icon presents the loading indicator."
+ );
+
+ await resolveDownloads(1);
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: true, icon: true },
+ "The icon presents the locale."
+ );
+
+ is(locale.innerText, "en", "The English language tag is shown.");
+
+ info(
+ "The page should now be automatically translated because the document language " +
+ "should be added to the always-translate pref"
+ );
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is translated automatically",
+ getH1,
+ "DON QUIJOTE DE LA MANCHA [es to en, html]"
+ );
+ });
+
+ info(
+ "Simulate clicking always-translate-language in the settings menu " +
+ "removing the document language from the alwaysTranslateLanguages pref"
+ );
+ await openSettingsMenu();
+
+ await assertIsAlwaysTranslateLanguage("es", true);
+ await toggleAlwaysTranslateLanguage();
+ await assertIsAlwaysTranslateLanguage("es", false);
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "Only the button appears"
+ );
+
+ info(
+ "The page should no longer automatically translated because the document language " +
+ "should be removed from the always-translate pref"
+ );
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await cleanup();
+});
+
+/**
+ * Tests the effect of toggling the always-translate-language menuitem after the page has
+ * been manually translated. This should not reload or retranslate the page, but just check
+ * the box.
+ */
+add_task(
+ async function test_activate_always_translate_language_after_manual_translation() {
+ const { cleanup, resolveDownloads, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.alwaysTranslateLanguages", "pl,fr"]],
+ });
+
+ const { button } = await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ info(
+ 'The document language "es" is not in the alwaysTranslateLanguages pref, ' +
+ "so the page should be untranslated, in its original form"
+ );
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await waitForTranslationsPopupEvent("popupshown", () => {
+ click(button, "Opening the popup");
+ });
+
+ await waitForTranslationsPopupEvent("popuphidden", () => {
+ click(
+ getByL10nId("translations-panel-translate-button"),
+ "Start translating by clicking the translate button."
+ );
+ });
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: true, locale: false, icon: true },
+ "The icon presents the loading indicator."
+ );
+
+ await resolveDownloads(1);
+
+ const { locale } = await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: true, icon: true },
+ "The icon presents the locale."
+ );
+
+ is(locale.innerText, "en", "The English language tag is shown.");
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The pages H1 is translated.",
+ getH1,
+ "DON QUIJOTE DE LA MANCHA [es to en, html]"
+ );
+ });
+
+ info(
+ "Simulate clicking always-translate-language in the settings menu, " +
+ "adding the document language to the alwaysTranslateLanguages pref"
+ );
+ await openSettingsMenu();
+
+ await assertIsAlwaysTranslateLanguage("es", false);
+ await toggleAlwaysTranslateLanguage();
+ await assertIsAlwaysTranslateLanguage("es", true);
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: true, icon: true },
+ "The continues to present the locale without pending downloads."
+ );
+
+ is(locale.innerText, "en", "The English language tag is shown.");
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The pages H1 is translated.",
+ getH1,
+ "DON QUIJOTE DE LA MANCHA [es to en, html]"
+ );
+ });
+
+ info(
+ "Simulate clicking always-translate-language in the settings menu " +
+ "removing the document language from the alwaysTranslateLanguages pref"
+ );
+ await assertIsAlwaysTranslateLanguage("es", true);
+ await toggleAlwaysTranslateLanguage();
+ await assertIsAlwaysTranslateLanguage("es", false);
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "Only the button appears"
+ );
+
+ info(
+ "The page should no longer automatically translated because the document language " +
+ "should be removed from the always-translate pref"
+ );
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await cleanup();
+ }
+);
+
+/**
+ * Tests the effect of unchecking the always-translate language menuitem after the page has
+ * been manually restored to its original form.
+ * This should have no effect on the page, and further page loads should no longer auto-translate.
+ */
+add_task(
+ async function test_deactivate_always_translate_language_after_restore() {
+ const { cleanup, resolveDownloads, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.alwaysTranslateLanguages", "pl,fr"]],
+ });
+
+ const { button } = await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The translations button is visible."
+ );
+
+ info(
+ 'The document language "es" is not in the alwaysTranslateLanguages pref, ' +
+ "so the page should be untranslated, in its original form"
+ );
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ info(
+ "Simulate clicking always-translate-language in the settings menu, " +
+ "adding the document language to the alwaysTranslateLanguages pref"
+ );
+ await openSettingsMenu();
+
+ await assertIsAlwaysTranslateLanguage("es", false);
+ await toggleAlwaysTranslateLanguage();
+ await assertIsAlwaysTranslateLanguage("es", true);
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: true, locale: false, icon: true },
+ "The icon presents the loading indicator."
+ );
+
+ await resolveDownloads(1);
+
+ const { locale } = await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: true, icon: true },
+ "The icon presents the locale."
+ );
+
+ is(locale.innerText, "en", "The English language tag is shown.");
+
+ info(
+ "The page should now be automatically translated because the document language " +
+ "should be added to the always-translate pref"
+ );
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is translated automatically",
+ getH1,
+ "DON QUIJOTE DE LA MANCHA [es to en, html]"
+ );
+ });
+
+ await navigate(
+ SPANISH_PAGE_URL_DOT_ORG,
+ "Navigate to a different Spanish page"
+ );
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: true, locale: false, icon: true },
+ "The icon presents the loading indicator."
+ );
+
+ await resolveDownloads(1);
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: true, icon: true },
+ "The icon presents the locale."
+ );
+
+ is(locale.innerText, "en", "The English language tag is shown.");
+
+ info(
+ "The page should now be automatically translated because the document language " +
+ "should be added to the always-translate pref"
+ );
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is translated automatically",
+ getH1,
+ "DON QUIJOTE DE LA MANCHA [es to en, html]"
+ );
+ });
+
+ await waitForTranslationsPopupEvent("popupshown", () => {
+ click(button, "Re-opening the popup");
+ });
+
+ await waitForTranslationsPopupEvent("popuphidden", () => {
+ click(
+ getByL10nId("translations-panel-restore-button"),
+ "Click the restore language button."
+ );
+ });
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is reverted to have an icon."
+ );
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is restored to Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ info(
+ "Simulate clicking always-translate-language in the settings menu, " +
+ "removing the document language to the alwaysTranslateLanguages pref"
+ );
+ await openSettingsMenu();
+
+ await assertIsAlwaysTranslateLanguage("es", true);
+ await toggleAlwaysTranslateLanguage();
+ await assertIsAlwaysTranslateLanguage("es", false);
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button shows only the icon."
+ );
+
+ await navigate(SPANISH_PAGE_URL_DOT_ORG, "Reload the page");
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button shows only the icon."
+ );
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is restored to Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_basics.js b/browser/components/translations/tests/browser/browser_translations_panel_basics.js
new file mode 100644
index 0000000000..df2b0da4e5
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_basics.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests a basic panel open, translation, and restoration to the original language.
+ */
+add_task(async function test_translations_panel_basics() {
+ const { cleanup, resolveDownloads, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ const { button } = await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await waitForTranslationsPopupEvent("popupshown", () => {
+ click(button, "Opening the popup");
+ });
+
+ await waitForTranslationsPopupEvent("popuphidden", () => {
+ click(
+ getByL10nId("translations-panel-translate-button"),
+ "Start translating by clicking the translate button."
+ );
+ });
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: true, locale: false, icon: true },
+ "The icon presents the loading indicator."
+ );
+
+ await resolveDownloads(1);
+
+ const { locale } = await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: true, icon: true },
+ "The icon presents the locale."
+ );
+
+ is(locale.innerText, "en", "The English language tag is shown.");
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The pages H1 is translated.",
+ getH1,
+ "DON QUIJOTE DE LA MANCHA [es to en, html]"
+ );
+ });
+
+ await waitForTranslationsPopupEvent("popupshown", () => {
+ click(button, "Re-opening the popup");
+ });
+
+ ok(
+ getByL10nId("translations-panel-translate-button").disabled,
+ "The translate button is disabled when re-opening."
+ );
+
+ await waitForTranslationsPopupEvent("popuphidden", () => {
+ click(
+ getByL10nId("translations-panel-restore-button"),
+ "Click the restore language button."
+ );
+ });
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is restored to Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is reverted to have an icon."
+ );
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_beta_langs.js b/browser/components/translations/tests/browser/browser_translations_panel_beta_langs.js
new file mode 100644
index 0000000000..f44f2ad9e8
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_beta_langs.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that languages are displayed correctly as being in beta or not.
+ */
+add_task(async function test_translations_panel_display_beta_languages() {
+ const { cleanup } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ function assertBetaDisplay(selectElement) {
+ const betaL10nId = "translations-panel-displayname-beta";
+ const options = selectElement.querySelectorAll("menuitem");
+ if (options.length === 0) {
+ throw new Error("Could not find the menuitems.");
+ }
+
+ for (const option of options) {
+ for (const languagePair of LANGUAGE_PAIRS) {
+ if (
+ languagePair.fromLang === option.value ||
+ languagePair.toLang === option.value
+ ) {
+ if (option.getAttribute("data-l10n-id") === betaL10nId) {
+ is(
+ languagePair.isBeta,
+ true,
+ `Since data-l10n-id was ${betaL10nId} for ${option.value}, then it must be part of a beta language pair, but it was not.`
+ );
+ }
+ if (!languagePair.isBeta) {
+ is(
+ option.getAttribute("data-l10n-id") === betaL10nId,
+ false,
+ `Since the languagePair is non-beta, the language option ${option.value} should not have a data-l10-id of ${betaL10nId}, but it does.`
+ );
+ }
+ }
+ }
+ }
+ }
+
+ const { button } = await assertTranslationsButton(
+ { button: true },
+ "The button is available."
+ );
+
+ await waitForTranslationsPopupEvent("popupshown", () => {
+ click(button, "Opening the popup");
+ });
+
+ assertBetaDisplay(document.getElementById("translations-panel-to"));
+
+ await waitForTranslationsPopupEvent("popuphidden", () => {
+ click(
+ getByL10nId("translations-panel-translate-cancel"),
+ "Click the cancel button."
+ );
+ });
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_button.js b/browser/components/translations/tests/browser/browser_translations_panel_button.js
new file mode 100644
index 0000000000..57e09260fb
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_button.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that the translations button is correctly visible when navigating between pages.
+ */
+add_task(async function test_button_visible_navigation() {
+ info("Start at a page in Spanish.");
+ const { cleanup } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ await assertTranslationsButton(
+ { button: true },
+ "The button should be visible since the page can be translated from Spanish."
+ );
+
+ navigate(ENGLISH_PAGE_URL, "Navigate to an English page.");
+
+ await assertTranslationsButton(
+ { button: false },
+ "The button should be invisible since the page is in English."
+ );
+
+ navigate(SPANISH_PAGE_URL, "Navigate back to a Spanish page.");
+
+ await assertTranslationsButton(
+ { button: true },
+ "The button should be visible again since the page is in Spanish."
+ );
+
+ await cleanup();
+});
+
+/**
+ * Test that the translations button is correctly visible when opening and switch tabs.
+ */
+add_task(async function test_button_visible() {
+ info("Start at a page in Spanish.");
+
+ const { cleanup, tab: spanishTab } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ await assertTranslationsButton(
+ { button: true },
+ "The button should be visible since the page can be translated from Spanish."
+ );
+
+ const { removeTab, tab: englishTab } = await addTab(
+ ENGLISH_PAGE_URL,
+ "Creating a new tab for a page in English."
+ );
+
+ await assertTranslationsButton(
+ { button: false },
+ "The button should be invisible since the tab is in English."
+ );
+
+ await switchTab(spanishTab);
+
+ await assertTranslationsButton(
+ { button: true },
+ "The button should be visible again since the page is in Spanish."
+ );
+
+ await switchTab(englishTab);
+
+ await assertTranslationsButton(
+ { button: false },
+ "Don't show for english pages"
+ );
+
+ await removeTab();
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_cancel.js b/browser/components/translations/tests/browser/browser_translations_panel_cancel.js
new file mode 100644
index 0000000000..b9bf532d96
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_cancel.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests a panel open, and hitting the cancel button.
+ */
+add_task(async function test_translations_panel_cancel() {
+ const { cleanup } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ const { button } = await assertTranslationsButton(
+ { button: true },
+ "The button is available."
+ );
+
+ await waitForTranslationsPopupEvent("popupshown", () => {
+ click(button, "Opening the popup");
+ });
+
+ await waitForTranslationsPopupEvent("popuphidden", () => {
+ click(
+ getByL10nId("translations-panel-translate-cancel"),
+ "Click the cancel button."
+ );
+ });
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_gear.js b/browser/components/translations/tests/browser/browser_translations_panel_gear.js
new file mode 100644
index 0000000000..46838accb4
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_gear.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test managing the languages menu item.
+ */
+add_task(async function test_translations_panel_manage_languages() {
+ const { cleanup } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ const { button } = await assertTranslationsButton(
+ { button: true },
+ "The button is available."
+ );
+
+ await waitForTranslationsPopupEvent("popupshown", () => {
+ click(button, "Opening the popup");
+ });
+
+ const gearIcon = getByL10nId("translations-panel-settings-button");
+ click(gearIcon, "Open the preferences menu");
+
+ const manageLanguages = getByL10nId(
+ "translations-panel-settings-manage-languages"
+ );
+ info("Choose to manage the languages.");
+ manageLanguages.doCommand();
+
+ await TestUtils.waitForCondition(
+ () => gBrowser.currentURI.spec === "about:preferences#general",
+ "Waiting for about:preferences to be opened."
+ );
+
+ info("Remove the about:preferences tab");
+ gBrowser.removeCurrentTab();
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_never_translate_language.js b/browser/components/translations/tests/browser/browser_translations_panel_never_translate_language.js
new file mode 100644
index 0000000000..c85443f457
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_never_translate_language.js
@@ -0,0 +1,343 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the effect of toggling the never-translate-language menuitem.
+ * Checking the box on an untranslated page should immediately hide the button.
+ * The button should not appear again for sites in the disabled language.
+ */
+add_task(async function test_toggle_never_translate_language_menuitem() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.neverTranslateLanguages", "pl,fr"]],
+ });
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The translations button is visible."
+ );
+
+ info(
+ 'The document language "es" is not in the neverTranslateLanguages pref, ' +
+ "so the page should be untranslated, in its original form."
+ );
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ info(
+ "Simulate clicking never-translate-language in the settings menu, " +
+ "adding the document language from the neverTranslateLanguages pref"
+ );
+ await openSettingsMenu();
+
+ await assertIsNeverTranslateLanguage("es", false);
+ await toggleNeverTranslateLanguage();
+ await assertIsNeverTranslateLanguage("es", true);
+
+ await assertTranslationsButton(
+ { button: false },
+ "The translations button should be invisible"
+ );
+
+ info(
+ "The page should still be in its original, untranslated form because " +
+ "the document language is in the neverTranslateLanguages pref"
+ );
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await navigate(SPANISH_PAGE_URL, "Reload the page");
+
+ await assertTranslationsButton(
+ { button: false },
+ "The translations button should be invisible"
+ );
+
+ info(
+ "The page should still be in its original, untranslated form because " +
+ "the document language is in the neverTranslateLanguages pref"
+ );
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await navigate(
+ SPANISH_PAGE_URL_DOT_ORG,
+ "Navigate to a different Spanish page"
+ );
+
+ await assertTranslationsButton(
+ { button: false },
+ "The translations button should be invisible"
+ );
+
+ info(
+ "The page should still be in its original, untranslated form because " +
+ "the document language is in the neverTranslateLanguages pref"
+ );
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await cleanup();
+});
+
+/**
+ * Tests the effect of toggling the never-translate-language menuitem on a page where
+ * where translation is already active.
+ * Checking the box on a translated page should restore the page and hide the button.
+ * The button should not appear again for sites in the disabled language.
+ */
+add_task(
+ async function test_toggle_never_translate_language_menuitem_with_active_translations() {
+ const { cleanup, resolveDownloads, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.neverTranslateLanguages", "pl,fr"]],
+ });
+
+ const { button } = await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ info(
+ 'The document language "es" is not in the alwaysTranslateLanguages pref, ' +
+ "so the page should be untranslated, in its original form"
+ );
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await waitForTranslationsPopupEvent("popupshown", () => {
+ click(button, "Opening the popup");
+ });
+
+ await waitForTranslationsPopupEvent("popuphidden", () => {
+ click(
+ getByL10nId("translations-panel-translate-button"),
+ "Start translating by clicking the translate button."
+ );
+ });
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: true, locale: false, icon: true },
+ "The icon presents the loading indicator."
+ );
+
+ await resolveDownloads(1);
+
+ const { locale } = await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: true, icon: true },
+ "The icon presents the locale."
+ );
+
+ is(locale.innerText, "en", "The English language tag is shown.");
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The pages H1 is translated.",
+ getH1,
+ "DON QUIJOTE DE LA MANCHA [es to en, html]"
+ );
+ });
+
+ info(
+ "Simulate clicking never-translate-language in the settings menu, " +
+ "adding the document language from the neverTranslateLanguages pref"
+ );
+ await openSettingsMenu();
+
+ await assertIsNeverTranslateLanguage("es", false);
+ await toggleNeverTranslateLanguage();
+ await assertIsNeverTranslateLanguage("es", true);
+
+ await assertTranslationsButton(
+ { button: false },
+ "The translations button should be invisible"
+ );
+
+ info(
+ "The page should still be in its original, untranslated form because " +
+ "the document language is in the neverTranslateLanguages pref"
+ );
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await navigate(SPANISH_PAGE_URL, "Reload the page");
+
+ await assertTranslationsButton(
+ { button: false },
+ "The translations button should be invisible"
+ );
+
+ info(
+ "The page should still be in its original, untranslated form because " +
+ "the document language is in the neverTranslateLanguages pref"
+ );
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await cleanup();
+ }
+);
+
+/**
+ * Tests the effect of toggling the never-translate-language menuitem on a page where
+ * where translation is already active via always-translate.
+ * Checking the box on a translated page should restore the page and hide the button.
+ * The language should be moved from always-translate to never-translate.
+ * The button should not appear again for sites in the disabled language.
+ */
+add_task(
+ async function test_toggle_never_translate_language_menuitem_with_always_translate_active() {
+ const { cleanup, resolveDownloads, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [
+ ["browser.translations.alwaysTranslateLanguages", "uk,it"],
+ ["browser.translations.neverTranslateLanguages", "pl,fr"],
+ ],
+ });
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ info(
+ "Simulate clicking always-translate-language in the settings menu, " +
+ "adding the document language to the alwaysTranslateLanguages pref"
+ );
+ await openSettingsMenu();
+
+ await assertIsAlwaysTranslateLanguage("es", false);
+ await assertIsNeverTranslateLanguage("es", false);
+
+ await toggleAlwaysTranslateLanguage();
+
+ await assertIsAlwaysTranslateLanguage("es", true);
+ await assertIsNeverTranslateLanguage("es", false);
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: true, locale: false, icon: true },
+ "The icon presents the loading indicator."
+ );
+
+ await resolveDownloads(1);
+
+ const { locale } = await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: true, icon: true },
+ "The icon presents the locale."
+ );
+
+ is(locale.innerText, "en", "The English language tag is shown.");
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The pages H1 is translated.",
+ getH1,
+ "DON QUIJOTE DE LA MANCHA [es to en, html]"
+ );
+ });
+
+ info(
+ "Simulate clicking never-translate-language in the settings menu, " +
+ "adding the document language from the neverTranslateLanguages pref " +
+ "and removing it from the alwaysTranslateLanguages pref"
+ );
+ await openSettingsMenu();
+
+ await assertIsAlwaysTranslateLanguage("es", true);
+ await assertIsNeverTranslateLanguage("es", false);
+
+ await toggleNeverTranslateLanguage();
+
+ await assertIsAlwaysTranslateLanguage("es", false);
+ await assertIsNeverTranslateLanguage("es", true);
+
+ await assertTranslationsButton(
+ { button: false },
+ "The translations button should be invisible"
+ );
+
+ info(
+ "The page should still be in its original, untranslated form because " +
+ "the document language is in the neverTranslateLanguages pref"
+ );
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await navigate(SPANISH_PAGE_URL, "Reload the page");
+
+ await assertTranslationsButton(
+ { button: false },
+ "The translations button should be invisible"
+ );
+
+ info(
+ "The page should still be in its original, untranslated form because " +
+ "the document language is in the neverTranslateLanguages pref"
+ );
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_never_translate_site.js b/browser/components/translations/tests/browser/browser_translations_panel_never_translate_site.js
new file mode 100644
index 0000000000..b54b5a4a2c
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_never_translate_site.js
@@ -0,0 +1,432 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the effect of toggling the never-translate-site menuitem.
+ * Checking the box on an untranslated page should immediately hide the button.
+ * The button should not appear again for sites that share the same content principal
+ * of the disabled site.
+ */
+add_task(async function test_toggle_never_translate_site_menuitem() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ permissionsUrls: [SPANISH_PAGE_URL],
+ });
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The translations button is visible."
+ );
+
+ info(
+ "Translations permissions are currently allowed for this test page " +
+ "and the page should be untranslated, in its original form."
+ );
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ info(
+ "Simulate clicking never-translate-site in the settings menu, " +
+ "denying translations permissions for this content window principal"
+ );
+ await openSettingsMenu();
+
+ await assertIsNeverTranslateSite(SPANISH_PAGE_URL, false);
+ await toggleNeverTranslateSite();
+ await assertIsNeverTranslateSite(SPANISH_PAGE_URL, true);
+
+ await assertTranslationsButton(
+ { button: false },
+ "The translations button should be invisible"
+ );
+
+ info("The page should still be in its original, untranslated form");
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await navigate(SPANISH_PAGE_URL, "Reload the page");
+
+ await assertTranslationsButton(
+ { button: false },
+ "The translations button should be invisible"
+ );
+
+ info("The page should still be in its original, untranslated form");
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await navigate(
+ SPANISH_PAGE_URL_2,
+ "Navigate to a Spanish page with the same content principal"
+ );
+
+ await assertTranslationsButton(
+ { button: false },
+ "The translations button should be invisible, because this content principal is denied"
+ );
+
+ info("The page should still be in its original, untranslated form");
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await navigate(
+ SPANISH_PAGE_URL_DOT_ORG,
+ "Navigate to a Spanish page with a different content principal"
+ );
+
+ await assertTranslationsButton(
+ { button: false },
+ "The translations button should be visible, because this content principal " +
+ "has not been denied translations permissions"
+ );
+
+ info("The page should still be in its original, untranslated form");
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await cleanup();
+});
+
+/**
+ * Tests the effect of toggling the never-translate-site menuitem on a page where
+ * where translation is already active.
+ * Checking the box on a translated page should restore the page and hide the button.
+ * The button should not appear again for sites that share the same content principal
+ * of the disabled site.
+ */
+add_task(
+ async function test_toggle_never_translate_site_menuitem_with_active_translations() {
+ const { cleanup, resolveDownloads, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ permissionsUrls: [SPANISH_PAGE_URL],
+ });
+
+ const { button } = await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The translations button is visible."
+ );
+
+ info(
+ "Translations permissions are currently allowed for this test page " +
+ "and the page should be untranslated, in its original form."
+ );
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await waitForTranslationsPopupEvent("popupshown", () => {
+ click(button, "Opening the popup");
+ });
+
+ await waitForTranslationsPopupEvent("popuphidden", () => {
+ click(
+ getByL10nId("translations-panel-translate-button"),
+ "Start translating by clicking the translate button."
+ );
+ });
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: true, locale: false, icon: true },
+ "The icon presents the loading indicator."
+ );
+
+ await resolveDownloads(1);
+
+ const { locale } = await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: true, icon: true },
+ "The icon presents the locale."
+ );
+
+ is(locale.innerText, "en", "The English language tag is shown.");
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The pages H1 is translated.",
+ getH1,
+ "DON QUIJOTE DE LA MANCHA [es to en, html]"
+ );
+ });
+
+ info(
+ "Simulate clicking never-translate-site in the settings menu, " +
+ "denying translations permissions for this content window principal"
+ );
+ await openSettingsMenu();
+
+ await assertIsNeverTranslateSite(SPANISH_PAGE_URL, false);
+ await toggleNeverTranslateSite();
+ await assertIsNeverTranslateSite(SPANISH_PAGE_URL, true);
+
+ await assertTranslationsButton(
+ { button: false },
+ "The translations button should be invisible"
+ );
+
+ info("The page should still be in its original, untranslated form");
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await navigate(SPANISH_PAGE_URL, "Reload the page");
+
+ await assertTranslationsButton(
+ { button: false },
+ "The translations button should be invisible"
+ );
+
+ info("The page should still be in its original, untranslated form");
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await navigate(
+ SPANISH_PAGE_URL_2,
+ "Navigate to a Spanish page with the same content principal"
+ );
+
+ await assertTranslationsButton(
+ { button: false },
+ "The translations button should be invisible, because this content principal is denied"
+ );
+
+ info("The page should still be in its original, untranslated form");
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await navigate(
+ SPANISH_PAGE_URL_DOT_ORG,
+ "Navigate to a Spanish page with a different content principal"
+ );
+
+ await assertTranslationsButton(
+ { button: false },
+ "The translations button should be visible, because this content principal " +
+ "has not been denied translations permissions"
+ );
+
+ info("The page should still be in its original, untranslated form");
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await cleanup();
+ }
+);
+
+/**
+ * Tests the effect of toggling the never-translate-site menuitem on a page where
+ * where translation is already active via always-translate.
+ * Checking the box on a translated page should restore the page and hide the button.
+ * The button should not appear again for sites that share the same content principal
+ * of the disabled site, and no auto-translation should occur.
+ * Other sites should still auto-translate for this language.
+ */
+add_task(
+ async function test_toggle_never_translate_site_menuitem_with_always_translate_active() {
+ const { cleanup, resolveDownloads, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.alwaysTranslateLanguages", "uk,it"]],
+ permissionsUrls: [SPANISH_PAGE_URL],
+ });
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ info(
+ "Simulate clicking always-translate-language in the settings menu, " +
+ "adding the document language to the alwaysTranslateLanguages pref"
+ );
+ await openSettingsMenu();
+
+ await assertIsAlwaysTranslateLanguage("es", false);
+ await assertIsNeverTranslateSite(SPANISH_PAGE_URL, false);
+
+ await toggleAlwaysTranslateLanguage();
+
+ await assertIsAlwaysTranslateLanguage("es", true);
+ await assertIsNeverTranslateSite(SPANISH_PAGE_URL, false);
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: true, locale: false, icon: true },
+ "The icon presents the loading indicator."
+ );
+
+ await resolveDownloads(1);
+
+ const { locale } = await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: true, icon: true },
+ "The icon presents the locale."
+ );
+
+ is(locale.innerText, "en", "The English language tag is shown.");
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The pages H1 is translated.",
+ getH1,
+ "DON QUIJOTE DE LA MANCHA [es to en, html]"
+ );
+ });
+
+ info(
+ "Simulate clicking never-translate-site in the settings menu, " +
+ "denying translations permissions for this content window principal"
+ );
+ await openSettingsMenu();
+
+ await assertIsAlwaysTranslateLanguage("es", true);
+ await assertIsNeverTranslateSite(SPANISH_PAGE_URL, false);
+
+ await toggleNeverTranslateSite();
+
+ await assertIsAlwaysTranslateLanguage("es", true);
+ await assertIsNeverTranslateSite(SPANISH_PAGE_URL, true);
+
+ await assertTranslationsButton(
+ { button: false },
+ "The translations button should be invisible"
+ );
+
+ info("The page should still be in its original, untranslated form");
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await navigate(SPANISH_PAGE_URL, "Reload the page");
+
+ await assertTranslationsButton(
+ { button: false },
+ "The translations button should be invisible"
+ );
+
+ info("The page should still be in its original, untranslated form");
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await navigate(
+ SPANISH_PAGE_URL_2,
+ "Navigate to a Spanish page with the same content principal"
+ );
+
+ await assertTranslationsButton(
+ { button: false },
+ "The translations button should be invisible, because this content principal is denied"
+ );
+
+ info("The page should still be in its original, untranslated form");
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await navigate(
+ SPANISH_PAGE_URL_DOT_ORG,
+ "Navigate to a Spanish page with a different content principal"
+ );
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: true, locale: false, icon: true },
+ "The icon presents the loading indicator."
+ );
+
+ await resolveDownloads(1);
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: true, icon: true },
+ "The icon presents the locale."
+ );
+
+ is(locale.innerText, "en", "The English language tag is shown.");
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The pages H1 is translated.",
+ getH1,
+ "DON QUIJOTE DE LA MANCHA [es to en, html]"
+ );
+ });
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_retry.js b/browser/components/translations/tests/browser/browser_translations_panel_retry.js
new file mode 100644
index 0000000000..e9b3f93257
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_retry.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests translating, and then immediately translating to a new language.
+ */
+add_task(async function test_translations_panel_retry() {
+ const { cleanup, resolveDownloads, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ const { button } = await assertTranslationsButton(
+ { button: true },
+ "The button is available."
+ );
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await waitForTranslationsPopupEvent("popupshown", () => {
+ click(button, "Opening the popup");
+ });
+
+ await waitForTranslationsPopupEvent("popuphidden", () => {
+ click(
+ getByL10nId("translations-panel-translate-button"),
+ "Start translating by clicking the translate button."
+ );
+ });
+
+ await resolveDownloads(1);
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The pages H1 is translated.",
+ getH1,
+ "DON QUIJOTE DE LA MANCHA [es to en, html]"
+ );
+ });
+
+ await waitForTranslationsPopupEvent("popupshown", () => {
+ click(button, "Re-opening the popup");
+ });
+
+ info('Switch to language to "fr"');
+ const toSelect = getById("translations-panel-to");
+ toSelect.value = "fr";
+ toSelect.dispatchEvent(new Event("command"));
+
+ await waitForTranslationsPopupEvent("popuphidden", () => {
+ click(
+ getByL10nId("translations-panel-translate-button"),
+ "Re-translate the page by clicking the translate button."
+ );
+ });
+
+ // This is a pivot language which requires 2 models.
+ await resolveDownloads(2);
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The pages H1 is translated using the changed languages.",
+ getH1,
+ "DON QUIJOTE DE LA MANCHA [es to fr, html]"
+ );
+ });
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_switch_languages.js b/browser/components/translations/tests/browser/browser_translations_panel_switch_languages.js
new file mode 100644
index 0000000000..58bcb212d7
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_switch_languages.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests switching the language.
+ */
+add_task(async function test_translations_panel_switch_language() {
+ const { cleanup, resolveDownloads, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ const { button } = await assertTranslationsButton(
+ { button: true },
+ "The button is available."
+ );
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await waitForTranslationsPopupEvent("popupshown", () => {
+ click(button, "Opening the popup");
+ });
+
+ const translateButton = getByL10nId("translations-panel-translate-button");
+ const fromSelect = getById("translations-panel-from");
+ const toSelect = getById("translations-panel-to");
+
+ ok(!translateButton.disabled, "The translate button starts as enabled");
+ is(fromSelect.value, "es", "The from select starts as Spanish");
+ is(toSelect.value, "en", "The to select starts as English");
+
+ info('Switch from language to "es"');
+ fromSelect.value = "en";
+ fromSelect.dispatchEvent(new Event("command"));
+
+ ok(
+ translateButton.disabled,
+ "The translate button is disabled when the languages are the same"
+ );
+
+ info('Switch from language back to "es"');
+ fromSelect.value = "es";
+ fromSelect.dispatchEvent(new Event("command"));
+
+ ok(
+ !translateButton.disabled,
+ "When the languages are different it can be translated"
+ );
+
+ info("Switch to language to nothing");
+ fromSelect.value = "";
+ fromSelect.dispatchEvent(new Event("command"));
+
+ ok(
+ translateButton.disabled,
+ "The translate button is disabled nothing is selected."
+ );
+
+ info('Switch from language to "en"');
+ fromSelect.value = "en";
+ fromSelect.dispatchEvent(new Event("command"));
+
+ info('Switch to language to "fr"');
+ toSelect.value = "fr";
+ toSelect.dispatchEvent(new Event("command"));
+
+ ok(!translateButton.disabled, "The translate button can now be used");
+
+ await waitForTranslationsPopupEvent("popuphidden", () => {
+ click(
+ translateButton,
+ "Start translating by clicking the translate button."
+ );
+ });
+
+ await resolveDownloads(1);
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The pages H1 is translated using the changed languages.",
+ getH1,
+ "DON QUIJOTE DE LA MANCHA [en to fr, html]"
+ );
+ });
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_telemetry_open_panel.js b/browser/components/translations/tests/browser/browser_translations_telemetry_open_panel.js
new file mode 100644
index 0000000000..41aa730863
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_telemetry_open_panel.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the telemetry event for opening the translations panel.
+ */
+add_task(async function test_translations_telemetry_open_panel() {
+ const { cleanup } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ await TestTranslationsTelemetry.assertEvent(
+ "OpenPanel",
+ Glean.translationsPanel.open,
+ {
+ expectedLength: 0,
+ }
+ );
+
+ const { button } = await assertTranslationsButton(
+ { button: true },
+ "The button is available."
+ );
+
+ await waitForTranslationsPopupEvent("popupshown", () => {
+ click(button, "Opening the popup");
+ });
+
+ await waitForTranslationsPopupEvent("popuphidden", () => {
+ click(
+ getByL10nId("translations-panel-translate-cancel"),
+ "Click the cancel button."
+ );
+ });
+
+ await TestTranslationsTelemetry.assertEvent(
+ "OpenPanel",
+ Glean.translationsPanel.open,
+ {
+ expectedLength: 1,
+ finalValuePredicates: [
+ value => value.extra.opened_from === "translationsButton",
+ ],
+ }
+ );
+
+ await waitForTranslationsPopupEvent("popupshown", () => {
+ click(button, "Opening the popup");
+ });
+
+ await waitForTranslationsPopupEvent("popuphidden", () => {
+ click(
+ getByL10nId("translations-panel-translate-cancel"),
+ "Click the cancel button."
+ );
+ });
+
+ await TestTranslationsTelemetry.assertEvent(
+ "OpenPanel",
+ Glean.translationsPanel.open,
+ {
+ expectedLength: 2,
+ allValuePredicates: [
+ value => value.extra.opened_from === "translationsButton",
+ ],
+ }
+ );
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_telemetry_translation_failure.js b/browser/components/translations/tests/browser/browser_translations_telemetry_translation_failure.js
new file mode 100644
index 0000000000..1268e6452b
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_telemetry_translation_failure.js
@@ -0,0 +1,195 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+
+/**
+ * Tests the telemetry event for a manual translation request failure.
+ */
+add_task(
+ async function test_translations_telemetry_manual_translation_failure() {
+ PromiseTestUtils.expectUncaughtRejection(
+ /Intentionally rejecting downloads./
+ );
+
+ const { cleanup, rejectDownloads, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ const { button } = await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await TestTranslationsTelemetry.assertCounter(
+ "RequestCount",
+ Glean.translations.requestsCount,
+ 0
+ );
+ await TestTranslationsTelemetry.assertRate(
+ "ErrorRate",
+ Glean.translations.errorRate,
+ {
+ expectedNumerator: 0,
+ expectedDenominator: 0,
+ }
+ );
+ await TestTranslationsTelemetry.assertEvent(
+ "TranslationRequest",
+ Glean.translations.translationRequest,
+ {
+ expectedLength: 0,
+ }
+ );
+
+ await waitForTranslationsPopupEvent("popupshown", () => {
+ click(button, "Opening the popup");
+ });
+
+ await waitForTranslationsPopupEvent("popuphidden", () => {
+ click(
+ getByL10nId("translations-panel-translate-button"),
+ "Start translating by clicking the translate button."
+ );
+ });
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: true, locale: false, icon: true },
+ "The icon presents the loading indicator."
+ );
+
+ await rejectDownloads(1);
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await TestTranslationsTelemetry.assertCounter(
+ "RequestCount",
+ Glean.translations.requestsCount,
+ 1
+ );
+ await TestTranslationsTelemetry.assertRate(
+ "ErrorRate",
+ Glean.translations.errorRate,
+ {
+ expectedNumerator: 1,
+ expectedDenominator: 1,
+ }
+ );
+ await TestTranslationsTelemetry.assertEvent(
+ "Error",
+ Glean.translations.error,
+ {
+ expectedLength: 1,
+ finalValuePredicates: [
+ value =>
+ value.extra.reason === "Error: Intentionally rejecting downloads.",
+ ],
+ }
+ );
+ await TestTranslationsTelemetry.assertEvent(
+ "TranslationRequest",
+ Glean.translations.translationRequest,
+ {
+ expectedLength: 1,
+ finalValuePredicates: [
+ value => value.extra.from_language === "es",
+ value => value.extra.to_language === "en",
+ value => value.extra.auto_translate === "false",
+ ],
+ }
+ );
+
+ await cleanup();
+ }
+);
+
+/**
+ * Tests the telemetry event for an automatic translation request failure.
+ */
+add_task(async function test_translations_telemetry_auto_translation_failure() {
+ PromiseTestUtils.expectUncaughtRejection(
+ /Intentionally rejecting downloads./
+ );
+
+ const { cleanup, rejectDownloads, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.alwaysTranslateLanguages", "es"]],
+ });
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: true, locale: false, icon: true },
+ "The icon presents the loading indicator."
+ );
+
+ await rejectDownloads(1);
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await TestTranslationsTelemetry.assertCounter(
+ "RequestCount",
+ Glean.translations.requestsCount,
+ 1
+ );
+ await TestTranslationsTelemetry.assertRate(
+ "ErrorRate",
+ Glean.translations.errorRate,
+ {
+ expectedNumerator: 1,
+ expectedDenominator: 1,
+ }
+ );
+ await TestTranslationsTelemetry.assertEvent(
+ "Error",
+ Glean.translations.error,
+ {
+ expectedLength: 1,
+ finalValuePredicates: [
+ value =>
+ value.extra.reason === "Error: Intentionally rejecting downloads.",
+ ],
+ }
+ );
+ await TestTranslationsTelemetry.assertEvent(
+ "TranslationRequest",
+ Glean.translations.translationRequest,
+ {
+ expectedLength: 1,
+ finalValuePredicates: [
+ value => value.extra.from_language === "es",
+ value => value.extra.to_language === "en",
+ value => value.extra.auto_translate === "true",
+ ],
+ }
+ );
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_telemetry_translation_request.js b/browser/components/translations/tests/browser/browser_translations_telemetry_translation_request.js
new file mode 100644
index 0000000000..ce22615630
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_telemetry_translation_request.js
@@ -0,0 +1,173 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the telemetry event for a manual translation request.
+ */
+add_task(async function test_translations_telemetry_manual_translation() {
+ const { cleanup, resolveDownloads, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ const { button } = await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is in Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+
+ await TestTranslationsTelemetry.assertCounter(
+ "RequestCount",
+ Glean.translations.requestsCount,
+ 0
+ );
+ await TestTranslationsTelemetry.assertRate(
+ "ErrorRate",
+ Glean.translations.errorRate,
+ {
+ expectedNumerator: 0,
+ expectedDenominator: 0,
+ }
+ );
+ await TestTranslationsTelemetry.assertEvent(
+ "TranslationRequest",
+ Glean.translations.translationRequest,
+ {
+ expectedLength: 0,
+ }
+ );
+
+ await waitForTranslationsPopupEvent("popupshown", () => {
+ click(button, "Opening the popup");
+ });
+
+ await waitForTranslationsPopupEvent("popuphidden", () => {
+ click(
+ getByL10nId("translations-panel-translate-button"),
+ "Start translating by clicking the translate button."
+ );
+ });
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: true, locale: false, icon: true },
+ "The icon presents the loading indicator."
+ );
+
+ await resolveDownloads(1);
+
+ const { locale } = await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: true, icon: true },
+ "The icon presents the locale."
+ );
+
+ is(locale.innerText, "en", "The English language tag is shown.");
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The pages H1 is translated.",
+ getH1,
+ "DON QUIJOTE DE LA MANCHA [es to en, html]"
+ );
+ });
+
+ await TestTranslationsTelemetry.assertCounter(
+ "RequestCount",
+ Glean.translations.requestsCount,
+ 1
+ );
+ await TestTranslationsTelemetry.assertRate(
+ "ErrorRate",
+ Glean.translations.errorRate,
+ {
+ expectedNumerator: 0,
+ expectedDenominator: 1,
+ }
+ );
+ await TestTranslationsTelemetry.assertEvent(
+ "TranslationRequest",
+ Glean.translations.translationRequest,
+ {
+ expectedLength: 1,
+ finalValuePredicates: [
+ value => value.extra.from_language === "es",
+ value => value.extra.to_language === "en",
+ value => value.extra.auto_translate === "false",
+ ],
+ }
+ );
+
+ await cleanup();
+});
+
+/**
+ * Tests the telemetry event for an automatic translation request.
+ */
+add_task(async function test_translations_telemetry_auto_translation() {
+ const { cleanup, resolveDownloads, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.alwaysTranslateLanguages", "es"]],
+ });
+
+ await assertTranslationsButton(
+ { button: true, circleArrows: true, locale: false, icon: true },
+ "The icon presents the loading indicator."
+ );
+
+ await resolveDownloads(1);
+
+ const { locale } = await assertTranslationsButton(
+ { button: true, circleArrows: false, locale: true, icon: true },
+ "The icon presents the locale."
+ );
+
+ is(locale.innerText, "en", "The English language tag is shown.");
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The pages H1 is translated.",
+ getH1,
+ "DON QUIJOTE DE LA MANCHA [es to en, html]"
+ );
+ });
+
+ await TestTranslationsTelemetry.assertCounter(
+ "RequestCount",
+ Glean.translations.requestsCount,
+ 1
+ );
+ await TestTranslationsTelemetry.assertRate(
+ "ErrorRate",
+ Glean.translations.errorRate,
+ {
+ expectedNumerator: 0,
+ expectedDenominator: 1,
+ }
+ );
+ await TestTranslationsTelemetry.assertEvent(
+ "TranslationRequest",
+ Glean.translations.translationRequest,
+ {
+ expectedLength: 1,
+ finalValuePredicates: [
+ value => value.extra.from_language === "es",
+ value => value.extra.to_language === "en",
+ value => value.extra.auto_translate === "true",
+ ],
+ }
+ );
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/head.js b/browser/components/translations/tests/browser/head.js
new file mode 100644
index 0000000000..56839971b7
--- /dev/null
+++ b/browser/components/translations/tests/browser/head.js
@@ -0,0 +1,348 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/translations/tests/browser/shared-head.js",
+ this
+);
+
+/**
+ * Assert some property about the translations button.
+ *
+ * @param {Record<string, boolean>} visibleAssertions
+ * @param {string} message The message for the assertion.
+ * @returns {HTMLElement}
+ */
+async function assertTranslationsButton(visibleAssertions, message) {
+ const elements = {
+ button: document.getElementById("translations-button"),
+ icon: document.getElementById("translations-button-icon"),
+ circleArrows: document.getElementById("translations-button-circle-arrows"),
+ locale: document.getElementById("translations-button-locale"),
+ };
+
+ for (const [name, element] of Object.entries(elements)) {
+ if (!element) {
+ throw new Error("Could not find the " + name);
+ }
+ }
+
+ try {
+ // Test that the visibilities match.
+ await TestUtils.waitForCondition(() => {
+ for (const [name, visible] of Object.entries(visibleAssertions)) {
+ if (elements[name].hidden === visible) {
+ return false;
+ }
+ }
+ return true;
+ }, message);
+ } catch (error) {
+ // On a mismatch, report it.
+ for (const [name, expected] of Object.entries(visibleAssertions)) {
+ is(!elements[name].hidden, expected, `Visibility for "${name}"`);
+ }
+ }
+
+ ok(true, message);
+
+ return elements;
+}
+
+/**
+ * A convenience function to open the settings menu of the
+ * translations panel. Fails the test if the menu cannot be opened.
+ */
+async function openSettingsMenu() {
+ const { button } = await assertTranslationsButton(
+ { button: true },
+ "The button is available."
+ );
+
+ await waitForTranslationsPopupEvent("popupshown", () => {
+ click(button, "Opening the popup");
+ });
+
+ const gearIcon = getByL10nId("translations-panel-settings-button");
+ click(gearIcon, "Open the settings menu");
+}
+
+/**
+ * Simulates the effect of clicking the always-translate-language menuitem.
+ * Requires that the settings menu of the translations panel is open,
+ * otherwise the test will fail.
+ */
+async function toggleAlwaysTranslateLanguage() {
+ const alwaysTranslateLanguage = getByL10nId(
+ "translations-panel-settings-always-translate-language"
+ );
+ info("Toggle the always-translate-language menuitem");
+ await alwaysTranslateLanguage.doCommand();
+}
+
+/**
+ * Simulates the effect of clicking the never-translate-language menuitem.
+ * Requires that the settings menu of the translations panel is open,
+ * otherwise the test will fail.
+ */
+async function toggleNeverTranslateLanguage() {
+ const neverTranslateLanguage = getByL10nId(
+ "translations-panel-settings-never-translate-language"
+ );
+ info("Toggle the never-translate-language menuitem");
+ await neverTranslateLanguage.doCommand();
+}
+
+/**
+ * Simulates the effect of clicking the never-translate-site menuitem.
+ * Requires that the settings menu of the translations panel is open,
+ * otherwise the test will fail.
+ */
+async function toggleNeverTranslateSite() {
+ const neverTranslateSite = getByL10nId(
+ "translations-panel-settings-never-translate-site"
+ );
+ info("Toggle the never-translate-site menuitem");
+ await neverTranslateSite.doCommand();
+}
+
+/**
+ * Asserts that the always-translate-language checkbox matches the expected checked state.
+ *
+ * @param {string} langTag - A BCP-47 language tag
+ * @param {boolean} expectChecked - Whether the checkbox should be checked
+ */
+async function assertIsAlwaysTranslateLanguage(langTag, expectChecked) {
+ await assertCheckboxState(
+ "translations-panel-settings-always-translate-language",
+ expectChecked
+ );
+}
+
+/**
+ * Asserts that the never-translate-language checkbox matches the expected checked state.
+ *
+ * @param {string} langTag - A BCP-47 language tag
+ * @param {boolean} expectChecked - Whether the checkbox should be checked
+ */
+async function assertIsNeverTranslateLanguage(langTag, expectChecked) {
+ await assertCheckboxState(
+ "translations-panel-settings-never-translate-language",
+ expectChecked
+ );
+}
+
+/**
+ * Asserts that the never-translate-site checkbox matches the expected checked state.
+ *
+ * @param {string} url - The url of a website
+ * @param {boolean} expectChecked - Whether the checkbox should be checked
+ */
+async function assertIsNeverTranslateSite(url, expectChecked) {
+ await assertCheckboxState(
+ "translations-panel-settings-never-translate-site",
+ expectChecked
+ );
+}
+
+/**
+ * Asserts that the state of a checkbox with a given dataL10nId is
+ * checked or not, based on the value of expected being true or false.
+ *
+ * @param {string} dataL10nId - The data-l10n-id of the checkbox.
+ * @param {boolean} expectChecked - Whether the checkbox should be checked.
+ */
+async function assertCheckboxState(dataL10nId, expectChecked) {
+ const menuItems = getAllByL10nId(dataL10nId);
+ for (const menuItem of menuItems) {
+ await TestUtils.waitForCondition(
+ () =>
+ menuItem.getAttribute("checked") === (expectChecked ? "true" : "false"),
+ "Waiting for checkbox state"
+ );
+ is(
+ menuItem.getAttribute("checked"),
+ expectChecked ? "true" : "false",
+ `Should match expected checkbox state for ${dataL10nId}`
+ );
+ }
+}
+
+/**
+ * Navigate to a URL and indicate a message as to why.
+ */
+async function navigate(url, message) {
+ info(message);
+
+ // Load a blank page first to ensure that tests don't hang.
+ // I don't know why this is needed, but it appears to be necessary.
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, BLANK_PAGE);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, url);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+}
+
+/**
+ * Add a tab to the page
+ *
+ * @param {string} url
+ */
+async function addTab(url) {
+ info(`Adding tab for ` + url);
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ url,
+ true // Wait for laod
+ );
+ return {
+ tab,
+ removeTab() {
+ BrowserTestUtils.removeTab(tab);
+ },
+ };
+}
+
+async function switchTab(tab) {
+ info("Switching tabs");
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+}
+
+function click(button, message) {
+ info(message);
+ EventUtils.synthesizeMouseAtCenter(button, {});
+}
+
+/**
+ * @param {Element} element
+ * @returns {boolean}
+ */
+function isVisible(element) {
+ const win = element.ownerDocument.ownerGlobal;
+ const { visibility, display } = win.getComputedStyle(element);
+ return visibility === "visible" && display !== "none";
+}
+
+/**
+ * Get an element by its l10n id, as this is a user-visible way to find an element.
+ * The `l10nId` represents the text that a user would actually see.
+ *
+ * @param {string} l10nId
+ * @param {Document} doc
+ * @returns {Element}
+ */
+function getByL10nId(l10nId, doc = document) {
+ const elements = doc.querySelectorAll(`[data-l10n-id="${l10nId}"]`);
+ if (elements.length === 0) {
+ throw new Error("Could not find the element by l10n id: " + l10nId);
+ }
+ for (const element of elements) {
+ if (isVisible(element)) {
+ return element;
+ }
+ }
+ throw new Error("The element is not visible in the DOM: " + l10nId);
+}
+
+/**
+ * Get all elements that match the l10n id.
+ *
+ * @param {string} l10nId
+ * @param {Document} doc
+ * @returns {Element}
+ */
+function getAllByL10nId(l10nId, doc = document) {
+ const elements = doc.querySelectorAll(`[data-l10n-id="${l10nId}"]`);
+ if (elements.length === 0) {
+ throw new Error("Could not find the element by l10n id: " + l10nId);
+ }
+ return elements;
+}
+
+/**
+ * @param {string} id
+ * @param {Document} [doc]
+ * @returns {Element}
+ */
+function getById(id, doc = document) {
+ const element = doc.getElementById(id);
+ if (!element) {
+ throw new Error("Could not find the element by id: #" + id);
+ }
+ if (isVisible(element)) {
+ return element;
+ }
+ throw new Error("The element is not visible in the DOM: #" + id);
+}
+
+/**
+ * A non-throwing version of `getByL10nId`.
+ *
+ * @param {string} l10nId
+ * @returns {Element | null}
+ */
+function maybeGetByL10nId(l10nId, doc = document) {
+ const selector = `[data-l10n-id="${l10nId}"]`;
+ const elements = doc.querySelectorAll(selector);
+ for (const element of elements) {
+ if (isVisible(element)) {
+ return element;
+ }
+ }
+ return null;
+}
+
+/**
+ * XUL popups will fire the popupshown and popuphidden events. These will fire for
+ * any type of popup in the browser. This function waits for one of those events, and
+ * checks that the viewId of the popup is PanelUI-profiler
+ *
+ * @param {"popupshown" | "popuphidden"} eventName
+ * @param {Function} callback
+ * @returns {Promise<void>}
+ */
+async function waitForTranslationsPopupEvent(eventName, callback) {
+ const panel = document.getElementById("translations-panel");
+ if (!panel) {
+ throw new Error("Unable to find the translations panel element.");
+ }
+ const promise = BrowserTestUtils.waitForEvent(panel, eventName);
+ callback();
+ info("Waiting for the translations panel popup to be shown");
+ await promise;
+ // Wait a single tick on the event loop.
+ await new Promise(resolve => setTimeout(resolve, 0));
+}
+
+/**
+ * When switching between between views in the popup panel, wait for the view to
+ * be fully shown.
+ *
+ * @param {Function} callback
+ */
+async function waitForViewShown(callback) {
+ const panel = document.getElementById("translations-panel");
+ if (!panel) {
+ throw new Error("Unable to find the translations panel element.");
+ }
+ const promise = BrowserTestUtils.waitForEvent(panel, "ViewShown");
+ callback();
+ info("Waiting for the translations panel view to be shown");
+ await promise;
+ await new Promise(resolve => setTimeout(resolve, 0));
+}
+
+const ENGLISH_PAGE_URL = TRANSLATIONS_TESTER_EN;
+const SPANISH_PAGE_URL = TRANSLATIONS_TESTER_ES;
+const SPANISH_PAGE_URL_2 = TRANSLATIONS_TESTER_ES_2;
+const SPANISH_PAGE_URL_DOT_ORG = TRANSLATIONS_TESTER_ES_DOT_ORG;
+const LANGUAGE_PAIRS = [
+ { fromLang: "es", toLang: "en", isBeta: false },
+ { fromLang: "en", toLang: "es", isBeta: false },
+ { fromLang: "fr", toLang: "en", isBeta: false },
+ { fromLang: "en", toLang: "fr", isBeta: false },
+ { fromLang: "en", toLang: "uk", isBeta: true },
+ { fromLang: "uk", toLang: "en", isBeta: true },
+];