diff options
Diffstat (limited to 'browser/components/translations')
29 files changed, 2722 insertions, 582 deletions
diff --git a/browser/components/translations/content/fullPageTranslationsPanel.inc.xhtml b/browser/components/translations/content/fullPageTranslationsPanel.inc.xhtml index bc0c5b319f..6b3e19538d 100644 --- a/browser/components/translations/content/fullPageTranslationsPanel.inc.xhtml +++ b/browser/components/translations/content/fullPageTranslationsPanel.inc.xhtml @@ -24,7 +24,7 @@ <html:h1 class="translations-panel-header-wrapper"> <html:span id="full-page-translations-panel-header"></html:span> </html:h1> - <hbox class="translations-panel-beta"> + <hbox class="translations-panel-beta" role="image" aria-label="Beta"> <image class="translations-panel-beta-icon"></image> </hbox> <toolbarbutton id="translations-panel-settings" @@ -49,7 +49,7 @@ flex="1" value="detect" size="large" - aria-labelledby="translations-panel-from-label" + aria-labelledby="full-page-translations-panel-from-label" oncommand="FullPageTranslationsPanel.onChangeFromLanguage(event)"> <menupopup id="full-page-translations-panel-from-menupopup" class="translations-panel-language-menupopup-from"> @@ -63,7 +63,7 @@ flex="1" value="detect" size="large" - aria-labelledby="translations-panel-to-label" + aria-labelledby="full-page-translations-panel-to-label" oncommand="FullPageTranslationsPanel.onChangeToLanguage(event)"> <menupopup id="full-page-translations-panel-to-menupopup" class="translations-panel-language-menupopup-to"> @@ -85,7 +85,7 @@ </vbox> </vbox> - <html:moz-button-group class="panel-footer translations-panel-footer"> + <html:moz-button-group class="panel-footer translations-panel-footer translations-panel-button-group"> <button id="full-page-translations-panel-restore-button" class="footer-button" oncommand="FullPageTranslationsPanel.onRestore(event);" diff --git a/browser/components/translations/content/fullPageTranslationsPanel.js b/browser/components/translations/content/fullPageTranslationsPanel.js index eddd3566f1..2875333d61 100644 --- a/browser/components/translations/content/fullPageTranslationsPanel.js +++ b/browser/components/translations/content/fullPageTranslationsPanel.js @@ -1041,8 +1041,6 @@ var FullPageTranslationsPanel = new (class { isFirstUserInteraction = null, } ) { - await window.ensureCustomElements("moz-button-group"); - const { panel, appMenuButton } = this.elements; const openedFromAppMenu = target.id === appMenuButton.id; const { docLangTag } = await this.#getCachedDetectedLanguages(); @@ -1107,10 +1105,6 @@ var FullPageTranslationsPanel = new (class { return; } - const window = - gBrowser.selectedBrowser.browsingContext.top.embedderElement.ownerGlobal; - window.ensureCustomElements("moz-support-link"); - const { button } = this.buttonElements; const { requestedTranslationPair } = diff --git a/browser/components/translations/content/selectTranslationsPanel.inc.xhtml b/browser/components/translations/content/selectTranslationsPanel.inc.xhtml index 8c643ea3f6..287bd65679 100644 --- a/browser/components/translations/content/selectTranslationsPanel.inc.xhtml +++ b/browser/components/translations/content/selectTranslationsPanel.inc.xhtml @@ -8,95 +8,158 @@ type="arrow" role="alertdialog" noautofocus="true" + tabspecific="true" + locationspecific="true" aria-labelledby="translations-panel-header" - orient="vertical" - onpopupshown="SelectTranslationsPanel.handlePanelPopupShownEvent(event)" - onpopuphidden="SelectTranslationsPanel.handlePanelPopupHiddenEvent(event)"> + orient="vertical"> <hbox class="panel-header select-translations-panel-header"> <html:h1 class="translations-panel-header-wrapper"> <html:span id="select-translations-panel-header" data-l10n-id="select-translations-panel-header"> </html:span> </html:h1> - <hbox class="translations-panel-beta"> + <hbox class="translations-panel-beta" role="image" aria-label="Beta"> <image id="select-translations-panel-beta-icon" class="translations-panel-beta-icon"> </image> </hbox> - <toolbarbutton id="select-translations-panel-settings" + <toolbarbutton id="select-translations-panel-settings-button" class="panel-info-button translations-panel-settings-gear-icon" data-l10n-id="translations-panel-settings-button" - closemenu="none" /> + tabindex="0" + closemenu="none"/> </hbox> - <vbox class="select-translations-panel-content"> - <hbox id="select-translations-panel-lang-selection"> - <vbox flex="1"> - <label id="select-translations-panel-from-label" - class="select-translations-panel-label" - data-l10n-id="select-translations-panel-from-label"> - </label> - <menulist id="select-translations-panel-from" - flex="1" - value="" - size="large" - data-l10n-id="translations-panel-choose-language" - aria-labelledby="select-translations-panel-from-label" - noinitialselection="true" - oncommand="SelectTranslationsPanel.onChangeFromLanguage(event)"> - <menupopup id="select-translations-panel-from-menupopup" - onpopupshown="SelectTranslationsPanel.handlePanelPopupShownEvent(event)" - class="translations-panel-language-menupopup-from"> - <!-- The list of <menuitem> will be dynamically inserted. --> - </menupopup> - </menulist> - </vbox> - <vbox flex="1"> - <label id="select-translations-panel-to-label" - class="select-translations-panel-label" - data-l10n-id="select-translations-panel-to-label"> - </label> - <menulist id="select-translations-panel-to" - flex="1" - value="" - size="large" - data-l10n-id="translations-panel-choose-language" - aria-labelledby="select-translations-panel-to-label" - noinitialselection="true" - oncommand="SelectTranslationsPanel.onChangeToLanguage(event)"> - <menupopup id="select-translations-panel-to-menupopup" - onpopupshown="SelectTranslationsPanel.handlePanelPopupShownEvent(event)" - class="translations-panel-language-menupopup-to"> - <!-- The list of <menuitem> will be dynamically inserted. --> - </menupopup> - </menulist> - </vbox> - </hbox> - </vbox> - <vbox class="select-translations-panel-content"> - <html:textarea id="select-translations-panel-text-area" - class="select-translations-panel-text-area" - readonly="true" - tabindex="0"> - </html:textarea> - </vbox> - - <hbox class="select-translations-panel-content"> + <html:div id="select-translations-panel-init-failure-content" + class="translations-panel-content" + hidden="true"> + <html:moz-message-bar id="select-translations-panel-init-failure-message-bar" + type="error" + class="select-translations-panel-message-bar" + data-l10n-id="select-translations-panel-init-failure-message" + data-l10n-attrs="message"> + </html:moz-message-bar> + </html:div> + <html:div id="select-translations-panel-unsupported-language-content" hidden="true"> + <vbox flex="1" class="select-translations-panel-content"> + <html:moz-message-bar id="select-translations-panel-unsupported-language-message-bar" + type="info" + class="select-translations-panel-message-bar" + data-l10n-id="select-translations-panel-unsupported-language-message-unknown" + data-l10n-attrs="message"> + </html:moz-message-bar> + <label id="select-translations-panel-try-another-language-label" + class="select-translations-panel-label" + data-l10n-id="select-translations-panel-try-another-language-label"> + </label> + <menulist id="select-translations-panel-try-another-language" + flex="1" + value="" + size="large" + data-l10n-id="translations-panel-choose-language" + aria-labelledby="select-translations-panel-try-another-language-label" + noinitialselection="true"> + <menupopup id="select-translations-panel-try-another-language-menupopup" + class="translations-panel-language-menupopup-from"> + <!-- The list of <menuitem> will be dynamically inserted. --> + </menupopup> + </menulist> + </vbox> + </html:div> + <html:div id="select-translations-panel-main-content"> + <vbox class="select-translations-panel-content"> + <hbox id="select-translations-panel-lang-selection"> + <vbox flex="1"> + <label id="select-translations-panel-from-label" + class="select-translations-panel-label" + data-l10n-id="select-translations-panel-from-label"> + </label> + <menulist id="select-translations-panel-from" + flex="1" + value="" + size="large" + data-l10n-id="translations-panel-choose-language" + aria-labelledby="select-translations-panel-from-label" + noinitialselection="true"> + <menupopup id="select-translations-panel-from-menupopup" + class="translations-panel-language-menupopup-from"> + <!-- The list of <menuitem> will be dynamically inserted. --> + </menupopup> + </menulist> + </vbox> + <vbox flex="1"> + <label id="select-translations-panel-to-label" + class="select-translations-panel-label" + data-l10n-id="select-translations-panel-to-label"> + </label> + <menulist id="select-translations-panel-to" + flex="1" + value="" + size="large" + data-l10n-id="translations-panel-choose-language" + aria-labelledby="select-translations-panel-to-label" + noinitialselection="true"> + <menupopup id="select-translations-panel-to-menupopup" + class="translations-panel-language-menupopup-to"> + <!-- The list of <menuitem> will be dynamically inserted. --> + </menupopup> + </menulist> + </vbox> + </hbox> + </vbox> + <vbox class="select-translations-panel-content"> + <html:textarea id="select-translations-panel-text-area" + class="select-translations-panel-text-area" + readonly="true" + tabindex="0"> + </html:textarea> + <html:moz-message-bar id="select-translations-panel-translation-failure-message-bar" + type="error" + hidden="true" + data-l10n-id="select-translations-panel-translation-failure-message" + data-l10n-attrs="message"> + </html:moz-message-bar> + </vbox> + </html:div> + <html:div id="select-translations-panel-footer" + class="panel-footer translations-panel-footer"> <button id="select-translations-panel-copy-button" - class="footer-button select-translations-panel-button select-translations-panel-copy-button" + class="footer-button select-translations-panel-copy-button" data-l10n-id="select-translations-panel-copy-button"> </button> - </hbox> - - <html:moz-button-group class="panel-footer translations-panel-footer"> - <button id="select-translations-panel-translate-full-page-button" - class="footer-button select-translations-panel-button" - data-l10n-id="select-translations-panel-translate-full-page-button"> - </button> - <button id="select-translations-panel-done-button" - class="footer-button select-translations-panel-button" - data-l10n-id="select-translations-panel-done-button" - default="true" - oncommand = "SelectTranslationsPanel.close()"> - </button> - </html:moz-button-group> + <html:moz-button-group id="select-translations-panel-footer-button-group" + class="translations-panel-button-group"> + <button id="select-translations-panel-cancel-button" + class="footer-button" + hidden="true" + data-l10n-id="select-translations-panel-cancel-button"> + </button> + <button id="select-translations-panel-done-button-secondary" + hidden="true" + class="footer-button" + data-l10n-id="select-translations-panel-done-button"> + </button> + <button id="select-translations-panel-translate-full-page-button" + class="footer-button" + data-l10n-id="select-translations-panel-translate-full-page-button"> + </button> + <button id="select-translations-panel-done-button-primary" + class="footer-button" + data-l10n-id="select-translations-panel-done-button" + default="true"> + </button> + <button id="select-translations-panel-translate-button" + class="footer-button" + data-l10n-id="select-translations-panel-translate-button" + hidden="true" + default="true" + disabled="true"> + </button> + <button id="select-translations-panel-try-again-button" + class="footer-button" + data-l10n-id="select-translations-panel-try-again-button" + hidden="true" + default="true"> + </button> + </html:moz-button-group> + </html:div> </panel> </html:template> diff --git a/browser/components/translations/content/selectTranslationsPanel.js b/browser/components/translations/content/selectTranslationsPanel.js index bb825eaefa..36452d4cc0 100644 --- a/browser/components/translations/content/selectTranslationsPanel.js +++ b/browser/components/translations/content/selectTranslationsPanel.js @@ -16,6 +16,13 @@ ChromeUtils.defineESModuleGetters(this, { Translator: "chrome://global/content/translations/Translator.mjs", }); +XPCOMUtils.defineLazyServiceGetter( + this, + "ClipboardHelper", + "@mozilla.org/widget/clipboardhelper;1", + "nsIClipboardHelper" +); + /** * This singleton class controls the SelectTranslations panel. * @@ -122,18 +129,28 @@ var SelectTranslationsPanel = new (class { #lazyElements; /** - * The internal state of the SelectTranslationsPanel. + * Set to true the first time event listeners are initialized. * - * @type {SelectTranslationsPanelState} + * @type {boolean} */ - #translationState = { phase: "closed" }; + #eventListenersInitialized = false; + + /** + * This value is true if this page does not allow Full Page Translations, + * e.g. PDFs, reader mode, internal Firefox pages. + * + * Many of these are cases where the SelectTranslationsPanel is available + * even though the FullPageTranslationsPanel is not, so this helps inform + * whether the translate-full-page button should be allowed in this context. + */ + #isFullPageTranslationsRestrictedForPage = true; /** - * The Translator for the current language pair. + * The internal state of the SelectTranslationsPanel. * - * @type {Translator} + * @type {SelectTranslationsPanelState} */ - #translator; + #translationState = { phase: "closed" }; /** * An Id that increments with each translation, used to help keep track @@ -171,18 +188,37 @@ var SelectTranslationsPanel = new (class { TranslationsPanelShared.defineLazyElements(document, this.#lazyElements, { betaIcon: "select-translations-panel-beta-icon", + cancelButton: "select-translations-panel-cancel-button", copyButton: "select-translations-panel-copy-button", - doneButton: "select-translations-panel-done-button", + doneButtonPrimary: "select-translations-panel-done-button-primary", + doneButtonSecondary: "select-translations-panel-done-button-secondary", fromLabel: "select-translations-panel-from-label", fromMenuList: "select-translations-panel-from", fromMenuPopup: "select-translations-panel-from-menupopup", header: "select-translations-panel-header", + initFailureContent: "select-translations-panel-init-failure-content", + initFailureMessageBar: + "select-translations-panel-init-failure-message-bar", + mainContent: "select-translations-panel-main-content", + settingsButton: "select-translations-panel-settings-button", textArea: "select-translations-panel-text-area", toLabel: "select-translations-panel-to-label", toMenuList: "select-translations-panel-to", toMenuPopup: "select-translations-panel-to-menupopup", + translateButton: "select-translations-panel-translate-button", translateFullPageButton: "select-translations-panel-translate-full-page-button", + translationFailureMessageBar: + "select-translations-panel-translation-failure-message-bar", + tryAgainButton: "select-translations-panel-try-again-button", + tryAnotherSourceMenuList: + "select-translations-panel-try-another-language", + tryAnotherSourceMenuPopup: + "select-translations-panel-try-another-language-menupopup", + unsupportedLanguageContent: + "select-translations-panel-unsupported-language-content", + unsupportedLanguageMessageBar: + "select-translations-panel-unsupported-language-message-bar", }); } @@ -213,12 +249,32 @@ var SelectTranslationsPanel = new (class { // Since none of the detected languages were supported, check to see if the // document has a specified language tag that is supported. - const actor = TranslationsParent.getTranslationsActor( - gBrowser.selectedBrowser - ); - const detectedLanguages = actor.languageState.detectedLanguages; - if (detectedLanguages?.isDocLangTagSupported) { - return detectedLanguages.docLangTag; + try { + const actor = TranslationsParent.getTranslationsActor( + gBrowser.selectedBrowser + ); + const detectedLanguages = actor.languageState.detectedLanguages; + if (detectedLanguages?.isDocLangTagSupported) { + return detectedLanguages.docLangTag; + } + } catch (error) { + // Failed to retrieve the Translations actor to detect the document language. + // This is most likely due to attempting to retrieve the actor in a page that + // is restricted for Full Page Translations, such as a PDF or reader mode, but + // Select Translations is often still available, so we can safely continue to + // the final return fallback. + if ( + !TranslationsParent.isFullPageTranslationsRestrictedForPage(gBrowser) + ) { + // If we failed to retrieve the TranslationsParent actor on a non-restricted page, + // we should warn about this, because it is unexpected. The SelectTranslationsPanel + // itself will display an error state if this causes a failure, and this will help + // diagnose the issue if this scenario should ever occur. + this.console?.warn( + "Failed to retrieve the TranslationsParent actor on a page where Full Page Translations is not restricted." + ); + this.console?.error(error); + } } // No supported language was found, so return the top detected language @@ -233,19 +289,27 @@ var SelectTranslationsPanel = new (class { * @param {string} textToTranslate - The text for which the language detection and target language retrieval are performed. * @returns {Promise<{fromLang?: string, toLang?: string}>} - An object containing the language pair for the translation. * The `fromLang` property is omitted if it is a language that is not currently supported by Firefox Translations. - * The `toLang` property is omitted if it is the same as `fromLang`. */ async getLangPairPromise(textToTranslate) { + if ( + TranslationsParent.isInAutomation() && + !TranslationsParent.isTranslationsEngineMocked() + ) { + // If we are in automation, and the Translations Engine is NOT mocked, then that means + // we are in a test case in which we are not explicitly testing Select Translations, + // and the code to get the supported languages below will not be available. However, + // we still need to ensure that the translate-selection menuitem in the context menu + // is compatible with all code in other tests, so we will return "en" for the purpose + // of being able to localize and display the context-menu item in other test cases. + return { toLang: "en" }; + } + const [fromLang, toLang] = await Promise.all([ SelectTranslationsPanel.getTopSupportedDetectedLanguage(textToTranslate), TranslationsParent.getTopPreferredSupportedToLang(), ]); - return { - fromLang, - // If the fromLang and toLang are the same, discard the toLang. - toLang: fromLang === toLang ? undefined : toLang, - }; + return { fromLang, toLang }; } /** @@ -300,11 +364,49 @@ var SelectTranslationsPanel = new (class { */ async #initializeLanguageMenuLists(langPairPromise) { const { fromLang, toLang } = await langPairPromise; - const { fromMenuList, toMenuList } = this.elements; + const { + fromMenuList, + fromMenuPopup, + toMenuList, + toMenuPopup, + tryAnotherSourceMenuList, + } = this.elements; + await Promise.all([ this.#initializeLanguageMenuList(fromLang, fromMenuList), this.#initializeLanguageMenuList(toLang, toMenuList), + this.#initializeLanguageMenuList(null, tryAnotherSourceMenuList), ]); + + this.#maybeTranslateOnEvents(["keypress"], fromMenuList); + this.#maybeTranslateOnEvents(["keypress"], toMenuList); + + this.#maybeTranslateOnEvents(["popuphidden"], fromMenuPopup); + this.#maybeTranslateOnEvents(["popuphidden"], toMenuPopup); + } + + /** + * Initializes event listeners on the panel class the first time + * this function is called, and is a no-op on subsequent calls. + */ + #initializeEventListeners() { + if (this.#eventListenersInitialized) { + // Event listeners have already been initialized, do nothing. + return; + } + + const { panel, fromMenuList, toMenuList, tryAnotherSourceMenuList } = + this.elements; + + panel.addEventListener("popupshown", this); + panel.addEventListener("popuphidden", this); + + panel.addEventListener("command", this); + fromMenuList.addEventListener("command", this); + toMenuList.addEventListener("command", this); + tryAnotherSourceMenuList.addEventListener("command", this); + + this.#eventListenersInitialized = true; } /** @@ -320,20 +422,60 @@ var SelectTranslationsPanel = new (class { */ async open(event, screenX, screenY, sourceText, langPairPromise) { if (this.#isOpen()) { + await this.#forceReopen( + event, + screenX, + screenY, + sourceText, + langPairPromise + ); return; } - this.#registerSourceText(sourceText); - await this.#ensureLangListsBuilt(); + try { + this.#isFullPageTranslationsRestrictedForPage = + TranslationsParent.isFullPageTranslationsRestrictedForPage(gBrowser); + this.#initializeEventListeners(); + await this.#ensureLangListsBuilt(); + await Promise.all([ + this.#cachePlaceholderText(), + this.#initializeLanguageMenuLists(langPairPromise), + this.#registerSourceText(sourceText, langPairPromise), + ]); + this.#maybeRequestTranslation(); + } catch (error) { + this.console?.error(error); + this.#changeStateToInitFailure( + event, + screenX, + screenY, + sourceText, + langPairPromise + ); + } - await Promise.all([ - this.#cachePlaceholderText(), - this.#initializeLanguageMenuLists(langPairPromise), - ]); + this.#openPopup(event, screenX, screenY); + } - this.#displayIdlePlaceholder(); - this.#maybeRequestTranslation(); - await this.#openPopup(event, screenX, screenY); + /** + * Forces the panel to close and reopen at the same location. + * + * This should never be called in the regular flow of events, but is good to have in case + * the panel somehow gets into an invalid state. + * + * @param {Event} event - The triggering event for opening the panel. + * @param {number} screenX - The x-axis location of the screen at which to open the popup. + * @param {number} screenY - The y-axis location of the screen at which to open the popup. + * @param {string} sourceText - The text to translate. + * @param {Promise} langPairPromise - Promise resolving to language pair data for initializing dropdowns. + * + * @returns {Promise<void>} + */ + async #forceReopen(event, screenX, screenY, sourceText, langPairPromise) { + this.console?.warn("The SelectTranslationsPanel was forced to reopen."); + this.close(); + this.#changeStateToClosed(); + await this.open(event, screenX, screenY, sourceText, langPairPromise); } /** @@ -343,9 +485,7 @@ var SelectTranslationsPanel = new (class { * @param {number} screenX - The x-axis location of the screen at which to open the popup. * @param {number} screenY - The y-axis location of the screen at which to open the popup. */ - async #openPopup(event, screenX, screenY) { - await window.ensureCustomElements("moz-button-group"); - + #openPopup(event, screenX, screenY) { this.console?.log("Showing SelectTranslationsPanel"); const { panel } = this.elements; panel.openPopupAtScreen(screenX, screenY, /* isContextMenu */ false, event); @@ -356,20 +496,38 @@ var SelectTranslationsPanel = new (class { * on the length of the text. * * @param {string} sourceText - The text to translate. + * @param {Promise<{fromLang?: string, toLang?: string}>} langPairPromise * * @returns {Promise<void>} */ - #registerSourceText(sourceText) { + async #registerSourceText(sourceText, langPairPromise) { const { textArea } = this.elements; - this.#changeStateTo("idle", /* retainEntries */ false, { - sourceText, - }); + const { fromLang, toLang } = await langPairPromise; + const isFromLangSupported = await TranslationsParent.isSupportedAsFromLang( + fromLang + ); + + if (isFromLangSupported) { + this.#changeStateTo("idle", /* retainEntries */ false, { + sourceText, + fromLanguage: fromLang, + toLanguage: toLang, + }); + } else { + this.#changeStateTo("unsupported", /* retainEntries */ false, { + sourceText, + detectedLanguage: fromLang, + toLanguage: toLang, + }); + } if (sourceText.length < SelectTranslationsPanel.textLengthThreshold) { textArea.style.height = SelectTranslationsPanel.shortTextHeight; } else { textArea.style.height = SelectTranslationsPanel.longTextHeight; } + + this.#maybeTranslateOnEvents(["focus"], textArea); } /** @@ -385,24 +543,122 @@ var SelectTranslationsPanel = new (class { } /** - * Handles events when a popup is shown within the panel, including showing - * the panel itself. + * Opens the settings menu popup at the settings button gear-icon. + */ + #openSettingsPopup() { + const { settingsButton } = this.elements; + const popup = settingsButton.ownerDocument.getElementById( + "select-translations-panel-settings-menupopup" + ); + popup.openPopup(settingsButton, "after_start"); + } + + /** + * Opens the "About translation in Firefox" Mozilla support page in a new tab. + */ + onAboutTranslations() { + this.close(); + const window = + gBrowser.selectedBrowser.browsingContext.top.embedderElement.ownerGlobal; + window.openTrustedLinkIn( + "https://support.mozilla.org/kb/website-translation", + "tab", + { + forceForeground: true, + triggeringPrincipal: + Services.scriptSecurityManager.getSystemPrincipal(), + } + ); + } + + /** + * Opens the Translations section of about:preferences in a new tab. + */ + openTranslationsSettingsPage() { + this.close(); + const window = + gBrowser.selectedBrowser.browsingContext.top.embedderElement.ownerGlobal; + window.openTrustedLinkIn("about:preferences#general-translations", "tab"); + } + + /** + * Handles events when a command event is triggered within the panel. * - * @param {Event} event - The event that triggered the popup to show. + * @param {Element} target - The event target */ - handlePanelPopupShownEvent(event) { - const { panel, fromMenuPopup, toMenuPopup } = this.elements; - switch (event.target.id) { - case panel.id: { - this.#updatePanelUIFromState(); + #handleCommandEvent(target) { + const { + cancelButton, + copyButton, + doneButtonPrimary, + doneButtonSecondary, + fromMenuList, + fromMenuPopup, + settingsButton, + toMenuList, + toMenuPopup, + translateButton, + translateFullPageButton, + tryAgainButton, + tryAnotherSourceMenuList, + tryAnotherSourceMenuPopup, + } = this.elements; + switch (target.id) { + case cancelButton.id: + case doneButtonPrimary.id: + case doneButtonSecondary.id: { + this.close(); break; } + case copyButton.id: { + this.onClickCopyButton(); + break; + } + case fromMenuList.id: case fromMenuPopup.id: { - this.#maybeTranslateOnEvents(["popuphidden"], fromMenuPopup); + this.onChangeFromLanguage(); + break; + } + case settingsButton.id: { + this.#openSettingsPopup(); break; } + case toMenuList.id: case toMenuPopup.id: { - this.#maybeTranslateOnEvents(["popuphidden"], toMenuPopup); + this.onChangeToLanguage(); + break; + } + case translateButton.id: { + this.onClickTranslateButton(); + break; + } + case translateFullPageButton.id: { + this.onClickTranslateFullPageButton(); + break; + } + case tryAgainButton.id: { + this.onClickTryAgainButton(); + break; + } + case tryAnotherSourceMenuList.id: + case tryAnotherSourceMenuPopup.id: { + this.onChangeTryAnotherSourceLanguage(); + break; + } + } + } + + /** + * Handles events when a popup is shown within the panel, including showing + * the panel itself. + * + * @param {Element} target - The event target + */ + #handlePopupShownEvent(target) { + const { panel } = this.elements; + switch (target.id) { + case panel.id: { + this.#updatePanelUIFromState(); break; } } @@ -412,13 +668,44 @@ var SelectTranslationsPanel = new (class { * Handles events when a popup is closed within the panel, including closing * the panel itself. * - * @param {Event} event - The event that triggered the popup to close. + * @param {Element} target - The event target */ - handlePanelPopupHiddenEvent(event) { + #handlePopupHiddenEvent(target) { const { panel } = this.elements; - switch (event.target.id) { + switch (target.id) { case panel.id: { this.#changeStateToClosed(); + this.#removeActiveTranslationListeners(); + break; + } + } + } + + /** + * Handles events in the SelectTranslationsPanel. + * + * @param {Event} event - The event to handle. + */ + handleEvent(event) { + let target = event.target; + + // If a menuitem within a menulist is the target, those don't have ids, + // so we want to traverse until we get to a parent element with an id. + while (!target.id && target.parentElement) { + target = target.parentElement; + } + + switch (event.type) { + case "command": { + this.#handleCommandEvent(target); + break; + } + case "popupshown": { + this.#handlePopupShownEvent(target); + break; + } + case "popuphidden": { + this.#handlePopupHiddenEvent(target); break; } } @@ -428,18 +715,138 @@ var SelectTranslationsPanel = new (class { * Handles events when the panels select from-language is changed. */ onChangeFromLanguage() { - const { fromMenuList, toMenuList } = this.elements; - this.#maybeTranslateOnEvents(["blur", "keypress"], fromMenuList); - this.#maybeStealLanguageFrom(toMenuList); + this.#updateConditionalUIEnabledState(); } /** * Handles events when the panels select to-language is changed. */ onChangeToLanguage() { - const { toMenuList, fromMenuList } = this.elements; - this.#maybeTranslateOnEvents(["blur", "keypress"], toMenuList); - this.#maybeStealLanguageFrom(fromMenuList); + this.#updateConditionalUIEnabledState(); + } + + /** + * Handles events when the panel's try-another-source language is changed. + */ + onChangeTryAnotherSourceLanguage() { + const { tryAnotherSourceMenuList, translateButton } = this.elements; + if (tryAnotherSourceMenuList.value) { + translateButton.disabled = false; + } + } + + /** + * Handles events when the panel's copy button is clicked. + */ + onClickCopyButton() { + try { + ClipboardHelper.copyString(this.getTranslatedText()); + } catch (error) { + this.console?.error(error); + return; + } + + this.#checkCopyButton(); + } + + /** + * Handles events when the panel's translate button is clicked. + */ + onClickTranslateButton() { + const { fromMenuList, tryAnotherSourceMenuList } = this.elements; + fromMenuList.value = tryAnotherSourceMenuList.value; + this.#maybeRequestTranslation(); + } + + /** + * Handles events when the panel's translate-full-page button is clicked. + */ + onClickTranslateFullPageButton() { + const { panel } = this.elements; + const { fromLanguage, toLanguage } = this.#getSelectedLanguagePair(); + + try { + const actor = TranslationsParent.getTranslationsActor( + gBrowser.selectedBrowser + ); + panel.addEventListener( + "popuphidden", + () => + actor.translate( + fromLanguage, + toLanguage, + false // reportAsAutoTranslate + ), + { once: true } + ); + } catch (error) { + // This situation would only occur if the translate-full-page button as invoked + // while Translations actor is not available. the logic within this class explicitly + // hides the button in this case, and this should not be possible under normal conditions, + // but if this button were to somehow still be invoked, the best thing we can do here is log + // an error to the console because the FullPageTranslationsPanel assumes that the actor is available. + this.console?.error(error); + } + + this.close(); + } + + /** + * Handles events when the panel's try-again button is clicked. + */ + onClickTryAgainButton() { + switch (this.phase()) { + case "translation-failure": { + // If the translation failed, we just need to try translating again. + this.#maybeRequestTranslation(); + break; + } + case "init-failure": { + // If the initialization failed, we need to close the panel and try reopening it + // which will attempt to initialize everything again after failure. + const { panel } = this.elements; + const { event, screenX, screenY, sourceText, langPairPromise } = + this.#translationState; + + panel.addEventListener( + "popuphidden", + () => this.open(event, screenX, screenY, sourceText, langPairPromise), + { once: true } + ); + + this.close(); + break; + } + default: { + this.console?.error( + `Unexpected state "${this.phase()}" on try-again button click.` + ); + } + } + } + + /** + * Changes the copy button's visual icon to checked, and its localized text to "Copied". + */ + #checkCopyButton() { + const { copyButton } = this.elements; + copyButton.classList.add("copied"); + document.l10n.setAttributes( + copyButton, + "select-translations-panel-copy-button-copied" + ); + } + + /** + * Changes the copy button's visual icon to unchecked, and its localized text to "Copy". + */ + #uncheckCopyButton() { + const { copyButton } = this.elements; + copyButton.classList.remove("copied"); + document.l10n.setAttributes( + copyButton, + "select-translations-panel-copy-button" + ); } /** @@ -455,21 +862,6 @@ var SelectTranslationsPanel = new (class { } /** - * Deselects the language from the target menu list if both menu lists - * have the same language selected, simulating the effect of one menu - * list stealing the selected language value from the other. - * - * @param {Element} menuList - The target menu list element to update. - */ - async #maybeStealLanguageFrom(menuList) { - const { fromLanguage, toLanguage } = this.#getSelectedLanguagePair(); - if (fromLanguage === toLanguage) { - await this.#deselectLanguage(menuList); - this.#maybeFocusMenuList(menuList); - } - } - - /** * Focuses on the given menu list if provided and empty, or defaults to focusing one * of the from-menu or to-menu lists if either is empty. * @@ -525,21 +917,6 @@ var SelectTranslationsPanel = new (class { } /** - * Checks if the translator's language configuration matches the given language pair. - * - * @param {string} fromLanguage - The from-language to compare. - * @param {string} toLanguage - The to-language to compare. - * - * @returns {boolean} - True if the translator's languages match the given pair, otherwise false. - */ - #translatorMatchesLangPair(fromLanguage, toLanguage) { - return ( - this.#translator?.fromLanguage === fromLanguage && - this.#translator?.toLanguage === toLanguage - ); - } - - /** * Retrieves the currently selected language pair from the menu lists. * * @returns {{fromLanguage: string, toLanguage: string}} An object containing the selected languages. @@ -575,9 +952,9 @@ var SelectTranslationsPanel = new (class { /** * Retrieves the current phase of the translation state. * - * @returns {SelectTranslationsPanelState} + * @returns {string} */ - #phase() { + phase() { return this.#translationState.phase; } @@ -585,14 +962,14 @@ var SelectTranslationsPanel = new (class { * @returns {boolean} True if the panel is open, otherwise false. */ #isOpen() { - return this.#phase() !== "closed"; + return this.phase() !== "closed"; } /** * @returns {boolean} True if the panel is closed, otherwise false. */ #isClosed() { - return this.#phase() === "closed"; + return this.phase() === "closed"; } /** @@ -604,17 +981,16 @@ var SelectTranslationsPanel = new (class { * @throws {Error} If an invalid phase is specified. */ #changeStateTo(phase, retainEntries, data = null) { - const { textArea } = this.elements; switch (phase) { - case "translating": { - textArea.classList.add("translating"); - break; - } case "closed": case "idle": + case "init-failure": + case "translation-failure": case "translatable": - case "translated": { - textArea.classList.remove("translating"); + case "translating": + case "translated": + case "unsupported": { + // Phase is valid, continue on. break; } default: { @@ -622,7 +998,7 @@ var SelectTranslationsPanel = new (class { } } - const previousPhase = this.#phase(); + const previousPhase = this.phase(); if (data && retainEntries) { // Change the phase and apply new entries from data, but retain non-overwritten entries from previous state. this.#translationState = { ...this.#translationState, phase, ...data }; @@ -637,19 +1013,26 @@ var SelectTranslationsPanel = new (class { this.#translationState = { phase }; } - if (previousPhase === this.#phase()) { + if (previousPhase === this.phase()) { // Do not continue on to update the UI because the phase didn't change. return; } - const { fromLanguage, toLanguage } = this.#translationState; + const { fromLanguage, toLanguage, detectedLanguage } = + this.#translationState; + const sourceLanguage = fromLanguage ? fromLanguage : detectedLanguage; this.console?.debug( - `SelectTranslationsPanel (${fromLanguage ? fromLanguage : "??"}-${ + `SelectTranslationsPanel (${sourceLanguage ? sourceLanguage : "??"}-${ toLanguage ? toLanguage : "??" }) state change (${previousPhase} => ${phase})` ); this.#updatePanelUIFromState(); + document.dispatchEvent( + new CustomEvent("SelectTranslationsPanelStateChanged", { + detail: { phase }, + }) + ); } /** @@ -665,7 +1048,7 @@ var SelectTranslationsPanel = new (class { * @throws {Error} If the current state is not "translatable". */ #changeStateToTranslating() { - const phase = this.#phase(); + const phase = this.phase(); if (phase !== "translatable") { throw new Error(`Invalid state change (${phase} => translating)`); } @@ -678,7 +1061,7 @@ var SelectTranslationsPanel = new (class { * @throws {Error} If the current state is not "translating". */ #changeStateToTranslated(translatedText) { - const phase = this.#phase(); + const phase = this.phase(); if (phase !== "translating") { throw new Error(`Invalid state change (${phase} => translated)`); } @@ -688,45 +1071,176 @@ var SelectTranslationsPanel = new (class { } /** - * Transitions the phase of the state based on the given language pair. + * Changes the phase to "init-failure". + */ + #changeStateToInitFailure( + event, + screenX, + screenY, + sourceText, + langPairPromise + ) { + this.#changeStateTo("init-failure", /* retainEntries */ true, { + event, + screenX, + screenY, + sourceText, + langPairPromise, + }); + } + + /** + * Changes the phase from "translating" to "translation-failure". + */ + #changeStateToTranslationFailure() { + const phase = this.phase(); + if (phase !== "translating") { + this.console?.error( + `Invalid state change (${phase} => translation-failure)` + ); + } + this.#changeStateTo("translation-failure", /* retainEntries */ true); + } + + /** + * Transitions the phase to "translatable" if the proper conditions are met, + * otherwise retains the same phase as before. * * @param {string} fromLanguage - The BCP-47 from-language tag. * @param {string} toLanguage - The BCP-47 to-language tag. - * - * @returns {SelectTranslationsPanelState} The new phase of the translation state. */ - #changeStateByLanguagePair(fromLanguage, toLanguage) { + #maybeChangeStateToTranslatable(fromLanguage, toLanguage) { const { - phase: previousPhase, fromLanguage: previousFromLanguage, toLanguage: previousToLanguage, } = this.#translationState; - let nextPhase = "translatable"; + const langSelectionChanged = () => + previousFromLanguage !== fromLanguage || + previousToLanguage !== toLanguage; + + const shouldTranslateEvenIfLangSelectionHasNotChanged = () => { + const phase = this.phase(); + return ( + // The panel has just opened, and this is the initial translation. + phase === "idle" || + // The previous translation failed and we are about to try again. + phase === "translation-failure" + ); + }; if ( - // No from-language is selected, so we cannot translate. - !fromLanguage || - // No to-language is selected, so we cannot translate. - !toLanguage || - // The same language has been selected, so we cannot translate. - fromLanguage === toLanguage - ) { - nextPhase = "idle"; - } else if ( - // The languages have not changed, so there is nothing to do. - previousFromLanguage === fromLanguage && - previousToLanguage === toLanguage + // A valid from-language is actively selected. + fromLanguage && + // A valid to-language is actively selected. + toLanguage && + // The language selection has changed, requiring a new translation. + (langSelectionChanged() || + // We should try to translate even if the language selection has not changed. + shouldTranslateEvenIfLangSelectionHasNotChanged()) ) { - nextPhase = previousPhase; + this.#changeStateTo("translatable", /* retainEntries */ true, { + fromLanguage, + toLanguage, + }); } + } - this.#changeStateTo(nextPhase, /* retainEntries */ true, { - fromLanguage, - toLanguage, - }); + /** + * Handles changes to the copy button based on the current translation state. + * + * @param {string} phase - The current phase of the translation state. + */ + #handleCopyButtonChanges(phase) { + switch (phase) { + case "closed": + case "translation-failure": + case "translated": { + this.#uncheckCopyButton(); + break; + } + case "idle": + case "init-failure": + case "translatable": + case "translating": + case "unsupported": { + // Do nothing. + break; + } + default: { + throw new Error(`Invalid state change to '${phase}'`); + } + } + } - return nextPhase; + /** + * Handles changes to the text area's background image based on the current translation state. + * + * @param {string} phase - The current phase of the translation state. + */ + #handleTextAreaBackgroundChanges(phase) { + const { textArea } = this.elements; + switch (phase) { + case "translating": { + textArea.classList.add("translating"); + break; + } + case "closed": + case "idle": + case "init-failure": + case "translation-failure": + case "translatable": + case "translated": + case "unsupported": { + textArea.classList.remove("translating"); + break; + } + default: { + throw new Error(`Invalid state change to '${phase}'`); + } + } + } + + /** + * Handles changes to the primary UI components based on the current translation state. + * + * @param {string} phase - The current phase of the translation state. + */ + #handlePrimaryUIChanges(phase) { + switch (phase) { + case "closed": + case "idle": { + this.#displayIdlePlaceholder(); + break; + } + case "init-failure": { + this.#displayInitFailureMessage(); + break; + } + case "translation-failure": { + this.#displayTranslationFailureMessage(); + break; + } + case "translatable": { + // Do nothing. + break; + } + case "translating": { + this.#displayTranslatingPlaceholder(); + break; + } + case "translated": { + this.#displayTranslatedText(); + break; + } + case "unsupported": { + this.#displayUnsupportedLanguageMessage(); + break; + } + default: { + throw new Error(`Invalid state change to '${phase}'`); + } + } } /** @@ -745,9 +1259,7 @@ var SelectTranslationsPanel = new (class { // Continue only if the current translationId matches. translationId === this.#translationId && // Continue only if the given language pair is still the actively selected pair. - this.#isSelectedLangPair(fromLanguage, toLanguage) && - // Continue only if the given language pair matches the current translator. - this.#translatorMatchesLangPair(fromLanguage, toLanguage) + this.#isSelectedLangPair(fromLanguage, toLanguage) ); } @@ -755,6 +1267,8 @@ var SelectTranslationsPanel = new (class { * Displays the placeholder text for the translation state's "idle" phase. */ #displayIdlePlaceholder() { + this.#showMainContent(); + const { textArea } = SelectTranslationsPanel.elements; textArea.value = this.#idlePlaceholderText; this.#updateTextDirection(); @@ -766,6 +1280,8 @@ var SelectTranslationsPanel = new (class { * Displays the placeholder text for the translation state's "translating" phase. */ #displayTranslatingPlaceholder() { + this.#showMainContent(); + const { textArea } = SelectTranslationsPanel.elements; textArea.value = this.#translatingPlaceholderText; this.#updateTextDirection(); @@ -777,6 +1293,8 @@ var SelectTranslationsPanel = new (class { * Displays the translated text for the translation state's "translated" phase. */ #displayTranslatedText() { + this.#showMainContent(); + const { toLanguage } = this.#getSelectedLanguagePair(); const { textArea } = SelectTranslationsPanel.elements; textArea.value = this.getTranslatedText(); @@ -786,38 +1304,238 @@ var SelectTranslationsPanel = new (class { } /** + * Sets attributes on panel elements that are specifically relevant + * to the SelectTranslationsPanel's state. + * + * @param {object} options - Options of which attributes to set. + * @param {Record<string, Element[]>} options.makeHidden - Make these elements hidden. + * @param {Record<string, Element[]>} options.makeVisible - Make these elements visible. + */ + #setPanelElementAttributes({ makeHidden = [], makeVisible = [] }) { + for (const element of makeHidden) { + element.hidden = true; + } + for (const element of makeVisible) { + element.hidden = false; + } + } + + /** * Enables or disables UI components that are conditional on a valid language pair being selected. */ #updateConditionalUIEnabledState() { const { fromLanguage, toLanguage } = this.#getSelectedLanguagePair(); - const { copyButton, translateFullPageButton, textArea } = this.elements; + const { + copyButton, + textArea, + translateButton, + translateFullPageButton, + tryAnotherSourceMenuList, + } = this.elements; const invalidLangPairSelected = !fromLanguage || !toLanguage; - const isTranslating = this.#phase() === "translating"; + const isTranslating = this.phase() === "translating"; textArea.disabled = invalidLangPairSelected; - translateFullPageButton.disabled = invalidLangPairSelected; copyButton.disabled = invalidLangPairSelected || isTranslating; + translateButton.disabled = !tryAnotherSourceMenuList.value; + translateFullPageButton.disabled = + invalidLangPairSelected || + fromLanguage === toLanguage || + this.#isFullPageTranslationsRestrictedForPage; } /** * Updates the panel UI based on the current phase of the translation state. */ #updatePanelUIFromState() { - switch (this.#phase()) { - case "idle": { - this.#displayIdlePlaceholder(); - break; - } - case "translating": { - this.#displayTranslatingPlaceholder(); - break; - } - case "translated": { - this.#displayTranslatedText(); - break; + const phase = this.phase(); + this.#handlePrimaryUIChanges(phase); + this.#handleCopyButtonChanges(phase); + this.#handleTextAreaBackgroundChanges(phase); + } + + /** + * Shows the panel's main-content group of elements. + */ + #showMainContent() { + const { + cancelButton, + copyButton, + doneButtonPrimary, + doneButtonSecondary, + initFailureContent, + mainContent, + unsupportedLanguageContent, + textArea, + translateButton, + translateFullPageButton, + translationFailureMessageBar, + tryAgainButton, + } = this.elements; + this.#setPanelElementAttributes({ + makeHidden: [ + cancelButton, + doneButtonSecondary, + initFailureContent, + translateButton, + translationFailureMessageBar, + tryAgainButton, + unsupportedLanguageContent, + ...(this.#isFullPageTranslationsRestrictedForPage + ? [translateFullPageButton] + : []), + ], + makeVisible: [ + mainContent, + copyButton, + doneButtonPrimary, + textArea, + ...(this.#isFullPageTranslationsRestrictedForPage + ? [] + : [translateFullPageButton]), + ], + }); + } + + /** + * Shows the panel's unsupported-language group of elements. + */ + #showUnsupportedLanguageContent() { + const { + cancelButton, + copyButton, + doneButtonPrimary, + doneButtonSecondary, + initFailureContent, + mainContent, + unsupportedLanguageContent, + translateButton, + translateFullPageButton, + tryAgainButton, + } = this.elements; + this.#setPanelElementAttributes({ + makeHidden: [ + cancelButton, + doneButtonPrimary, + copyButton, + initFailureContent, + mainContent, + translateFullPageButton, + tryAgainButton, + ], + makeVisible: [ + doneButtonSecondary, + translateButton, + unsupportedLanguageContent, + ], + }); + } + + /** + * Displays the panel content for when the language dropdowns fail to populate. + */ + #displayInitFailureMessage() { + const { + cancelButton, + copyButton, + doneButtonPrimary, + doneButtonSecondary, + initFailureContent, + mainContent, + unsupportedLanguageContent, + translateButton, + translateFullPageButton, + tryAgainButton, + } = this.elements; + this.#setPanelElementAttributes({ + makeHidden: [ + doneButtonPrimary, + doneButtonSecondary, + copyButton, + mainContent, + translateButton, + translateFullPageButton, + unsupportedLanguageContent, + ], + makeVisible: [initFailureContent, cancelButton, tryAgainButton], + }); + tryAgainButton.focus({ focusVisible: true }); + } + + /** + * Displays the panel content for when a translation fails to complete. + */ + #displayTranslationFailureMessage() { + const { + cancelButton, + copyButton, + doneButtonPrimary, + doneButtonSecondary, + initFailureContent, + mainContent, + textArea, + translateButton, + translateFullPageButton, + translationFailureMessageBar, + tryAgainButton, + unsupportedLanguageContent, + } = this.elements; + this.#setPanelElementAttributes({ + makeHidden: [ + doneButtonPrimary, + doneButtonSecondary, + copyButton, + initFailureContent, + translateButton, + translateFullPageButton, + textArea, + unsupportedLanguageContent, + ], + makeVisible: [ + cancelButton, + mainContent, + translationFailureMessageBar, + tryAgainButton, + ], + }); + tryAgainButton.focus({ focusVisible: true }); + } + + /** + * Displays the panel's unsupported language message bar, showing + * the panel's unsupported-language elements. + */ + #displayUnsupportedLanguageMessage() { + const { detectedLanguage } = this.#translationState; + const { unsupportedLanguageMessageBar, tryAnotherSourceMenuList } = + this.elements; + const displayNames = new Services.intl.DisplayNames(undefined, { + type: "language", + }); + try { + const language = displayNames.of(detectedLanguage); + if (language) { + document.l10n.setAttributes( + unsupportedLanguageMessageBar, + "select-translations-panel-unsupported-language-message-known", + { language } + ); + } else { + // Will be immediately caught. + throw new Error(); } + } catch { + // Either displayNames.of() threw, or we threw due to no display name found. + // In either case, localize the message for an unknown language. + document.l10n.setAttributes( + unsupportedLanguageMessageBar, + "select-translations-panel-unsupported-language-message-unknown" + ); } + this.#updateConditionalUIEnabledState(); + this.#showUnsupportedLanguageContent(); + this.#maybeFocusMenuList(tryAnotherSourceMenuList); } /** @@ -868,25 +1586,16 @@ var SelectTranslationsPanel = new (class { * * @returns {Promise<Translator>} A promise that resolves to a `Translator` instance for the given language pair. */ - async #getOrCreateTranslator(fromLanguage, toLanguage) { - if (this.#translatorMatchesLangPair(fromLanguage, toLanguage)) { - return this.#translator; - } - + async #createTranslator(fromLanguage, toLanguage) { this.console?.log( `Creating new Translator (${fromLanguage}-${toLanguage})` ); - if (this.#translator) { - this.#translator.destroy(); - this.#translator = null; - } - this.#translator = await Translator.create( - fromLanguage, - toLanguage, - this.#requestTranslationsPort - ); - return this.#translator; + const translator = await Translator.create(fromLanguage, toLanguage, { + allowSameLanguage: true, + requestTranslationsPort: this.#requestTranslationsPort, + }); + return translator; } /** @@ -897,14 +1606,16 @@ var SelectTranslationsPanel = new (class { if (this.#isClosed()) { return; } + const { fromLanguage, toLanguage } = this.#getSelectedLanguagePair(); - const nextState = this.#changeStateByLanguagePair(fromLanguage, toLanguage); - if (nextState !== "translatable") { + this.#maybeChangeStateToTranslatable(fromLanguage, toLanguage); + + if (this.phase() !== "translatable") { return; } const translationId = ++this.#translationId; - this.#getOrCreateTranslator(fromLanguage, toLanguage) + this.#createTranslator(fromLanguage, toLanguage) .then(translator => { if ( this.#shouldContinueTranslation( @@ -928,13 +1639,12 @@ var SelectTranslationsPanel = new (class { ) ) { this.#changeStateToTranslated(translatedText); - } else if (this.#isOpen()) { - this.#changeStateTo("idle", /* retainEntires */ false, { - sourceText: this.getSourceText(), - }); } }) - .catch(error => this.console?.error(error)); + .catch(error => { + this.console?.error(error); + this.#changeStateToTranslationFailure(); + }); } /** @@ -952,11 +1662,10 @@ var SelectTranslationsPanel = new (class { for (const eventType of eventTypes) { let callback; switch (eventType) { - case "blur": + case "focus": case "popuphidden": { callback = () => { this.#maybeRequestTranslation(); - this.#removeTranslationListeners(target); }; break; } @@ -965,7 +1674,6 @@ var SelectTranslationsPanel = new (class { if (event.key === "Enter") { this.#maybeRequestTranslation(); } - this.#removeTranslationListeners(target); }; break; } @@ -975,21 +1683,39 @@ var SelectTranslationsPanel = new (class { ); } } - target.addEventListener(eventType, callback, { once: true }); + target.addEventListener(eventType, callback); target.translationListenerCallbacks.push({ eventType, callback }); } } } /** + * Removes all translation event listeners from any panel elements that would have one. + */ + #removeActiveTranslationListeners() { + const { fromMenuList, fromMenuPopup, textArea, toMenuList, toMenuPopup } = + SelectTranslationsPanel.elements; + this.#removeTranslationListenersFrom(fromMenuList); + this.#removeTranslationListenersFrom(fromMenuPopup); + this.#removeTranslationListenersFrom(textArea); + this.#removeTranslationListenersFrom(toMenuList); + this.#removeTranslationListenersFrom(toMenuPopup); + } + + /** * Removes all translation event listeners from the target element. * * @param {Element} target - The element from which event listeners are to be removed. */ - #removeTranslationListeners(target) { + #removeTranslationListenersFrom(target) { + if (!target.translationListenerCallbacks) { + return; + } + for (const { eventType, callback } of target.translationListenerCallbacks) { target.removeEventListener(eventType, callback); } + target.translationListenerCallbacks = []; } })(); diff --git a/browser/components/translations/tests/browser/browser.toml b/browser/components/translations/tests/browser/browser.toml index 472ae28866..78ffd04c09 100644 --- a/browser/components/translations/tests/browser/browser.toml +++ b/browser/components/translations/tests/browser/browser.toml @@ -105,6 +105,8 @@ skip-if = ["os == 'linux' && !debug"] # Bug 1863227 ["browser_translations_full_page_telemetry_translation_request.js"] +["browser_translations_select_context_menu_engine_unsupported.js"] + ["browser_translations_select_context_menu_feature_disabled.js"] ["browser_translations_select_context_menu_with_full_page_translations_active.js"] @@ -115,11 +117,19 @@ skip-if = ["os == 'linux' && !debug"] # Bug 1863227 ["browser_translations_select_context_menu_with_text_selected.js"] +["browser_translations_select_panel_close_on_new_tab.js"] + +["browser_translations_select_panel_copy_button.js"] + ["browser_translations_select_panel_engine_cache.js"] ["browser_translations_select_panel_fallback_to_doc_language.js"] -["browser_translations_select_panel_open_to_idle_state.js"] +["browser_translations_select_panel_init_failure.js"] + +["browser_translations_select_panel_pdf.js"] + +["browser_translations_select_panel_reader_mode.js"] ["browser_translations_select_panel_retranslate_on_change_language_directly.js"] @@ -133,6 +143,10 @@ skip-if = ["os == 'linux' && !debug"] # Bug 1863227 ["browser_translations_select_panel_select_same_from_and_to_languages_from_dropdown_menu.js"] +["browser_translations_select_panel_settings_menu.js"] + +["browser_translations_select_panel_translate_full_page_button.js"] + ["browser_translations_select_panel_translate_on_change_language_directly.js"] ["browser_translations_select_panel_translate_on_change_language_from_dropdown_menu.js"] @@ -142,3 +156,11 @@ skip-if = ["os == 'linux' && !debug"] # Bug 1863227 ["browser_translations_select_panel_translate_on_change_language_multiple_times_from_dropdown_menu.js"] ["browser_translations_select_panel_translate_on_open.js"] + +["browser_translations_select_panel_translation_failure_after_unsupported_language.js"] + +["browser_translations_select_panel_translation_failure_on_open.js"] + +["browser_translations_select_panel_translation_failure_on_retranslate.js"] + +["browser_translations_select_panel_unsupported_language.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 index 383f2094a7..6d5b10f26c 100644 --- 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 @@ -36,7 +36,7 @@ add_task(async function test_about_preferences_manage_languages() { is( downloadAllLabel.getAttribute("data-l10n-id"), - "translations-manage-install-description", + "translations-manage-download-description", "The first row is all of the languages." ); is(frenchLabel.textContent, "French", "There is a French row."); @@ -178,7 +178,7 @@ add_task(async function test_about_preferences_download_reject() { click(frenchDownload, "Downloading French"); is( - maybeGetByL10nId("translations-manage-error-install", document), + maybeGetByL10nId("translations-manage-error-download", document), null, "No error messages are present." ); @@ -200,13 +200,13 @@ add_task(async function test_about_preferences_download_reject() { } await waitForCondition( - () => maybeGetByL10nId("translations-manage-error-install", document), + () => maybeGetByL10nId("translations-manage-error-download", document), "The error message is now visible." ); click(frenchDownload, "Attempting to download French again", document); is( - maybeGetByL10nId("translations-manage-error-install", document), + maybeGetByL10nId("translations-manage-error-download", document), null, "The error message is hidden again." ); 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 index f618b27814..39495a823c 100644 --- 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 @@ -161,7 +161,7 @@ async function testLanguageList(translateSection, menuList) { let langButton = languagelist.children[0].querySelector("moz-button"); let clickButton = BrowserTestUtils.waitForEvent(langButton, "click"); - langButton.dispatchEvent(new Event("click")); + langButton.click(); await clickButton; if (i < langNum - 1) { @@ -242,9 +242,7 @@ add_task(async function test_translations_settings_download_languages() { langList.children[i].querySelector("moz-button"), "click" ); - langList.children[i] - .querySelector("moz-button") - .dispatchEvent(new Event("click")); + langList.children[i].querySelector("moz-button").click(); await clickButton; is( @@ -259,9 +257,7 @@ add_task(async function test_translations_settings_download_languages() { langList.children[i].querySelector("moz-button"), "click" ); - langList.children[i] - .querySelector("moz-button") - .dispatchEvent(new Event("click")); + langList.children[i].querySelector("moz-button").click(); await clickButton; is( diff --git a/browser/components/translations/tests/browser/browser_translations_select_context_menu_engine_unsupported.js b/browser/components/translations/tests/browser/browser_translations_select_context_menu_engine_unsupported.js new file mode 100644 index 0000000000..b5fe9de0ef --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_context_menu_engine_unsupported.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test checks the availability of the translate-selection menu item in the context menu, + * ensuring it is not visible when the hardware does not support Translations. In this case + * we simulate this scenario by setting "browser.translations.simulateUnsupportedEngine" to true. + */ +add_task( + async function test_translate_selection_menuitem_is_unavailable_when_engine_is_unsupported() { + const { cleanup, runInPage } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [ + ["browser.translations.enable", true], + ["browser.translations.select.enable", true], + ["browser.translations.simulateUnsupportedEngine", true], + ], + }); + + await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem( + runInPage, + { + selectSpanishSentence: true, + openAtSpanishSentence: true, + expectMenuItemVisible: false, + }, + "The translate-selection context menu item should be unavailable the translations engine is unsupported." + ); + + 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 index 83e836489f..cb0f3601d9 100644 --- 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 @@ -41,8 +41,8 @@ add_task( /** * 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. + * The menu item should still offer to translate the link text to the top preferred language, + * since the Select Translations Panel should pass through the text for same-language translation. */ add_task( async function test_translate_selection_menuitem_translate_link_text_in_preferred_language() { @@ -63,10 +63,10 @@ add_task( selectSpanishSentence: false, openAtEnglishHyperlink: true, expectMenuItemVisible: true, - expectedTargetLanguage: null, + expectedTargetLanguage: "en", }, "The translate-selection context menu item should be localized to translate the link text" + - "without a target language." + "to the target language." ); 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 index 6b44f2ca1f..562eef3efb 100644 --- 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 @@ -43,8 +43,8 @@ add_task( /** * 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. + * still be localized to the user's preferred language as a target, since the Select Translations + * Panel allows passing through the text for same-language translation. */ add_task( async function test_translate_selection_menuitem_when_selected_text_is_preferred_language() { @@ -65,9 +65,9 @@ add_task( selectEnglishSentence: true, openAtEnglishSentence: true, expectMenuItemVisible: true, - expectedTargetLanguage: null, + expectedTargetLanguage: "en", }, - "The translate-selection context menu item should not display a target language " + + "The translate-selection context menu item should still display a target language " + "when the selected text is in the preferred language." ); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_close_on_new_tab.js b/browser/components/translations/tests/browser/browser_translations_select_panel_close_on_new_tab.js new file mode 100644 index 0000000000..86e563b157 --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_close_on_new_tab.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case tests the scenario where the SelectTranslationsPanel is open + * and the user opens a new tab while the panel is still open. The panel should + * close appropriately, as the content relevant to the selection is no longer + * in the active tab. + */ +add_task( + async function test_select_translations_panel_translate_sentence_on_open() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectFrenchSentence: true, + openAtFrenchSentence: true, + expectedFromLanguage: "fr", + expectedToLanguage: "en", + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + let tab; + + await SelectTranslationsTestUtils.waitForPanelPopupEvent( + "popuphidden", + async () => { + tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + SPANISH_PAGE_URL, + true // waitForLoad + ); + } + ); + + BrowserTestUtils.removeTab(tab); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_copy_button.js b/browser/components/translations/tests/browser/browser_translations_select_panel_copy_button.js new file mode 100644 index 0000000000..29eafb980d --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_copy_button.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case tests functionality of the SelectTranslationsPanel copy button + * when retranslating by closing the panel and re-opening the panel to new links + * or selections of text. + */ +add_task(async function test_select_translations_panel_copy_button_on_reopen() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + openAtSpanishHyperlink: true, + expectedFromLanguage: "es", + expectedToLanguage: "en", + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickCopyButton(); + await SelectTranslationsTestUtils.clickDoneButton(); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectEnglishSection: true, + openAtEnglishSection: true, + expectedFromLanguage: "en", + expectedToLanguage: "en", + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickCopyButton(); + await SelectTranslationsTestUtils.clickDoneButton(); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectFrenchSection: true, + openAtFrenchSection: true, + expectedFromLanguage: "fr", + expectedToLanguage: "en", + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickCopyButton(); + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); +}); + +/** + * This test case tests functionality of the SelectTranslationsPanel copy button + * when retranslating by changing the from-language and to-language values for + * the same selection of source text. + */ +add_task( + async function test_select_translations_panel_copy_button_on_retranslate() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectFrenchSection: true, + openAtFrenchSection: true, + expectedFromLanguage: "fr", + expectedToLanguage: "en", + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickCopyButton(); + await SelectTranslationsTestUtils.changeSelectedFromLanguage(["es"], { + openDropdownMenu: true, + downloadHandler: resolveDownloads, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickCopyButton(); + await SelectTranslationsTestUtils.changeSelectedToLanguage(["es"], { + openDropdownMenu: false, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickCopyButton(); + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_init_failure.js b/browser/components/translations/tests/browser/browser_translations_select_panel_init_failure.js new file mode 100644 index 0000000000..9e17b0705f --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_init_failure.js @@ -0,0 +1,119 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case verifies the scenario of clicking the cancel button to close + * the SelectTranslationsPanel after the language lists fail to initialize upon + * opening the panel, and the proper error message is displayed. + */ +add_task(async function test_select_translations_panel_init_failure_cancel() { + const { cleanup, runInPage } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + TranslationsPanelShared.simulateLangListError(); + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectFrenchSentence: true, + openAtFrenchSentence: true, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewInitFailure, + }); + + await SelectTranslationsTestUtils.clickCancelButton(); + + await cleanup(); +}); + +/** + * This test case verifies the scenario of opening the SelectTranslationsPanel to a valid + * language pair, but having the language lists fail to initialize, then clicking the try-again + * button multiple times until both initialization and translation succeed. + */ +add_task( + async function test_select_translations_panel_init_failure_try_again_into_translation() { + const { cleanup, runInPage, resolveDownloads, rejectDownloads } = + await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + TranslationsPanelShared.simulateLangListError(); + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectFrenchSentence: true, + openAtFrenchSentence: true, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewInitFailure, + }); + + TranslationsPanelShared.simulateLangListError(); + await SelectTranslationsTestUtils.waitForPanelPopupEvent( + "popupshown", + SelectTranslationsTestUtils.clickTryAgainButton, + SelectTranslationsTestUtils.assertPanelViewInitFailure + ); + + await SelectTranslationsTestUtils.waitForPanelPopupEvent( + "popupshown", + async () => + SelectTranslationsTestUtils.clickTryAgainButton({ + downloadHandler: rejectDownloads, + }), + SelectTranslationsTestUtils.assertPanelViewTranslationFailure + ); + + await SelectTranslationsTestUtils.clickTryAgainButton({ + downloadHandler: resolveDownloads, + viewAssertion: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); + } +); + +/** + * This test case verifies the scenario of opening the SelectTranslationsPanel to an unsupported + * language, but having the language lists fail to initialize, then clicking the try-again + * button multiple times until the unsupported-language view is shown. + */ +add_task( + async function test_select_translations_panel_init_failure_try_again_into_unsupported() { + const { cleanup, runInPage } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: [ + // Do not include Spanish. + { fromLang: "fr", toLang: "en" }, + { fromLang: "en", toLang: "fr" }, + ], + prefs: [["browser.translations.select.enable", true]], + }); + + TranslationsPanelShared.simulateLangListError(); + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectSpanishSection: true, + openAtSpanishSection: true, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewInitFailure, + }); + + TranslationsPanelShared.simulateLangListError(); + await SelectTranslationsTestUtils.waitForPanelPopupEvent( + "popupshown", + SelectTranslationsTestUtils.clickTryAgainButton, + SelectTranslationsTestUtils.assertPanelViewInitFailure + ); + + await SelectTranslationsTestUtils.waitForPanelPopupEvent( + "popupshown", + SelectTranslationsTestUtils.clickTryAgainButton, + SelectTranslationsTestUtils.assertPanelViewUnsupportedLanguage + ); + + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_open_to_idle_state.js b/browser/components/translations/tests/browser/browser_translations_select_panel_open_to_idle_state.js deleted file mode 100644 index d5a1096e70..0000000000 --- a/browser/components/translations/tests/browser/browser_translations_select_panel_open_to_idle_state.js +++ /dev/null @@ -1,61 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -/** - * This test case verifies the select translations panel's functionality when opened with an unsupported - * from-language, ensuring it opens with the correct view with no from-language selected. - */ -add_task( - async function test_select_translations_panel_open_no_selected_from_lang() { - const { cleanup, runInPage } = await loadTestPage({ - page: SELECT_TEST_PAGE_URL, - languagePairs: [ - // Do not include Spanish. - { fromLang: "fr", toLang: "en" }, - { fromLang: "en", toLang: "fr" }, - ], - prefs: [["browser.translations.select.enable", true]], - }); - - await SelectTranslationsTestUtils.openPanel(runInPage, { - selectSpanishSentence: true, - openAtSpanishSentence: true, - expectedFromLanguage: null, - expectedToLanguage: "en", - onOpenPanel: - SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected, - }); - - await SelectTranslationsTestUtils.clickDoneButton(); - - await cleanup(); - } -); - -/** - * This test case verifies the select translations panel's functionality when opened with an undetermined - * to-language, ensuring it opens with the correct view with no to-language selected. - */ -add_task( - async function test_select_translations_panel_open_no_selected_to_lang() { - const { cleanup, runInPage } = await loadTestPage({ - page: SELECT_TEST_PAGE_URL, - languagePairs: LANGUAGE_PAIRS, - prefs: [["browser.translations.select.enable", true]], - }); - - await SelectTranslationsTestUtils.openPanel(runInPage, { - selectEnglishSentence: true, - openAtEnglishSentence: true, - expectedFromLanguage: "en", - expectedToLanguage: null, - onOpenPanel: SelectTranslationsTestUtils.assertPanelViewNoToLangSelected, - }); - - await SelectTranslationsTestUtils.clickDoneButton(); - - await cleanup(); - } -); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_pdf.js b/browser/components/translations/tests/browser/browser_translations_select_panel_pdf.js new file mode 100644 index 0000000000..fd675e9cea --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_pdf.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case verifies that the Select Translations Panel functionality + * is available and works within PDF files. + */ +add_task(async function test_the_select_translations_panel_in_pdf_files() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: PDF_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectPdfSpan: true, + openAtPdfSpan: true, + expectedFromLanguage: "en", + expectedToLanguage: "en", + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.changeSelectedFromLanguage(["es"], { + openDropdownMenu: true, + pivotTranslation: false, + downloadHandler: resolveDownloads, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.changeSelectedToLanguage(["fr"], { + openDropdownMenu: false, + pivotTranslation: true, + downloadHandler: resolveDownloads, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); +}); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_reader_mode.js b/browser/components/translations/tests/browser/browser_translations_select_panel_reader_mode.js new file mode 100644 index 0000000000..5f05b1a878 --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_reader_mode.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case verifies that the Select Translations Panel functionality + * is available and works within reader mode. + */ +add_task(async function test_the_select_translations_panel_in_reader_mode() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await toggleReaderMode(); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectH1: true, + openAtH1: true, + expectedFromLanguage: "en", + expectedToLanguage: "en", + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.changeSelectedFromLanguage(["es"], { + openDropdownMenu: true, + pivotTranslation: false, + downloadHandler: resolveDownloads, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.changeSelectedToLanguage(["fr"], { + openDropdownMenu: false, + pivotTranslation: true, + downloadHandler: resolveDownloads, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); +}); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_select_same_from_and_to_languages_directly.js b/browser/components/translations/tests/browser/browser_translations_select_panel_select_same_from_and_to_languages_directly.js index 95feac6708..673faee796 100644 --- a/browser/components/translations/tests/browser/browser_translations_select_panel_select_same_from_and_to_languages_directly.js +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_select_same_from_and_to_languages_directly.js @@ -10,29 +10,24 @@ */ add_task( async function test_select_translations_panel_select_same_from_language_directly() { - const { cleanup, runInPage } = await loadTestPage({ + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ page: SELECT_TEST_PAGE_URL, - languagePairs: [ - // Do not include Spanish. - { fromLang: "fr", toLang: "en" }, - { fromLang: "en", toLang: "fr" }, - ], + languagePairs: LANGUAGE_PAIRS, prefs: [["browser.translations.select.enable", true]], }); await SelectTranslationsTestUtils.openPanel(runInPage, { selectSpanishSection: true, openAtSpanishSection: true, - expectedFromLanguage: null, + expectedFromLanguage: "es", expectedToLanguage: "en", - onOpenPanel: - SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected, + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, }); await SelectTranslationsTestUtils.changeSelectedFromLanguage(["en"], { openDropdownMenu: false, - onChangeLanguage: - SelectTranslationsTestUtils.assertPanelViewNoFromToSelected, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, }); await cleanup(); @@ -41,29 +36,29 @@ add_task( /** * This test case verifies the behavior of switching the to-language to the same value - * that is currently selected in the from-language, effectively stealing the from-language's - * value, leaving it unselected and focused. + * that is currently selected in the from-language, creating a passthrough translation + * of the source text directly into the text area. */ add_task( async function test_select_translations_panel_select_same_to_language_directly() { - const { cleanup, runInPage } = await loadTestPage({ + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ page: SELECT_TEST_PAGE_URL, languagePairs: LANGUAGE_PAIRS, prefs: [["browser.translations.select.enable", true]], }); await SelectTranslationsTestUtils.openPanel(runInPage, { - selectEnglishSection: true, - openAtEnglishSection: true, - expectedFromLanguage: "en", - expectedToLanguage: null, - onOpenPanel: SelectTranslationsTestUtils.assertPanelViewNoToLangSelected, + selectFrenchSentence: true, + openAtFrenchSentence: true, + expectedFromLanguage: "fr", + expectedToLanguage: "en", + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, }); - await SelectTranslationsTestUtils.changeSelectedToLanguage(["en"], { + await SelectTranslationsTestUtils.changeSelectedToLanguage(["fr"], { openDropdownMenu: false, - onChangeLanguage: - SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, }); await cleanup(); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_select_same_from_and_to_languages_from_dropdown_menu.js b/browser/components/translations/tests/browser/browser_translations_select_panel_select_same_from_and_to_languages_from_dropdown_menu.js index 5c27be411f..eea7a76bf2 100644 --- a/browser/components/translations/tests/browser/browser_translations_select_panel_select_same_from_and_to_languages_from_dropdown_menu.js +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_select_same_from_and_to_languages_from_dropdown_menu.js @@ -10,29 +10,24 @@ */ add_task( async function test_select_translations_panel_select_same_from_language_via_popup() { - const { cleanup, runInPage } = await loadTestPage({ + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ page: SELECT_TEST_PAGE_URL, - languagePairs: [ - // Do not include Spanish. - { fromLang: "fr", toLang: "en" }, - { fromLang: "en", toLang: "fr" }, - ], + languagePairs: LANGUAGE_PAIRS, prefs: [["browser.translations.select.enable", true]], }); await SelectTranslationsTestUtils.openPanel(runInPage, { selectSpanishSection: true, openAtSpanishSection: true, - expectedFromLanguage: null, + expectedFromLanguage: "es", expectedToLanguage: "en", - onOpenPanel: - SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected, + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, }); await SelectTranslationsTestUtils.changeSelectedFromLanguage(["en"], { openDropdownMenu: true, - onChangeLanguage: - SelectTranslationsTestUtils.assertPanelViewNoFromToSelected, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, }); await cleanup(); @@ -42,28 +37,28 @@ add_task( /** * This test case verifies the behavior of switching the to-language to the same value * that is currently selected in the from-language by opening the language dropdown menu, - * effectively stealing the from-language's value, leaving it unselected and focused. + * creating a passthrough translation of the source text directly into the text area. */ add_task( async function test_select_translations_panel_select_same_to_language_via_popup() { - const { cleanup, runInPage } = await loadTestPage({ + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ page: SELECT_TEST_PAGE_URL, languagePairs: LANGUAGE_PAIRS, prefs: [["browser.translations.select.enable", true]], }); await SelectTranslationsTestUtils.openPanel(runInPage, { - selectEnglishSection: true, - openAtEnglishSection: true, - expectedFromLanguage: "en", - expectedToLanguage: null, - onOpenPanel: SelectTranslationsTestUtils.assertPanelViewNoToLangSelected, + selectFrenchSentence: true, + openAtFrenchSentence: true, + expectedFromLanguage: "fr", + expectedToLanguage: "en", + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, }); - await SelectTranslationsTestUtils.changeSelectedToLanguage(["en"], { + await SelectTranslationsTestUtils.changeSelectedToLanguage(["fr"], { openDropdownMenu: true, - onChangeLanguage: - SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, }); await cleanup(); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_settings_menu.js b/browser/components/translations/tests/browser/browser_translations_select_panel_settings_menu.js new file mode 100644 index 0000000000..b6263325d5 --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_settings_menu.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case tests the scenario of clicking the settings menu item + * that leads to the translations section of the about:preferences settings + * page in Firefox. + */ +add_task(async function test_select_translations_panel_open_settings_page() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectFrenchSentence: true, + openAtFrenchSentence: true, + expectedFromLanguage: "fr", + expectedToLanguage: "en", + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.openPanelSettingsMenu(); + SelectTranslationsTestUtils.clickTranslationsSettingsPageMenuItem(); + + 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(); +}); + +/** + * This test case tests the scenario of opening the SelectTranslationsPanel + * settings menu from the unsupported-language panel state. + */ +add_task( + async function test_select_translations_panel_open_settings_menu_from_unsupported_language() { + const { cleanup, runInPage } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: [ + // Do not include Spanish. + { fromLang: "fr", toLang: "en" }, + { fromLang: "en", toLang: "fr" }, + ], + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectSpanishSection: true, + openAtSpanishSection: true, + onOpenPanel: + SelectTranslationsTestUtils.assertPanelViewUnsupportedLanguage, + }); + + await SelectTranslationsTestUtils.openPanelSettingsMenu(); + + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_translate_full_page_button.js b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_full_page_button.js new file mode 100644 index 0000000000..a2e9727798 --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_full_page_button.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Simulates clicking the translate-full-page button with a from-language that + * matches the language of the given document. + */ +add_task( + async function test_select_translations_panel_translat_full_page_button_matching_doc_lang() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + openAtSpanishHyperlink: true, + expectedFromLanguage: "es", + expectedToLanguage: "en", + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickTranslateFullPageButton(); + + await FullPageTranslationsTestUtils.assertPageIsTranslated( + "es", + "en", + runInPage + ); + + await cleanup(); + } +); + +/** + * Simulates clicking the translate-full-page button after changing the from-language + * and to-language values to values that don't match the document language or the + * user's app locale, ensuring that the current selection is respected. + */ +add_task( + async function test_select_translations_panel_translat_full_page_button_matching_doc_lang() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectSpanishSection: true, + openAtSpanishSection: true, + expectedFromLanguage: "es", + expectedToLanguage: "en", + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.changeSelectedFromLanguage(["fr"], { + openDropdownMenu: false, + downloadHandler: resolveDownloads, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.changeSelectedToLanguage(["uk"], { + openDropdownMenu: true, + downloadHandler: resolveDownloads, + pivotTranslation: true, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickTranslateFullPageButton(); + + await FullPageTranslationsTestUtils.assertPageIsTranslated( + "fr", + "uk", + runInPage + ); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_directly.js b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_directly.js index 64d067d1f4..7ea721fb0f 100644 --- a/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_directly.js +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_directly.js @@ -12,21 +12,17 @@ add_task( async function test_select_translations_panel_translate_on_change_from_language_directly() { const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ page: SELECT_TEST_PAGE_URL, - languagePairs: [ - // Do not include Spanish. - { fromLang: "fr", toLang: "en" }, - { fromLang: "en", toLang: "fr" }, - ], + languagePairs: LANGUAGE_PAIRS, prefs: [["browser.translations.select.enable", true]], }); await SelectTranslationsTestUtils.openPanel(runInPage, { selectSpanishSection: true, openAtSpanishSection: true, - expectedFromLanguage: null, + expectedFromLanguage: "es", expectedToLanguage: "en", - onOpenPanel: - SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected, + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, }); await SelectTranslationsTestUtils.changeSelectedFromLanguage(["fr"], { @@ -56,8 +52,8 @@ add_task( selectEnglishSection: true, openAtEnglishSection: true, expectedFromLanguage: "en", - expectedToLanguage: null, - onOpenPanel: SelectTranslationsTestUtils.assertPanelViewNoToLangSelected, + expectedToLanguage: "en", + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, }); await SelectTranslationsTestUtils.changeSelectedToLanguage(["es"], { diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_from_dropdown_menu.js b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_from_dropdown_menu.js index 0cd205d721..c0ba02f6db 100644 --- a/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_from_dropdown_menu.js +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_from_dropdown_menu.js @@ -12,21 +12,17 @@ add_task( async function test_select_translations_panel_translate_on_change_from_language() { const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ page: SELECT_TEST_PAGE_URL, - languagePairs: [ - // Do not include Spanish. - { fromLang: "fr", toLang: "en" }, - { fromLang: "en", toLang: "fr" }, - ], + languagePairs: LANGUAGE_PAIRS, prefs: [["browser.translations.select.enable", true]], }); await SelectTranslationsTestUtils.openPanel(runInPage, { selectSpanishSection: true, openAtSpanishSection: true, - expectedFromLanguage: null, + expectedFromLanguage: "es", expectedToLanguage: "en", - onOpenPanel: - SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected, + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, }); await SelectTranslationsTestUtils.changeSelectedFromLanguage(["fr"], { @@ -56,8 +52,8 @@ add_task( selectEnglishSection: true, openAtEnglishSection: true, expectedFromLanguage: "en", - expectedToLanguage: null, - onOpenPanel: SelectTranslationsTestUtils.assertPanelViewNoToLangSelected, + expectedToLanguage: "en", + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, }); await SelectTranslationsTestUtils.changeSelectedToLanguage(["es"], { diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_directly.js b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_directly.js index b3c02a96f6..cd60f73e6c 100644 --- a/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_directly.js +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_directly.js @@ -13,7 +13,8 @@ add_task( const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ page: SELECT_TEST_PAGE_URL, languagePairs: [ - // Do not include Spanish. + { fromLang: "es", toLang: "en" }, + { fromLang: "en", toLang: "es" }, { fromLang: "fa", toLang: "en" }, { fromLang: "en", toLang: "fa" }, { fromLang: "fi", toLang: "en" }, @@ -31,10 +32,10 @@ add_task( await SelectTranslationsTestUtils.openPanel(runInPage, { selectSpanishSentence: true, openAtSpanishSentence: true, - expectedFromLanguage: null, + expectedFromLanguage: "es", expectedToLanguage: "en", - onOpenPanel: - SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected, + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, }); await SelectTranslationsTestUtils.changeSelectedFromLanguage( @@ -79,8 +80,8 @@ add_task( await SelectTranslationsTestUtils.openPanel(runInPage, { openAtEnglishHyperlink: true, expectedFromLanguage: "en", - expectedToLanguage: null, - onOpenPanel: SelectTranslationsTestUtils.assertPanelViewNoToLangSelected, + expectedToLanguage: "en", + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, }); await SelectTranslationsTestUtils.changeSelectedToLanguage( diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_from_dropdown_menu.js b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_from_dropdown_menu.js index 50c877cfbc..1b2044ef97 100644 --- a/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_from_dropdown_menu.js +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_from_dropdown_menu.js @@ -13,7 +13,8 @@ add_task( const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ page: SELECT_TEST_PAGE_URL, languagePairs: [ - // Do not include Spanish. + { fromLang: "es", toLang: "en" }, + { fromLang: "en", toLang: "es" }, { fromLang: "fa", toLang: "en" }, { fromLang: "en", toLang: "fa" }, { fromLang: "fi", toLang: "en" }, @@ -31,10 +32,10 @@ add_task( await SelectTranslationsTestUtils.openPanel(runInPage, { selectSpanishSentence: true, openAtSpanishSentence: true, - expectedFromLanguage: null, + expectedFromLanguage: "es", expectedToLanguage: "en", - onOpenPanel: - SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected, + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, }); await SelectTranslationsTestUtils.changeSelectedFromLanguage( @@ -80,8 +81,8 @@ add_task( selectEnglishSentence: true, openAtEnglishSentence: true, expectedFromLanguage: "en", - expectedToLanguage: null, - onOpenPanel: SelectTranslationsTestUtils.assertPanelViewNoToLangSelected, + expectedToLanguage: "en", + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, }); await SelectTranslationsTestUtils.changeSelectedToLanguage( diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_translation_failure_after_unsupported_language.js b/browser/components/translations/tests/browser/browser_translations_select_panel_translation_failure_after_unsupported_language.js new file mode 100644 index 0000000000..79ce46b7dc --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_translation_failure_after_unsupported_language.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case tests the scenario of encountering the translation failure message + * as a result of changing the source language from the unsupported-language state. + */ +add_task( + async function test_select_translations_panel_failure_after_unsupported_language() { + const { cleanup, runInPage, resolveDownloads, rejectDownloads } = + await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: [ + // Do not include Spanish. + { fromLang: "fr", toLang: "en" }, + { fromLang: "en", toLang: "fr" }, + ], + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectSpanishSentence: true, + openAtSpanishSentence: true, + onOpenPanel: + SelectTranslationsTestUtils.assertPanelViewUnsupportedLanguage, + }); + + await SelectTranslationsTestUtils.changeSelectedTryAnotherSourceLanguage( + "fr" + ); + + await SelectTranslationsTestUtils.clickTranslateButton({ + downloadHandler: rejectDownloads, + viewAssertion: + SelectTranslationsTestUtils.assertPanelViewTranslationFailure, + }); + + await SelectTranslationsTestUtils.clickTryAgainButton({ + downloadHandler: rejectDownloads, + viewAssertion: + SelectTranslationsTestUtils.assertPanelViewTranslationFailure, + }); + + await SelectTranslationsTestUtils.clickTryAgainButton({ + downloadHandler: resolveDownloads, + viewAssertion: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_translation_failure_on_open.js b/browser/components/translations/tests/browser/browser_translations_select_panel_translation_failure_on_open.js new file mode 100644 index 0000000000..0fc133f269 --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_translation_failure_on_open.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case tests the scenario of opening the SelectTranslationsPanel to a translation + * attempt that fails, followed by closing the panel via the cancel button, and then re-attempting + * the translation by re-opening the panel and having it succeed. + */ +add_task( + async function test_select_translations_panel_translation_failure_on_open_then_cancel_and_reopen() { + const { cleanup, runInPage, rejectDownloads, resolveDownloads } = + await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectFrenchSentence: true, + openAtFrenchSentence: true, + expectedFromLanguage: "fr", + expectedToLanguage: "en", + downloadHandler: rejectDownloads, + onOpenPanel: + SelectTranslationsTestUtils.assertPanelViewTranslationFailure, + }); + + await SelectTranslationsTestUtils.clickCancelButton(); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectFrenchSentence: true, + openAtFrenchSentence: true, + expectedFromLanguage: "fr", + expectedToLanguage: "en", + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); + } +); + +/** + * This test case tests the scenario of opening the SelectTranslationsPanel to a translation + * attempt that fails, followed by clicking the try-again button multiple times to retry the + * translation until it finally succeeds. + */ +add_task( + async function test_select_translations_panel_translation_failure_on_open_then_try_again() { + const { cleanup, runInPage, rejectDownloads, resolveDownloads } = + await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectFrenchSentence: true, + openAtFrenchSentence: true, + expectedFromLanguage: "fr", + expectedToLanguage: "en", + downloadHandler: rejectDownloads, + onOpenPanel: + SelectTranslationsTestUtils.assertPanelViewTranslationFailure, + }); + + await SelectTranslationsTestUtils.clickTryAgainButton({ + downloadHandler: rejectDownloads, + viewAssertion: + SelectTranslationsTestUtils.assertPanelViewTranslationFailure, + }); + + await SelectTranslationsTestUtils.clickTryAgainButton({ + downloadHandler: rejectDownloads, + viewAssertion: + SelectTranslationsTestUtils.assertPanelViewTranslationFailure, + }); + + await SelectTranslationsTestUtils.clickTryAgainButton({ + downloadHandler: resolveDownloads, + viewAssertion: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_translation_failure_on_retranslate.js b/browser/components/translations/tests/browser/browser_translations_select_panel_translation_failure_on_retranslate.js new file mode 100644 index 0000000000..d19439c6a4 --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_translation_failure_on_retranslate.js @@ -0,0 +1,128 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case tests the scenario of encountering the translation failure message + * as a result of changing the selected from-language, along with moving from the failure + * state to a successful translation also by changing the selected from-language. + */ +add_task( + async function test_select_translations_panel_translation_failure_on_change_from_language() { + const { cleanup, runInPage, rejectDownloads, resolveDownloads } = + await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectFrenchSentence: true, + openAtFrenchSentence: true, + expectedFromLanguage: "fr", + expectedToLanguage: "en", + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.changeSelectedFromLanguage(["es"], { + openDropdownMenu: false, + downloadHandler: rejectDownloads, + onChangeLanguage: + SelectTranslationsTestUtils.assertPanelViewTranslationFailure, + }); + + await SelectTranslationsTestUtils.changeSelectedFromLanguage(["uk"], { + openDropdownMenu: true, + downloadHandler: rejectDownloads, + onChangeLanguage: + SelectTranslationsTestUtils.assertPanelViewTranslationFailure, + }); + + await SelectTranslationsTestUtils.changeSelectedFromLanguage(["es"], { + openDropdownMenu: true, + downloadHandler: resolveDownloads, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.changeSelectedFromLanguage(["uk"], { + openDropdownMenu: false, + downloadHandler: rejectDownloads, + onChangeLanguage: + SelectTranslationsTestUtils.assertPanelViewTranslationFailure, + }); + + await SelectTranslationsTestUtils.clickTryAgainButton({ + downloadHandler: resolveDownloads, + viewAssertion: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); + } +); + +/** + * This test case tests the scenario of encountering the translation failure message + * as a result of changing the selected to-language, along with moving from the failure + * state to a successful translation also by changing the selected to-language. + */ +add_task( + async function test_select_translations_panel_translation_failure_on_change_to_language() { + const { cleanup, runInPage, rejectDownloads, resolveDownloads } = + await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectFrenchSentence: true, + openAtFrenchSentence: true, + expectedFromLanguage: "fr", + expectedToLanguage: "en", + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.changeSelectedToLanguage(["es"], { + openDropdownMenu: false, + downloadHandler: rejectDownloads, + onChangeLanguage: + SelectTranslationsTestUtils.assertPanelViewTranslationFailure, + }); + + await SelectTranslationsTestUtils.changeSelectedToLanguage(["uk"], { + openDropdownMenu: true, + downloadHandler: rejectDownloads, + onChangeLanguage: + SelectTranslationsTestUtils.assertPanelViewTranslationFailure, + }); + + await SelectTranslationsTestUtils.changeSelectedToLanguage(["es"], { + openDropdownMenu: true, + downloadHandler: resolveDownloads, + pivotTranslation: true, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.changeSelectedToLanguage(["uk"], { + openDropdownMenu: false, + downloadHandler: rejectDownloads, + onChangeLanguage: + SelectTranslationsTestUtils.assertPanelViewTranslationFailure, + }); + + await SelectTranslationsTestUtils.clickTryAgainButton({ + downloadHandler: resolveDownloads, + pivotTranslation: true, + viewAssertion: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_unsupported_language.js b/browser/components/translations/tests/browser/browser_translations_select_panel_unsupported_language.js new file mode 100644 index 0000000000..0898bd125b --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_unsupported_language.js @@ -0,0 +1,163 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case verifies the behavior of opening the SelectTranslationsPanel to an unsupported language + * and then clicking the done button to close the panel. + */ +add_task( + async function test_select_translations_panel_unsupported_click_done_button() { + const { cleanup, runInPage } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: [ + // Do not include Spanish. + { fromLang: "fr", toLang: "en" }, + { fromLang: "en", toLang: "fr" }, + ], + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectSpanishSentence: true, + openAtSpanishSentence: true, + onOpenPanel: + SelectTranslationsTestUtils.assertPanelViewUnsupportedLanguage, + }); + + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); + } +); + +/** + * This test case verifies the behavior of opening the SelectTranslationsPanel to an unsupported language + * then changing the source language to the same language as the app locale, triggering a same-language + * translation, then changing the from-language and to-language multiple times. + */ +add_task( + async function test_select_translations_panel_unsupported_then_to_same_language_translation() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: [ + // Do not include Spanish. + { fromLang: "fa", toLang: "en" }, + { fromLang: "en", toLang: "fa" }, + { fromLang: "fi", toLang: "en" }, + { fromLang: "en", toLang: "fi" }, + { fromLang: "fr", toLang: "en" }, + { fromLang: "en", toLang: "fr" }, + { fromLang: "sl", toLang: "en" }, + { fromLang: "en", toLang: "sl" }, + { fromLang: "uk", toLang: "en" }, + { fromLang: "en", toLang: "uk" }, + ], + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectSpanishSentence: true, + openAtSpanishSentence: true, + onOpenPanel: + SelectTranslationsTestUtils.assertPanelViewUnsupportedLanguage, + }); + + await SelectTranslationsTestUtils.changeSelectedTryAnotherSourceLanguage( + "en" + ); + + await SelectTranslationsTestUtils.clickTranslateButton({ + viewAssertion: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.changeSelectedToLanguage(["uk", "fi"], { + openDropdownMenu: false, + downloadHandler: resolveDownloads, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.changeSelectedFromLanguage(["sl", "fr"], { + openDropdownMenu: true, + downloadHandler: resolveDownloads, + pivotTranslation: true, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.changeSelectedToLanguage(["fr"], { + openDropdownMenu: true, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); + } +); + +/** + * This test case verifies the behavior of opening the SelectTranslationsPanel to an unsupported language + * then changing the source language to a valid language, followed by changing the from-language and to-language + * multiple times. + */ +add_task( + async function test_select_translations_panel_unsupported_into_different_language_translation() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: [ + // Do not include Spanish. + { fromLang: "fa", toLang: "en" }, + { fromLang: "en", toLang: "fa" }, + { fromLang: "fi", toLang: "en" }, + { fromLang: "en", toLang: "fi" }, + { fromLang: "fr", toLang: "en" }, + { fromLang: "en", toLang: "fr" }, + { fromLang: "sl", toLang: "en" }, + { fromLang: "en", toLang: "sl" }, + { fromLang: "uk", toLang: "en" }, + { fromLang: "en", toLang: "uk" }, + ], + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectSpanishSentence: true, + openAtSpanishSentence: true, + onOpenPanel: + SelectTranslationsTestUtils.assertPanelViewUnsupportedLanguage, + }); + + await SelectTranslationsTestUtils.changeSelectedTryAnotherSourceLanguage( + "fr" + ); + + await SelectTranslationsTestUtils.clickTranslateButton({ + downloadHandler: resolveDownloads, + viewAssertion: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.changeSelectedFromLanguage(["uk", "fi"], { + openDropdownMenu: false, + downloadHandler: resolveDownloads, + pivotTranslation: true, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.changeSelectedToLanguage(["sl", "uk"], { + openDropdownMenu: true, + downloadHandler: resolveDownloads, + pivotTranslation: true, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.changeSelectedFromLanguage(["uk"], { + openDropdownMenu: false, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/head.js b/browser/components/translations/tests/browser/head.js index 454de9146b..4bd5dc074f 100644 --- a/browser/components/translations/tests/browser/head.js +++ b/browser/components/translations/tests/browser/head.js @@ -297,6 +297,20 @@ class SharedTranslationsTestUtils { } /** + * Asserts that the given element has the expected L10nId. + * + * @param {Element} element - The element to assert against. + * @param {string} l10nId - The expected localization id. + */ + static _assertL10nId(element, l10nId) { + is( + element.getAttribute("data-l10n-id"), + l10nId, + `The element ${element.id} should have L10n Id ${l10nId}.` + ); + } + + /** * Asserts that the mainViewId of the panel matches the given string. * * @param {FullPageTranslationsPanel | SelectTranslationsPanel} panel @@ -369,6 +383,28 @@ class SharedTranslationsTestUtils { } /** + * Asserts that the given elements are focusable in order + * via the tab key, starting with the first element already + * focused and ending back on that same first element. + * + * @param {Element[]} elements - The focusable elements. + */ + static _assertTabIndexOrder(elements) { + const activeElementAtStart = document.activeElement; + + if (elements.length) { + elements[0].focus(); + elements.push(elements[0]); + } + for (const element of elements) { + SharedTranslationsTestUtils._assertHasFocus(element); + EventUtils.synthesizeKey("KEY_Tab"); + } + + activeElementAtStart.focus(); + } + + /** * Executes the provided callback before waiting for the event and then waits for the given event * to be fired for the element corresponding to the provided elementId. * @@ -696,11 +732,7 @@ class FullPageTranslationsTestUtils { */ static #assertPanelHeaderL10nId(l10nId) { const { header } = FullPageTranslationsPanel.elements; - is( - header.getAttribute("data-l10n-id"), - l10nId, - "The translations panel header should match the expected data-l10n-id" - ); + SharedTranslationsTestUtils._assertL10nId(header, l10nId); } /** @@ -710,11 +742,7 @@ class FullPageTranslationsTestUtils { */ static #assertPanelErrorL10nId(l10nId) { const { errorMessage } = FullPageTranslationsPanel.elements; - is( - errorMessage.getAttribute("data-l10n-id"), - l10nId, - "The translations panel error message should match the expected data-l10n-id" - ); + SharedTranslationsTestUtils._assertL10nId(errorMessage, l10nId); } /** @@ -1111,7 +1139,7 @@ class FullPageTranslationsTestUtils { static async #clickSettingsMenuItemByL10nId(l10nId) { info(`Toggling the "${l10nId}" settings menu item.`); click(getByL10nId(l10nId), `Clicking the "${l10nId}" settings menu item.`); - await closeSettingsMenuIfOpen(); + await closeFullPagePanelSettingsMenuIfOpen(); } /** @@ -1363,10 +1391,21 @@ class SelectTranslationsTestUtils { * @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. * - * The following options will only work when testing SELECT_TEST_PAGE_URL. - * * @param {boolean} options.expectMenuItemVisible - Whether the select-translations menu item should be present in the context menu. * @param {boolean} options.expectedTargetLanguage - The expected target language to be shown in the context menu. + * + * The following options will work on all test pages that have an <h1> element. + * + * @param {boolean} options.selectH1 - Selects the first H1 element of the page. + * @param {boolean} options.openAtH1 - Opens the context menu at the first H1 element of the page. + * + * The following options will work only in the PDF_TEST_PAGE_URL. + * + * @param {boolean} options.selectPdfSpan - Selects the first span of text on the first page of a pdf. + * @param {boolean} options.openAtPdfSpan - Opens the context menu at the first span of text on the first page of a pdf. + * + * The following options will only work when testing SELECT_TEST_PAGE_URL. + * * @param {boolean} options.selectFrenchSection - Selects the section of French text. * @param {boolean} options.selectEnglishSection - Selects the section of English text. * @param {boolean} options.selectSpanishSection - Selects the section of Spanish text. @@ -1390,12 +1429,16 @@ class SelectTranslationsTestUtils { { expectMenuItemVisible, expectedTargetLanguage, + selectH1, + selectPdfSpan, selectFrenchSection, selectEnglishSection, selectSpanishSection, selectFrenchSentence, selectEnglishSentence, selectSpanishSentence, + openAtH1, + openAtPdfSpan, openAtFrenchSection, openAtEnglishSection, openAtSpanishSection, @@ -1419,12 +1462,16 @@ class SelectTranslationsTestUtils { await SelectTranslationsTestUtils.openContextMenu(runInPage, { expectMenuItemVisible, expectedTargetLanguage, + selectH1, + selectPdfSpan, selectFrenchSection, selectEnglishSection, selectSpanishSection, selectFrenchSentence, selectEnglishSentence, selectSpanishSentence, + openAtH1, + openAtPdfSpan, openAtFrenchSection, openAtEnglishSection, openAtSpanishSection, @@ -1501,21 +1548,6 @@ class SelectTranslationsTestUtils { } /** - * Elements that should always be visible in the SelectTranslationsPanel. - */ - static #alwaysPresentElements = { - betaIcon: true, - copyButton: true, - doneButton: true, - fromLabel: true, - fromMenuList: true, - header: true, - toLabel: true, - toMenuList: true, - textArea: true, - }; - - /** * Asserts that for each provided expectation, the visible state of the corresponding * element in FullPageTranslationsPanel.elements both exists and matches the visibility expectation. * @@ -1526,7 +1558,31 @@ class SelectTranslationsTestUtils { SharedTranslationsTestUtils._assertPanelElementVisibility( SelectTranslationsPanel.elements, { - ...SelectTranslationsTestUtils.#alwaysPresentElements, + betaIcon: false, + cancelButton: false, + copyButton: false, + doneButtonPrimary: false, + doneButtonSecondary: false, + fromLabel: false, + fromMenuList: false, + fromMenuPopup: false, + header: false, + initFailureContent: false, + initFailureMessageBar: false, + mainContent: false, + settingsButton: false, + textArea: false, + toLabel: false, + toMenuList: false, + toMenuPopup: false, + translateButton: false, + translateFullPageButton: false, + translationFailureMessageBar: false, + tryAgainButton: false, + tryAnotherSourceMenuList: false, + tryAnotherSourceMenuPopup: false, + unsupportedLanguageContent: false, + unsupportedLanguageMessageBar: false, // Overwrite any of the above defaults with the passed in expectations. ...expectations, } @@ -1534,26 +1590,172 @@ class SelectTranslationsTestUtils { } /** + * Waits for the panel's translation state to reach the given phase, + * if it is not currently in that phase already. + * + * @param {string} phase - The phase of the panel's translation state to wait for. + */ + static async waitForPanelState(phase) { + const currentPhase = SelectTranslationsPanel.phase(); + if (currentPhase !== phase) { + info( + `Waiting for SelectTranslationsPanel to change state from "${currentPhase}" to "${phase}"` + ); + await BrowserTestUtils.waitForEvent( + document, + "SelectTranslationsPanelStateChanged", + event => event.detail.phase === phase + ); + } + } + + /** * Asserts that the SelectTranslationsPanel UI matches the expected * state when the panel has completed its translation. */ - static assertPanelViewTranslated() { - const { textArea } = SelectTranslationsPanel.elements; + static async assertPanelViewTranslated() { + const { + copyButton, + doneButtonPrimary, + fromMenuList, + settingsButton, + textArea, + toMenuList, + translateFullPageButton, + } = SelectTranslationsPanel.elements; + const sameLanguageSelected = fromMenuList.value === toMenuList.value; + await SelectTranslationsTestUtils.waitForPanelState("translated"); ok( !textArea.classList.contains("translating"), "The textarea should not have the translating class." ); + const isFullPageTranslationsRestrictedForPage = + TranslationsParent.isFullPageTranslationsRestrictedForPage(gBrowser); SelectTranslationsTestUtils.#assertPanelElementVisibility({ - ...SelectTranslationsTestUtils.#alwaysPresentElements, + betaIcon: true, + copyButton: true, + doneButtonPrimary: true, + fromLabel: true, + fromMenuList: true, + header: true, + mainContent: true, + settingsButton: true, + textArea: true, + toLabel: true, + toMenuList: true, + translateFullPageButton: !isFullPageTranslationsRestrictedForPage, }); SelectTranslationsTestUtils.#assertConditionalUIEnabled({ - textArea: true, copyButton: true, - translateFullPageButton: true, + doneButtonPrimary: true, + textArea: true, + translateFullPageButton: + !sameLanguageSelected && !isFullPageTranslationsRestrictedForPage, }); + + await waitForCondition( + () => + !copyButton.classList.contains("copied") && + copyButton.getAttribute("data-l10n-id") === + "select-translations-panel-copy-button", + "Waiting for copy button to match the not-copied state." + ); + SelectTranslationsTestUtils.#assertPanelHasTranslatedText(); SelectTranslationsTestUtils.#assertPanelTextAreaHeight(); - SelectTranslationsTestUtils.#assertPanelTextAreaOverflow(); + await SelectTranslationsTestUtils.#assertPanelTextAreaOverflow(); + + let footerButtons; + if (sameLanguageSelected || isFullPageTranslationsRestrictedForPage) { + footerButtons = [copyButton, doneButtonPrimary]; + } else { + footerButtons = + AppConstants.platform === "win" + ? [copyButton, doneButtonPrimary, translateFullPageButton] + : [copyButton, translateFullPageButton, doneButtonPrimary]; + } + + SharedTranslationsTestUtils._assertTabIndexOrder([ + settingsButton, + fromMenuList, + toMenuList, + textArea, + ...footerButtons, + ]); + } + + /** + * Asserts that the SelectTranslationsPanel UI matches the expected + * state when the language lists fail to initialize upon opening the panel. + */ + static async assertPanelViewInitFailure() { + const { cancelButton, settingsButton, tryAgainButton } = + SelectTranslationsPanel.elements; + await SelectTranslationsTestUtils.waitForPanelState("init-failure"); + SelectTranslationsTestUtils.#assertPanelElementVisibility({ + header: true, + betaIcon: true, + cancelButton: true, + initFailureContent: true, + initFailureMessageBar: true, + settingsButton: true, + tryAgainButton: true, + }); + SharedTranslationsTestUtils._assertTabIndexOrder([ + settingsButton, + ...(AppConstants.platform === "win" + ? [tryAgainButton, cancelButton] + : [cancelButton, tryAgainButton]), + ]); + SharedTranslationsTestUtils._assertHasFocus(tryAgainButton); + } + + /** + * Asserts that the SelectTranslationsPanel UI matches the expected + * state when a translation has failed to complete. + */ + static async assertPanelViewTranslationFailure() { + const { + cancelButton, + fromMenuList, + settingsButton, + toMenuList, + translationFailureMessageBar, + tryAgainButton, + } = SelectTranslationsPanel.elements; + await SelectTranslationsTestUtils.waitForPanelState("translation-failure"); + SelectTranslationsTestUtils.#assertPanelElementVisibility({ + header: true, + betaIcon: true, + cancelButton: true, + fromLabel: true, + fromMenuList: true, + mainContent: true, + settingsButton: true, + toLabel: true, + toMenuList: true, + translationFailureMessageBar: true, + tryAgainButton: true, + }); + is( + document.activeElement, + tryAgainButton, + "The try-again button should have focus." + ); + is( + translationFailureMessageBar.getAttribute("role"), + "alert", + "The translation failure message bar is an alert." + ); + SharedTranslationsTestUtils._assertTabIndexOrder([ + settingsButton, + fromMenuList, + toMenuList, + ...(AppConstants.platform === "win" + ? [tryAgainButton, cancelButton] + : [cancelButton, tryAgainButton]), + ]); + SharedTranslationsTestUtils._assertHasFocus(tryAgainButton); } static #assertPanelTextAreaDirection(langTag = null) { @@ -1571,23 +1773,65 @@ class SelectTranslationsTestUtils { } /** + * Asserts that the SelectTranslationsPanel UI matches the expected + * state when the panel has completed its translation. + */ + static async assertPanelViewUnsupportedLanguage() { + await SelectTranslationsTestUtils.waitForPanelState("unsupported"); + const { + doneButtonSecondary, + settingsButton, + translateButton, + tryAnotherSourceMenuList, + unsupportedLanguageMessageBar, + } = SelectTranslationsPanel.elements; + SelectTranslationsTestUtils.#assertPanelElementVisibility({ + betaIcon: true, + doneButtonSecondary: true, + header: true, + settingsButton: true, + translateButton: true, + tryAnotherSourceMenuList: true, + unsupportedLanguageContent: true, + unsupportedLanguageMessageBar: true, + }); + SelectTranslationsTestUtils.#assertConditionalUIEnabled({ + doneButtonSecondary: true, + translateButton: false, + }); + ok( + translateButton.disabled, + "The translate button should be disabled when first shown." + ); + SharedTranslationsTestUtils._assertL10nId( + unsupportedLanguageMessageBar, + "select-translations-panel-unsupported-language-message-known" + ); + SharedTranslationsTestUtils._assertHasFocus(tryAnotherSourceMenuList); + SharedTranslationsTestUtils._assertTabIndexOrder([ + settingsButton, + tryAnotherSourceMenuList, + doneButtonSecondary, + ]); + } + + /** * Asserts that the SelectTranslationsPanel translated text area is * both scrollable and scrolled to the top. */ - static #assertPanelTextAreaOverflow() { + static async #assertPanelTextAreaOverflow() { const { textArea } = SelectTranslationsPanel.elements; - is( - textArea.style.overflow, - "auto", - "The translated-text area should be scrollable." - ); - if (textArea.scrollHeight > textArea.clientHeight) { - is( - textArea.scrollTop, - 0, - "The translated-text area should be scrolled to the top." + if (textArea.style.overflow !== "auto") { + await BrowserTestUtils.waitForMutationCondition( + textArea, + { attributes: true, attributeFilter: ["style"] }, + () => textArea.style.overflow === "auto" ); } + if (textArea.scrollHeight > textArea.clientHeight) { + info("Ensuring that the textarea is scrolled to the top."); + await waitForCondition(() => textArea.scrollTop === 0); + } } /** @@ -1619,88 +1863,44 @@ class SelectTranslationsTestUtils { * Asserts that the SelectTranslationsPanel UI matches the expected * state when the panel is actively translating text. */ - static assertPanelViewActivelyTranslating() { + static async assertPanelViewActivelyTranslating() { const { textArea } = SelectTranslationsPanel.elements; + const isFullPageTranslationsRestrictedForPage = + TranslationsParent.isFullPageTranslationsRestrictedForPage(gBrowser); + await SelectTranslationsTestUtils.waitForPanelState("translating"); ok( textArea.classList.contains("translating"), "The textarea should have the translating class." ); SelectTranslationsTestUtils.#assertPanelElementVisibility({ - ...SelectTranslationsTestUtils.#alwaysPresentElements, + betaIcon: true, + copyButton: true, + doneButtonPrimary: true, + fromLabel: true, + fromMenuList: true, + header: true, + mainContent: true, + settingsButton: true, + textArea: true, + toLabel: true, + toMenuList: true, + translateFullPageButton: !isFullPageTranslationsRestrictedForPage, }); SelectTranslationsTestUtils.#assertPanelHasTranslatingPlaceholder(); } /** - * Asserts that the SelectTranslationsPanel UI matches the expected - * state when no from-language is selected in the panel. - */ - static async assertPanelViewNoFromLangSelected() { - const { textArea } = SelectTranslationsPanel.elements; - ok( - !textArea.classList.contains("translating"), - "The textarea should not have the translating class." - ); - SelectTranslationsTestUtils.#assertPanelElementVisibility({ - ...SelectTranslationsTestUtils.#alwaysPresentElements, - }); - await SelectTranslationsTestUtils.#assertPanelHasIdlePlaceholder(); - SelectTranslationsTestUtils.#assertConditionalUIEnabled({ - textArea: false, - copyButton: false, - translateFullPageButton: false, - }); - SelectTranslationsTestUtils.assertSelectedFromLanguage(null); - } - - /** - * Asserts that the SelectTranslationsPanel UI matches the expected - * state when no to-language is selected in the panel. - */ - static async assertPanelViewNoToLangSelected() { - const { textArea } = SelectTranslationsPanel.elements; - ok( - !textArea.classList.contains("translating"), - "The textarea should not have the translating class." - ); - SelectTranslationsTestUtils.#assertPanelElementVisibility({ - ...SelectTranslationsTestUtils.#alwaysPresentElements, - }); - SelectTranslationsTestUtils.assertSelectedToLanguage(null); - SelectTranslationsTestUtils.#assertConditionalUIEnabled({ - textArea: false, - copyButton: false, - translateFullPageButton: false, - }); - await SelectTranslationsTestUtils.#assertPanelHasIdlePlaceholder(); - } - - /** - * Asserts that the SelectTranslationsPanel UI contains the - * idle placeholder text. - */ - static async #assertPanelHasIdlePlaceholder() { - const { textArea } = SelectTranslationsPanel.elements; - const expected = await document.l10n.formatValue( - "select-translations-panel-idle-placeholder-text" - ); - is( - textArea.value, - expected, - "Translated text area should be the idle placeholder." - ); - SelectTranslationsTestUtils.#assertPanelTextAreaDirection(); - } - - /** * Asserts that the SelectTranslationsPanel UI contains the * translating placeholder text. */ static async #assertPanelHasTranslatingPlaceholder() { - const { textArea } = SelectTranslationsPanel.elements; + const { textArea, fromMenuList, toMenuList } = + SelectTranslationsPanel.elements; const expected = await document.l10n.formatValue( "select-translations-panel-translating-placeholder-text" ); + const isFullPageTranslationsRestrictedForPage = + TranslationsParent.isFullPageTranslationsRestrictedForPage(gBrowser); is( textArea.value, expected, @@ -1710,7 +1910,10 @@ class SelectTranslationsTestUtils { SelectTranslationsTestUtils.#assertConditionalUIEnabled({ textArea: true, copyButton: false, - translateFullPageButton: true, + doneButtonPrimary: true, + translateFullPageButton: + fromMenuList.value !== toMenuList.value && + !isFullPageTranslationsRestrictedForPage, }); } @@ -1723,6 +1926,27 @@ class SelectTranslationsTestUtils { SelectTranslationsPanel.elements; const fromLanguage = fromMenuList.value; const toLanguage = toMenuList.value; + const isFullPageTranslationsRestrictedForPage = + TranslationsParent.isFullPageTranslationsRestrictedForPage(gBrowser); + + SelectTranslationsTestUtils.#assertPanelTextAreaDirection(toLanguage); + SelectTranslationsTestUtils.#assertConditionalUIEnabled({ + textArea: true, + copyButton: true, + doneButtonPrimary: true, + translateFullPageButton: + fromLanguage !== toLanguage && !isFullPageTranslationsRestrictedForPage, + }); + + if (fromLanguage === toLanguage) { + is( + SelectTranslationsPanel.getSourceText(), + SelectTranslationsPanel.getTranslatedText(), + "The source text should passthrough as the translated text." + ); + return; + } + const translatedSuffix = ` [${fromLanguage} to ${toLanguage}]`; ok( textArea.value.endsWith(translatedSuffix), @@ -1734,45 +1958,32 @@ class SelectTranslationsTestUtils { translatedSuffix.length, "Expected translated text length to correspond to the source text length." ); - SelectTranslationsTestUtils.#assertPanelTextAreaDirection(toLanguage); - SelectTranslationsTestUtils.#assertConditionalUIEnabled({ - textArea: true, - copyButton: true, - translateFullPageButton: true, - }); } /** * Asserts the enabled state of action buttons in the SelectTranslationsPanel. * - * @param {boolean} expectEnabled - Whether the buttons should be enabled (true) or not (false). + * @param {Record<string, boolean>} enabledStates + * - An object that maps whether each element should be enabled (true) or disabled (false). */ - static #assertConditionalUIEnabled({ - copyButton: copyButtonEnabled, - translateFullPageButton: translateFullPageButtonEnabled, - textArea: textAreaEnabled, - }) { - const { copyButton, translateFullPageButton, textArea } = - SelectTranslationsPanel.elements; - is( - copyButton.disabled, - !copyButtonEnabled, - `The copy button should be ${copyButtonEnabled ? "enabled" : "disabled"}.` - ); - is( - translateFullPageButton.disabled, - !translateFullPageButtonEnabled, - `The translate-full-page button should be ${ - translateFullPageButtonEnabled ? "enabled" : "disabled" - }.` - ); - is( - textArea.disabled, - !textAreaEnabled, - `The translated-text area should be ${ - textAreaEnabled ? "enabled" : "disabled" - }.` - ); + static #assertConditionalUIEnabled(enabledStates) { + const elements = SelectTranslationsPanel.elements; + + for (const [elementName, expectEnabled] of Object.entries(enabledStates)) { + const element = elements[elementName]; + if (!element) { + throw new Error( + `SelectTranslationsPanel element '${elementName}' not found.` + ); + } + is( + element.disabled, + !expectEnabled, + `The element '${elementName} should be ${ + expectEnabled ? "enabled" : "disabled" + }.` + ); + } } /** @@ -1820,22 +2031,235 @@ class SelectTranslationsTestUtils { */ static async clickDoneButton() { logAction(); - const { doneButton } = SelectTranslationsPanel.elements; - assertVisibility({ visible: { doneButton } }); + const { doneButtonPrimary, doneButtonSecondary } = + SelectTranslationsPanel.elements; + let visibleDoneButton; + let hiddenDoneButton; + if (BrowserTestUtils.isVisible(doneButtonPrimary)) { + visibleDoneButton = doneButtonPrimary; + hiddenDoneButton = doneButtonSecondary; + } else if (BrowserTestUtils.isVisible(doneButtonSecondary)) { + visibleDoneButton = doneButtonSecondary; + hiddenDoneButton = doneButtonPrimary; + } else { + throw new Error( + "Expected either the primary or secondary done button to be visible." + ); + } + assertVisibility({ + visible: { visibleDoneButton }, + hidden: { hiddenDoneButton }, + }); + await SelectTranslationsTestUtils.waitForPanelPopupEvent( + "popuphidden", + () => { + click(visibleDoneButton, "Clicking the done button"); + } + ); + } + + /** + * Simulates clicking the cancel button and waits for the panel to close. + */ + static async clickCancelButton() { + logAction(); + const { cancelButton } = SelectTranslationsPanel.elements; + assertVisibility({ visible: { cancelButton } }); await SelectTranslationsTestUtils.waitForPanelPopupEvent( "popuphidden", () => { - click(doneButton, "Clicking the done button"); + click(cancelButton, "Clicking the cancel button"); } ); } /** + * Simulates clicking the copy button and asserts that all relevant states are correctly updated. + */ + static async clickCopyButton() { + logAction(); + const { copyButton } = SelectTranslationsPanel.elements; + + assertVisibility({ visible: { copyButton } }); + is( + SelectTranslationsPanel.phase(), + "translated", + 'The copy button should only be clickable in the "translated" phase' + ); + + click(copyButton, "Clicking the copy button"); + await waitForCondition( + () => + copyButton.classList.contains("copied") && + copyButton.getAttribute("data-l10n-id") === + "select-translations-panel-copy-button-copied", + "Waiting for copy button to match the copied state." + ); + + const copiedText = SpecialPowers.getClipboardData("text/plain"); + is( + // Because of differences in the clipboard code on Windows, we are going + // to explicitly sanitize carriage returns here when checking equality. + copiedText.replaceAll("\r", ""), + SelectTranslationsPanel.getTranslatedText().replaceAll("\r", ""), + "The clipboard should contain the translated text." + ); + } + + /** + * Simulates clicking the Translate button in the SelectTranslationsPanel, + * then waits for any pending translation effects, based on the provided options. + * + * @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. + * @param {Function} [config.viewAssertion] + * - An optional callback function to execute for asserting the panel UI state. + */ + static async clickTranslateButton({ + downloadHandler, + pivotTranslation, + viewAssertion, + }) { + logAction(); + const { + doneButtonSecondary, + settingsButton, + translateButton, + tryAnotherSourceMenuList, + } = SelectTranslationsPanel.elements; + assertVisibility({ visible: { doneButtonPrimary: translateButton } }); + + ok(!translateButton.disabled, "The translate button should be enabled."); + SharedTranslationsTestUtils._assertTabIndexOrder([ + settingsButton, + tryAnotherSourceMenuList, + ...(AppConstants.platform === "win" + ? [translateButton, doneButtonSecondary] + : [doneButtonSecondary, translateButton]), + ]); + + click(translateButton); + await SelectTranslationsTestUtils.waitForPanelState("translatable"); + if (downloadHandler) { + await this.handleDownloads({ downloadHandler, pivotTranslation }); + } + if (viewAssertion) { + await viewAssertion(); + } + } + + /** + * Simulates clicking the translate-full-page button. + */ + static async clickTranslateFullPageButton() { + logAction(); + const { translateFullPageButton } = SelectTranslationsPanel.elements; + assertVisibility({ visible: { translateFullPageButton } }); + click(translateFullPageButton); + await FullPageTranslationsTestUtils.assertTranslationsButton( + { button: true, circleArrows: true, locale: false, icon: true }, + "The icon presents the loading indicator." + ); + } + + /** + * Simulates clicking the try-again 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. + * @param {Function} [config.viewAssertion] + * - An optional callback function to execute for asserting the panel UI state. + */ + static async clickTryAgainButton({ + downloadHandler, + pivotTranslation, + viewAssertion, + } = {}) { + logAction(); + const { tryAgainButton } = SelectTranslationsPanel.elements; + assertVisibility({ visible: { tryAgainButton } }); + click(tryAgainButton, "Clicking the try-again button"); + await SelectTranslationsTestUtils.waitForPanelState("translatable"); + if (downloadHandler) { + await this.handleDownloads({ downloadHandler, pivotTranslation }); + } + if (viewAssertion) { + await viewAssertion(); + } + } + + /** + * Opens the SelectTranslationsPanel settings menu. + * Requires that the translations panel is already open. + */ + static async openPanelSettingsMenu() { + logAction(); + const { settingsButton } = SelectTranslationsPanel.elements; + assertVisibility({ visible: { settingsButton } }); + await SharedTranslationsTestUtils._waitForPopupEvent( + "select-translations-panel-settings-menupopup", + "popupshown", + () => click(settingsButton, "Opening the settings menu") + ); + const settingsPageMenuItem = document.getElementById( + "select-translations-panel-open-settings-page-menuitem" + ); + const aboutTranslationsMenuItem = document.getElementById( + "select-translations-panel-about-translations-menuitem" + ); + + assertVisibility({ + visible: { + settingsPageMenuItem, + aboutTranslationsMenuItem, + }, + }); + } + + /** + * Clicks the SelectTranslationsPanel settings menu item + * that leads to the Translations Settings in about:preferences. + */ + static clickTranslationsSettingsPageMenuItem() { + logAction(); + const settingsPageMenuItem = document.getElementById( + "select-translations-panel-open-settings-page-menuitem" + ); + assertVisibility({ visible: { settingsPageMenuItem } }); + click(settingsPageMenuItem); + } + + /** * 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.expectMenuItemVisible - Whether the select-translations menu item should be present in the context menu. + * @param {boolean} options.expectedTargetLanguage - The expected target language to be shown in the context menu. + * + * The following options will work on all test pages that have an <h1> element. + * + * @param {boolean} options.selectH1 - Selects the first H1 element of the page. + * @param {boolean} options.openAtH1 - Opens the context menu at the first H1 element of the page. + * + * The following options will work only in the PDF_TEST_PAGE_URL. + * + * @param {boolean} options.selectPdfSpan - Selects the first span of text on the first page of a pdf. + * @param {boolean} options.openAtPdfSpan - Opens the context menu at the first span of text on the first page of a pdf. + * * The following options will only work when testing SELECT_TEST_PAGE_URL. * * @param {boolean} options.selectFrenchSection - Selects the section of French text. @@ -1868,8 +2292,8 @@ class SelectTranslationsTestUtils { const selectorFunction = TranslationsTest.getSelectors()[data.selectorFunctionName]; if (typeof selectorFunction === "function") { - const paragraph = selectorFunction(); - TranslationsTest.selectContentElement(paragraph); + const element = await selectorFunction(); + TranslationsTest.selectContentElement(element); } }, { selectorFunctionName } @@ -1877,6 +2301,8 @@ class SelectTranslationsTestUtils { } }; + await maybeSelectContentFrom("H1"); + await maybeSelectContentFrom("PdfSpan"); await maybeSelectContentFrom("FrenchSection"); await maybeSelectContentFrom("EnglishSection"); await maybeSelectContentFrom("SpanishSection"); @@ -1898,7 +2324,7 @@ class SelectTranslationsTestUtils { const selectorFunction = TranslationsTest.getSelectors()[data.selectorFunctionName]; if (typeof selectorFunction === "function") { - const element = selectorFunction(); + const element = await selectorFunction(); await TranslationsTest.rightClickContentElement(element); } }, @@ -1909,6 +2335,8 @@ class SelectTranslationsTestUtils { } }; + await maybeOpenContextMenuAt("H1"); + await maybeOpenContextMenuAt("PdfSpan"); await maybeOpenContextMenuAt("FrenchSection"); await maybeOpenContextMenuAt("EnglishSection"); await maybeOpenContextMenuAt("SpanishSection"); @@ -1931,28 +2359,10 @@ class SelectTranslationsTestUtils { * @returns {Promise<void>} */ static async handleDownloads({ downloadHandler, pivotTranslation }) { - const { textArea } = SelectTranslationsPanel.elements; - if (downloadHandler) { - if (textArea.style.overflow !== "hidden") { - await BrowserTestUtils.waitForMutationCondition( - textArea, - { attributes: true, attributeFilter: ["style"] }, - () => textArea.style.overflow === "hidden" - ); - } - await SelectTranslationsTestUtils.assertPanelViewActivelyTranslating(); await downloadHandler(pivotTranslation ? 2 : 1); } - - if (textArea.style.overflow === "hidden") { - await BrowserTestUtils.waitForMutationCondition( - textArea, - { attributes: true, attributeFilter: ["style"] }, - () => textArea.style.overflow === "auto" - ); - } } /** @@ -1965,6 +2375,7 @@ class SelectTranslationsTestUtils { * @returns {Promise<void>} */ static async changeSelectedFromLanguage(langTags, options) { + logAction(langTags); const { fromMenuList, fromMenuPopup } = SelectTranslationsPanel.elements; const { openDropdownMenu } = options; @@ -1980,6 +2391,28 @@ class SelectTranslationsTestUtils { } /** + * Change the selected language in the try-another-source-language dropdown. + * + * @param {string} langTag - A BCP-47 language tag. + */ + static async changeSelectedTryAnotherSourceLanguage(langTag) { + logAction(langTag); + const { tryAnotherSourceMenuList, translateButton } = + SelectTranslationsPanel.elements; + await SelectTranslationsTestUtils.#changeSelectedLanguageDirectly( + [langTag], + { menuList: tryAnotherSourceMenuList }, + { + onChangeLanguage: () => + ok( + !translateButton.disabled, + "The translate button should be enabled after selecting a language." + ), + } + ); + } + + /** * Switches the selected to-language to the provided language tag. * * @param {string[]} langTags - An array of BCP-47 language tags. @@ -1991,6 +2424,7 @@ class SelectTranslationsTestUtils { * @returns {Promise<void>} */ static async changeSelectedToLanguage(langTags, options) { + logAction(langTags); const { toMenuList, toMenuPopup } = SelectTranslationsPanel.elements; const { openDropdownMenu } = options; @@ -2019,6 +2453,7 @@ class SelectTranslationsTestUtils { */ static async #changeSelectedLanguageDirectly(langTags, elements, options) { const { menuList } = elements; + const { textArea } = SelectTranslationsPanel.elements; const { onChangeLanguage, downloadHandler } = options; for (const langTag of langTags) { @@ -2028,14 +2463,23 @@ class SelectTranslationsTestUtils { () => menuList.value === langTag ); + menuList.focus(); menuList.value = langTag; menuList.dispatchEvent(new Event("command")); await menuListUpdated; } - if (downloadHandler) { - menuList.focus(); + // Either of these events should trigger a translation after the selected + // language has been changed directly. + if (Math.random() < 0.5) { + info("Attempting to trigger translation via text-area focus."); + textArea.focus(); + } else { + info("Attempting to trigger translation via pressing Enter."); EventUtils.synthesizeKey("KEY_Enter"); + } + + if (downloadHandler) { await SelectTranslationsTestUtils.handleDownloads(options); } |