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