summaryrefslogtreecommitdiffstats
path: root/browser/components/translations
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/translations')
-rw-r--r--browser/components/translations/content/translationsPanel.inc.xhtml145
-rw-r--r--browser/components/translations/content/translationsPanel.js1630
-rw-r--r--browser/components/translations/jar.mn6
-rw-r--r--browser/components/translations/moz.build10
-rw-r--r--browser/components/translations/tests/browser/browser.toml110
-rw-r--r--browser/components/translations/tests/browser/browser_translations_about_preferences_manage_downloaded_languages.js225
-rw-r--r--browser/components/translations/tests/browser/browser_translations_about_preferences_settings_always_translate_languages.js97
-rw-r--r--browser/components/translations/tests/browser/browser_translations_about_preferences_settings_never_translate_languages.js89
-rw-r--r--browser/components/translations/tests/browser/browser_translations_about_preferences_settings_never_translate_sites.js124
-rw-r--r--browser/components/translations/tests/browser/browser_translations_about_preferences_settings_ui.js87
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_a11y_focus.js32
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_always_translate_language_bad_data.js40
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_always_translate_language_basic.js79
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_always_translate_language_manual.js80
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_always_translate_language_restore.js101
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_app_menu_never_translate_language.js40
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_app_menu_never_translate_site.js140
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_auto_translate_error_view.js86
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_auto_translate_revisit_view.js86
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_basics.js69
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_button.js77
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_cancel.js27
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_close_panel_never_translate_language_with_translations_active.js147
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_close_panel_never_translate_language_with_translations_inactive.js93
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_close_panel_never_translate_site.js159
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_engine_destroy.js55
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_engine_destroy_pending.js72
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_engine_unsupported.js59
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_engine_unsupported_lang.js28
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_firstrun.js33
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_firstrun_revisit.js57
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_fuzzing.js240
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_gear.js32
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_never_translate_language.js186
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_never_translate_site.js247
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_never_translate_site_auto.js109
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_never_translate_site_basic.js62
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_never_translate_site_manual.js84
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_retry.js54
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_settings_unsupported_lang.js74
-rw-r--r--browser/components/translations/tests/browser/browser_translations_panel_switch_languages.js70
-rw-r--r--browser/components/translations/tests/browser/browser_translations_reader_mode.js115
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_context_menu_feature_disabled.js114
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_context_menu_with_full_page_translations_active.js162
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_context_menu_with_hyperlink.js109
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_context_menu_with_no_text_selected.js37
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_context_menu_with_text_selected.js76
-rw-r--r--browser/components/translations/tests/browser/browser_translations_telemetry_firstrun_auto_translate.js117
-rw-r--r--browser/components/translations/tests/browser/browser_translations_telemetry_firstrun_basics.js93
-rw-r--r--browser/components/translations/tests/browser/browser_translations_telemetry_firstrun_translation_failure.js149
-rw-r--r--browser/components/translations/tests/browser/browser_translations_telemetry_firstrun_unsupported_lang.js122
-rw-r--r--browser/components/translations/tests/browser/browser_translations_telemetry_open_panel.js85
-rw-r--r--browser/components/translations/tests/browser/browser_translations_telemetry_panel_auto_offer.js83
-rw-r--r--browser/components/translations/tests/browser/browser_translations_telemetry_panel_auto_offer_settings.js110
-rw-r--r--browser/components/translations/tests/browser/browser_translations_telemetry_switch_languages.js156
-rw-r--r--browser/components/translations/tests/browser/browser_translations_telemetry_translation_failure.js212
-rw-r--r--browser/components/translations/tests/browser/browser_translations_telemetry_translation_request.js170
-rw-r--r--browser/components/translations/tests/browser/head.js1423
58 files changed, 8574 insertions, 0 deletions
diff --git a/browser/components/translations/content/translationsPanel.inc.xhtml b/browser/components/translations/content/translationsPanel.inc.xhtml
new file mode 100644
index 0000000000..18769eec83
--- /dev/null
+++ b/browser/components/translations/content/translationsPanel.inc.xhtml
@@ -0,0 +1,145 @@
+<!-- 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-header"
+ orient="vertical"
+ onclick="TranslationsPanel.handlePanelButtonEvent(event)"
+ onpopupshown="TranslationsPanel.handlePanelPopupShownEvent(event)"
+ onpopuphidden="TranslationsPanel.handlePanelPopupHiddenEvent(event)">
+ <panelmultiview id="translations-panel-multiview"
+ mainViewId="translations-panel-view-default">
+ <panelview id="translations-panel-view-default"
+ class="PanelUI-subView translations-panel-view"
+ role="document"
+ mainview-with-header="true"
+ has-custom-header="true">
+ <hbox class="panel-header translations-panel-header">
+ <html:h1 class="translations-panel-header-wrapper">
+ <html:span id="translations-panel-header"></html:span>
+ </html:h1>
+ <hbox class="translations-panel-beta">
+ <image class="translations-panel-beta-icon"></image>
+ </hbox>
+ <toolbarbutton id="translations-panel-settings" class="panel-info-button"
+ data-l10n-id="translations-panel-settings-button"
+ closemenu="none"
+ oncommand="TranslationsPanel.openSettingsPopup(this)"/>
+ </hbox>
+
+ <vbox class="translations-panel-content">
+ <html:div id="translations-panel-intro">
+ <html:span data-l10n-id="translations-panel-intro-description"></html:span>
+ <html:a id="translations-panel-intro-learn-more-link"
+ is="moz-support-link"
+ data-l10n-id="translations-panel-learn-more-link"
+ support-page="website-translation"
+ onclick="TranslationsPanel.onLearnMoreLink()" />
+ </html:div>
+ <vbox id="translations-panel-lang-selection">
+ <label data-l10n-id="translations-panel-from-label" id="translations-panel-from-label"></label>
+ <menulist id="translations-panel-from"
+ flex="1"
+ value="detect"
+ size="large"
+ aria-labelledby="translations-panel-from-label"
+ oncommand="TranslationsPanel.onChangeFromLanguage(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" id="translations-panel-to-label"></label>
+ <menulist id="translations-panel-to"
+ flex="1"
+ value="detect"
+ size="large"
+ aria-labelledby="translations-panel-to-label"
+ oncommand="TranslationsPanel.onChangeToLanguage(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" hidden="true">
+ <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>
+
+ <html:moz-button-group class="panel-footer translations-panel-footer">
+ <button id="translations-panel-restore-button"
+ class="footer-button"
+ oncommand="TranslationsPanel.onRestore(event);"
+ data-l10n-id="translations-panel-restore-button">
+ </button>
+ <button id="translations-panel-cancel"
+ class="footer-button"
+ oncommand="TranslationsPanel.onCancel(event);"
+ data-l10n-id="translations-panel-translate-cancel">
+ </button>
+ <button id="translations-panel-translate"
+ class="footer-button"
+ oncommand="TranslationsPanel.onTranslate(event);"
+ data-l10n-id="translations-panel-translate-button"
+ default="true">
+ </button>
+ </html:moz-button-group>
+ </panelview>
+
+ <panelview id="translations-panel-view-unsupported-language"
+ class="PanelUI-subView translations-panel-view"
+ role="document"
+ has-custom-header="true">
+ <hbox class="panel-header translations-panel-header">
+ <image class="translations-panel-error-icon" />
+ <html:h1 id="translations-panel-unsupported-language-header">
+ <html:span data-l10n-id="translations-panel-error-unsupported"></html:span>
+ </html:h1>
+ </hbox>
+
+ <vbox class="translations-panel-content">
+ <html:div>
+ <html:span id="translations-panel-error-unsupported-hint"></html:span>
+ <html:a id="translations-panel-unsupported-learn-more-link"
+ is="moz-support-link"
+ data-l10n-id="translations-panel-learn-more-link"
+ support-page="website-translation"
+ onclick="TranslationsPanel.onLearnMoreLink()" />
+ </html:div>
+ </vbox>
+
+ <html:moz-button-group class="panel-footer translations-panel-footer">
+ <button id="translations-panel-change-source-language"
+ class="footer-button"
+ oncommand="TranslationsPanel.onChangeSourceLanguage(event);"
+ data-l10n-id="translations-panel-error-change-button">
+ </button>
+ <button id="translations-panel-dismiss-error"
+ class="footer-button"
+ oncommand="TranslationsPanel.onCancel(event);"
+ data-l10n-id="translations-panel-error-dismiss-button"
+ default="true">
+ </button>
+ </html:moz-button-group>
+ </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..71aaacac4f
--- /dev/null
+++ b/browser/components/translations/content/translationsPanel.js
@@ -0,0 +1,1630 @@
+/* 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, {
+ PageActions: "resource:///modules/PageActions.sys.mjs",
+ 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",
+ RESTORE_PAGE: "RESTORE_PAGE",
+ TRANSLATE_PAGE: "TRANSLATE_PAGE",
+ CLOSE_PANEL: "CLOSE_PANEL",
+});
+
+/**
+ * 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 CheckboxPageAction {
+ /**
+ * 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 CheckboxPageAction.#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}
+ */
+ alwaysTranslateLanguage() {
+ switch (this.#state()) {
+ case CheckboxPageAction.#computeState(1, 1, 0, 1):
+ case CheckboxPageAction.#computeState(1, 1, 0, 0):
+ return PageAction.RESTORE_PAGE;
+ case CheckboxPageAction.#computeState(0, 0, 1, 0):
+ case CheckboxPageAction.#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}
+ */
+ neverTranslateLanguage() {
+ switch (this.#state()) {
+ case CheckboxPageAction.#computeState(1, 1, 0, 1):
+ case CheckboxPageAction.#computeState(1, 1, 0, 0):
+ case CheckboxPageAction.#computeState(1, 0, 0, 1):
+ case CheckboxPageAction.#computeState(1, 0, 0, 0):
+ return PageAction.RESTORE_PAGE;
+ case CheckboxPageAction.#computeState(0, 1, 0, 0):
+ case CheckboxPageAction.#computeState(0, 0, 0, 1):
+ case CheckboxPageAction.#computeState(0, 1, 0, 1):
+ case CheckboxPageAction.#computeState(0, 0, 0, 0):
+ return PageAction.CLOSE_PANEL;
+ }
+ 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}
+ */
+ neverTranslateSite() {
+ switch (this.#state()) {
+ case CheckboxPageAction.#computeState(1, 1, 0, 0):
+ case CheckboxPageAction.#computeState(1, 0, 1, 0):
+ case CheckboxPageAction.#computeState(1, 0, 0, 0):
+ return PageAction.RESTORE_PAGE;
+ case CheckboxPageAction.#computeState(0, 1, 0, 1):
+ return PageAction.TRANSLATE_PAGE;
+ case CheckboxPageAction.#computeState(0, 0, 1, 0):
+ case CheckboxPageAction.#computeState(0, 1, 0, 0):
+ case CheckboxPageAction.#computeState(0, 0, 0, 0):
+ return PageAction.CLOSE_PANEL;
+ }
+ 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. Note that this script is loaded in very early to
+ * the browser loading process, and may run before the console is avialable. In
+ * this case the console will return as `undefined`.
+ *
+ * @returns {Console | void}
+ */
+ get console() {
+ if (!this.#console) {
+ try {
+ this.#console = console.createInstance({
+ maxLogLevelPref: "browser.translations.logLevel",
+ prefix: "Translations",
+ });
+ } catch {
+ // The console may not be initialized yet.
+ }
+ }
+ return this.#console;
+ }
+
+ /**
+ * Tracks if the popup is open, or scheduled to be open.
+ *
+ * @type {boolean}
+ */
+ #isPopupOpen = false;
+
+ /**
+ * 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("cancelButton", "translations-panel-cancel");
+ getter(
+ "changeSourceLanguageButton",
+ "translations-panel-change-source-language"
+ );
+ getter("dismissErrorButton", "translations-panel-dismiss-error");
+ 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("fromLabel", "translations-panel-from-label");
+ getter("header", "translations-panel-header");
+ getter("intro", "translations-panel-intro");
+ getter("introLearnMoreLink", "translations-panel-intro-learn-more-link");
+ getter("langSelection", "translations-panel-lang-selection");
+ getter("multiview", "translations-panel-multiview");
+ getter("restoreButton", "translations-panel-restore-button");
+ getter("toLabel", "translations-panel-to-label");
+ getter("toMenuList", "translations-panel-to");
+ getter("translateButton", "translations-panel-translate");
+ getter(
+ "unsupportedHeader",
+ "translations-panel-unsupported-language-header"
+ );
+ getter("unsupportedHint", "translations-panel-error-unsupported-hint");
+ getter(
+ "unsupportedLearnMoreLink",
+ "translations-panel-unsupported-learn-more-link"
+ );
+
+ // Getters by class
+ getter(
+ "alwaysTranslateLanguageMenuItem",
+ ".always-translate-language-menuitem"
+ );
+ getter("manageLanguagesMenuItem", ".manage-languages-menuitem");
+ getter(
+ "neverTranslateLanguageMenuItem",
+ ".never-translate-language-menuitem"
+ );
+ getter("neverTranslateSiteMenuItem", ".never-translate-site-menuitem");
+ }
+
+ return this.#lazyElements;
+ }
+
+ #lazyButtonElements = null;
+
+ /**
+ * When accessing `this.elements` the first time, it de-lazifies the custom components
+ * that are needed for the popup. Avoid that by having a second element lookup
+ * just for modifying the button.
+ */
+ get buttonElements() {
+ if (!this.#lazyButtonElements) {
+ this.#lazyButtonElements = {
+ button: document.getElementById("translations-button"),
+ buttonLocale: document.getElementById("translations-button-locale"),
+ buttonCircleArrows: document.getElementById(
+ "translations-button-circle-arrows"
+ ),
+ };
+ }
+ return this.#lazyButtonElements;
+ }
+
+ /**
+ * 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, intro } =
+ this.elements;
+ error.hidden = false;
+ intro.hidden = true;
+ 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().getDetectedLanguages();
+ 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 TranslationsParent.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, displayName } of fromLanguages) {
+ const fromMenuItem = document.createXULElement("menuitem");
+ fromMenuItem.setAttribute("value", langTag);
+ fromMenuItem.setAttribute("label", displayName);
+ popup.appendChild(fromMenuItem);
+ }
+ }
+
+ for (const popup of toPopups) {
+ for (const { langTag, displayName } of toLanguages) {
+ const toMenuItem = document.createXULElement("menuitem");
+ toMenuItem.setAttribute("value", langTag);
+ toMenuItem.setAttribute("label", displayName);
+ popup.appendChild(toMenuItem);
+ }
+ }
+
+ this.#langListsPhase = "initialized";
+ } catch (error) {
+ this.console?.error(error);
+ this.#langListsPhase = "error";
+ }
+ }
+
+ /**
+ * Reactively sets the views based on the async state changes of the engine, and
+ * other component state changes.
+ *
+ * @param {TranslationsLanguageState} languageState
+ */
+ #updateViewFromTranslationStatus(
+ languageState = this.#getTranslationsActor().languageState
+ ) {
+ const { translateButton, toMenuList, fromMenuList, header, cancelButton } =
+ this.elements;
+ const { requestedTranslationPair, isEngineReady } = languageState;
+
+ if (
+ requestedTranslationPair &&
+ !isEngineReady &&
+ toMenuList.value === requestedTranslationPair.toLanguage &&
+ fromMenuList.value === requestedTranslationPair.fromLanguage
+ ) {
+ // A translation has been requested, but is not ready yet.
+ document.l10n.setAttributes(
+ translateButton,
+ "translations-panel-translate-button-loading"
+ );
+ translateButton.disabled = true;
+ cancelButton.hidden = false;
+ this.updateUIForReTranslation(false /* isReTranslation */);
+ } else {
+ document.l10n.setAttributes(
+ translateButton,
+ "translations-panel-translate-button"
+ );
+ translateButton.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 ||
+ // This is the requested translation pair.
+ (requestedTranslationPair &&
+ requestedTranslationPair.fromLanguage === fromMenuList.value &&
+ requestedTranslationPair.toLanguage === toMenuList.value);
+ }
+
+ if (requestedTranslationPair && isEngineReady) {
+ const { fromLanguage, toLanguage } = requestedTranslationPair;
+ const displayNames = new Services.intl.DisplayNames(undefined, {
+ type: "language",
+ });
+ cancelButton.hidden = true;
+ this.updateUIForReTranslation(true /* isReTranslation */);
+
+ document.l10n.setAttributes(header, "translations-panel-revisit-header", {
+ fromLanguage: displayNames.of(fromLanguage),
+ toLanguage: displayNames.of(toLanguage),
+ });
+ } else {
+ document.l10n.setAttributes(header, "translations-panel-header");
+ }
+ }
+
+ /**
+ * @param {boolean} isReTranslation
+ */
+ updateUIForReTranslation(isReTranslation) {
+ const { restoreButton, fromLabel, fromMenuList, toLabel } = this.elements;
+ restoreButton.hidden = !isReTranslation;
+ // When offering to re-translate a page, hide the "from" language so users don't
+ // get confused.
+ fromLabel.hidden = isReTranslation;
+ fromMenuList.hidden = isReTranslation;
+ if (isReTranslation) {
+ fromLabel.style.marginBlockStart = "";
+ toLabel.style.marginBlockStart = 0;
+ } else {
+ fromLabel.style.marginBlockStart = 0;
+ toLabel.style.marginBlockStart = "";
+ }
+ }
+
+ /**
+ * Returns true if the panel is currently showing the default view, otherwise false.
+ *
+ * @returns {boolean}
+ */
+ #isShowingDefaultView() {
+ if (!this.#lazyElements) {
+ // Nothing has been initialized.
+ return false;
+ }
+ const { multiview } = this.elements;
+ return (
+ multiview.getAttribute("mainViewId") === "translations-panel-view-default"
+ );
+ }
+
+ /**
+ * Show the default view of choosing a source and target language.
+ *
+ * @param {TranslationsParent} actor
+ * @param {boolean} force - Force the page to show translation options.
+ */
+ async #showDefaultView(actor, force = false) {
+ const {
+ fromMenuList,
+ multiview,
+ panel,
+ error,
+ toMenuList,
+ translateButton,
+ langSelection,
+ intro,
+ header,
+ } = this.elements;
+
+ this.#updateViewFromTranslationStatus();
+
+ // Unconditionally hide the intro text in case the panel is re-shown.
+ intro.hidden = true;
+
+ if (this.#langListsPhase === "error") {
+ // There was an error, display it in the view rather than the language
+ // dropdowns.
+ const { cancelButton, 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(actor),
+ });
+
+ translateButton.disabled = true;
+ this.updateUIForReTranslation(false /* isReTranslation */);
+ cancelButton.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 { cancelButton } = this.elements;
+
+ if (langTags?.isDocLangTagSupported) {
+ fromMenuList.value = langTags?.docLangTag ?? "";
+ } else {
+ fromMenuList.value = "";
+ }
+ toMenuList.value = langTags?.userLangTag ?? "";
+
+ this.onChangeLanguages();
+
+ this.updateUIForReTranslation(false /* isReTranslation */);
+ cancelButton.hidden = false;
+ multiview.setAttribute("mainViewId", "translations-panel-view-default");
+
+ if (!this._hasShownPanel) {
+ actor.firstShowUriSpec = gBrowser.currentURI.spec;
+ }
+
+ if (
+ this._hasShownPanel &&
+ gBrowser.currentURI.spec !== actor.firstShowUriSpec
+ ) {
+ document.l10n.setAttributes(header, "translations-panel-header");
+ actor.firstShowUriSpec = null;
+ intro.hidden = true;
+ } else {
+ Services.prefs.setBoolPref("browser.translations.panelShown", true);
+ intro.hidden = false;
+ document.l10n.setAttributes(header, "translations-panel-intro-header");
+ }
+ } 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 langTags = await this.#getCachedDetectedLanguages();
+ const { docLangTag, isDocLangTagSupported } = langTags;
+
+ const { panel } = this.elements;
+ const alwaysTranslateMenuItems = panel.ownerDocument.querySelectorAll(
+ ".always-translate-language-menuitem"
+ );
+ const neverTranslateMenuItems = panel.ownerDocument.querySelectorAll(
+ ".never-translate-language-menuitem"
+ );
+ const alwaysOfferTranslationsMenuItems =
+ panel.ownerDocument.querySelectorAll(
+ ".always-offer-translations-menuitem"
+ );
+
+ const alwaysOfferTranslations =
+ TranslationsParent.shouldAlwaysOfferTranslations();
+ const alwaysTranslateLanguage =
+ TranslationsParent.shouldAlwaysTranslateLanguage(langTags);
+ const neverTranslateLanguage =
+ TranslationsParent.shouldNeverTranslateLanguage(docLangTag);
+ const shouldDisable =
+ !docLangTag ||
+ !isDocLangTagSupported ||
+ docLangTag === new Intl.Locale(Services.locale.appLocaleAsBCP47).language;
+
+ for (const menuitem of alwaysOfferTranslationsMenuItems) {
+ menuitem.setAttribute(
+ "checked",
+ alwaysOfferTranslations ? "true" : "false"
+ );
+ }
+ for (const menuitem of alwaysTranslateMenuItems) {
+ menuitem.setAttribute(
+ "checked",
+ alwaysTranslateLanguage ? "true" : "false"
+ );
+ menuitem.disabled = shouldDisable;
+ }
+ for (const menuitem of neverTranslateMenuItems) {
+ menuitem.setAttribute(
+ "checked",
+ neverTranslateLanguage ? "true" : "false"
+ );
+ menuitem.disabled = shouldDisable;
+ }
+ }
+
+ /**
+ * Updates the checked states of the settings menu checkboxes that
+ * pertain to site permissions.
+ */
+ async #updateSettingsMenuSiteCheckboxStates() {
+ const { panel } = this.elements;
+ const neverTranslateSiteMenuItems = panel.ownerDocument.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.ownerDocument.querySelectorAll(
+ ".always-translate-language-menuitem"
+ );
+ const neverTranslateMenuItems = panel.ownerDocument.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 { fromMenuList, toMenuList, intro } = this.elements;
+ if (!this.#isShowingDefaultView()) {
+ await this.#showDefaultView(this.#getTranslationsActor());
+ }
+ intro.hidden = true;
+ fromMenuList.value = fromLanguage;
+ toMenuList.value = toLanguage;
+ this.onChangeLanguages();
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Handle logic and telemetry for changing the selected from-language option.
+ *
+ * @param {Event} event
+ */
+ onChangeFromLanguage(event) {
+ const { target } = event;
+ if (target?.value) {
+ TranslationsParent.telemetry().panel().onChangeFromLanguage(target.value);
+ }
+ this.onChangeLanguages();
+ }
+
+ /**
+ * Handle logic and telemetry for changing the selected to-language option.
+ *
+ * @param {Event} event
+ */
+ onChangeToLanguage(event) {
+ const { target } = event;
+ if (target?.value) {
+ TranslationsParent.telemetry().panel().onChangeToLanguage(target.value);
+ }
+ this.onChangeLanguages();
+ }
+
+ /**
+ * When changing the language selection, the translate button will need updating.
+ */
+ onChangeLanguages() {
+ this.#updateViewFromTranslationStatus();
+ }
+
+ /**
+ * Hide the pop up (for event handlers).
+ */
+ close() {
+ PanelMultiView.hidePopup(this.elements.panel);
+ }
+
+ /*
+ * Handler for clicking the learn more link from linked text
+ * within the translations panel.
+ */
+ onLearnMoreLink() {
+ TranslationsParent.telemetry().panel().onLearnMoreLink();
+ TranslationsPanel.close();
+ }
+
+ /*
+ * Handler for clicking the learn more link from the gear menu.
+ */
+ onAboutTranslations() {
+ TranslationsParent.telemetry().panel().onAboutTranslations();
+ PanelMultiView.hidePopup(this.elements.panel);
+ const window =
+ gBrowser.selectedBrowser.browsingContext.top.embedderElement.ownerGlobal;
+ window.openTrustedLinkIn(
+ "https://support.mozilla.org/kb/website-translation",
+ "tab",
+ {
+ forceForeground: true,
+ triggeringPrincipal:
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ }
+ );
+ }
+
+ /**
+ * 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;
+ PanelMultiView.hidePopup(panel);
+
+ await this.#showDefaultView(
+ this.#getTranslationsActor(),
+ true /* force this view to be shown */
+ );
+
+ await this.#openPanelPopup(this.elements.appMenuButton, {
+ event,
+ viewName: "defaultView",
+ maintainFlow: true,
+ });
+ }
+
+ /**
+ * @param {TranslationsActor} actor
+ */
+ async #reloadLangList(actor) {
+ try {
+ await this.#ensureLangListsBuilt();
+ await this.#showDefaultView(actor);
+ } catch (error) {
+ this.elements.errorHintAction.disabled = false;
+ }
+ }
+
+ /**
+ * Handle telemetry events when buttons are invoked in the panel.
+ *
+ * @param {Event} event
+ */
+ handlePanelButtonEvent(event) {
+ const {
+ cancelButton,
+ changeSourceLanguageButton,
+ dismissErrorButton,
+ restoreButton,
+ translateButton,
+ } = this.elements;
+ switch (event.target.id) {
+ case cancelButton.id: {
+ TranslationsParent.telemetry().panel().onCancelButton();
+ break;
+ }
+ case changeSourceLanguageButton.id: {
+ TranslationsParent.telemetry().panel().onChangeSourceLanguageButton();
+ break;
+ }
+ case dismissErrorButton.id: {
+ TranslationsParent.telemetry().panel().onDismissErrorButton();
+ break;
+ }
+ case restoreButton.id: {
+ TranslationsParent.telemetry().panel().onRestorePageButton();
+ break;
+ }
+ case translateButton.id: {
+ TranslationsParent.telemetry().panel().onTranslateButton();
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handle telemetry events when popups are shown in the panel.
+ *
+ * @param {Event} event
+ */
+ handlePanelPopupShownEvent(event) {
+ const { panel, fromMenuList, toMenuList } = this.elements;
+ switch (event.target.id) {
+ case panel.id: {
+ // This telemetry event is invoked externally because it requires
+ // extra logic about from where the panel was opened and whether
+ // or not the flow should be maintained or started anew.
+ break;
+ }
+ case fromMenuList.firstChild.id: {
+ TranslationsParent.telemetry().panel().onOpenFromLanguageMenu();
+ break;
+ }
+ case toMenuList.firstChild.id: {
+ TranslationsParent.telemetry().panel().onOpenToLanguageMenu();
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handle telemetry events when popups are hidden in the panel.
+ *
+ * @param {Event} event
+ */
+ handlePanelPopupHiddenEvent(event) {
+ const { panel, fromMenuList, toMenuList } = this.elements;
+ switch (event.target.id) {
+ case panel.id: {
+ TranslationsParent.telemetry().panel().onClose();
+ this.#isPopupOpen = false;
+ this.elements.error.hidden = true;
+ break;
+ }
+ case fromMenuList.firstChild.id: {
+ TranslationsParent.telemetry().panel().onCloseFromLanguageMenu();
+ break;
+ }
+ case toMenuList.firstChild.id: {
+ TranslationsParent.telemetry().panel().onCloseToLanguageMenu();
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handle telemetry events when the settings menu is shown.
+ */
+ handleSettingsPopupShownEvent() {
+ TranslationsParent.telemetry().panel().onOpenSettingsMenu();
+ }
+
+ /**
+ * Handle telemetry events when the settings menu is hidden.
+ */
+ handleSettingsPopupHiddenEvent() {
+ TranslationsParent.telemetry().panel().onCloseSettingsMenu();
+ }
+
+ /**
+ * Opens the Translations panel popup at the given target.
+ *
+ * @param {object} target - The target element at which to open the popup.
+ * @param {object} telemetryData
+ * @param {string} telemetryData.event
+ * The trigger event for opening the popup.
+ * @param {string} telemetryData.viewName
+ * The name of the view shown by the panel.
+ * @param {boolean} telemetryData.autoShow
+ * True if the panel was automatically opened, otherwise false.
+ * @param {boolean} telemetryData.maintainFlow
+ * Whether or not to maintain the flow of telemetry.
+ * @param {boolean} telemetryData.isFirstUserInteraction
+ * Whether or not this is the first user interaction with the panel.
+ */
+ async #openPanelPopup(
+ target,
+ {
+ event = null,
+ viewName = null,
+ autoShow = false,
+ maintainFlow = false,
+ isFirstUserInteraction = null,
+ }
+ ) {
+ await window.ensureCustomElements("moz-button-group");
+
+ const { panel, appMenuButton } = this.elements;
+ const openedFromAppMenu = target.id === appMenuButton.id;
+ const { docLangTag } = await this.#getCachedDetectedLanguages();
+
+ TranslationsParent.telemetry().panel().onOpen({
+ viewName,
+ autoShow,
+ docLangTag,
+ maintainFlow,
+ openedFromAppMenu,
+ isFirstUserInteraction,
+ });
+
+ this.#isPopupOpen = true;
+
+ PanelMultiView.openPopup(panel, target, {
+ position: "bottomright topright",
+ triggerEvent: event,
+ }).catch(error => this.console?.error(error));
+ }
+
+ /**
+ * Keeps track of open requests to guard against race conditions.
+ *
+ * @type {Promise<void> | null}
+ */
+ #openPromise = null;
+
+ /**
+ * Opens the TranslationsPanel.
+ *
+ * @param {Event} event
+ * @param {boolean} reportAsAutoShow
+ * True to report to telemetry that the panel was opened automatically, otherwise false.
+ */
+ async open(event, reportAsAutoShow = false) {
+ if (this.#openPromise) {
+ // There is already an open event happening, do not open.
+ return;
+ }
+
+ this.#openPromise = this.#openImpl(event, reportAsAutoShow);
+ this.#openPromise.finally(() => {
+ this.#openPromise = null;
+ });
+ }
+
+ /**
+ * Implementation function for opening the panel. Prefer TranslationsPanel.open.
+ *
+ * @param {Event} event
+ */
+ async #openImpl(event, reportAsAutoShow) {
+ event.stopPropagation();
+ if (
+ (event.type == "click" && event.button != 0) ||
+ (event.type == "keypress" &&
+ event.charCode != KeyEvent.DOM_VK_SPACE &&
+ event.keyCode != KeyEvent.DOM_VK_RETURN)
+ ) {
+ // Allow only left click, space, or enter.
+ return;
+ }
+
+ const window =
+ gBrowser.selectedBrowser.browsingContext.top.embedderElement.ownerGlobal;
+ window.ensureCustomElements("moz-support-link");
+
+ const { button } = this.buttonElements;
+
+ const { requestedTranslationPair, locationChangeId } =
+ this.#getTranslationsActor().languageState;
+
+ // Store this value because it gets modified when #showDefaultView is called below.
+ const isFirstUserInteraction = !this._hasShownPanel;
+
+ await this.#ensureLangListsBuilt();
+
+ if (requestedTranslationPair) {
+ await this.#showRevisitView(requestedTranslationPair).catch(error => {
+ this.console?.error(error);
+ });
+ } else {
+ await this.#showDefaultView(this.#getTranslationsActor()).catch(error => {
+ this.console?.error(error);
+ });
+ }
+
+ this.#populateSettingsMenuItems();
+
+ const targetButton =
+ button.contains(event.target) ||
+ event.type === "TranslationsParent:OfferTranslation"
+ ? button
+ : this.elements.appMenuButton;
+
+ if (!TranslationsParent.isActiveLocation(locationChangeId)) {
+ this.console?.log(`A translation panel open request was stale.`, {
+ locationChangeId,
+ newlocationChangeId:
+ this.#getTranslationsActor().languageState.locationChangeId,
+ currentURISpec: gBrowser.currentURI.spec,
+ });
+ return;
+ }
+
+ this.console?.log(`Showing a translation panel`, gBrowser.currentURI.spec);
+
+ await this.#openPanelPopup(targetButton, {
+ event,
+ autoShow: reportAsAutoShow,
+ viewName: requestedTranslationPair ? "revisitView" : "defaultView",
+ maintainFlow: false,
+ isFirstUserInteraction,
+ });
+ }
+
+ /**
+ * 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,
+ false // reportAsAutoTranslate
+ );
+ }
+
+ /**
+ * Handle the cancel button being clicked.
+ */
+ onCancel() {
+ PanelMultiView.hidePopup(this.elements.panel);
+ }
+
+ /**
+ * A handler for opening the settings context menu.
+ */
+ openSettingsPopup(button) {
+ this.#updateSettingsMenuLanguageCheckboxStates();
+ this.#updateSettingsMenuSiteCheckboxStates();
+ const popup = button.ownerDocument.getElementById(
+ "translations-panel-settings-menupopup"
+ );
+ popup.openPopup(button, "after_end");
+ }
+
+ /**
+ * Creates a new CheckboxPageAction based on the current translated
+ * state of the page and the state of the persistent options in the
+ * translations panel settings.
+ *
+ * @returns {CheckboxPageAction}
+ */
+ getCheckboxPageActionFor() {
+ 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 CheckboxPageAction(
+ this.#isTranslationsActive(),
+ alwaysTranslateLanguage,
+ neverTranslateLanguage,
+ neverTranslateSite
+ );
+ }
+
+ /**
+ * Redirect the user to about:preferences
+ */
+ openManageLanguages() {
+ TranslationsParent.telemetry().panel().onManageLanguages();
+ 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.RESTORE_PAGE: {
+ await this.onRestore();
+ break;
+ }
+ case PageAction.TRANSLATE_PAGE: {
+ await this.onTranslate();
+ break;
+ }
+ case PageAction.CLOSE_PANEL: {
+ PanelMultiView.hidePopup(this.elements.panel);
+ 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 langTags = await this.#getCachedDetectedLanguages();
+ const { docLangTag } = langTags;
+ if (!docLangTag) {
+ throw new Error("Expected to have a document language tag.");
+ }
+ const pageAction =
+ this.getCheckboxPageActionFor().alwaysTranslateLanguage();
+ const toggledOn =
+ TranslationsParent.toggleAlwaysTranslateLanguagePref(langTags);
+ TranslationsParent.telemetry()
+ .panel()
+ .onAlwaysTranslateLanguage(docLangTag, toggledOn);
+ this.#updateSettingsMenuLanguageCheckboxStates();
+ await this.#doPageAction(pageAction);
+ }
+
+ /**
+ * Toggle offering translations.
+ */
+ async onAlwaysOfferTranslations() {
+ const toggledOn = TranslationsParent.toggleAutomaticallyPopupPref();
+ TranslationsParent.telemetry().panel().onAlwaysOfferTranslations(toggledOn);
+ }
+
+ /**
+ * 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.getCheckboxPageActionFor().neverTranslateLanguage();
+ const toggledOn =
+ TranslationsParent.toggleNeverTranslateLanguagePref(docLangTag);
+ TranslationsParent.telemetry()
+ .panel()
+ .onNeverTranslateLanguage(docLangTag, toggledOn);
+ 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.getCheckboxPageActionFor().neverTranslateSite();
+ const toggledOn =
+ await this.#getTranslationsActor().toggleNeverTranslateSitePermissions();
+ TranslationsParent.telemetry().panel().onNeverTranslateSite(toggledOn);
+ 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);
+ }
+
+ /**
+ * An event handler that allows the TranslationsPanel object
+ * to be compatible with the addTabsProgressListener function.
+ *
+ * @param {tabbrowser} browser
+ */
+ onLocationChange(browser) {
+ if (browser.currentURI.spec.startsWith("about:reader")) {
+ // Hide the translations button when entering reader mode.
+ this.buttonElements.button.hidden = true;
+ }
+ }
+
+ /**
+ * Update the view to show an error.
+ *
+ * @param {TranslationParent} actor
+ */
+ async #showEngineError(actor) {
+ const { button } = this.buttonElements;
+ await this.#ensureLangListsBuilt();
+ if (!this.#isShowingDefaultView()) {
+ await this.#showDefaultView(actor).catch(e => {
+ this.console?.error(e);
+ });
+ }
+ 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.
+ await this.#openPanelPopup(targetButton, {
+ autoShow: true,
+ viewName: "errorView",
+ maintainFlow: true,
+ });
+ }
+
+ /**
+ * Set the state of the translations button in the URL bar.
+ *
+ * @param {CustomEvent} event
+ */
+ handleEvent = event => {
+ switch (event.type) {
+ case "TranslationsParent:OfferTranslation": {
+ if (Services.wm.getMostRecentBrowserWindow()?.gBrowser === gBrowser) {
+ this.open(event, /* reportAsAutoShow */ true);
+ }
+ break;
+ }
+ case "TranslationsParent:LanguageState": {
+ const { actor } = event.detail;
+ const {
+ detectedLanguages,
+ requestedTranslationPair,
+ error,
+ isEngineReady,
+ } = actor.languageState;
+
+ const { button, buttonLocale, buttonCircleArrows } =
+ this.buttonElements;
+
+ 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;
+ }
+
+ if (this.#isPopupOpen) {
+ // Make sure to use the language state that is passed by the event.detail, and
+ // don't read it from the actor here, as it's possible the actor isn't available
+ // via the gBrowser.selectedBrowser.
+ this.#updateViewFromTranslationStatus(actor.languageState);
+ }
+
+ 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 we can translate this language.
+ (hasSupportedLanguage &&
+ TranslationsParent.getIsTranslationsEngineSupported())
+ ) {
+ // Keep track if the button was originally hidden, because it will be shown now.
+ const wasButtonHidden = button.hidden;
+
+ button.hidden = false;
+ if (requestedTranslationPair) {
+ // The translation is active, update the urlbar button.
+ button.setAttribute("translationsactive", true);
+ if (isEngineReady) {
+ const displayNames = new Services.intl.DisplayNames(undefined, {
+ type: "language",
+ });
+
+ document.l10n.setAttributes(
+ button,
+ "urlbar-translations-button-translated",
+ {
+ fromLanguage: displayNames.of(
+ requestedTranslationPair.fromLanguage
+ ),
+ toLanguage: displayNames.of(
+ requestedTranslationPair.toLanguage
+ ),
+ }
+ );
+ // Show the locale of the page in the button.
+ buttonLocale.hidden = false;
+ buttonCircleArrows.hidden = true;
+ buttonLocale.innerText = requestedTranslationPair.toLanguage;
+ } else {
+ document.l10n.setAttributes(
+ button,
+ "urlbar-translations-button-loading"
+ );
+ // 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;
+
+ // Follow the same rules for displaying the first-run intro text for the
+ // button's accessible tooltip label.
+ if (
+ this._hasShownPanel &&
+ gBrowser.currentURI.spec !== actor.firstShowUriSpec
+ ) {
+ document.l10n.setAttributes(
+ button,
+ "urlbar-translations-button2"
+ );
+ } else {
+ document.l10n.setAttributes(
+ button,
+ "urlbar-translations-button-intro"
+ );
+ }
+ }
+
+ // The button was hidden, but now it is shown.
+ if (wasButtonHidden) {
+ PageActions.sendPlacedInUrlbarTrigger(button);
+ }
+ } else if (!button.hidden) {
+ // There are no translations visible, hide the button.
+ button.hidden = true;
+ }
+
+ switch (error) {
+ case null:
+ break;
+ case "engine-load-failure":
+ this.#showEngineError(actor).catch(viewError =>
+ this.console.error(viewError)
+ );
+ break;
+ default:
+ console.error("Unknown translation error", error);
+ }
+ break;
+ }
+ }
+ };
+})();
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ TranslationsPanel,
+ "_hasShownPanel",
+ "browser.translations.panelShown",
+ false
+);
diff --git a/browser/components/translations/jar.mn b/browser/components/translations/jar.mn
new file mode 100644
index 0000000000..5f30e6f73f
--- /dev/null
+++ b/browser/components/translations/jar.mn
@@ -0,0 +1,6 @@
+# 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)
diff --git a/browser/components/translations/moz.build b/browser/components/translations/moz.build
new file mode 100644
index 0000000000..212b93e509
--- /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.toml"]
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/browser/components/translations/tests/browser/browser.toml b/browser/components/translations/tests/browser/browser.toml
new file mode 100644
index 0000000000..bc617bf2fd
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser.toml
@@ -0,0 +1,110 @@
+[DEFAULT]
+support-files = [
+ "head.js",
+ "!/toolkit/components/translations/tests/browser/shared-head.js",
+ "!/toolkit/components/translations/tests/browser/translations-test.mjs",
+]
+
+["browser_translations_about_preferences_manage_downloaded_languages.js"]
+
+["browser_translations_about_preferences_settings_always_translate_languages.js"]
+
+["browser_translations_about_preferences_settings_never_translate_languages.js"]
+
+["browser_translations_about_preferences_settings_never_translate_sites.js"]
+
+["browser_translations_about_preferences_settings_ui.js"]
+
+["browser_translations_panel_a11y_focus.js"]
+
+["browser_translations_panel_always_translate_language_bad_data.js"]
+
+["browser_translations_panel_always_translate_language_basic.js"]
+
+["browser_translations_panel_always_translate_language_manual.js"]
+
+["browser_translations_panel_always_translate_language_restore.js"]
+
+["browser_translations_panel_app_menu_never_translate_language.js"]
+
+["browser_translations_panel_app_menu_never_translate_site.js"]
+
+["browser_translations_panel_auto_translate_error_view.js"]
+
+["browser_translations_panel_auto_translate_revisit_view.js"]
+
+["browser_translations_panel_basics.js"]
+
+["browser_translations_panel_button.js"]
+
+["browser_translations_panel_cancel.js"]
+
+["browser_translations_panel_close_panel_never_translate_language_with_translations_active.js"]
+
+["browser_translations_panel_close_panel_never_translate_language_with_translations_inactive.js"]
+
+["browser_translations_panel_close_panel_never_translate_site.js"]
+
+["browser_translations_panel_engine_destroy.js"]
+
+["browser_translations_panel_engine_destroy_pending.js"]
+
+["browser_translations_panel_engine_unsupported.js"]
+
+["browser_translations_panel_engine_unsupported_lang.js"]
+
+["browser_translations_panel_firstrun.js"]
+
+["browser_translations_panel_firstrun_revisit.js"]
+
+["browser_translations_panel_fuzzing.js"]
+skip-if = ["true"]
+
+["browser_translations_panel_gear.js"]
+
+["browser_translations_panel_never_translate_language.js"]
+
+["browser_translations_panel_never_translate_site_auto.js"]
+
+["browser_translations_panel_never_translate_site_basic.js"]
+
+["browser_translations_panel_never_translate_site_manual.js"]
+
+["browser_translations_panel_retry.js"]
+skip-if = ["os == 'linux' && !debug"] # Bug 1863227
+
+["browser_translations_panel_settings_unsupported_lang.js"]
+
+["browser_translations_panel_switch_languages.js"]
+
+["browser_translations_reader_mode.js"]
+
+["browser_translations_select_context_menu_feature_disabled.js"]
+
+["browser_translations_select_context_menu_with_full_page_translations_active.js"]
+
+["browser_translations_select_context_menu_with_hyperlink.js"]
+
+["browser_translations_select_context_menu_with_no_text_selected.js"]
+
+["browser_translations_select_context_menu_with_text_selected.js"]
+
+["browser_translations_telemetry_firstrun_auto_translate.js"]
+
+["browser_translations_telemetry_firstrun_basics.js"]
+
+["browser_translations_telemetry_firstrun_translation_failure.js"]
+
+["browser_translations_telemetry_firstrun_unsupported_lang.js"]
+
+["browser_translations_telemetry_open_panel.js"]
+
+["browser_translations_telemetry_panel_auto_offer.js"]
+
+["browser_translations_telemetry_panel_auto_offer_settings.js"]
+
+["browser_translations_telemetry_switch_languages.js"]
+
+["browser_translations_telemetry_translation_failure.js"]
+
+["browser_translations_telemetry_translation_request.js"]
diff --git a/browser/components/translations/tests/browser/browser_translations_about_preferences_manage_downloaded_languages.js b/browser/components/translations/tests/browser/browser_translations_about_preferences_manage_downloaded_languages.js
new file mode 100644
index 0000000000..383f2094a7
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_about_preferences_manage_downloaded_languages.js
@@ -0,0 +1,225 @@
+/* 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,
+ ukrainianLabel,
+ ukrainianDownload,
+ ukrainianDelete,
+ },
+ } = await setupAboutPreferences(LANGUAGE_PAIRS, {
+ prefs: [["browser.translations.newSettingsUI.enable", false]],
+ });
+
+ is(
+ downloadAllLabel.getAttribute("data-l10n-id"),
+ "translations-manage-install-description",
+ "The first row is all of the languages."
+ );
+ is(frenchLabel.textContent, "French", "There is a French row.");
+ is(spanishLabel.textContent, "Spanish", "There is a Spanish row.");
+ is(ukrainianLabel.textContent, "Ukrainian", "There is a Ukrainian row.");
+
+ await ensureVisibility({
+ message: "Everything starts out as available to download",
+ visible: {
+ downloadAll,
+ frenchDownload,
+ spanishDownload,
+ ukrainianDownload,
+ },
+ hidden: { deleteAll, frenchDelete, spanishDelete, ukrainianDelete },
+ });
+
+ click(frenchDownload, "Downloading French");
+
+ Assert.deepEqual(
+ await remoteClients.translationModels.resolvePendingDownloads(
+ frenchModels.length
+ ),
+ frenchModels,
+ "French models were downloaded."
+ );
+
+ await ensureVisibility({
+ message: "French can now be deleted, and delete all is available.",
+ visible: {
+ downloadAll,
+ deleteAll,
+ frenchDelete,
+ spanishDownload,
+ ukrainianDownload,
+ },
+ hidden: { frenchDownload, spanishDelete, ukrainianDelete },
+ });
+
+ click(frenchDelete, "Deleting French");
+
+ await ensureVisibility({
+ message: "Everything can be downloaded.",
+ visible: {
+ downloadAll,
+ frenchDownload,
+ spanishDownload,
+ ukrainianDownload,
+ },
+ hidden: { deleteAll, frenchDelete, spanishDelete, ukrainianDelete },
+ });
+
+ click(downloadAll, "Downloading all languages.");
+
+ const allModels = [
+ "lex.50.50.enes.s2t.bin",
+ "lex.50.50.enfr.s2t.bin",
+ "lex.50.50.enuk.s2t.bin",
+ "lex.50.50.esen.s2t.bin",
+ "lex.50.50.fren.s2t.bin",
+ "lex.50.50.uken.s2t.bin",
+ "model.enes.intgemm.alphas.bin",
+ "model.enfr.intgemm.alphas.bin",
+ "model.enuk.intgemm.alphas.bin",
+ "model.esen.intgemm.alphas.bin",
+ "model.fren.intgemm.alphas.bin",
+ "model.uken.intgemm.alphas.bin",
+ "vocab.enes.spm",
+ "vocab.enfr.spm",
+ "vocab.enuk.spm",
+ "vocab.esen.spm",
+ "vocab.fren.spm",
+ "vocab.uken.spm",
+ ];
+ Assert.deepEqual(
+ await remoteClients.translationModels.resolvePendingDownloads(
+ allModels.length
+ ),
+ allModels,
+ "All models were downloaded."
+ );
+ Assert.deepEqual(
+ await remoteClients.translationsWasm.resolvePendingDownloads(1),
+ ["bergamot-translator"],
+ "Wasm was downloaded."
+ );
+
+ await ensureVisibility({
+ message: "Everything can be deleted.",
+ visible: { deleteAll, frenchDelete, spanishDelete, ukrainianDelete },
+ hidden: { downloadAll, frenchDownload, spanishDownload, ukrainianDownload },
+ });
+
+ click(deleteAll, "Deleting all languages.");
+
+ await ensureVisibility({
+ message: "Everything can be downloaded again",
+ visible: {
+ downloadAll,
+ frenchDownload,
+ spanishDownload,
+ ukrainianDownload,
+ },
+ hidden: { deleteAll, frenchDelete, spanishDelete, ukrainianDelete },
+ });
+
+ click(frenchDownload, "Downloading French.");
+ click(spanishDownload, "Downloading Spanish.");
+ click(ukrainianDownload, "Downloading Ukrainian.");
+
+ Assert.deepEqual(
+ await remoteClients.translationModels.resolvePendingDownloads(
+ allModels.length
+ ),
+ allModels,
+ "All models were downloaded again."
+ );
+
+ remoteClients.translationsWasm.assertNoNewDownloads();
+
+ await ensureVisibility({
+ message: "Everything is downloaded again.",
+ visible: { deleteAll, frenchDelete, spanishDelete, ukrainianDelete },
+ hidden: { downloadAll, frenchDownload, spanishDownload, ukrainianDownload },
+ });
+
+ await cleanup();
+});
+
+add_task(async function test_about_preferences_download_reject() {
+ const {
+ cleanup,
+ remoteClients,
+ elements: { document, frenchDownload },
+ } = await setupAboutPreferences(LANGUAGE_PAIRS, {
+ prefs: [["browser.translations.newSettingsUI.enable", false]],
+ });
+
+ click(frenchDownload, "Downloading French");
+
+ is(
+ maybeGetByL10nId("translations-manage-error-install", document),
+ null,
+ "No error messages are present."
+ );
+
+ const failureErrors = await captureTranslationsError(() =>
+ remoteClients.translationModels.rejectPendingDownloads(frenchModels.length)
+ );
+
+ ok(
+ !!failureErrors.length,
+ `The errors for download should have been reported, found ${failureErrors.length} errors`
+ );
+ for (const { error } of failureErrors) {
+ is(
+ error?.message,
+ "Failed to download file.",
+ "The error reported was a download error."
+ );
+ }
+
+ await waitForCondition(
+ () => maybeGetByL10nId("translations-manage-error-install", document),
+ "The error message is now visible."
+ );
+
+ click(frenchDownload, "Attempting to download French again", document);
+ is(
+ maybeGetByL10nId("translations-manage-error-install", document),
+ null,
+ "The error message is hidden again."
+ );
+
+ const successErrors = await captureTranslationsError(() =>
+ remoteClients.translationModels.resolvePendingDownloads(frenchModels.length)
+ );
+
+ is(
+ successErrors.length,
+ 0,
+ "Expected no errors downloading French the second time"
+ );
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_always_translate_languages.js b/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_always_translate_languages.js
new file mode 100644
index 0000000000..9f40003bfc
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_always_translate_languages.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(
+ async function test_about_preferences_always_translate_language_settings() {
+ const {
+ cleanup,
+ elements: { settingsButton },
+ } = await setupAboutPreferences(LANGUAGE_PAIRS, {
+ prefs: [["browser.translations.newSettingsUI.enable", false]],
+ });
+
+ info("Ensuring the list of always-translate languages is empty");
+ is(
+ getAlwaysTranslateLanguagesFromPref().length,
+ 0,
+ "The list of always-translate languages is empty"
+ );
+
+ info("Adding two languages to the alwaysTranslateLanguages pref");
+ Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, "fr,de");
+
+ const dialogWindow = await waitForOpenDialogWindow(
+ "chrome://browser/content/preferences/dialogs/translations.xhtml",
+ () => {
+ click(
+ settingsButton,
+ "Opening the about:preferences Translations Settings"
+ );
+ }
+ );
+ let tree = dialogWindow.document.getElementById(
+ "alwaysTranslateLanguagesTree"
+ );
+ let remove = dialogWindow.document.getElementById(
+ "removeAlwaysTranslateLanguage"
+ );
+ let removeAll = dialogWindow.document.getElementById(
+ "removeAllAlwaysTranslateLanguages"
+ );
+
+ is(
+ tree.view.rowCount,
+ 2,
+ "The always-translate languages list has 2 items"
+ );
+ ok(remove.disabled, "The 'Remove Language' button is disabled");
+ ok(!removeAll.disabled, "The 'Remove All Languages' button is enabled");
+
+ info("Selecting the first always-translate language.");
+ tree.view.selection.select(0);
+ ok(!remove.disabled, "The 'Remove Language' button is enabled");
+
+ click(remove, "Clicking the remove-language button");
+ is(
+ tree.view.rowCount,
+ 1,
+ "The always-translate languages list now contains 1 item"
+ );
+ is(
+ getAlwaysTranslateLanguagesFromPref().length,
+ 1,
+ "One language tag in the pref"
+ );
+
+ info("Removing all languages from the alwaysTranslateLanguages pref");
+ Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, "");
+ is(tree.view.rowCount, 0, "The always-translate languages list is empty");
+ ok(remove.disabled, "The 'Remove Language' button is disabled");
+ ok(removeAll.disabled, "The 'Remove All Languages' button is disabled");
+
+ info("Adding more languages to the alwaysTranslateLanguages pref");
+ Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, "fr,en,es");
+ is(
+ tree.view.rowCount,
+ 3,
+ "The always-translate languages list has 3 items"
+ );
+ ok(remove.disabled, "The 'Remove Language' button is disabled");
+ ok(!removeAll.disabled, "The 'Remove All Languages' button is enabled");
+
+ click(removeAll, "Clicking the remove-all languages button");
+ is(tree.view.rowCount, 0, "The always-translate languages list is empty");
+ ok(remove.disabled, "The 'Remove Language' button is disabled");
+ ok(removeAll.disabled, "The 'Remove All Languages' button is disabled");
+ is(
+ getAlwaysTranslateLanguagesFromPref().length,
+ 0,
+ "There are no languages in the alwaysTranslateLanguages pref"
+ );
+
+ await waitForCloseDialogWindow(dialogWindow);
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_never_translate_languages.js b/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_never_translate_languages.js
new file mode 100644
index 0000000000..758cfb4fba
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_never_translate_languages.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(
+ async function test_about_preferences_never_translate_language_settings() {
+ const {
+ cleanup,
+ elements: { settingsButton },
+ } = await setupAboutPreferences(LANGUAGE_PAIRS, {
+ prefs: [["browser.translations.newSettingsUI.enable", false]],
+ });
+
+ info("Ensuring the list of never-translate languages is empty");
+ is(
+ getNeverTranslateLanguagesFromPref().length,
+ 0,
+ "The list of never-translate languages is empty"
+ );
+
+ info("Adding two languages to the neverTranslateLanguages pref");
+ Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, "fr,de");
+
+ const dialogWindow = await waitForOpenDialogWindow(
+ "chrome://browser/content/preferences/dialogs/translations.xhtml",
+ () => {
+ click(
+ settingsButton,
+ "Opening the about:preferences Translations Settings"
+ );
+ }
+ );
+ let tree = dialogWindow.document.getElementById(
+ "neverTranslateLanguagesTree"
+ );
+ let remove = dialogWindow.document.getElementById(
+ "removeNeverTranslateLanguage"
+ );
+ let removeAll = dialogWindow.document.getElementById(
+ "removeAllNeverTranslateLanguages"
+ );
+
+ is(tree.view.rowCount, 2, "The never-translate languages list has 2 items");
+ ok(remove.disabled, "The 'Remove Language' button is disabled");
+ ok(!removeAll.disabled, "The 'Remove All Languages' button is enabled");
+
+ info("Selecting the first never-translate language.");
+ tree.view.selection.select(0);
+ ok(!remove.disabled, "The 'Remove Language' button is enabled");
+
+ click(remove, "Clicking the remove-language button");
+ is(
+ tree.view.rowCount,
+ 1,
+ "The never-translate languages list now contains 1 item"
+ );
+ is(
+ getNeverTranslateLanguagesFromPref().length,
+ 1,
+ "One language tag in the pref"
+ );
+
+ info("Removing all languages from the neverTranslateLanguages pref");
+ Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, "");
+ is(tree.view.rowCount, 0, "The never-translate languages list is empty");
+ ok(remove.disabled, "The 'Remove Language' button is disabled");
+ ok(removeAll.disabled, "The 'Remove All Languages' button is disabled");
+
+ info("Adding more languages to the neverTranslateLanguages pref");
+ Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, "fr,en,es");
+ is(tree.view.rowCount, 3, "The never-translate languages list has 3 items");
+ ok(remove.disabled, "The 'Remove Language' button is disabled");
+ ok(!removeAll.disabled, "The 'Remove All Languages' button is enabled");
+
+ click(removeAll, "Clicking the remove-all languages button");
+ is(tree.view.rowCount, 0, "The never-translate languages list is empty");
+ ok(remove.disabled, "The 'Remove Language' button is disabled");
+ ok(removeAll.disabled, "The 'Remove All Languages' button is disabled");
+ is(
+ getNeverTranslateLanguagesFromPref().length,
+ 0,
+ "There are no languages in the neverTranslateLanguages pref"
+ );
+
+ await waitForCloseDialogWindow(dialogWindow);
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_never_translate_sites.js b/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_never_translate_sites.js
new file mode 100644
index 0000000000..dea7b1c473
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_never_translate_sites.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+add_task(async function test_about_preferences_never_translate_site_settings() {
+ const {
+ cleanup,
+ elements: { settingsButton },
+ } = await setupAboutPreferences(LANGUAGE_PAIRS, {
+ prefs: [["browser.translations.newSettingsUI.enable", false]],
+ permissionsUrls: [
+ "https://example.com",
+ "https://example.org",
+ "https://example.net",
+ ],
+ });
+
+ info("Ensuring the list of never-translate sites is empty");
+ is(
+ getNeverTranslateSitesFromPerms().length,
+ 0,
+ "The list of never-translate sites is empty"
+ );
+
+ info("Adding two sites to the neverTranslateSites perms");
+ PermissionTestUtils.add(
+ "https://example.com",
+ TRANSLATIONS_PERMISSION,
+ Services.perms.DENY_ACTION
+ );
+ PermissionTestUtils.add(
+ "https://example.org",
+ TRANSLATIONS_PERMISSION,
+ Services.perms.DENY_ACTION
+ );
+ PermissionTestUtils.add(
+ "https://example.net",
+ TRANSLATIONS_PERMISSION,
+ Services.perms.DENY_ACTION
+ );
+
+ const dialogWindow = await waitForOpenDialogWindow(
+ "chrome://browser/content/preferences/dialogs/translations.xhtml",
+ () => {
+ click(
+ settingsButton,
+ "Opening the about:preferences Translations Settings"
+ );
+ }
+ );
+ let tree = dialogWindow.document.getElementById("neverTranslateSitesTree");
+ let remove = dialogWindow.document.getElementById("removeNeverTranslateSite");
+ let removeAll = dialogWindow.document.getElementById(
+ "removeAllNeverTranslateSites"
+ );
+
+ is(tree.view.rowCount, 3, "The never-translate sites list has 2 items");
+ ok(remove.disabled, "The 'Remove Site' button is disabled");
+ ok(!removeAll.disabled, "The 'Remove All Sites' button is enabled");
+
+ info("Selecting the first never-translate site.");
+ tree.view.selection.select(0);
+ ok(!remove.disabled, "The 'Remove Site' button is enabled");
+
+ click(remove, "Clicking the remove-site button");
+ is(
+ tree.view.rowCount,
+ 2,
+ "The never-translate sites list now contains 2 items"
+ );
+ is(
+ getNeverTranslateSitesFromPerms().length,
+ 2,
+ "There are 2 sites with permissions"
+ );
+
+ info("Removing all sites from the neverTranslateSites perms");
+ PermissionTestUtils.remove("https://example.com", TRANSLATIONS_PERMISSION);
+ PermissionTestUtils.remove("https://example.org", TRANSLATIONS_PERMISSION);
+ PermissionTestUtils.remove("https://example.net", TRANSLATIONS_PERMISSION);
+
+ is(tree.view.rowCount, 0, "The never-translate sites list is empty");
+ ok(remove.disabled, "The 'Remove Site' button is disabled");
+ ok(removeAll.disabled, "The 'Remove All Sites' button is disabled");
+
+ info("Adding more sites to the neverTranslateSites perms");
+ PermissionTestUtils.add(
+ "https://example.org",
+ TRANSLATIONS_PERMISSION,
+ Services.perms.DENY_ACTION
+ );
+ PermissionTestUtils.add(
+ "https://example.com",
+ TRANSLATIONS_PERMISSION,
+ Services.perms.DENY_ACTION
+ );
+ PermissionTestUtils.add(
+ "https://example.net",
+ TRANSLATIONS_PERMISSION,
+ Services.perms.DENY_ACTION
+ );
+
+ is(tree.view.rowCount, 3, "The never-translate sites list has 3 items");
+ ok(remove.disabled, "The 'Remove Site' button is disabled");
+ ok(!removeAll.disabled, "The 'Remove All Sites' button is enabled");
+
+ click(removeAll, "Clicking the remove-all sites button");
+ is(tree.view.rowCount, 0, "The never-translate sites list is empty");
+ ok(remove.disabled, "The 'Remove Site' button is disabled");
+ ok(removeAll.disabled, "The 'Remove All Sites' button is disabled");
+ is(
+ getNeverTranslateSitesFromPerms().length,
+ 0,
+ "There are no sites in the neverTranslateSites perms"
+ );
+
+ await waitForCloseDialogWindow(dialogWindow);
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_ui.js b/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_ui.js
new file mode 100644
index 0000000000..ee81b84a36
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_ui.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_translations_settings_pane_elements() {
+ const {
+ cleanup,
+ elements: { settingsButton },
+ } = await setupAboutPreferences(LANGUAGE_PAIRS, {
+ prefs: [["browser.translations.newSettingsUI.enable", true]],
+ });
+
+ assertVisibility({
+ message: "Expect paneGeneral elements to be visible.",
+ visible: { settingsButton },
+ });
+
+ const {
+ backButton,
+ header,
+ translationsSettingsDescription,
+ translateAlwaysHeader,
+ translateNeverHeader,
+ translateAlwaysAddButton,
+ translateNeverAddButton,
+ translateNeverSiteHeader,
+ translateNeverSiteDesc,
+ translateDownloadLanguagesHeader,
+ translateDownloadLanguagesLearnMore,
+ } =
+ await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane(
+ settingsButton
+ );
+
+ assertVisibility({
+ message: "Expect paneTranslations elements to be visible.",
+ visible: {
+ backButton,
+ header,
+ translationsSettingsDescription,
+ translateAlwaysHeader,
+ translateNeverHeader,
+ translateAlwaysAddButton,
+ translateNeverAddButton,
+ translateNeverSiteHeader,
+ translateNeverSiteDesc,
+ translateDownloadLanguagesHeader,
+ translateDownloadLanguagesLearnMore,
+ },
+ hidden: {
+ settingsButton,
+ },
+ });
+
+ const promise = BrowserTestUtils.waitForEvent(
+ document,
+ "paneshown",
+ false,
+ event => event.detail.category === "paneGeneral"
+ );
+
+ click(backButton);
+ await promise;
+
+ assertVisibility({
+ message: "Expect paneGeneral elements to be visible.",
+ visible: {
+ settingsButton,
+ },
+ hidden: {
+ backButton,
+ header,
+ translationsSettingsDescription,
+ translateAlwaysHeader,
+ translateNeverHeader,
+ translateAlwaysAddButton,
+ translateNeverAddButton,
+ translateNeverSiteHeader,
+ translateNeverSiteDesc,
+ translateDownloadLanguagesHeader,
+ translateDownloadLanguagesLearnMore,
+ },
+ });
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_a11y_focus.js b/browser/components/translations/tests/browser/browser_translations_panel_a11y_focus.js
new file mode 100644
index 0000000000..e2e8663f6d
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_a11y_focus.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the a11y focus behavior.
+ */
+add_task(async function test_translations_panel_a11y_focus() {
+ const { cleanup } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The button is available."
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ openWithKeyboard: true,
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ is(
+ document.activeElement.getAttribute("data-l10n-id"),
+ "translations-panel-settings-button",
+ "The settings button is focused."
+ );
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_always_translate_language_bad_data.js b/browser/components/translations/tests/browser/browser_translations_panel_always_translate_language_bad_data.js
new file mode 100644
index 0000000000..ac026fb78f
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_always_translate_language_bad_data.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that having an "always translate" set to your app locale doesn't break things.
+ */
+add_task(async function test_always_translate_with_bad_data() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: ENGLISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.alwaysTranslateLanguages", "en,fr"]],
+ });
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ openFromAppMenu: true,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("en", {
+ checked: false,
+ disabled: true,
+ });
+ await closeSettingsMenuIfOpen();
+ await closeTranslationsPanelIfOpen();
+
+ info("Checking that the page is untranslated");
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is untranslated and in the original English.",
+ getH1,
+ '"The Wonderful Wizard of Oz" by L. Frank Baum'
+ );
+ });
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_always_translate_language_basic.js b/browser/components/translations/tests/browser/browser_translations_panel_always_translate_language_basic.js
new file mode 100644
index 0000000000..2644d78f33
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_always_translate_language_basic.js
@@ -0,0 +1,79 @@
+/* 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,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The translations button is visible."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: false,
+ });
+ await FullPageTranslationsTestUtils.clickAlwaysTranslateLanguage({
+ downloadHandler: resolveDownloads,
+ });
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage,
+ "The page should be automatically translated."
+ );
+
+ await navigate("Navigate to a different Spanish page", {
+ url: SPANISH_PAGE_URL_DOT_ORG,
+ downloadHandler: resolveDownloads,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage,
+ "The page should be automatically translated."
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewRevisit,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+ await FullPageTranslationsTestUtils.clickAlwaysTranslateLanguage();
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: false,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "Only the button appears"
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_always_translate_language_manual.js b/browser/components/translations/tests/browser/browser_translations_panel_always_translate_language_manual.js
new file mode 100644
index 0000000000..8456b4cc08
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_always_translate_language_manual.js
@@ -0,0 +1,80 @@
+/* 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 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,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.clickTranslateButton({
+ downloadHandler: resolveDownloads,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewRevisit,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: false,
+ });
+ await FullPageTranslationsTestUtils.clickAlwaysTranslateLanguage();
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewRevisit,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+ await FullPageTranslationsTestUtils.clickAlwaysTranslateLanguage();
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: false,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "Only the button appears"
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_always_translate_language_restore.js b/browser/components/translations/tests/browser/browser_translations_panel_always_translate_language_restore.js
new file mode 100644
index 0000000000..6cf89d2a03
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_always_translate_language_restore.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * 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,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The translations button is visible."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: false,
+ });
+ await FullPageTranslationsTestUtils.clickAlwaysTranslateLanguage({
+ downloadHandler: resolveDownloads,
+ });
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage,
+ "The page should be automatically translated."
+ );
+
+ await navigate("Navigate to a different Spanish page", {
+ url: SPANISH_PAGE_URL_DOT_ORG,
+ downloadHandler: resolveDownloads,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage,
+ "The page should be automatically translated."
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewRevisit,
+ });
+
+ await FullPageTranslationsTestUtils.clickRestoreButton();
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is reverted to have an icon."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+ await FullPageTranslationsTestUtils.clickAlwaysTranslateLanguage();
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: false,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button shows only the icon."
+ );
+
+ await navigate("Reload the page", { url: SPANISH_PAGE_URL_DOT_ORG });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button shows only the icon."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_app_menu_never_translate_language.js b/browser/components/translations/tests/browser/browser_translations_panel_app_menu_never_translate_language.js
new file mode 100644
index 0000000000..ee2905ab99
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_app_menu_never_translate_language.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the effect of unchecking the never-translate-language menuitem, removing
+ * the language from the never-translate languages list.
+ * The translations button should reappear.
+ */
+add_task(async function test_uncheck_never_translate_language_shows_button() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.neverTranslateLanguages", "es"]],
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The translations button is available"
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ openFromAppMenu: true,
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateLanguage("es", {
+ checked: true,
+ });
+ await FullPageTranslationsTestUtils.clickNeverTranslateLanguage();
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateLanguage("es", {
+ checked: false,
+ });
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_app_menu_never_translate_site.js b/browser/components/translations/tests/browser/browser_translations_panel_app_menu_never_translate_site.js
new file mode 100644
index 0000000000..50fff4dff8
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_app_menu_never_translate_site.js
@@ -0,0 +1,140 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the effect of unchecking the never-translate-site menuitem,
+ * regranting translations permissions to this page.
+ * The translations button should reappear.
+ */
+add_task(async function test_uncheck_never_translate_site_shows_button() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The translations button is visible."
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: false }
+ );
+ await FullPageTranslationsTestUtils.clickNeverTranslateSite();
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: true }
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ openFromAppMenu: true,
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: true }
+ );
+ await FullPageTranslationsTestUtils.clickNeverTranslateSite();
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: false }
+ );
+
+ await cleanup();
+});
+
+/**
+ * Tests the effect of unchecking the never-translate-site menuitem while
+ * the current language is in the always-translate languages list, regranting
+ * translations permissions to this page.
+ * The page should automatically translate.
+ */
+add_task(
+ async function test_uncheck_never_translate_site_with_always_translate_language() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: BLANK_PAGE,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.alwaysTranslateLanguages", "es"]],
+ });
+
+ await navigate("Navigate to a Spanish page", {
+ url: SPANISH_PAGE_URL,
+ downloadHandler: resolveDownloads,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewRevisit,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: false }
+ );
+
+ await FullPageTranslationsTestUtils.clickNeverTranslateSite();
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: true }
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ openFromAppMenu: true,
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: true }
+ );
+
+ await FullPageTranslationsTestUtils.clickNeverTranslateSite();
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: false }
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_auto_translate_error_view.js b/browser/components/translations/tests/browser/browser_translations_panel_auto_translate_error_view.js
new file mode 100644
index 0000000000..e71ee1392b
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_auto_translate_error_view.js
@@ -0,0 +1,86 @@
+/* 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"
+);
+
+/**
+ * This tests a specific defect where the error view was not showing properly
+ * when navigating to an auto-translated page after visiting a page in an unsupported
+ * language and viewing the panel.
+ *
+ * This test case tests the case where the auto translate fails and the panel
+ * automatically opens the panel to show the error view.
+ *
+ * See https://bugzilla.mozilla.org/show_bug.cgi?id=1845611 for more information.
+ */
+add_task(
+ async function test_revisit_view_updates_with_auto_translate_failure() {
+ const { cleanup, resolveDownloads, rejectDownloads, runInPage } =
+ await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: [
+ // Do not include French.
+ { fromLang: "en", toLang: "es" },
+ { fromLang: "es", toLang: "en" },
+ ],
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The translations button is visible."
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: false,
+ });
+ await FullPageTranslationsTestUtils.clickAlwaysTranslateLanguage({
+ downloadHandler: resolveDownloads,
+ });
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ await navigate("Navigate to a page in an unsupported language", {
+ url: FRENCH_PAGE_URL,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: false },
+ "The translations button should be unavailable."
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ openFromAppMenu: true,
+ onOpenPanel:
+ FullPageTranslationsTestUtils.assertPanelViewUnsupportedLanguage,
+ });
+
+ info("Destroy the engine process so that an error will happen.");
+ await TranslationsParent.destroyEngineProcess();
+
+ await navigate("Navigate back to a Spanish page.", {
+ url: SPANISH_PAGE_URL_DOT_ORG,
+ downloadHandler: rejectDownloads,
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewError,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_auto_translate_revisit_view.js b/browser/components/translations/tests/browser/browser_translations_panel_auto_translate_revisit_view.js
new file mode 100644
index 0000000000..dd4ffcecfd
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_auto_translate_revisit_view.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This tests a specific defect where the revisit view was not showing properly
+ * when navigating to an auto-translated page after visiting a page in an unsupported
+ * language and viewing the panel.
+ *
+ * This test case tests the case where the auto translate succeeds and the user
+ * manually opens the panel to show the revisit view.
+ *
+ * See https://bugzilla.mozilla.org/show_bug.cgi?id=1845611 for more information.
+ */
+add_task(
+ async function test_revisit_view_updates_with_auto_translate_success() {
+ const { cleanup, resolveDownloads, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: [
+ // Do not include French.
+ { fromLang: "en", toLang: "es" },
+ { fromLang: "es", toLang: "en" },
+ ],
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The translations button is visible."
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: false,
+ });
+ await FullPageTranslationsTestUtils.clickAlwaysTranslateLanguage({
+ downloadHandler: resolveDownloads,
+ });
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ await navigate("Navigate to a page in an unsupported language", {
+ url: FRENCH_PAGE_URL,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: false },
+ "The translations button should be unavailable."
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ openFromAppMenu: true,
+ onOpenPanel:
+ FullPageTranslationsTestUtils.assertPanelViewUnsupportedLanguage,
+ });
+
+ await navigate("Navigate back to the Spanish page.", {
+ url: SPANISH_PAGE_URL_DOT_ORG,
+ downloadHandler: resolveDownloads,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ openFromAppMenu: true,
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewRevisit,
+ });
+
+ 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..ef2e2c4708
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_basics.js
@@ -0,0 +1,69 @@
+/* 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 FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ is(button.getAttribute("data-l10n-id"), "urlbar-translations-button2");
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ const panel = document.getElementById("translations-panel");
+ const label = document.getElementById(panel.getAttribute("aria-labelledby"));
+ ok(label, "The a11y label for the panel can be found.");
+ assertVisibility({ visible: { label } });
+
+ await FullPageTranslationsTestUtils.clickTranslateButton();
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: true, locale: false, icon: true },
+ "The icon presents the loading indicator."
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewLoading,
+ });
+
+ await FullPageTranslationsTestUtils.clickCancelButton();
+
+ await resolveDownloads(1);
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewRevisit,
+ });
+
+ await FullPageTranslationsTestUtils.clickRestoreButton();
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await FullPageTranslationsTestUtils.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_button.js b/browser/components/translations/tests/browser/browser_translations_panel_button.js
new file mode 100644
index 0000000000..209cfc18a6
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_button.js
@@ -0,0 +1,77 @@
+/* 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() {
+ const { cleanup } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The button should be visible since the page can be translated from Spanish."
+ );
+
+ await navigate("Navigate to an English page.", { url: ENGLISH_PAGE_URL });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: false },
+ "The button should be invisible since the page is in English."
+ );
+
+ await navigate("Navigate back to a Spanish page.", { url: SPANISH_PAGE_URL });
+
+ await FullPageTranslationsTestUtils.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() {
+ const { cleanup, tab: spanishTab } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ await FullPageTranslationsTestUtils.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 FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: false },
+ "The button should be invisible since the tab is in English."
+ );
+
+ await switchTab(spanishTab, "spanish tab");
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The button should be visible again since the page is in Spanish."
+ );
+
+ await switchTab(englishTab, "english tab");
+
+ await FullPageTranslationsTestUtils.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..17e680dd87
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_cancel.js
@@ -0,0 +1,27 @@
+/* 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,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The button is available."
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.clickCancelButton();
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_close_panel_never_translate_language_with_translations_active.js b/browser/components/translations/tests/browser/browser_translations_panel_close_panel_never_translate_language_with_translations_active.js
new file mode 100644
index 0000000000..0fbffd891a
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_close_panel_never_translate_language_with_translations_active.js
@@ -0,0 +1,147 @@
+"use strict";
+
+/**
+ * Tests the effect of checking the never-translate-language menuitem on a page where
+ * translations are active (always-translate-language is enabled).
+ * Checking the box on the page automatically closes/hides the translations panel.
+ */
+add_task(
+ async function test_panel_closes_on_toggle_never_translate_language_with_translations_active() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The translations button is available"
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ openFromAppMenu: true,
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: false,
+ });
+ await FullPageTranslationsTestUtils.clickAlwaysTranslateLanguage({
+ downloadHandler: resolveDownloads,
+ });
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage,
+ "The page should be automatically translated."
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewRevisit,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateLanguage("es", {
+ checked: false,
+ });
+ await FullPageTranslationsTestUtils.waitForTranslationsPopupEvent(
+ "popuphidden",
+ async () => {
+ await FullPageTranslationsTestUtils.clickNeverTranslateLanguage();
+ }
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+ await cleanup();
+ }
+);
+
+/**
+ * Tests the effect of checking the never-translate-language menuitem on
+ * a page where translations are active through always-translate-language
+ * and inactive on a site through never-translate-site.
+ * Checking the box on the page automatically closes/hides the translations panel.
+ */
+add_task(
+ async function test_panel_closes_on_toggle_never_translate_language_with_always_translate_language_and_never_translate_site_active() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The translations button is available"
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ openFromAppMenu: true,
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: false,
+ });
+ await FullPageTranslationsTestUtils.clickAlwaysTranslateLanguage({
+ downloadHandler: resolveDownloads,
+ });
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage,
+ "The page should be automatically translated."
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewRevisit,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: false }
+ );
+ await FullPageTranslationsTestUtils.clickNeverTranslateSite();
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: true }
+ );
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ openFromAppMenu: true,
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: true }
+ );
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateLanguage("es", {
+ checked: false,
+ });
+ await FullPageTranslationsTestUtils.waitForTranslationsPopupEvent(
+ "popuphidden",
+ async () => {
+ await FullPageTranslationsTestUtils.clickNeverTranslateLanguage();
+ }
+ );
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_close_panel_never_translate_language_with_translations_inactive.js b/browser/components/translations/tests/browser/browser_translations_panel_close_panel_never_translate_language_with_translations_inactive.js
new file mode 100644
index 0000000000..5df2468646
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_close_panel_never_translate_language_with_translations_inactive.js
@@ -0,0 +1,93 @@
+"use strict";
+
+/**
+ * Tests the effect of checking the never-translate-language menuitem on a page where
+ * translations and never translate site are inactive.
+ * Checking the box on the page automatically closes/hides the translations panel.
+ */
+add_task(async function test_panel_closes_on_toggle_never_translate_language() {
+ const { cleanup } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The translations button is available"
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ openFromAppMenu: true,
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateLanguage("es", {
+ checked: false,
+ });
+ await FullPageTranslationsTestUtils.waitForTranslationsPopupEvent(
+ "popuphidden",
+ async () => {
+ await FullPageTranslationsTestUtils.clickNeverTranslateLanguage();
+ }
+ );
+ await cleanup();
+});
+
+/**
+ * Tests the effect of checking the never-translate-language menuitem on a page where
+ * translations are inactive (never-translate-site is enabled).
+ * Checking the box on the page automatically closes/hides the translations panel.
+ */
+add_task(
+ async function test_panel_closes_on_toggle_never_translate_language_with_never_translate_site_enabled() {
+ const { cleanup } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The translations button is available"
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ openFromAppMenu: true,
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: false }
+ );
+ await FullPageTranslationsTestUtils.clickNeverTranslateSite();
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: true }
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ openFromAppMenu: true,
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: true }
+ );
+
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateLanguage("es", {
+ checked: false,
+ });
+ await FullPageTranslationsTestUtils.waitForTranslationsPopupEvent(
+ "popuphidden",
+ async () => {
+ await FullPageTranslationsTestUtils.clickNeverTranslateLanguage();
+ }
+ );
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_close_panel_never_translate_site.js b/browser/components/translations/tests/browser/browser_translations_panel_close_panel_never_translate_site.js
new file mode 100644
index 0000000000..78679a046a
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_close_panel_never_translate_site.js
@@ -0,0 +1,159 @@
+"use strict";
+
+/**
+ * Tests the effect of checking the never-translate-site menuitem on a page where
+ * always-translate-language and never-translate-language are inactive.
+ * Checking the box on the page automatically closes/hides the translations panel.
+ */
+add_task(async function test_panel_closes_on_toggle_never_translate_site() {
+ const { cleanup } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The translations button is available"
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ openFromAppMenu: true,
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: false }
+ );
+ await FullPageTranslationsTestUtils.waitForTranslationsPopupEvent(
+ "popuphidden",
+ async () => {
+ await FullPageTranslationsTestUtils.clickNeverTranslateSite();
+ }
+ );
+
+ await cleanup();
+});
+
+/**
+ * Tests the effect of checking the never-translate-site menuitem on a page where
+ * translations are active (always-translate-language is enabled).
+ * Checking the box on the page automatically restores the page and closes/hides the translations panel.
+ */
+add_task(
+ async function test_panel_closes_on_toggle_never_translate_site_with_translations_active() {
+ const { cleanup, resolveDownloads, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The translations button is available"
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ openFromAppMenu: true,
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: false,
+ });
+ await FullPageTranslationsTestUtils.clickAlwaysTranslateLanguage({
+ downloadHandler: resolveDownloads,
+ });
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage,
+ "The page should be automatically translated."
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewRevisit,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: false }
+ );
+
+ await FullPageTranslationsTestUtils.waitForTranslationsPopupEvent(
+ "popuphidden",
+ async () => {
+ await FullPageTranslationsTestUtils.clickNeverTranslateSite();
+ }
+ );
+
+ await cleanup();
+ }
+);
+
+/**
+ * Tests the effect of checking the never-translate-site menuitem on a page where
+ * translations are inactive (never-translate-language is active).
+ * Checking the box on the page automatically closes/hides the translations panel.
+ */
+add_task(
+ async function test_panel_closes_on_toggle_never_translate_site_with_translations_inactive() {
+ const { cleanup } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The translations button is available"
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ openFromAppMenu: true,
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateLanguage("es", {
+ checked: false,
+ });
+ await FullPageTranslationsTestUtils.clickNeverTranslateLanguage();
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateLanguage("es", {
+ checked: true,
+ });
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ openFromAppMenu: true,
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateLanguage("es", {
+ checked: true,
+ });
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: false }
+ );
+ await FullPageTranslationsTestUtils.waitForTranslationsPopupEvent(
+ "popuphidden",
+ async () => {
+ await FullPageTranslationsTestUtils.clickNeverTranslateSite();
+ }
+ );
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_engine_destroy.js b/browser/components/translations/tests/browser/browser_translations_panel_engine_destroy.js
new file mode 100644
index 0000000000..0a58dd7fa6
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_engine_destroy.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Manually destroy the engine, and test that the page is still translated afterwards.
+ */
+add_task(async function test_translations_engine_destroy() {
+ const { cleanup, resolveDownloads, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The button is available."
+ );
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.clickTranslateButton({
+ downloadHandler: resolveDownloads,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ info("Destroy the engine process");
+ await TranslationsParent.destroyEngineProcess();
+
+ info("Mutate the page's content to re-trigger a translation.");
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ getH1().innerText = "New text for the H1";
+ });
+
+ info("The engine downloads should be requested again.");
+ resolveDownloads(1);
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The mutated content should be translated.",
+ getH1,
+ "NEW TEXT FOR THE H1 [es to en]"
+ );
+ });
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_engine_destroy_pending.js b/browser/components/translations/tests/browser/browser_translations_panel_engine_destroy_pending.js
new file mode 100644
index 0000000000..ace1a845df
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_engine_destroy_pending.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Manually destroy the engine while a page is in the background, and test that the page
+ * is still translated after switching back to it.
+ */
+add_task(async function test_translations_engine_destroy_pending() {
+ const {
+ cleanup,
+ resolveDownloads,
+ runInPage,
+ tab: spanishTab,
+ } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The button is available."
+ );
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.clickTranslateButton({
+ downloadHandler: resolveDownloads,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ const { removeTab: removeEnglishTab } = await addTab(
+ ENGLISH_PAGE_URL,
+ "Creating a new tab for a page in English."
+ );
+
+ info("Destroy the engine process");
+ await TranslationsParent.destroyEngineProcess();
+
+ info("Mutate the page's content to re-trigger a translation.");
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ getH1().innerText = "New text for the H1";
+ });
+
+ info("Wait for a second to ensure the mutation takes.");
+ await TestUtils.waitForTick();
+
+ await switchTab(spanishTab, "spanish tab");
+
+ info("The engine downloads should be requested again.");
+ resolveDownloads(1);
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The mutated content should be translated.",
+ getH1,
+ "NEW TEXT FOR THE H1 [es to en]"
+ );
+ });
+
+ await removeEnglishTab();
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_engine_unsupported.js b/browser/components/translations/tests/browser/browser_translations_panel_engine_unsupported.js
new file mode 100644
index 0000000000..f0804f35aa
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_engine_unsupported.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that the translations button does not appear when the translations
+ * engine is not supported.
+ */
+add_task(async function test_translations_button_hidden_when_cpu_unsupported() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.simulateUnsupportedEngine", true]],
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: false },
+ "The button is not available."
+ );
+
+ await cleanup();
+});
+
+/**
+ * Tests that the translate-page menuitem is not available in the app menu
+ * when the translations engine is not supported.
+ */
+add_task(
+ async function test_translate_page_app_menu_item_hidden_when_cpu_unsupported() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.simulateUnsupportedEngine", true]],
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ const appMenuButton = getById("PanelUI-menu-button");
+
+ click(appMenuButton, "Opening the app menu");
+ await BrowserTestUtils.waitForEvent(window.PanelUI.mainView, "ViewShown");
+
+ const translateSiteButton = document.getElementById(
+ "appMenu-translate-button"
+ );
+ is(
+ translateSiteButton.hidden,
+ true,
+ "The app-menu translate button should be hidden because when the engine is not supported."
+ );
+
+ click(appMenuButton, "Closing the app menu");
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_engine_unsupported_lang.js b/browser/components/translations/tests/browser/browser_translations_panel_engine_unsupported_lang.js
new file mode 100644
index 0000000000..79e5c6b119
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_engine_unsupported_lang.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests how the unsupported language flow works.
+ */
+add_task(async function test_unsupported_lang() {
+ const { cleanup } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: [
+ // Do not include Spanish.
+ { fromLang: "fr", toLang: "en" },
+ { fromLang: "en", toLang: "fr" },
+ ],
+ });
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ openFromAppMenu: true,
+ onOpenPanel:
+ FullPageTranslationsTestUtils.assertPanelViewUnsupportedLanguage,
+ });
+
+ await FullPageTranslationsTestUtils.clickChangeSourceLanguageButton();
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_firstrun.js b/browser/components/translations/tests/browser/browser_translations_panel_firstrun.js
new file mode 100644
index 0000000000..0c248a7837
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_firstrun.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that the first run message is displayed.
+ */
+add_task(async function test_translations_panel_firstrun() {
+ const { cleanup } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.panelShown", false]],
+ });
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewFirstShow,
+ });
+
+ await FullPageTranslationsTestUtils.clickCancelButton();
+
+ await navigate("Load a different page on the same site", {
+ url: SPANISH_PAGE_URL_2,
+ });
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.clickCancelButton();
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_firstrun_revisit.js b/browser/components/translations/tests/browser/browser_translations_panel_firstrun_revisit.js
new file mode 100644
index 0000000000..02c2d94db9
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_firstrun_revisit.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that the first-show intro message message is displayed
+ * when viewing the panel subsequent times on the same URL,
+ * but is no longer displayed after navigating to new URL,
+ * or when returning to the first URL after navigating away.
+ */
+add_task(async function test_translations_panel_firstrun() {
+ const { cleanup } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.panelShown", false]],
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewFirstShow,
+ });
+
+ await FullPageTranslationsTestUtils.clickCancelButton();
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewFirstShow,
+ });
+
+ await FullPageTranslationsTestUtils.clickCancelButton();
+
+ await navigate("Navigate to a different website", {
+ url: SPANISH_PAGE_URL_DOT_ORG,
+ });
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.clickCancelButton();
+
+ await navigate("Navigate back to the first website", {
+ url: SPANISH_PAGE_URL,
+ });
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.clickCancelButton();
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_fuzzing.js b/browser/components/translations/tests/browser/browser_translations_panel_fuzzing.js
new file mode 100644
index 0000000000..2f5285ac46
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_fuzzing.js
@@ -0,0 +1,240 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Manually destroy the engine while a page is in the background, and test that the page
+ * is still translated after switching back to it.
+ */
+add_task(async function test_translations_panel_fuzzing() {
+ const {
+ cleanup,
+ runInPage: runInSpanishPage,
+ tab: spanishTab,
+ } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ autoDownloadFromRemoteSettings: true,
+ });
+
+ /**
+ * @typedef {object} Tab
+ */
+
+ /** @type {Tab?} */
+ let englishTab;
+ /** @type {Function?} */
+ let removeEnglishTab;
+ /** @type {boolean} */
+ let isSpanishPageTranslated = false;
+ /** @type {"spanish" | "english"} */
+ let activeTab = "spanish";
+ /** @type {boolean} */
+ let isEngineMaybeDestroyed = true;
+ /** @type {boolean} */
+ let isTitleMutated = false;
+ /** @type {boolean} */
+ let hasVerifiedMutation = true;
+
+ function reportOperation(name) {
+ info(
+ `\n\nOperation: ${name} ` +
+ JSON.stringify({
+ activeTab,
+ englishTab: !!englishTab,
+ isSpanishPageTranslated,
+ isEngineMaybeDestroyed,
+ isTitleMutated,
+ })
+ );
+ }
+
+ /**
+ * A list of fuzzing operations. They return false when they are a noop given the
+ * conditions.
+ *
+ * @type {object} - Record<string, () => Promise<boolean>>
+ */
+ const operations = {
+ async addEnglishTab() {
+ if (!englishTab) {
+ reportOperation("addEnglishTab");
+ const { removeTab, tab } = await addTab(
+ ENGLISH_PAGE_URL,
+ "Creating a new tab for a page in English."
+ );
+
+ englishTab = tab;
+ removeEnglishTab = removeTab;
+ activeTab = "english";
+ return true;
+ }
+ return false;
+ },
+
+ async removeEnglishTab() {
+ if (removeEnglishTab) {
+ reportOperation("removeEnglishTab");
+ await removeEnglishTab();
+
+ englishTab = null;
+ removeEnglishTab = null;
+ activeTab = "spanish";
+ return true;
+ }
+ return false;
+ },
+
+ async translateSpanishPage() {
+ if (!isSpanishPageTranslated) {
+ reportOperation("translateSpanishPage");
+ if (activeTab === "english") {
+ await switchTab(spanishTab, "spanish tab");
+ }
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The button is available."
+ );
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.clickTranslateButton();
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: true, icon: true },
+ "Translations button is fully loaded."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInSpanishPage
+ );
+
+ isSpanishPageTranslated = true;
+ isEngineMaybeDestroyed = false;
+ activeTab = "spanish";
+ return true;
+ }
+ return false;
+ },
+
+ async destroyEngineProcess() {
+ if (
+ !isEngineMaybeDestroyed &&
+ // Don't destroy the engine process until the mutation has been verified.
+ // There is an artifical race (e.g. only in tests) that happens from a new
+ // engine being requested, and forcefully destroyed before the it can be
+ // initialized.
+ hasVerifiedMutation
+ ) {
+ reportOperation("destroyEngineProcess");
+ await TranslationsParent.destroyEngineProcess();
+ isEngineMaybeDestroyed = true;
+ }
+ return true;
+ },
+
+ async mutateSpanishPage() {
+ if (isSpanishPageTranslated && !isTitleMutated) {
+ reportOperation("mutateSpanishPage");
+
+ info("Mutate the page's content to re-trigger a translation.");
+ await runInSpanishPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ getH1().innerText = "New text for the H1";
+ });
+
+ if (isEngineMaybeDestroyed) {
+ info("The engine may be recreated now.");
+ }
+
+ isEngineMaybeDestroyed = false;
+ isTitleMutated = true;
+ hasVerifiedMutation = false;
+ return true;
+ }
+ return false;
+ },
+
+ async switchToSpanishTab() {
+ if (activeTab !== "spanish") {
+ reportOperation("switchToSpanishTab");
+ await switchTab(spanishTab, "spanish tab");
+ activeTab = "spanish";
+
+ if (isTitleMutated) {
+ await runInSpanishPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The mutated content should be translated.",
+ getH1,
+ "NEW TEXT FOR THE H1 [es to en]"
+ );
+ });
+ hasVerifiedMutation = true;
+ }
+
+ return true;
+ }
+ return false;
+ },
+
+ async switchToEnglishTab() {
+ if (activeTab !== "english" && englishTab) {
+ reportOperation("switchToEnglishTab");
+ await switchTab(englishTab, "english tab");
+ activeTab = "english";
+ return true;
+ }
+ return false;
+ },
+
+ async restoreSpanishPage() {
+ if (activeTab === "spanish" && isSpanishPageTranslated) {
+ reportOperation("restoreSpanishPage");
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewRevisit,
+ });
+
+ await FullPageTranslationsTestUtils.clickRestoreButton();
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(
+ runInSpanishPage
+ );
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is reverted to have an icon."
+ );
+
+ isSpanishPageTranslated = false;
+ isTitleMutated = false;
+ return true;
+ }
+ return false;
+ },
+ };
+
+ const fuzzSteps = 100;
+ info(`Starting the fuzzing with ${fuzzSteps} operations.`);
+ const opsArray = Object.values(operations);
+
+ for (let i = 0; i < fuzzSteps; i++) {
+ // Pick a random operation and check if that it was not a noop, otherwise continue
+ // trying to find a valid operation.
+ while (true) {
+ const operation = opsArray[Math.floor(Math.random() * opsArray.length)];
+ if (await operation()) {
+ break;
+ }
+ }
+ }
+
+ if (removeEnglishTab) {
+ await removeEnglishTab();
+ }
+ 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..c24fc61e3d
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_gear.js
@@ -0,0 +1,32 @@
+/* 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,
+ });
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.clickManageLanguages();
+
+ await 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..6c1fea7754
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_never_translate_language.js
@@ -0,0 +1,186 @@
+/* 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,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The translations button is visible."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateLanguage("es", {
+ checked: false,
+ });
+ await FullPageTranslationsTestUtils.clickNeverTranslateLanguage();
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateLanguage("es", {
+ checked: true,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await navigate("Reload the page", { url: SPANISH_PAGE_URL });
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await navigate("Navigate to a different Spanish page", {
+ url: SPANISH_PAGE_URL_DOT_ORG,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ 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,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.clickTranslateButton({
+ downloadHandler: resolveDownloads,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewRevisit,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateLanguage("es", {
+ checked: false,
+ });
+ await FullPageTranslationsTestUtils.clickNeverTranslateLanguage();
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateLanguage("es", {
+ checked: true,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await navigate("Reload the page", { url: SPANISH_PAGE_URL });
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ 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,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: false,
+ });
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateLanguage("es", {
+ checked: false,
+ });
+
+ await FullPageTranslationsTestUtils.clickAlwaysTranslateLanguage({
+ downloadHandler: resolveDownloads,
+ });
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateLanguage("es", {
+ checked: false,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewRevisit,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateLanguage("es", {
+ checked: false,
+ });
+
+ await FullPageTranslationsTestUtils.clickNeverTranslateLanguage();
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: false,
+ });
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateLanguage("es", {
+ checked: true,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await navigate("Reload the page", { url: SPANISH_PAGE_URL });
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ 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..858aa297df
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_never_translate_site.js
@@ -0,0 +1,247 @@
+/* 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,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The translations button is visible."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: false }
+ );
+ await FullPageTranslationsTestUtils.clickNeverTranslateSite();
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: true }
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await navigate("Navigate to a Spanish page with the same content principal", {
+ url: SPANISH_PAGE_URL_2,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await navigate(
+ "Navigate to a Spanish page with a different content principal",
+ { url: SPANISH_PAGE_URL_DOT_ORG }
+ );
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The translations button should be visible, because this content principal " +
+ "has not been denied translations permissions"
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ 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,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The translations button is visible."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.clickTranslateButton({
+ downloadHandler: resolveDownloads,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewRevisit,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: false }
+ );
+ await FullPageTranslationsTestUtils.clickNeverTranslateSite();
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: true }
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await navigate("Reload the page", { url: SPANISH_PAGE_URL });
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await navigate(
+ "Navigate to a Spanish page with the same content principal",
+ { url: SPANISH_PAGE_URL_2 }
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await navigate(
+ "Navigate to a Spanish page with a different content principal",
+ { url: SPANISH_PAGE_URL_DOT_ORG }
+ );
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The translations button should be visible, because this content principal " +
+ "has not been denied translations permissions"
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ 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,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: false,
+ });
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: false }
+ );
+
+ await FullPageTranslationsTestUtils.clickAlwaysTranslateLanguage({
+ downloadHandler: resolveDownloads,
+ });
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: false }
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewRevisit,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: false }
+ );
+
+ await FullPageTranslationsTestUtils.clickNeverTranslateSite();
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: true }
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await navigate("Reload the page", { url: SPANISH_PAGE_URL });
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await navigate(
+ "Navigate to a Spanish page with the same content principal",
+ { url: SPANISH_PAGE_URL_2 }
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await navigate(
+ "Navigate to a Spanish page with a different content principal",
+ {
+ url: SPANISH_PAGE_URL_DOT_ORG,
+ downloadHandler: resolveDownloads,
+ }
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_never_translate_site_auto.js b/browser/components/translations/tests/browser/browser_translations_panel_never_translate_site_auto.js
new file mode 100644
index 0000000000..28f3b570c3
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_never_translate_site_auto.js
@@ -0,0 +1,109 @@
+/* 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 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,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: false,
+ });
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: false }
+ );
+
+ await FullPageTranslationsTestUtils.clickAlwaysTranslateLanguage({
+ downloadHandler: resolveDownloads,
+ });
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: false }
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewRevisit,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: false }
+ );
+
+ await FullPageTranslationsTestUtils.clickNeverTranslateSite();
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: true }
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await navigate("Reload the page", { url: SPANISH_PAGE_URL });
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await navigate(
+ "Navigate to a Spanish page with the same content principal",
+ { url: SPANISH_PAGE_URL_2 }
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await navigate(
+ "Navigate to a Spanish page with a different content principal",
+ {
+ url: SPANISH_PAGE_URL_DOT_ORG,
+ downloadHandler: resolveDownloads,
+ }
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_never_translate_site_basic.js b/browser/components/translations/tests/browser/browser_translations_panel_never_translate_site_basic.js
new file mode 100644
index 0000000000..098ff54d6e
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_never_translate_site_basic.js
@@ -0,0 +1,62 @@
+/* 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,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The translations button is visible."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: false }
+ );
+ await FullPageTranslationsTestUtils.clickNeverTranslateSite();
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: true }
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await navigate("Navigate to a Spanish page with the same content principal", {
+ url: SPANISH_PAGE_URL_2,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await navigate(
+ "Navigate to a Spanish page with a different content principal",
+ { url: SPANISH_PAGE_URL_DOT_ORG }
+ );
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The translations button should be visible, because this content principal " +
+ "has not been denied translations permissions"
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_never_translate_site_manual.js b/browser/components/translations/tests/browser/browser_translations_panel_never_translate_site_manual.js
new file mode 100644
index 0000000000..d916342c33
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_never_translate_site_manual.js
@@ -0,0 +1,84 @@
+/* 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 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,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The translations button is visible."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.clickTranslateButton({
+ downloadHandler: resolveDownloads,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewRevisit,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: false }
+ );
+ await FullPageTranslationsTestUtils.clickNeverTranslateSite();
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateSite(
+ SPANISH_PAGE_URL,
+ { checked: true }
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await navigate("Reload the page", { url: SPANISH_PAGE_URL });
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await navigate(
+ "Navigate to a Spanish page with the same content principal",
+ { url: SPANISH_PAGE_URL_2 }
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await navigate(
+ "Navigate to a Spanish page with a different content principal",
+ { url: SPANISH_PAGE_URL_DOT_ORG }
+ );
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The translations button should be visible, because this content principal " +
+ "has not been denied translations permissions"
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ 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..76a6bd9429
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_retry.js
@@ -0,0 +1,54 @@
+/* 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,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The button is available."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.clickTranslateButton({
+ downloadHandler: resolveDownloads,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewRevisit,
+ });
+
+ FullPageTranslationsTestUtils.switchSelectedToLanguage("fr");
+
+ await FullPageTranslationsTestUtils.clickTranslateButton({
+ downloadHandler: resolveDownloads,
+ pivotTranslation: true,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "fr",
+ runInPage
+ );
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_panel_settings_unsupported_lang.js b/browser/components/translations/tests/browser/browser_translations_panel_settings_unsupported_lang.js
new file mode 100644
index 0000000000..18fcf484dd
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_settings_unsupported_lang.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This tests a specific defect where the language checkbox states were not being
+ * updated correctly when visiting a web page in an unsupported language after
+ * previously enabling always-translate-language or never-translate-language
+ * on a site with a supported language.
+ *
+ * See https://bugzilla.mozilla.org/show_bug.cgi?id=1845611 for more information.
+ */
+add_task(async function test_unsupported_language_settings_menu_checkboxes() {
+ const { cleanup, resolveDownloads, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: [
+ // Do not include French.
+ { fromLang: "en", toLang: "es" },
+ { fromLang: "es", toLang: "en" },
+ ],
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The translations button is visible."
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: false,
+ });
+ await FullPageTranslationsTestUtils.clickAlwaysTranslateLanguage({
+ downloadHandler: resolveDownloads,
+ });
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("es", {
+ checked: true,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ await navigate("Navigate to a page in an unsupported language.", {
+ url: FRENCH_PAGE_URL,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: false },
+ "The translations button should be unavailable."
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ openFromAppMenu: true,
+ onOpenPanel:
+ FullPageTranslationsTestUtils.assertPanelViewUnsupportedLanguage,
+ });
+ await FullPageTranslationsTestUtils.assertIsAlwaysTranslateLanguage("fr", {
+ checked: false,
+ disabled: true,
+ });
+ await FullPageTranslationsTestUtils.assertIsNeverTranslateLanguage("fr", {
+ checked: false,
+ disabled: true,
+ });
+
+ 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..3652c61d83
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_panel_switch_languages.js
@@ -0,0 +1,70 @@
+/* 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,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The button is available."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ const { translateButton } = TranslationsPanel.elements;
+
+ ok(!translateButton.disabled, "The translate button starts as enabled");
+
+ FullPageTranslationsTestUtils.assertSelectedFromLanguage("es");
+ FullPageTranslationsTestUtils.assertSelectedToLanguage("en");
+
+ FullPageTranslationsTestUtils.switchSelectedFromLanguage("en");
+
+ ok(
+ translateButton.disabled,
+ "The translate button is disabled when the languages are the same"
+ );
+
+ FullPageTranslationsTestUtils.switchSelectedFromLanguage("es");
+
+ ok(
+ !translateButton.disabled,
+ "When the languages are different it can be translated"
+ );
+
+ FullPageTranslationsTestUtils.switchSelectedFromLanguage("");
+
+ ok(
+ translateButton.disabled,
+ "The translate button is disabled nothing is selected."
+ );
+
+ FullPageTranslationsTestUtils.switchSelectedFromLanguage("en");
+ FullPageTranslationsTestUtils.switchSelectedToLanguage("fr");
+
+ ok(!translateButton.disabled, "The translate button can now be used");
+
+ await FullPageTranslationsTestUtils.clickTranslateButton({
+ downloadHandler: resolveDownloads,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "en",
+ "fr",
+ runInPage
+ );
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_reader_mode.js b/browser/components/translations/tests/browser/browser_translations_reader_mode.js
new file mode 100644
index 0000000000..d066257998
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_reader_mode.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that the translations button becomes hidden when entering reader mode.
+ */
+add_task(async function test_translations_button_hidden_in_reader_mode() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The translations button is visible."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await toggleReaderMode();
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: false },
+ "The translations button is now hidden in reader mode."
+ );
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is now the reader-mode header",
+ getH1,
+ "Translations Test"
+ );
+ });
+
+ await runInPage(async TranslationsTest => {
+ const { getLastParagraph } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's last paragraph is in Spanish.",
+ getLastParagraph,
+ "— Pues, aunque mováis más brazos que los del gigante Briareo, me lo habéis de pagar."
+ );
+ });
+
+ await toggleReaderMode();
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The translations button is visible again outside of reader mode."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await cleanup();
+});
+
+/**
+ * Tests that translations persist when entering reader mode after translating.
+ */
+add_task(async function test_translations_persist_in_reader_mode() {
+ const { cleanup, resolveDownloads, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The translations button is visible."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.clickTranslateButton({
+ downloadHandler: resolveDownloads,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ await toggleReaderMode();
+
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is now the translated reader-mode header",
+ getH1,
+ "TRANSLATIONS TEST [es to en, html]"
+ );
+ });
+
+ await runInPage(async TranslationsTest => {
+ const { getLastParagraph } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's last paragraph is in Spanish.",
+ getLastParagraph,
+ "— PUES, AUNQUE MOVÁIS MÁS BRAZOS QUE LOS DEL GIGANTE BRIAREO, ME LO HABÉIS DE PAGAR. [es to en, html]"
+ );
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: false },
+ "The translations button is now hidden in reader mode."
+ );
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_select_context_menu_feature_disabled.js b/browser/components/translations/tests/browser/browser_translations_select_context_menu_feature_disabled.js
new file mode 100644
index 0000000000..a6b3f71924
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_context_menu_feature_disabled.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This file ensures that the translate selection menu item is unavailable when the translation feature is disabled,
+// This file will be removed when the feature is released, as the pref will no longer exist.
+//
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1870366
+//
+// However, for the time being, I like having these tests to ensure there is no regression when the pref
+// is set to false.
+
+/**
+ * This test checks the availability of the translate-selection menu item in the context menu,
+ * ensuring it is not visible when the "browser.translations.select.enable" preference is set to false
+ * and no text is selected when the context menu is invoked.
+ */
+add_task(
+ async function test_translate_selection_menuitem_is_unavailable_with_feature_disabled_and_no_text_selected() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", false]],
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
+ runInPage,
+ {
+ selectSpanishParagraph: false,
+ openAtSpanishParagraph: true,
+ expectMenuItemVisible: false,
+ },
+ "The translate-selection context menu item should be unavailable when the feature is disabled."
+ );
+
+ await cleanup();
+ }
+);
+
+/**
+ * This test case verifies the functionality of the translate-selection context menu item
+ * when the selected text is not in the user's preferred language. The menu item should be
+ * localized to translate to the target language matching the user's top preferred language
+ * when the selected text is detected to be in a different language.
+ */
+add_task(
+ async function test_translate_selection_menuitem_is_unavailable_with_feature_disabled_and_text_selected() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", false]],
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
+ runInPage,
+ {
+ selectSpanishParagraph: true,
+ openAtSpanishParagraph: true,
+ expectMenuItemVisible: false,
+ },
+ "The translate-selection context menu item should be unavailable when the feature is disabled."
+ );
+
+ await cleanup();
+ }
+);
+
+/**
+ * This test checks the availability of the translate-selection menu item in the context menu,
+ * ensuring it is not visible when the "browser.translations.select.enable" preference is set to false
+ * and the context menu is invoked on a hyperlink. This would result in the menu item being available
+ * if the pref were set to true.
+ */
+add_task(
+ async function test_translate_selection_menuitem_is_unavailable_with_feature_disabled_and_clicking_a_hyperlink() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", false]],
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
+ runInPage,
+ {
+ selectSpanishParagraph: false,
+ openAtSpanishHyperlink: true,
+ expectMenuItemVisible: false,
+ },
+ "The translate-selection context menu item should be unavailable when the feature is disabled."
+ );
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_full_page_translations_active.js b/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_full_page_translations_active.js
new file mode 100644
index 0000000000..58cb655e38
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_full_page_translations_active.js
@@ -0,0 +1,162 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case checks the behavior of the translate-selection menu item in the context menu
+ * when full-page translations is active or inactive. The menu item should be available under
+ * the correct selected-text conditions while full-page translations is inactive, and it should
+ * never be available while full-page translations is active.
+ */
+add_task(
+ async function test_translate_selection_menuitem_with_text_selected_and_full_page_translations_active() {
+ const { cleanup, resolveDownloads, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
+ runInPage,
+ {
+ selectSpanishParagraph: true,
+ openAtSpanishParagraph: true,
+ expectMenuItemVisible: true,
+ expectedTargetLanguage: "en",
+ },
+ "The translate-selection context menu item should be available while full-page translations is inactive."
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.clickTranslateButton({
+ downloadHandler: resolveDownloads,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
+ runInPage,
+ {
+ selectSpanishParagraph: true,
+ openAtSpanishParagraph: true,
+ expectMenuItemVisible: false,
+ },
+ "The translate-selection context menu item should be unavailable while full-page translations is active."
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewRevisit,
+ });
+
+ await FullPageTranslationsTestUtils.clickRestoreButton();
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
+ runInPage,
+ {
+ selectSpanishParagraph: true,
+ openAtSpanishParagraph: true,
+ expectMenuItemVisible: true,
+ expectedTargetLanguage: "en",
+ },
+ "The translate-selection context menu item should be available while full-page translations is inactive."
+ );
+
+ await cleanup();
+ }
+);
+
+/**
+ * This test case checks the behavior of the translate-selection menu item in the context menu
+ * when full-page translations is active or inactive. The menu item should be available under
+ * the correct link-clicked conditions while full-page translations is inactive, and it should
+ * never be available while full-page translations is active.
+ */
+add_task(
+ async function test_translate_selection_menuitem_with_link_clicked_and_full_page_translations_active() {
+ const { cleanup, resolveDownloads, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
+ runInPage,
+ {
+ selectSpanishParagraph: false,
+ openAtSpanishHyperlink: true,
+ expectMenuItemVisible: true,
+ expectedTargetLanguage: "en",
+ },
+ "The translate-selection context menu item should be available while full-page translations is inactive."
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.clickTranslateButton({
+ downloadHandler: resolveDownloads,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
+ runInPage,
+ {
+ selectSpanishParagraph: false,
+ openAtSpanishHyperlink: true,
+ expectMenuItemVisible: false,
+ },
+ "The translate-selection context menu item should be unavailable while full-page translations is active."
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewRevisit,
+ });
+
+ await FullPageTranslationsTestUtils.clickRestoreButton();
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
+ runInPage,
+ {
+ selectSpanishParagraph: false,
+ openAtSpanishHyperlink: true,
+ expectMenuItemVisible: true,
+ expectedTargetLanguage: "en",
+ },
+ "The translate-selection context menu item should be available while full-page translations is inactive."
+ );
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_hyperlink.js b/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_hyperlink.js
new file mode 100644
index 0000000000..cefd83f046
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_hyperlink.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case verifies the functionality of the translate-selection context menu item
+ * when a hyperlink is right-clicked. The menu item should offer to translate the link text
+ * to a target language when the detected language of the link text does not match the preferred
+ * language.
+ */
+add_task(
+ async function test_translate_selection_menuitem_translate_link_text_to_target_language() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
+ runInPage,
+ {
+ selectSpanishParagraph: false,
+ openAtSpanishHyperlink: true,
+ expectMenuItemVisible: true,
+ expectedTargetLanguage: "en",
+ },
+ "The translate-selection context menu item should be localized to translate the link text" +
+ "to the target language."
+ );
+
+ await cleanup();
+ }
+);
+
+/**
+ * This test case verifies the functionality of the translate-selection context menu item
+ * when a hyperlink is right-clicked, and the link text is in the top preferred language.
+ * The menu item should offer to translate the link text without specifying a target language,
+ * since it is already in the preferred language for the user.
+ */
+add_task(
+ async function test_translate_selection_menuitem_translate_link_text_in_preferred_language() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
+ runInPage,
+ {
+ selectSpanishParagraph: false,
+ openAtEnglishHyperlink: true,
+ expectMenuItemVisible: true,
+ expectedTargetLanguage: null,
+ },
+ "The translate-selection context menu item should be localized to translate the link text" +
+ "without a target language."
+ );
+
+ await cleanup();
+ }
+);
+
+/**
+ * This test case ensures that the translate-selection context menu item functions correctly
+ * when text is actively selected but the context menu is invoked on an unselected hyperlink.
+ * The selected text content should take precedence over the link text, and the menu item should
+ * be localized to translate the selected text to the target language, rather than the hyperlink text.
+ */
+add_task(
+ async function test_translate_selection_menuitem_selected_text_takes_precedence_over_link_text() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
+ runInPage,
+ {
+ selectSpanishParagraph: true,
+ openAtEnglishHyperlink: true,
+ expectMenuItemVisible: true,
+ expectedTargetLanguage: "en",
+ },
+ "The translate-selection context menu item should be localized to translate the selection" +
+ "even though the hyperlink is the element on which the context menu was invoked."
+ );
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_no_text_selected.js b/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_no_text_selected.js
new file mode 100644
index 0000000000..82e5d3ba63
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_no_text_selected.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case verifies that the translate-selection context menu item is unavailable
+ * when no text is selected.
+ */
+add_task(
+ async function test_translate_selection_menuitem_is_unavailable_when_no_text_is_selected() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
+ runInPage,
+ {
+ selectSpanishParagraph: false,
+ openAtSpanishParagraph: true,
+ expectMenuItemVisible: false,
+ },
+ "The translate-selection context menu item should be unavailable when no text is selected."
+ );
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_text_selected.js b/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_text_selected.js
new file mode 100644
index 0000000000..deb5911a37
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_text_selected.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case verifies the functionality of the translate-selection context menu item
+ * when the selected text is not in the user's preferred language. The menu item should be
+ * localized to translate to the target language matching the user's top preferred language
+ * when the selected text is detected to be in a different language.
+ */
+add_task(
+ async function test_translate_selection_menuitem_when_selected_text_is_not_preferred_language() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
+ runInPage,
+ {
+ selectSpanishParagraph: true,
+ openAtSpanishParagraph: true,
+ expectMenuItemVisible: true,
+ expectedTargetLanguage: "en",
+ },
+ "The translate-selection context menu item should display a target language " +
+ "when the selected text is not the preferred language."
+ );
+
+ await cleanup();
+ }
+);
+
+/**
+ * This test case verifies the functionality of the translate-selection context menu item
+ * when the selected text is detected to be in the user's preferred language. The menu item
+ * should not be localized to display a target language when the selected text matches the
+ * user's top preferred language.
+ */
+add_task(
+ async function test_translate_selection_menuitem_when_selected_text_is_preferred_language() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: ENGLISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: false },
+ "The button is available."
+ );
+
+ await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
+ runInPage,
+ {
+ selectFirstParagraph: true,
+ openAtFirstParagraph: true,
+ expectMenuItemVisible: true,
+ expectedTargetLanguage: null,
+ },
+ "The translate-selection context menu item should not display a target language " +
+ "when the selected text is in the preferred language."
+ );
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_telemetry_firstrun_auto_translate.js b/browser/components/translations/tests/browser/browser_translations_telemetry_firstrun_auto_translate.js
new file mode 100644
index 0000000000..abfc3dc32e
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_telemetry_firstrun_auto_translate.js
@@ -0,0 +1,117 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the entire flow of opening the translation settings menu and initiating
+ * an auto-translate request on the first panel interaction.
+ */
+add_task(async function test_translations_telemetry_firstrun_auto_translate() {
+ const { cleanup, resolveDownloads, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.panelShown", false]],
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The button is available."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewFirstShow,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+ await FullPageTranslationsTestUtils.clickAlwaysTranslateLanguage({
+ downloadHandler: resolveDownloads,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
+ expectedEventCount: 1,
+ expectNewFlowId: true,
+ expectFirstInteraction: true,
+ finalValuePredicates: [
+ value => value.extra.auto_show === "false",
+ value => value.extra.view_name === "defaultView",
+ value => value.extra.opened_from === "translationsButton",
+ value => value.extra.document_language === "es",
+ ],
+ });
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.alwaysTranslateLanguage,
+ {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ expectFirstInteraction: true,
+ finalValuePredicates: [value => value.extra.language === "es"],
+ }
+ );
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.close, {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ expectFirstInteraction: true,
+ });
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.close, {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ expectFirstInteraction: true,
+ });
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translations.translationRequest,
+ {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ expectFirstInteraction: true,
+ }
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewRevisit,
+ });
+
+ await FullPageTranslationsTestUtils.clickRestoreButton();
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
+ expectedEventCount: 2,
+ expectNewFlowId: true,
+ expectFirstInteraction: false,
+ finalValuePredicates: [
+ value => value.extra.auto_show === "false",
+ value => value.extra.view_name === "revisitView",
+ value => value.extra.opened_from === "translationsButton",
+ value => value.extra.document_language === "es",
+ ],
+ });
+
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.restorePageButton,
+ {
+ expectedEventCount: 1,
+ expectFirstInteraction: false,
+ expectNewFlowId: false,
+ }
+ );
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.close, {
+ expectedEventCount: 2,
+ expectFirstInteraction: false,
+ expectNewFlowId: false,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The button is available."
+ );
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_telemetry_firstrun_basics.js b/browser/components/translations/tests/browser/browser_translations_telemetry_firstrun_basics.js
new file mode 100644
index 0000000000..200e06b9ce
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_telemetry_firstrun_basics.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that events in the first panel session are marked as first-interaction events
+ * and that events in the subsequent panel session are not marked as first-interaction events.
+ */
+add_task(async function test_translations_telemetry_firstrun_basics() {
+ const { cleanup } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.panelShown", false]],
+ });
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
+ expectedEventCount: 0,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The button is available."
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewFirstShow,
+ });
+
+ await FullPageTranslationsTestUtils.clickCancelButton();
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
+ expectedEventCount: 1,
+ expectNewFlowId: true,
+ expectFirstInteraction: true,
+ finalValuePredicates: [
+ value => value.extra.auto_show === "false",
+ value => value.extra.view_name === "defaultView",
+ value => value.extra.opened_from === "translationsButton",
+ value => value.extra.document_language === "es",
+ ],
+ });
+
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.cancelButton,
+ {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ expectFirstInteraction: true,
+ }
+ );
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.close, {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ expectFirstInteraction: true,
+ });
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewFirstShow,
+ });
+
+ await FullPageTranslationsTestUtils.clickCancelButton();
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
+ expectedEventCount: 2,
+ expectNewFlowId: true,
+ expectFirstInteraction: false,
+ allValuePredicates: [
+ value => value.extra.auto_show === "false",
+ value => value.extra.view_name === "defaultView",
+ value => value.extra.opened_from === "translationsButton",
+ value => value.extra.document_language === "es",
+ ],
+ });
+
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.cancelButton,
+ {
+ expectedEventCount: 2,
+ expectNewFlowId: false,
+ expectFirstInteraction: false,
+ }
+ );
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.close, {
+ expectedEventCount: 2,
+ expectNewFlowId: false,
+ expectFirstInteraction: false,
+ });
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_telemetry_firstrun_translation_failure.js b/browser/components/translations/tests/browser/browser_translations_telemetry_firstrun_translation_failure.js
new file mode 100644
index 0000000000..5397175039
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_telemetry_firstrun_translation_failure.js
@@ -0,0 +1,149 @@
+/* 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 that the first-interaction event status is maintained across a subsequent panel
+ * open, if re-opening the panel is due to a translation failure.
+ */
+add_task(async function test_translations_telemetry_firstrun_failure() {
+ const { cleanup, rejectDownloads, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.panelShown", false]],
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewFirstShow,
+ });
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
+ expectedEventCount: 1,
+ expectNewFlowId: true,
+ expectFirstInteraction: true,
+ finalValuePredicates: [
+ value => value.extra.auto_show === "false",
+ value => value.extra.view_name === "defaultView",
+ value => value.extra.opened_from === "translationsButton",
+ value => value.extra.document_language === "es",
+ ],
+ });
+
+ await FullPageTranslationsTestUtils.clickTranslateButton({
+ downloadHandler: rejectDownloads,
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewFirstShowError,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
+ expectedEventCount: 2,
+ expectNewFlowId: false,
+ expectFirstInteraction: true,
+ finalValuePredicates: [
+ value => value.extra.auto_show === "true",
+ value => value.extra.view_name === "errorView",
+ value => value.extra.opened_from === "translationsButton",
+ value => value.extra.document_language === "es",
+ ],
+ });
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.translateButton,
+ {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ expectFirstInteraction: true,
+ }
+ );
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.close, {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ expectFirstInteraction: true,
+ });
+ await TestTranslationsTelemetry.assertEvent(Glean.translations.error, {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ expectFirstInteraction: true,
+ finalValuePredicates: [
+ value =>
+ value.extra.reason === "Error: Intentionally rejecting downloads.",
+ ],
+ });
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translations.translationRequest,
+ {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ expectFirstInteraction: true,
+ finalValuePredicates: [
+ value => value.extra.from_language === "es",
+ value => value.extra.to_language === "en",
+ value => value.extra.auto_translate === "false",
+ value => value.extra.document_language === "es",
+ value => value.extra.top_preferred_language === "en",
+ ],
+ }
+ );
+
+ await FullPageTranslationsTestUtils.clickCancelButton();
+
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.cancelButton,
+ {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ expectFirstInteraction: true,
+ }
+ );
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.close, {
+ expectedEventCount: 2,
+ expectNewFlowId: false,
+ expectFirstInteraction: true,
+ });
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewFirstShow,
+ });
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
+ expectedEventCount: 3,
+ expectNewFlowId: true,
+ expectFirstInteraction: false,
+ finalValuePredicates: [
+ value => value.extra.auto_show === "false",
+ value => value.extra.view_name === "defaultView",
+ value => value.extra.opened_from === "translationsButton",
+ value => value.extra.document_language === "es",
+ ],
+ });
+
+ await FullPageTranslationsTestUtils.clickCancelButton();
+
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.cancelButton,
+ {
+ expectedEventCount: 2,
+ expectNewFlowId: false,
+ expectFirstInteraction: false,
+ }
+ );
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.close, {
+ expectedEventCount: 3,
+ expectNewFlowId: false,
+ expectFirstInteraction: false,
+ });
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_telemetry_firstrun_unsupported_lang.js b/browser/components/translations/tests/browser/browser_translations_telemetry_firstrun_unsupported_lang.js
new file mode 100644
index 0000000000..fa9c67ee37
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_telemetry_firstrun_unsupported_lang.js
@@ -0,0 +1,122 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that the first-interaction event status is maintained across a subsequent panel
+ * open, if re-opening the panel is due to requesting to change the source language.
+ */
+add_task(
+ async function test_translations_telemetry_firstrun_unsupported_lang() {
+ const { cleanup } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: [
+ // Do not include Spanish.
+ { fromLang: "fr", toLang: "en" },
+ { fromLang: "en", toLang: "fr" },
+ ],
+ prefs: [["browser.translations.panelShown", false]],
+ });
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ openFromAppMenu: true,
+ onOpenPanel:
+ FullPageTranslationsTestUtils.assertPanelViewUnsupportedLanguage,
+ });
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
+ expectedEventCount: 1,
+ expectNewFlowId: true,
+ expectFirstInteraction: true,
+ finalValuePredicates: [
+ value => value.extra.auto_show === "false",
+ value => value.extra.view_name === "defaultView",
+ value => value.extra.opened_from === "appMenu",
+ value => value.extra.document_language === "es",
+ ],
+ });
+
+ await FullPageTranslationsTestUtils.clickChangeSourceLanguageButton({
+ firstShow: true,
+ });
+
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.changeSourceLanguageButton,
+ {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ expectFirstInteraction: true,
+ }
+ );
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.close, {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ expectFirstInteraction: true,
+ });
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
+ expectedEventCount: 2,
+ expectNewFlowId: false,
+ expectFirstInteraction: true,
+ finalValuePredicates: [
+ value => value.extra.auto_show === "false",
+ value => value.extra.view_name === "defaultView",
+ value => value.extra.opened_from === "appMenu",
+ value => value.extra.document_language === "es",
+ ],
+ });
+
+ await FullPageTranslationsTestUtils.clickCancelButton();
+
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.cancelButton,
+ {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ expectFirstInteraction: true,
+ }
+ );
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.close, {
+ expectedEventCount: 2,
+ expectNewFlowId: false,
+ expectFirstInteraction: true,
+ });
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ openFromAppMenu: true,
+ onOpenPanel:
+ FullPageTranslationsTestUtils.assertPanelViewUnsupportedLanguage,
+ });
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
+ expectedEventCount: 3,
+ expectNewFlowId: true,
+ expectFirstInteraction: false,
+ finalValuePredicates: [
+ value => value.extra.auto_show === "false",
+ value => value.extra.view_name === "defaultView",
+ value => value.extra.opened_from === "appMenu",
+ value => value.extra.document_language === "es",
+ ],
+ });
+
+ await FullPageTranslationsTestUtils.clickDismissErrorButton();
+
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.dismissErrorButton,
+ {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ expectFirstInteraction: false,
+ }
+ );
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.close, {
+ expectedEventCount: 3,
+ expectNewFlowId: false,
+ expectFirstInteraction: false,
+ });
+
+ 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..64a02287e8
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_telemetry_open_panel.js
@@ -0,0 +1,85 @@
+/* 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(Glean.translationsPanel.open, {
+ expectedEventCount: 0,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The button is available."
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.clickCancelButton();
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
+ expectedEventCount: 1,
+ expectNewFlowId: true,
+ finalValuePredicates: [
+ value => value.extra.auto_show === "false",
+ value => value.extra.view_name === "defaultView",
+ value => value.extra.opened_from === "translationsButton",
+ value => value.extra.document_language === "es",
+ ],
+ });
+
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.cancelButton,
+ {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ }
+ );
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.close, {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ });
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.clickCancelButton();
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
+ expectedEventCount: 2,
+ expectNewFlowId: true,
+ allValuePredicates: [
+ value => value.extra.auto_show === "false",
+ value => value.extra.view_name === "defaultView",
+ value => value.extra.opened_from === "translationsButton",
+ value => value.extra.document_language === "es",
+ ],
+ });
+
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.cancelButton,
+ {
+ expectedEventCount: 2,
+ expectNewFlowId: false,
+ }
+ );
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.close, {
+ expectedEventCount: 2,
+ expectNewFlowId: false,
+ });
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_telemetry_panel_auto_offer.js b/browser/components/translations/tests/browser/browser_translations_telemetry_panel_auto_offer.js
new file mode 100644
index 0000000000..93ff473851
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_telemetry_panel_auto_offer.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that the popup is automatically offered.
+ */
+add_task(async function test_translations_panel_auto_offer() {
+ const { cleanup } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ autoOffer: true,
+ });
+
+ await FullPageTranslationsTestUtils.clickCancelButton();
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
+ expectedEventCount: 1,
+ expectNewFlowId: true,
+ allValuePredicates: [
+ value => value.extra.auto_show === "true",
+ value => value.extra.view_name === "defaultView",
+ value => value.extra.opened_from === "translationsButton",
+ value => value.extra.document_language === "es",
+ ],
+ });
+
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.cancelButton,
+ {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ }
+ );
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.close, {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ });
+
+ await navigate("Navigate to another page on the same domain.", {
+ url: SPANISH_PAGE_URL_2,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The button is still shown."
+ );
+
+ await navigate("Navigate to a page on a different domain.", {
+ url: SPANISH_PAGE_URL_DOT_ORG,
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.clickCancelButton();
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
+ expectedEventCount: 2,
+ expectNewFlowId: true,
+ allValuePredicates: [
+ value => value.extra.auto_show === "true",
+ value => value.extra.view_name === "defaultView",
+ value => value.extra.opened_from === "translationsButton",
+ value => value.extra.document_language === "es",
+ ],
+ });
+
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.cancelButton,
+ {
+ expectedEventCount: 2,
+ expectNewFlowId: false,
+ }
+ );
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.close, {
+ expectedEventCount: 2,
+ expectNewFlowId: false,
+ });
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_telemetry_panel_auto_offer_settings.js b/browser/components/translations/tests/browser/browser_translations_telemetry_panel_auto_offer_settings.js
new file mode 100644
index 0000000000..9eae81904d
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_telemetry_panel_auto_offer_settings.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that the automatic offering of the popup can be disabled.
+ */
+add_task(async function test_translations_panel_auto_offer_settings() {
+ const { cleanup } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ // Use the auto offer mechanics, but default the pref to the off position.
+ autoOffer: true,
+ prefs: [["browser.translations.automaticallyPopup", false]],
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The translations button is shown."
+ );
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
+ expectedEventCount: 0,
+ });
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+ await FullPageTranslationsTestUtils.openTranslationsSettingsMenu();
+ await FullPageTranslationsTestUtils.assertIsAlwaysOfferTranslationsEnabled(
+ false
+ );
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
+ expectedEventCount: 1,
+ expectNewFlowId: true,
+ allValuePredicates: [
+ value => value.extra.auto_show === "false",
+ value => value.extra.view_name === "defaultView",
+ value => value.extra.opened_from === "translationsButton",
+ value => value.extra.document_language === "es",
+ ],
+ });
+
+ await FullPageTranslationsTestUtils.clickAlwaysOfferTranslations();
+
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.alwaysOfferTranslations,
+ {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ allValuePredicates: [value => value.extra.toggled_on === "true"],
+ }
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+ await FullPageTranslationsTestUtils.assertIsAlwaysOfferTranslationsEnabled(
+ true
+ );
+
+ await FullPageTranslationsTestUtils.clickCancelButton();
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
+ expectedEventCount: 2,
+ expectNewFlowId: true,
+ allValuePredicates: [
+ value => value.extra.auto_show === "false",
+ value => value.extra.view_name === "defaultView",
+ value => value.extra.opened_from === "translationsButton",
+ value => value.extra.document_language === "es",
+ ],
+ });
+
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.cancelButton,
+ {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ }
+ );
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.close, {
+ expectedEventCount: 2,
+ expectNewFlowId: false,
+ });
+
+ await navigate(
+ "Wait for the popup to be shown when navigating to a different host.",
+ {
+ url: SPANISH_PAGE_URL_DOT_ORG,
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ }
+ );
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
+ expectedEventCount: 3,
+ expectNewFlowId: true,
+ finalValuePredicates: [
+ value => value.extra.auto_show === "true",
+ value => value.extra.view_name === "defaultView",
+ value => value.extra.opened_from === "translationsButton",
+ value => value.extra.document_language === "es",
+ ],
+ });
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_telemetry_switch_languages.js b/browser/components/translations/tests/browser/browser_translations_telemetry_switch_languages.js
new file mode 100644
index 0000000000..6c06abab95
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_telemetry_switch_languages.js
@@ -0,0 +1,156 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the telemetry events for switching the from-language.
+ */
+add_task(async function test_translations_telemetry_switch_from_language() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The button is available."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ FullPageTranslationsTestUtils.assertSelectedFromLanguage("es");
+ FullPageTranslationsTestUtils.switchSelectedFromLanguage("en");
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
+ expectedEventCount: 1,
+ expectNewFlowId: true,
+ finalValuePredicates: [
+ value => value.extra.auto_show === "false",
+ value => value.extra.view_name === "defaultView",
+ value => value.extra.opened_from === "translationsButton",
+ value => value.extra.document_language === "es",
+ ],
+ });
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.changeFromLanguage,
+ {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ finalValuePredicates: [value => value.extra.language === "en"],
+ }
+ );
+
+ FullPageTranslationsTestUtils.switchSelectedFromLanguage("es");
+
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.changeFromLanguage,
+ {
+ expectedEventCount: 2,
+ expectNewFlowId: false,
+ finalValuePredicates: [value => value.extra.language === "es"],
+ }
+ );
+
+ FullPageTranslationsTestUtils.switchSelectedFromLanguage("");
+
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.changeFromLanguage,
+ {
+ expectedEventCount: 2,
+ }
+ );
+
+ FullPageTranslationsTestUtils.switchSelectedFromLanguage("en");
+
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.changeFromLanguage,
+ {
+ expectedEventCount: 3,
+ expectNewFlowId: false,
+ finalValuePredicates: [value => value.extra.language === "en"],
+ }
+ );
+
+ await cleanup();
+});
+
+/**
+ * Tests the telemetry events for switching the to-language.
+ */
+add_task(async function test_translations_telemetry_switch_to_language() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The button is available."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ FullPageTranslationsTestUtils.assertSelectedToLanguage("en");
+ FullPageTranslationsTestUtils.switchSelectedToLanguage("fr");
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
+ expectedEventCount: 1,
+ expectNewFlowId: true,
+ finalValuePredicates: [
+ value => value.extra.auto_show === "false",
+ value => value.extra.view_name === "defaultView",
+ value => value.extra.opened_from === "translationsButton",
+ value => value.extra.document_language === "es",
+ ],
+ });
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.changeToLanguage,
+ {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ finalValuePredicates: [value => value.extra.language === "fr"],
+ }
+ );
+
+ FullPageTranslationsTestUtils.switchSelectedToLanguage("en");
+
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.changeToLanguage,
+ {
+ expectedEventCount: 2,
+ expectNewFlowId: false,
+ finalValuePredicates: [value => value.extra.language === "en"],
+ }
+ );
+
+ FullPageTranslationsTestUtils.switchSelectedToLanguage("");
+
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.changeToLanguage,
+ {
+ expectedEventCount: 2,
+ }
+ );
+
+ FullPageTranslationsTestUtils.switchSelectedToLanguage("en");
+
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.changeToLanguage,
+ {
+ expectedEventCount: 3,
+ expectNewFlowId: false,
+ finalValuePredicates: [value => value.extra.language === "en"],
+ }
+ );
+
+ 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..e7d0cfb4f4
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_telemetry_translation_failure.js
@@ -0,0 +1,212 @@
+/* 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() {
+ const { cleanup, rejectDownloads, runInPage } = await loadTestPage({
+ page: SPANISH_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await TestTranslationsTelemetry.assertCounter(
+ "RequestCount",
+ Glean.translations.requestsCount,
+ 0
+ );
+ await TestTranslationsTelemetry.assertRate(
+ "ErrorRate",
+ Glean.translations.errorRate,
+ {
+ expectedNumerator: 0,
+ expectedDenominator: 0,
+ }
+ );
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translations.translationRequest,
+ {
+ expectedEventCount: 0,
+ }
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
+ expectedEventCount: 1,
+ expectNewFlowId: true,
+ finalValuePredicates: [
+ value => value.extra.auto_show === "false",
+ value => value.extra.view_name === "defaultView",
+ value => value.extra.opened_from === "translationsButton",
+ value => value.extra.document_language === "es",
+ ],
+ });
+
+ await FullPageTranslationsTestUtils.clickTranslateButton({
+ downloadHandler: rejectDownloads,
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewError,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
+ expectedEventCount: 2,
+ expectNewFlowId: false,
+ finalValuePredicates: [
+ value => value.extra.auto_show === "true",
+ value => value.extra.view_name === "errorView",
+ value => value.extra.opened_from === "translationsButton",
+ value => value.extra.document_language === "es",
+ ],
+ });
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.translateButton,
+ {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ }
+ );
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.close, {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ });
+ await TestTranslationsTelemetry.assertCounter(
+ "RequestCount",
+ Glean.translations.requestsCount,
+ 1
+ );
+ await TestTranslationsTelemetry.assertRate(
+ "ErrorRate",
+ Glean.translations.errorRate,
+ {
+ expectedNumerator: 1,
+ expectedDenominator: 1,
+ }
+ );
+ await TestTranslationsTelemetry.assertEvent(Glean.translations.error, {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ finalValuePredicates: [
+ value =>
+ value.extra.reason === "Error: Intentionally rejecting downloads.",
+ ],
+ });
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translations.translationRequest,
+ {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ finalValuePredicates: [
+ value => value.extra.from_language === "es",
+ value => value.extra.to_language === "en",
+ value => value.extra.auto_translate === "false",
+ value => value.extra.document_language === "es",
+ value => value.extra.top_preferred_language === "en",
+ ],
+ }
+ );
+
+ await cleanup();
+ }
+);
+
+/**
+ * Tests the telemetry event for an automatic translation request failure.
+ */
+add_task(async function test_translations_telemetry_auto_translation_failure() {
+ const { cleanup, rejectDownloads, runInPage } = await loadTestPage({
+ page: BLANK_PAGE,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.alwaysTranslateLanguages", "es"]],
+ });
+
+ await navigate("Navigate to a Spanish page", {
+ url: SPANISH_PAGE_URL,
+ downloadHandler: rejectDownloads,
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewError,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await TestTranslationsTelemetry.assertCounter(
+ "RequestCount",
+ Glean.translations.requestsCount,
+ 1
+ );
+ await TestTranslationsTelemetry.assertRate(
+ "ErrorRate",
+ Glean.translations.errorRate,
+ {
+ expectedNumerator: 1,
+ expectedDenominator: 1,
+ }
+ );
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
+ expectedEventCount: 1,
+ expectNewFlowId: true,
+ finalValuePredicates: [
+ value => value.extra.auto_show === "true",
+ value => value.extra.view_name === "errorView",
+ value => value.extra.opened_from === "translationsButton",
+ value => value.extra.document_language === "es",
+ ],
+ });
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.close, {
+ expectedEventCount: 0,
+ expectNewFlowId: false,
+ });
+ await TestTranslationsTelemetry.assertEvent(Glean.translations.error, {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ finalValuePredicates: [
+ value =>
+ value.extra.reason === "Error: Intentionally rejecting downloads.",
+ ],
+ });
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translations.translationRequest,
+ {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ finalValuePredicates: [
+ value => value.extra.from_language === "es",
+ value => value.extra.to_language === "en",
+ value => value.extra.auto_translate === "true",
+ value => value.extra.document_language === "es",
+ value => value.extra.top_preferred_language === "en",
+ ],
+ }
+ );
+
+ await FullPageTranslationsTestUtils.clickCancelButton();
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.cancelButton,
+ {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ }
+ );
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.close, {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ });
+
+ 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..90bd81a8ed
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_telemetry_translation_request.js
@@ -0,0 +1,170 @@
+/* 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,
+ });
+
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: false, icon: true },
+ "The button is available."
+ );
+
+ await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
+
+ await TestTranslationsTelemetry.assertCounter(
+ "RequestCount",
+ Glean.translations.requestsCount,
+ 0
+ );
+ await TestTranslationsTelemetry.assertRate(
+ "ErrorRate",
+ Glean.translations.errorRate,
+ {
+ expectedNumerator: 0,
+ expectedDenominator: 0,
+ }
+ );
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translations.translationRequest,
+ {
+ expectedEventCount: 0,
+ }
+ );
+
+ await FullPageTranslationsTestUtils.openTranslationsPanel({
+ onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
+ });
+
+ await FullPageTranslationsTestUtils.clickTranslateButton({
+ downloadHandler: resolveDownloads,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ await TestTranslationsTelemetry.assertCounter(
+ "RequestCount",
+ Glean.translations.requestsCount,
+ 1
+ );
+ await TestTranslationsTelemetry.assertRate(
+ "ErrorRate",
+ Glean.translations.errorRate,
+ {
+ expectedNumerator: 0,
+ expectedDenominator: 1,
+ }
+ );
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
+ expectedEventCount: 1,
+ expectNewFlowId: true,
+ finalValuePredicates: [
+ value => value.extra.auto_show === "false",
+ value => value.extra.view_name === "defaultView",
+ value => value.extra.opened_from === "translationsButton",
+ value => value.extra.document_language === "es",
+ ],
+ });
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.translateButton,
+ {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ }
+ );
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.close, {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ });
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translations.translationRequest,
+ {
+ expectedEventCount: 1,
+ expectNewFlowId: false,
+ finalValuePredicates: [
+ value => value.extra.from_language === "es",
+ value => value.extra.to_language === "en",
+ value => value.extra.auto_translate === "false",
+ value => value.extra.document_language === "es",
+ value => value.extra.top_preferred_language === "en",
+ ],
+ }
+ );
+
+ 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: BLANK_PAGE,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.alwaysTranslateLanguages", "es"]],
+ });
+
+ await navigate("Navigate to a Spanish page", {
+ url: SPANISH_PAGE_URL,
+ downloadHandler: resolveDownloads,
+ });
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ await TestTranslationsTelemetry.assertCounter(
+ "RequestCount",
+ Glean.translations.requestsCount,
+ 1
+ );
+ await TestTranslationsTelemetry.assertRate(
+ "ErrorRate",
+ Glean.translations.errorRate,
+ {
+ expectedNumerator: 0,
+ expectedDenominator: 1,
+ }
+ );
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, {
+ expectedEventCount: 0,
+ });
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translationsPanel.translateButton,
+ {
+ expectedEventCount: 0,
+ }
+ );
+ await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.close, {
+ expectedEventCount: 0,
+ });
+ await TestTranslationsTelemetry.assertEvent(
+ Glean.translations.translationRequest,
+ {
+ expectedEventCount: 1,
+ expectNewFlowId: true,
+ finalValuePredicates: [
+ value => value.extra.from_language === "es",
+ value => value.extra.to_language === "en",
+ value => value.extra.auto_translate === "true",
+ value => value.extra.document_language === "es",
+ value => value.extra.top_preferred_language === "en",
+ ],
+ }
+ );
+
+ 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..bc9968308c
--- /dev/null
+++ b/browser/components/translations/tests/browser/head.js
@@ -0,0 +1,1423 @@
+/* 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
+);
+
+/**
+ * Opens a new tab in the foreground.
+ *
+ * @param {string} url
+ */
+async function addTab(url) {
+ logAction(url);
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ url,
+ true // Wait for laod
+ );
+ return {
+ tab,
+ removeTab() {
+ BrowserTestUtils.removeTab(tab);
+ },
+ };
+}
+
+/**
+ * Simulates clicking an element with the mouse.
+ *
+ * @param {element} element - The element to click.
+ * @param {string} [message] - A message to log to info.
+ */
+function click(element, message) {
+ logAction(message);
+ return new Promise(resolve => {
+ element.addEventListener(
+ "click",
+ function () {
+ resolve();
+ },
+ { once: true }
+ );
+
+ EventUtils.synthesizeMouseAtCenter(element, {
+ type: "mousedown",
+ isSynthesized: false,
+ });
+ EventUtils.synthesizeMouseAtCenter(element, {
+ type: "mouseup",
+ isSynthesized: false,
+ });
+ });
+}
+
+/**
+ * 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}"]`);
+ console.log(doc);
+ if (elements.length === 0) {
+ throw new Error("Could not find the element by l10n id: " + l10nId);
+ }
+ return elements;
+}
+
+/**
+ * Retrieves an element by its Id.
+ *
+ * @param {string} id
+ * @param {Document} [doc]
+ * @returns {Element}
+ * @throws Throws if the element is not visible in the DOM.
+ */
+function getById(id, doc = document) {
+ const element = maybeGetById(id, /* ensureIsVisible */ true, doc);
+ if (!element) {
+ throw new Error("The element is not visible in the DOM: #" + id);
+ }
+ return element;
+}
+
+/**
+ * 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 (BrowserTestUtils.isVisible(element)) {
+ return element;
+ }
+ }
+ throw new Error("The element is not visible in the DOM: " + l10nId);
+}
+
+/**
+ * Returns the intl display name of a given language tag.
+ *
+ * @param {string} langTag - A BCP-47 language tag.
+ */
+const getIntlDisplayName = (() => {
+ let displayNames = null;
+
+ return langTag => {
+ if (!displayNames) {
+ displayNames = new Services.intl.DisplayNames(undefined, {
+ type: "language",
+ fallback: "none",
+ });
+ }
+ return displayNames.of(langTag);
+ };
+})();
+
+/**
+ * Attempts to retrieve an element by its Id.
+ *
+ * @param {string} id - The Id of the element to retrieve.
+ * @param {boolean} [ensureIsVisible=true] - If set to true, the function will return null when the element is not visible.
+ * @param {Document} [doc=document] - The document from which to retrieve the element.
+ * @returns {Element | null} - The retrieved element.
+ * @throws Throws if no element was found by the given Id.
+ */
+function maybeGetById(id, ensureIsVisible = true, doc = document) {
+ const element = doc.getElementById(id);
+ if (!element) {
+ throw new Error("Could not find the element by id: #" + id);
+ }
+
+ if (!ensureIsVisible) {
+ return element;
+ }
+
+ if (BrowserTestUtils.isVisible(element)) {
+ return element;
+ }
+
+ return null;
+}
+
+/**
+ * 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 (BrowserTestUtils.isVisible(element)) {
+ return element;
+ }
+ }
+ return null;
+}
+
+/**
+ * Provide a uniform way to log actions. This abuses the Error stack to get the callers
+ * of the action. This should help in test debugging.
+ */
+function logAction(...params) {
+ const error = new Error();
+ const stackLines = error.stack.split("\n");
+ const actionName = stackLines[1]?.split("@")[0] ?? "";
+ const taskFileLocation = stackLines[2]?.split("@")[1] ?? "";
+ if (taskFileLocation.includes("head.js")) {
+ // Only log actions that were done at the test level.
+ return;
+ }
+
+ info(`Action: ${actionName}(${params.join(", ")})`);
+ info(
+ `Source: ${taskFileLocation.replace(
+ "chrome://mochitests/content/browser/",
+ ""
+ )}`
+ );
+}
+
+/**
+ * Navigate to a URL and indicate a message as to why.
+ */
+async function navigate(
+ message,
+ { url, onOpenPanel = null, downloadHandler = null, pivotTranslation = false }
+) {
+ logAction();
+ // When the translations panel is open from the app menu,
+ // it doesn't close on navigate the way that it does when it's
+ // open from the translations button, so ensure that we always
+ // close it when we navigate to a new page.
+ await closeTranslationsPanelIfOpen();
+
+ 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.startLoadingURIString(gBrowser.selectedBrowser, BLANK_PAGE);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ const loadTargetPage = async () => {
+ BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, url);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ if (downloadHandler) {
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: true, locale: false, icon: true },
+ "The icon presents the loading indicator."
+ );
+ await downloadHandler(pivotTranslation ? 2 : 1);
+ }
+ };
+
+ info(`Loading url: "${url}"`);
+ if (onOpenPanel) {
+ await FullPageTranslationsTestUtils.waitForTranslationsPopupEvent(
+ "popupshown",
+ loadTargetPage,
+ onOpenPanel
+ );
+ } else {
+ await loadTargetPage();
+ }
+}
+
+/**
+ * Switches to a given tab.
+ *
+ * @param {object} tab - The tab to switch to
+ * @param {string} name
+ */
+async function switchTab(tab, name) {
+ logAction("tab", name);
+ gBrowser.selectedTab = tab;
+ await new Promise(resolve => setTimeout(resolve, 0));
+}
+
+/**
+ * Click the reader-mode button if the reader-mode button is available.
+ * Fails if the reader-mode button is hidden.
+ */
+async function toggleReaderMode() {
+ logAction();
+ const readerButton = document.getElementById("reader-mode-button");
+ await waitForCondition(() => readerButton.hidden === false);
+
+ readerButton.getAttribute("readeractive")
+ ? info("Exiting reader mode")
+ : info("Entering reader mode");
+
+ const readyPromise = readerButton.getAttribute("readeractive")
+ ? waitForCondition(() => !readerButton.getAttribute("readeractive"))
+ : BrowserTestUtils.waitForContentEvent(
+ gBrowser.selectedBrowser,
+ "AboutReaderContentReady"
+ );
+
+ click(readerButton, "Clicking the reader-mode button");
+ await readyPromise;
+}
+
+/**
+ * A class containing test utility functions specific to testing full-page translations.
+ */
+class FullPageTranslationsTestUtils {
+ /**
+ * A collection of element visibility expectations for the default panel view.
+ */
+ static #defaultViewVisibilityExpectations = {
+ cancelButton: true,
+ fromMenuList: true,
+ fromLabel: true,
+ header: true,
+ langSelection: true,
+ toMenuList: true,
+ toLabel: true,
+ translateButton: true,
+ };
+
+ /**
+ * 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 {object} expectations
+ * @param {string} expectations.langTag - A BCP-47 language tag.
+ * @param {boolean} expectations.checked - Whether the checkbox is expected to be checked.
+ * @param {boolean} expectations.disabled - Whether the menuitem is expected to be disabled.
+ */
+ static async #assertCheckboxState(
+ dataL10nId,
+ { langTag = null, checked = true, disabled = false }
+ ) {
+ const menuItems = getAllByL10nId(dataL10nId);
+ for (const menuItem of menuItems) {
+ if (langTag) {
+ const {
+ args: { language },
+ } = document.l10n.getAttributes(menuItem);
+ is(
+ language,
+ getIntlDisplayName(langTag),
+ `Should match expected language display name for ${dataL10nId}`
+ );
+ }
+ is(
+ menuItem.disabled,
+ disabled,
+ `Should match expected disabled state for ${dataL10nId}`
+ );
+ await waitForCondition(
+ () => menuItem.getAttribute("checked") === (checked ? "true" : "false"),
+ "Waiting for checkbox state"
+ );
+ is(
+ menuItem.getAttribute("checked"),
+ checked ? "true" : "false",
+ `Should match expected checkbox state for ${dataL10nId}`
+ );
+ }
+ }
+
+ /**
+ * Asserts that the always-offer-translations checkbox matches the expected checked state.
+ *
+ * @param {boolean} checked
+ */
+ static async assertIsAlwaysOfferTranslationsEnabled(checked) {
+ info(
+ `Checking that always-offer-translations is ${
+ checked ? "enabled" : "disabled"
+ }`
+ );
+ await FullPageTranslationsTestUtils.#assertCheckboxState(
+ "translations-panel-settings-always-offer-translation",
+ { checked }
+ );
+ }
+
+ /**
+ * Asserts that the always-translate-language checkbox matches the expected checked state.
+ *
+ * @param {string} langTag - A BCP-47 language tag
+ * @param {object} expectations
+ * @param {boolean} expectations.checked - Whether the checkbox is expected to be checked.
+ * @param {boolean} expectations.disabled - Whether the menuitem is expected to be disabled.
+ */
+ static async assertIsAlwaysTranslateLanguage(
+ langTag,
+ { checked = true, disabled = false }
+ ) {
+ info(
+ `Checking that always-translate is ${
+ checked ? "enabled" : "disabled"
+ } for "${langTag}"`
+ );
+ await FullPageTranslationsTestUtils.#assertCheckboxState(
+ "translations-panel-settings-always-translate-language",
+ { langTag, checked, disabled }
+ );
+ }
+
+ /**
+ * Asserts that the never-translate-language checkbox matches the expected checked state.
+ *
+ * @param {string} langTag - A BCP-47 language tag
+ * @param {object} expectations
+ * @param {boolean} expectations.checked - Whether the checkbox is expected to be checked.
+ * @param {boolean} expectations.disabled - Whether the menuitem is expected to be disabled.
+ */
+ static async assertIsNeverTranslateLanguage(
+ langTag,
+ { checked = true, disabled = false }
+ ) {
+ info(
+ `Checking that never-translate is ${
+ checked ? "enabled" : "disabled"
+ } for "${langTag}"`
+ );
+ await FullPageTranslationsTestUtils.#assertCheckboxState(
+ "translations-panel-settings-never-translate-language",
+ { langTag, checked, disabled }
+ );
+ }
+
+ /**
+ * Asserts that the never-translate-site checkbox matches the expected checked state.
+ *
+ * @param {string} url - The url of a website
+ * @param {object} expectations
+ * @param {boolean} expectations.checked - Whether the checkbox is expected to be checked.
+ * @param {boolean} expectations.disabled - Whether the menuitem is expected to be disabled.
+ */
+ static async assertIsNeverTranslateSite(
+ url,
+ { checked = true, disabled = false }
+ ) {
+ info(
+ `Checking that never-translate is ${
+ checked ? "enabled" : "disabled"
+ } for "${url}"`
+ );
+ await FullPageTranslationsTestUtils.#assertCheckboxState(
+ "translations-panel-settings-never-translate-site",
+ { checked, disabled }
+ );
+ }
+
+ /**
+ * Asserts that the proper language tags are shown on the translations button.
+ *
+ * @param {string} fromLanguage - The BCP-47 language tag being translated from.
+ * @param {string} toLanguage - The BCP-47 language tag being translated into.
+ */
+ static async #assertLangTagIsShownOnTranslationsButton(
+ fromLanguage,
+ toLanguage
+ ) {
+ info(
+ `Ensuring that the translations button displays the language tag "${toLanguage}"`
+ );
+ const { button, locale } =
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: false, locale: true, icon: true },
+ "The icon presents the locale."
+ );
+ is(
+ locale.innerText,
+ toLanguage,
+ `The expected language tag "${toLanguage}" is shown.`
+ );
+ is(
+ button.getAttribute("data-l10n-id"),
+ "urlbar-translations-button-translated"
+ );
+ const fromLangDisplay = getIntlDisplayName(fromLanguage);
+ const toLangDisplay = getIntlDisplayName(toLanguage);
+ is(
+ button.getAttribute("data-l10n-args"),
+ `{"fromLanguage":"${fromLangDisplay}","toLanguage":"${toLangDisplay}"}`
+ );
+ }
+ /**
+ * Asserts that the Spanish test page has been translated by checking
+ * that the H1 element has been modified from its original form.
+ *
+ * @param {string} fromLanguage - The BCP-47 language tag being translated from.
+ * @param {string} toLanguage - The BCP-47 language tag being translated into.
+ * @param {Function} runInPage - Allows running a closure in the content page.
+ * @param {string} message - An optional message to log to info.
+ */
+ static async assertPageIsTranslated(
+ fromLanguage,
+ toLanguage,
+ runInPage,
+ message = null
+ ) {
+ if (message) {
+ info(message);
+ }
+ info("Checking that the page is translated");
+ const callback = async (TranslationsTest, { fromLang, toLang }) => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is translated.",
+ getH1,
+ `DON QUIJOTE DE LA MANCHA [${fromLang} to ${toLang}, html]`
+ );
+ };
+ await runInPage(callback, { fromLang: fromLanguage, toLang: toLanguage });
+ await FullPageTranslationsTestUtils.#assertLangTagIsShownOnTranslationsButton(
+ fromLanguage,
+ toLanguage
+ );
+ }
+
+ /**
+ * Asserts that the Spanish test page is untranslated by checking
+ * that the H1 element is still in its original Spanish form.
+ *
+ * @param {Function} runInPage - Allows running a closure in the content page.
+ * @param {string} message - An optional message to log to info.
+ */
+ static async assertPageIsUntranslated(runInPage, message = null) {
+ if (message) {
+ info(message);
+ }
+ info("Checking that the page is untranslated");
+ await runInPage(async TranslationsTest => {
+ const { getH1 } = TranslationsTest.getSelectors();
+ await TranslationsTest.assertTranslationResult(
+ "The page's H1 is untranslated and in the original Spanish.",
+ getH1,
+ "Don Quijote de La Mancha"
+ );
+ });
+ }
+
+ /**
+ * Asserts that for each provided expectation, the visible state of the corresponding
+ * element in TranslationsPanel.elements both exists and matches the visibility expectation.
+ *
+ * @param {object} expectations
+ * A list of expectations for the visibility of any subset of TranslationsPanel.elements
+ */
+ static #assertPanelElementVisibility(expectations = {}) {
+ // Assume nothing is visible by default, and overwrite them
+ // with any specific expectations provided in the argument.
+ const finalExpectations = {
+ cancelButton: false,
+ changeSourceLanguageButton: false,
+ dismissErrorButton: false,
+ error: false,
+ fromMenuList: false,
+ fromLabel: false,
+ header: false,
+ intro: false,
+ introLearnMoreLink: false,
+ langSelection: false,
+ restoreButton: false,
+ toLabel: false,
+ toMenuList: false,
+ translateButton: false,
+ unsupportedHeader: false,
+ unsupportedHint: false,
+ unsupportedLearnMoreLink: false,
+ ...expectations,
+ };
+
+ const elements = TranslationsPanel.elements;
+ const hidden = {};
+ const visible = {};
+
+ for (const propertyName in finalExpectations) {
+ ok(
+ elements.hasOwnProperty(propertyName),
+ `Expected translations panel elements to have property ${propertyName}`
+ );
+ if (finalExpectations[propertyName]) {
+ visible[propertyName] = elements[propertyName];
+ } else {
+ hidden[propertyName] = elements[propertyName];
+ }
+ }
+
+ assertVisibility({ hidden, visible });
+ }
+
+ /**
+ * Asserts that the TranslationsPanel header has the expected l10nId.
+ *
+ * @param {string} l10nId - The expected data-l10n-id of the header.
+ */
+ static #assertPanelHeaderL10nId(l10nId) {
+ const { header } = TranslationsPanel.elements;
+ is(
+ header.getAttribute("data-l10n-id"),
+ l10nId,
+ "The translations panel header should match the expected data-l10n-id"
+ );
+ }
+
+ /**
+ * Asserts that the mainViewId of the panel matches the given string.
+ *
+ * @param {string} expectedId
+ */
+ static #assertPanelMainViewId(expectedId) {
+ const mainViewId =
+ TranslationsPanel.elements.multiview.getAttribute("mainViewId");
+ is(
+ mainViewId,
+ expectedId,
+ "The full-page Translations panel mainViewId should match its expected value"
+ );
+ }
+
+ /**
+ * Asserts that panel element visibility matches the default panel view.
+ */
+ static assertPanelViewDefault() {
+ info("Checking that the panel shows the default view");
+ FullPageTranslationsTestUtils.#assertPanelMainViewId(
+ "translations-panel-view-default"
+ );
+ FullPageTranslationsTestUtils.#assertPanelElementVisibility({
+ ...FullPageTranslationsTestUtils.#defaultViewVisibilityExpectations,
+ });
+ FullPageTranslationsTestUtils.#assertPanelHeaderL10nId(
+ "translations-panel-header"
+ );
+ }
+
+ /**
+ * Asserts that panel element visibility matches the panel error view.
+ */
+ static assertPanelViewError() {
+ info("Checking that the panel shows the error view");
+ FullPageTranslationsTestUtils.#assertPanelMainViewId(
+ "translations-panel-view-default"
+ );
+ FullPageTranslationsTestUtils.#assertPanelElementVisibility({
+ error: true,
+ ...FullPageTranslationsTestUtils.#defaultViewVisibilityExpectations,
+ });
+ FullPageTranslationsTestUtils.#assertPanelHeaderL10nId(
+ "translations-panel-header"
+ );
+ }
+
+ /**
+ * Asserts that the panel element visibility matches the panel loading view.
+ */
+ static assertPanelViewLoading() {
+ info("Checking that the panel shows the loading view");
+ FullPageTranslationsTestUtils.assertPanelViewDefault();
+ const loadingButton = getByL10nId(
+ "translations-panel-translate-button-loading"
+ );
+ ok(loadingButton, "The loading button is present");
+ ok(loadingButton.disabled, "The loading button is disabled");
+ }
+
+ /**
+ * Asserts that panel element visibility matches the panel first-show view.
+ */
+ static assertPanelViewFirstShow() {
+ info("Checking that the panel shows the first-show view");
+ FullPageTranslationsTestUtils.#assertPanelMainViewId(
+ "translations-panel-view-default"
+ );
+ FullPageTranslationsTestUtils.#assertPanelElementVisibility({
+ intro: true,
+ introLearnMoreLink: true,
+ ...FullPageTranslationsTestUtils.#defaultViewVisibilityExpectations,
+ });
+ FullPageTranslationsTestUtils.#assertPanelHeaderL10nId(
+ "translations-panel-intro-header"
+ );
+ }
+
+ /**
+ * Asserts that panel element visibility matches the panel first-show error view.
+ */
+ static assertPanelViewFirstShowError() {
+ info("Checking that the panel shows the first-show error view");
+ FullPageTranslationsTestUtils.#assertPanelMainViewId(
+ "translations-panel-view-default"
+ );
+ FullPageTranslationsTestUtils.#assertPanelElementVisibility({
+ error: true,
+ intro: true,
+ introLearnMoreLink: true,
+ ...FullPageTranslationsTestUtils.#defaultViewVisibilityExpectations,
+ });
+ FullPageTranslationsTestUtils.#assertPanelHeaderL10nId(
+ "translations-panel-intro-header"
+ );
+ }
+
+ /**
+ * Asserts that panel element visibility matches the panel revisit view.
+ */
+ static assertPanelViewRevisit() {
+ info("Checking that the panel shows the revisit view");
+ FullPageTranslationsTestUtils.#assertPanelMainViewId(
+ "translations-panel-view-default"
+ );
+ FullPageTranslationsTestUtils.#assertPanelElementVisibility({
+ header: true,
+ langSelection: true,
+ restoreButton: true,
+ toLabel: true,
+ toMenuList: true,
+ translateButton: true,
+ });
+ FullPageTranslationsTestUtils.#assertPanelHeaderL10nId(
+ "translations-panel-revisit-header"
+ );
+ }
+
+ /**
+ * Asserts that panel element visibility matches the panel unsupported language view.
+ */
+ static assertPanelViewUnsupportedLanguage() {
+ info("Checking that the panel shows the unsupported-language view");
+ FullPageTranslationsTestUtils.#assertPanelMainViewId(
+ "translations-panel-view-unsupported-language"
+ );
+ FullPageTranslationsTestUtils.#assertPanelElementVisibility({
+ changeSourceLanguageButton: true,
+ dismissErrorButton: true,
+ unsupportedHeader: true,
+ unsupportedHint: true,
+ unsupportedLearnMoreLink: true,
+ });
+ }
+
+ /**
+ * Asserts that the selected from-language matches the provided language tag.
+ *
+ * @param {string} langTag - A BCP-47 language tag.
+ */
+ static assertSelectedFromLanguage(langTag) {
+ info(`Checking that the selected from-language matches ${langTag}`);
+ const { fromMenuList } = TranslationsPanel.elements;
+ is(
+ fromMenuList.value,
+ langTag,
+ "Expected selected from-language to match the given language tag"
+ );
+ }
+
+ /**
+ * Asserts that the selected to-language matches the provided language tag.
+ *
+ * @param {string} langTag - A BCP-47 language tag.
+ */
+ static assertSelectedToLanguage(langTag) {
+ info(`Checking that the selected to-language matches ${langTag}`);
+ const { toMenuList } = TranslationsPanel.elements;
+ is(
+ toMenuList.value,
+ langTag,
+ "Expected selected to-language to match the given language tag"
+ );
+ }
+
+ /**
+ * Assert some property about the translations button.
+ *
+ * @param {Record<string, boolean>} visibleAssertions
+ * @param {string} message The message for the assertion.
+ * @returns {HTMLElement}
+ */
+ static async 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 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;
+ }
+
+ /**
+ * Simulates the effect of clicking the always-offer-translations menuitem.
+ * Requires that the settings menu of the translations panel is open,
+ * otherwise the test will fail.
+ */
+ static async clickAlwaysOfferTranslations() {
+ logAction();
+ await FullPageTranslationsTestUtils.#clickSettingsMenuItemByL10nId(
+ "translations-panel-settings-always-offer-translation"
+ );
+ }
+
+ /**
+ * 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.
+ */
+ static async clickAlwaysTranslateLanguage({
+ downloadHandler = null,
+ pivotTranslation = false,
+ } = {}) {
+ logAction();
+ await FullPageTranslationsTestUtils.#clickSettingsMenuItemByL10nId(
+ "translations-panel-settings-always-translate-language"
+ );
+ if (downloadHandler) {
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: true, locale: false, icon: true },
+ "The icon presents the loading indicator."
+ );
+ await downloadHandler(pivotTranslation ? 2 : 1);
+ }
+ }
+
+ /**
+ * Simulates clicking the cancel button.
+ */
+ static async clickCancelButton() {
+ logAction();
+ const { cancelButton } = TranslationsPanel.elements;
+ assertVisibility({ visible: { cancelButton } });
+ await FullPageTranslationsTestUtils.waitForTranslationsPopupEvent(
+ "popuphidden",
+ () => {
+ click(cancelButton, "Clicking the cancel button");
+ }
+ );
+ }
+
+ /**
+ * Simulates clicking the change-source-language button.
+ *
+ * @param {object} config
+ * @param {boolean} config.firstShow
+ * - True if the first-show view should be expected
+ * False if the default view should be expected
+ */
+ static async clickChangeSourceLanguageButton({ firstShow = false } = {}) {
+ logAction();
+ const { changeSourceLanguageButton } = TranslationsPanel.elements;
+ assertVisibility({ visible: { changeSourceLanguageButton } });
+ await FullPageTranslationsTestUtils.waitForTranslationsPopupEvent(
+ "popupshown",
+ () => {
+ click(
+ changeSourceLanguageButton,
+ "Click the change-source-language button"
+ );
+ },
+ firstShow
+ ? FullPageTranslationsTestUtils.assertPanelViewFirstShow
+ : FullPageTranslationsTestUtils.assertPanelViewDefault
+ );
+ }
+
+ /**
+ * Simulates clicking the dismiss-error button.
+ */
+ static async clickDismissErrorButton() {
+ logAction();
+ const { dismissErrorButton } = TranslationsPanel.elements;
+ assertVisibility({ visible: { dismissErrorButton } });
+ await FullPageTranslationsTestUtils.waitForTranslationsPopupEvent(
+ "popuphidden",
+ () => {
+ click(dismissErrorButton, "Click the dismiss-error button");
+ }
+ );
+ }
+
+ /**
+ * Simulates the effect of clicking the manage-languages menuitem.
+ * Requires that the settings menu of the translations panel is open,
+ * otherwise the test will fail.
+ */
+ static async clickManageLanguages() {
+ logAction();
+ await FullPageTranslationsTestUtils.#clickSettingsMenuItemByL10nId(
+ "translations-panel-settings-manage-languages"
+ );
+ }
+
+ /**
+ * 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.
+ */
+ static async clickNeverTranslateLanguage() {
+ logAction();
+ await FullPageTranslationsTestUtils.#clickSettingsMenuItemByL10nId(
+ "translations-panel-settings-never-translate-language"
+ );
+ }
+
+ /**
+ * 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.
+ */
+ static async clickNeverTranslateSite() {
+ logAction();
+ await FullPageTranslationsTestUtils.#clickSettingsMenuItemByL10nId(
+ "translations-panel-settings-never-translate-site"
+ );
+ }
+
+ /**
+ * Simulates clicking the restore-page button.
+ */
+ static async clickRestoreButton() {
+ logAction();
+ const { restoreButton } = TranslationsPanel.elements;
+ assertVisibility({ visible: { restoreButton } });
+ await FullPageTranslationsTestUtils.waitForTranslationsPopupEvent(
+ "popuphidden",
+ () => {
+ click(restoreButton, "Click the restore-page button");
+ }
+ );
+ }
+
+ /*
+ * Simulates the effect of toggling a menu item in the translations panel
+ * settings menu. Requires that the settings menu is currently open,
+ * otherwise the test will fail.
+ */
+ static async #clickSettingsMenuItemByL10nId(l10nId) {
+ info(`Toggling the "${l10nId}" settings menu item.`);
+ click(getByL10nId(l10nId), `Clicking the "${l10nId}" settings menu item.`);
+ await closeSettingsMenuIfOpen();
+ }
+
+ /**
+ * Simulates clicking the translate button.
+ *
+ * @param {object} config
+ * @param {Function} config.downloadHandler
+ * - The function handle expected downloads, resolveDownloads() or rejectDownloads()
+ * Leave as null to test more granularly, such as testing opening the loading view,
+ * or allowing for the automatic downloading of files.
+ * @param {boolean} config.pivotTranslation
+ * - True if the expected translation is a pivot translation, otherwise false.
+ * Affects the number of expected downloads.
+ */
+ static async clickTranslateButton({
+ downloadHandler = null,
+ pivotTranslation = false,
+ } = {}) {
+ logAction();
+ const { translateButton } = TranslationsPanel.elements;
+ assertVisibility({ visible: { translateButton } });
+ await FullPageTranslationsTestUtils.waitForTranslationsPopupEvent(
+ "popuphidden",
+ () => {
+ click(translateButton);
+ }
+ );
+
+ if (downloadHandler) {
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: true, locale: false, icon: true },
+ "The icon presents the loading indicator."
+ );
+ await downloadHandler(pivotTranslation ? 2 : 1);
+ }
+ }
+
+ /**
+ * Opens the translations panel.
+ *
+ * @param {object} config
+ * @param {Function} config.onOpenPanel
+ * - A function to run as soon as the panel opens.
+ * @param {boolean} config.openFromAppMenu
+ * - Open the panel from the app menu. If false, uses the translations button.
+ * @param {boolean} config.openWithKeyboard
+ * - Open the panel by synthesizing the keyboard. If false, synthesizes the mouse.
+ */
+ static async openTranslationsPanel({
+ onOpenPanel = null,
+ openFromAppMenu = false,
+ openWithKeyboard = false,
+ }) {
+ logAction();
+ await closeTranslationsPanelIfOpen();
+ if (openFromAppMenu) {
+ await FullPageTranslationsTestUtils.#openTranslationsPanelViaAppMenu({
+ onOpenPanel,
+ openWithKeyboard,
+ });
+ } else {
+ await FullPageTranslationsTestUtils.#openTranslationsPanelViaTranslationsButton(
+ {
+ onOpenPanel,
+ openWithKeyboard,
+ }
+ );
+ }
+ }
+
+ /**
+ * Opens the translations panel via the app menu.
+ *
+ * @param {object} config
+ * @param {Function} config.onOpenPanel
+ * - A function to run as soon as the panel opens.
+ * @param {boolean} config.openWithKeyboard
+ * - Open the panel by synthesizing the keyboard. If false, synthesizes the mouse.
+ */
+ static async #openTranslationsPanelViaAppMenu({
+ onOpenPanel = null,
+ openWithKeyboard = false,
+ }) {
+ logAction();
+ const appMenuButton = getById("PanelUI-menu-button");
+ if (openWithKeyboard) {
+ hitEnterKey(appMenuButton, "Opening the app-menu button with keyboard");
+ } else {
+ click(appMenuButton, "Opening the app-menu button");
+ }
+ await BrowserTestUtils.waitForEvent(window.PanelUI.mainView, "ViewShown");
+
+ const translateSiteButton = getById("appMenu-translate-button");
+
+ is(
+ translateSiteButton.disabled,
+ false,
+ "The app-menu translate button should be enabled"
+ );
+
+ await FullPageTranslationsTestUtils.waitForTranslationsPopupEvent(
+ "popupshown",
+ () => {
+ if (openWithKeyboard) {
+ hitEnterKey(translateSiteButton, "Opening the popup with keyboard");
+ } else {
+ click(translateSiteButton, "Opening the popup");
+ }
+ },
+ onOpenPanel
+ );
+ }
+
+ /**
+ * Opens the translations panel via the translations button.
+ *
+ * @param {object} config
+ * @param {Function} config.onOpenPanel
+ * - A function to run as soon as the panel opens.
+ * @param {boolean} config.openWithKeyboard
+ * - Open the panel by synthesizing the keyboard. If false, synthesizes the mouse.
+ */
+ static async #openTranslationsPanelViaTranslationsButton({
+ onOpenPanel = null,
+ openWithKeyboard = false,
+ }) {
+ logAction();
+ const { button } =
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true },
+ "The translations button is visible."
+ );
+ await FullPageTranslationsTestUtils.waitForTranslationsPopupEvent(
+ "popupshown",
+ () => {
+ if (openWithKeyboard) {
+ hitEnterKey(button, "Opening the popup with keyboard");
+ } else {
+ click(button, "Opening the popup");
+ }
+ },
+ onOpenPanel
+ );
+ }
+
+ /**
+ * Opens the translations panel settings menu.
+ * Requires that the translations panel is already open.
+ */
+ static async openTranslationsSettingsMenu() {
+ logAction();
+ const gearIcons = getAllByL10nId("translations-panel-settings-button");
+ for (const gearIcon of gearIcons) {
+ if (gearIcon.hidden) {
+ continue;
+ }
+ click(gearIcon, "Open the settings menu");
+ info("Waiting for settings menu to open.");
+ const manageLanguages = await waitForCondition(() =>
+ maybeGetByL10nId("translations-panel-settings-manage-languages")
+ );
+ ok(
+ manageLanguages,
+ "The manage languages item should be visible in the settings menu."
+ );
+ return;
+ }
+ }
+
+ /**
+ * Switches the selected from-language to the provided language tag.
+ *
+ * @param {string} langTag - A BCP-47 language tag.
+ */
+ static switchSelectedFromLanguage(langTag) {
+ logAction(langTag);
+ const { fromMenuList } = TranslationsPanel.elements;
+ fromMenuList.value = langTag;
+ fromMenuList.dispatchEvent(new Event("command"));
+ }
+
+ /**
+ * Switches the selected to-language to the provided language tag.
+ *
+ * @param {string} langTag - A BCP-47 language tag.
+ */
+ static switchSelectedToLanguage(langTag) {
+ logAction(langTag);
+ const { toMenuList } = TranslationsPanel.elements;
+ toMenuList.value = langTag;
+ toMenuList.dispatchEvent(new Event("command"));
+ }
+
+ /**
+ * 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
+ * @param {Function} postEventAssertion
+ * An optional assertion to be made immediately after the event occurs.
+ * @returns {Promise<void>}
+ */
+ static async waitForTranslationsPopupEvent(
+ eventName,
+ callback,
+ postEventAssertion = null
+ ) {
+ // De-lazify the panel elements.
+ TranslationsPanel.elements;
+ const panel = document.getElementById("translations-panel");
+ if (!panel) {
+ throw new Error("Unable to find the translations panel element.");
+ }
+ const promise = BrowserTestUtils.waitForEvent(panel, eventName);
+ await callback();
+ info("Waiting for the translations panel popup to be shown");
+ await promise;
+ if (postEventAssertion) {
+ postEventAssertion();
+ }
+ // Wait a single tick on the event loop.
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+}
+
+/**
+ * A class containing test utility functions specific to testing select translations.
+ */
+class SelectTranslationsTestUtils {
+ /**
+ * Opens the context menu then asserts properties of the translate-selection item in the context menu.
+ *
+ * @param {Function} runInPage - A content-exposed function to run within the context of the page.
+ * @param {object} options - Options for how to open the context menu and what properties to assert about the translate-selection item.
+ * @param {boolean} options.selectFirstParagraph - Selects the first paragraph before opening the context menu.
+ * @param {boolean} options.selectSpanishParagraph - Selects the Spanish paragraph before opening the context menu.
+ * This is only available in SPANISH_TEST_PAGE.
+ * @param {boolean} options.expectMenuItemIsVisible - Whether the translate-selection item is expected to be visible.
+ * Does not assert visibility if left undefined.
+ * @param {string} options.expectedTargetLanguage - The target language for translation.
+ * @param {boolean} options.openAtFirstParagraph - Opens the context menu at the first paragraph in the test page.
+ * @param {boolean} options.openAtSpanishParagraph - Opens the context menu at the Spanish paragraph in the test page.
+ * This is only available in SPANISH_TEST_PAGE.
+ * @param {boolean} options.openAtEnglishHyperlink - Opens the context menu at the English hyperlink in the test page.
+ * This is only available in SPANISH_TEST_PAGE.
+ * @param {boolean} options.openAtSpanishHyperlink - Opens the context menu at the Spanish hyperlink in the test page.
+ * This is only available in SPANISH_TEST_PAGE.
+ * @param {string} [message] - A message to log to info.
+ * @throws Throws an error if the properties of the translate-selection item do not match the expected options.
+ */
+ static async assertContextMenuTranslateSelectionItem(
+ runInPage,
+ {
+ selectFirstParagraph,
+ selectSpanishParagraph,
+ expectMenuItemIsVisible,
+ expectedTargetLanguage,
+ openAtFirstParagraph,
+ openAtSpanishParagraph,
+ openAtEnglishHyperlink,
+ openAtSpanishHyperlink,
+ },
+ message
+ ) {
+ logAction();
+
+ if (message) {
+ info(message);
+ }
+
+ await closeTranslationsPanelIfOpen();
+ await closeContextMenuIfOpen();
+
+ await SelectTranslationsTestUtils.openContextMenu(runInPage, {
+ selectFirstParagraph,
+ selectSpanishParagraph,
+ openAtFirstParagraph,
+ openAtSpanishParagraph,
+ openAtEnglishHyperlink,
+ openAtSpanishHyperlink,
+ });
+
+ const menuItem = maybeGetById(
+ "context-translate-selection",
+ /* ensureIsVisible */ false
+ );
+
+ if (expectMenuItemIsVisible !== undefined) {
+ const visibility = expectMenuItemIsVisible ? "visible" : "hidden";
+ assertVisibility({ [visibility]: menuItem });
+ }
+
+ if (expectMenuItemIsVisible === true) {
+ if (expectedTargetLanguage) {
+ // Target language expected, check for the data-l10n-id with a `{$language}` argument.
+ const expectedL10nId =
+ selectFirstParagraph === true || selectSpanishParagraph === true
+ ? "main-context-menu-translate-selection-to-language"
+ : "main-context-menu-translate-link-text-to-language";
+ await waitForCondition(
+ () => menuItem.getAttribute("data-l10n-id") === expectedL10nId,
+ `Waiting for translate-selection context menu item to localize with target language ${expectedTargetLanguage}`
+ );
+
+ is(
+ menuItem.getAttribute("data-l10n-id"),
+ expectedL10nId,
+ "Expected the translate-selection context menu item to be localized with a target language."
+ );
+
+ const l10nArgs = JSON.parse(menuItem.getAttribute("data-l10n-args"));
+ is(
+ l10nArgs.language,
+ getIntlDisplayName(expectedTargetLanguage),
+ `Expected the translate-selection context menu item to have the target language '${expectedTargetLanguage}'.`
+ );
+ } else {
+ // No target language expected, check for the data-l10n-id that has no `{$language}` argument.
+ const expectedL10nId =
+ selectFirstParagraph === true || selectSpanishParagraph === true
+ ? "main-context-menu-translate-selection"
+ : "main-context-menu-translate-link-text";
+ await waitForCondition(
+ () => menuItem.getAttribute("data-l10n-id") === expectedL10nId,
+ "Waiting for translate-selection context menu item to localize without target language."
+ );
+
+ is(
+ menuItem.getAttribute("data-l10n-id"),
+ expectedL10nId,
+ "Expected the translate-selection context menu item to be localized without a target language."
+ );
+ }
+ }
+
+ await closeContextMenuIfOpen();
+ }
+
+ /**
+ * Opens the context menu at a specified element on the page, based on the provided options.
+ *
+ * @param {Function} runInPage - A content-exposed function to run within the context of the page.
+ * @param {object} options - Options for opening the context menu.
+ * @param {boolean} options.selectFirstParagraph - Selects the first paragraph before opening the context menu.
+ * @param {boolean} options.selectSpanishParagraph - Selects the Spanish paragraph before opening the context menu.
+ * This is only available in SPANISH_TEST_PAGE.
+ * @param {boolean} options.openAtFirstParagraph - Opens the context menu at the first paragraph in the test page.
+ * @param {boolean} options.openAtSpanishParagraph - Opens the context menu at the Spanish paragraph in the test page.
+ * This is only available in SPANISH_TEST_PAGE.
+ * @param {boolean} options.openAtEnglishHyperlink - Opens the context menu at the English hyperlink in the test page.
+ * This is only available in SPANISH_TEST_PAGE.
+ * @param {boolean} options.openAtSpanishHyperlink - Opens the context menu at the Spanish hyperlink in the test page.
+ * This is only available in SPANISH_TEST_PAGE.
+ * @throws Throws an error if no valid option was provided for opening the menu.
+ */
+ static async openContextMenu(
+ runInPage,
+ {
+ selectFirstParagraph,
+ selectSpanishParagraph,
+ openAtFirstParagraph,
+ openAtSpanishParagraph,
+ openAtEnglishHyperlink,
+ openAtSpanishHyperlink,
+ }
+ ) {
+ logAction();
+
+ if (selectFirstParagraph === true) {
+ await runInPage(async TranslationsTest => {
+ const { getFirstParagraph } = TranslationsTest.getSelectors();
+ const paragraph = getFirstParagraph();
+ TranslationsTest.selectContentElement(paragraph);
+ });
+ }
+
+ if (selectSpanishParagraph === true) {
+ await runInPage(async TranslationsTest => {
+ const { getSpanishParagraph } = TranslationsTest.getSelectors();
+ const paragraph = getSpanishParagraph();
+ TranslationsTest.selectContentElement(paragraph);
+ });
+ }
+
+ if (openAtFirstParagraph === true) {
+ await runInPage(async TranslationsTest => {
+ const { getFirstParagraph } = TranslationsTest.getSelectors();
+ const paragraph = getFirstParagraph();
+ await TranslationsTest.rightClickContentElement(paragraph);
+ });
+ return;
+ }
+
+ if (openAtSpanishParagraph === true) {
+ await runInPage(async TranslationsTest => {
+ const { getSpanishParagraph } = TranslationsTest.getSelectors();
+ const paragraph = getSpanishParagraph();
+ await TranslationsTest.rightClickContentElement(paragraph);
+ });
+ return;
+ }
+
+ if (openAtEnglishHyperlink === true) {
+ await runInPage(async TranslationsTest => {
+ const { getEnglishHyperlink } = TranslationsTest.getSelectors();
+ const hyperlink = getEnglishHyperlink();
+ await TranslationsTest.rightClickContentElement(hyperlink);
+ });
+ return;
+ }
+
+ if (openAtSpanishHyperlink === true) {
+ await runInPage(async TranslationsTest => {
+ const { getSpanishHyperlink } = TranslationsTest.getSelectors();
+ const hyperlink = getSpanishHyperlink();
+ await TranslationsTest.rightClickContentElement(hyperlink);
+ });
+ return;
+ }
+
+ throw new Error(
+ "openContextMenu() was not provided a declaration for which element to open the menu at."
+ );
+ }
+}
+
+class TranslationsSettingsTestUtils {
+ /**
+ * Opens the Translation Settings page by clicking the settings button sent in the argument.
+ *
+ * @param {HTMLElement} settingsButton
+ * @returns {Element}
+ */
+ static async openAboutPreferencesTranslationsSettingsPane(settingsButton) {
+ const document = gBrowser.selectedBrowser.contentDocument;
+
+ const promise = BrowserTestUtils.waitForEvent(
+ document,
+ "paneshown",
+ false,
+ event => event.detail.category === "paneTranslations"
+ );
+
+ click(settingsButton, "Click settings button");
+ await promise;
+
+ const elements = {
+ backButton: document.getElementById("translations-settings-back-button"),
+ header: document.getElementById("translations-settings-header"),
+ translationsSettingsDescription: document.getElementById(
+ "translations-settings-description"
+ ),
+ translateAlwaysHeader: document.getElementById(
+ "translations-settings-always-translate"
+ ),
+ translateNeverHeader: document.getElementById(
+ "translations-settings-never-translate"
+ ),
+ translateAlwaysAddButton: document.getElementById(
+ "translations-settings-always-translate-list"
+ ),
+ translateNeverAddButton: document.getElementById(
+ "translations-settings-never-translate-list"
+ ),
+ translateNeverSiteHeader: document.getElementById(
+ "translations-settings-never-sites-header"
+ ),
+ translateNeverSiteDesc: document.getElementById(
+ "translations-settings-never-sites"
+ ),
+ translateDownloadLanguagesHeader: document.getElementById(
+ "translations-settings-download-languages"
+ ),
+ translateDownloadLanguagesLearnMore: document.getElementById(
+ "download-languages-learn-more"
+ ),
+ };
+
+ return elements;
+ }
+}