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