/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; Services.scriptloader.loadSubScript( "chrome://mochitests/content/browser/toolkit/components/translations/tests/browser/shared-head.js", this ); /** * Opens a new tab in the foreground. * * @param {string} url */ async function addTab(url) { logAction(url); const tab = await BrowserTestUtils.openNewForegroundTab( gBrowser, url, true // Wait for load ); return { tab, removeTab() { BrowserTestUtils.removeTab(tab); }, }; } /** * Simulates clicking an element with the mouse. * * @param {element} element - The element to click. * @param {string} [message] - A message to log to info. */ function click(element, message) { logAction(message); return new Promise(resolve => { element.addEventListener( "click", function () { resolve(); }, { once: true } ); EventUtils.synthesizeMouseAtCenter(element, { type: "mousedown", isSynthesized: false, }); EventUtils.synthesizeMouseAtCenter(element, { type: "mouseup", isSynthesized: false, }); }); } /** * Get all elements that match the l10n id. * * @param {string} l10nId * @param {Document} doc * @returns {Element} */ function getAllByL10nId(l10nId, doc = document) { const elements = doc.querySelectorAll(`[data-l10n-id="${l10nId}"]`); if (elements.length === 0) { throw new Error("Could not find the element by l10n id: " + l10nId); } return elements; } /** * Retrieves an element by its Id. * * @param {string} id * @param {Document} [doc] * @returns {Element} * @throws Throws if the element is not visible in the DOM. */ function getById(id, doc = document) { const element = maybeGetById(id, /* ensureIsVisible */ true, doc); if (!element) { throw new Error("The element is not visible in the DOM: #" + id); } return element; } /** * Get an element by its l10n id, as this is a user-visible way to find an element. * The `l10nId` represents the text that a user would actually see. * * @param {string} l10nId * @param {Document} doc * @returns {Element} */ function getByL10nId(l10nId, doc = document) { const elements = doc.querySelectorAll(`[data-l10n-id="${l10nId}"]`); if (elements.length === 0) { throw new Error("Could not find the element by l10n id: " + l10nId); } for (const element of elements) { if (BrowserTestUtils.isVisible(element)) { return element; } } throw new Error("The element is not visible in the DOM: " + l10nId); } /** * Returns the intl display name of a given language tag. * * @param {string} langTag - A BCP-47 language tag. */ const getIntlDisplayName = (() => { let displayNames = null; return langTag => { if (!displayNames) { displayNames = new Services.intl.DisplayNames(undefined, { type: "language", fallback: "none", }); } return displayNames.of(langTag); }; })(); /** * Attempts to retrieve an element by its Id. * * @param {string} id - The Id of the element to retrieve. * @param {boolean} [ensureIsVisible=true] - If set to true, the function will return null when the element is not visible. * @param {Document} [doc=document] - The document from which to retrieve the element. * @returns {Element | null} - The retrieved element. * @throws Throws if no element was found by the given Id. */ function maybeGetById(id, ensureIsVisible = true, doc = document) { const element = doc.getElementById(id); if (!element) { throw new Error("Could not find the element by id: #" + id); } if (!ensureIsVisible) { return element; } if (BrowserTestUtils.isVisible(element)) { return element; } return null; } /** * A non-throwing version of `getByL10nId`. * * @param {string} l10nId * @returns {Element | null} */ function maybeGetByL10nId(l10nId, doc = document) { const selector = `[data-l10n-id="${l10nId}"]`; const elements = doc.querySelectorAll(selector); for (const element of elements) { if (BrowserTestUtils.isVisible(element)) { return element; } } return null; } /** * Provide a uniform way to log actions. This abuses the Error stack to get the callers * of the action. This should help in test debugging. */ function logAction(...params) { const error = new Error(); const stackLines = error.stack.split("\n"); const actionName = stackLines[1]?.split("@")[0] ?? ""; const taskFileLocation = stackLines[2]?.split("@")[1] ?? ""; if (taskFileLocation.includes("head.js")) { // Only log actions that were done at the test level. return; } info(`Action: ${actionName}(${params.join(", ")})`); info( `Source: ${taskFileLocation.replace( "chrome://mochitests/content/browser/", "" )}` ); } /** * Navigate to a URL and indicate a message as to why. */ async function navigate( message, { url, onOpenPanel = null, downloadHandler = null, pivotTranslation = false } ) { logAction(); // When the translations panel is open from the app menu, // it doesn't close on navigate the way that it does when it's // open from the translations button, so ensure that we always // close it when we navigate to a new page. await closeAllOpenPanelsAndMenus(); info(message); // Load a blank page first to ensure that tests don't hang. // I don't know why this is needed, but it appears to be necessary. BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, BLANK_PAGE); await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); const loadTargetPage = async () => { BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, url); await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); if (downloadHandler) { await FullPageTranslationsTestUtils.assertTranslationsButton( { button: true, circleArrows: true, locale: false, icon: true }, "The icon presents the loading indicator." ); await downloadHandler(pivotTranslation ? 2 : 1); } }; info(`Loading url: "${url}"`); if (onOpenPanel) { await FullPageTranslationsTestUtils.waitForPanelPopupEvent( "popupshown", loadTargetPage, onOpenPanel ); } else { await loadTargetPage(); } } /** * Switches to a given tab. * * @param {object} tab - The tab to switch to * @param {string} name */ async function switchTab(tab, name) { logAction("tab", name); gBrowser.selectedTab = tab; await new Promise(resolve => setTimeout(resolve, 0)); } /** * Click the reader-mode button if the reader-mode button is available. * Fails if the reader-mode button is hidden. */ async function toggleReaderMode() { logAction(); const readerButton = document.getElementById("reader-mode-button"); await waitForCondition(() => readerButton.hidden === false); readerButton.getAttribute("readeractive") ? info("Exiting reader mode") : info("Entering reader mode"); const readyPromise = readerButton.getAttribute("readeractive") ? waitForCondition(() => !readerButton.getAttribute("readeractive")) : BrowserTestUtils.waitForContentEvent( gBrowser.selectedBrowser, "AboutReaderContentReady" ); click(readerButton, "Clicking the reader-mode button"); await readyPromise; } /** * A collection of shared functionality utilized by * FullPageTranslationsTestUtils and SelectTranslationsTestUtils. * * Using functions from the aforementioned classes is preferred over * using functions from this class directly. */ class SharedTranslationsTestUtils { /** * Asserts that the specified element currently has focus. * * @param {Element} element - The element to check for focus. */ static _assertHasFocus(element) { is( document.activeElement, element, `The element '${element.id}' should have focus.` ); } /** * 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 * @param {string} expectedId - The expected id that mainViewId is set to. */ static _assertPanelMainViewId(panel, expectedId) { const mainViewId = panel.elements.multiview.getAttribute("mainViewId"); is( mainViewId, expectedId, "The mainViewId should match its expected value" ); } /** * Asserts that the selected language in the menu matches the langTag or l10nId. * * @param {Element} menuList - The menu list element to check. * @param {object} options - Options containing 'langTag' and 'l10nId' to assert against. * @param {string} [options.langTag] - The BCP-47 language tag to match. * @param {string} [options.l10nId] - The localization Id to match. */ static _assertSelectedLanguage(menuList, { langTag, l10nId }) { ok( menuList.label, `The label for the menulist ${menuList.id} should not be empty.` ); if (langTag) { is( menuList.value, langTag, `Expected ${menuList.id} selection to match '${langTag}'` ); } if (l10nId) { is( menuList.getAttribute("data-l10n-id"), l10nId, `Expected ${menuList.id} l10nId to match '${l10nId}'` ); } } /** * Asserts the visibility of the given elements based on the given expectations. * * @param {object} elements - An object containing the elements to be checked for visibility. * @param {object} expectations - An object where each property corresponds to a property in elements, * and its value is a boolean indicating whether the element should * be visible (true) or hidden (false). * @throws Throws if elements does not contain a property for each property in expectations. */ static _assertPanelElementVisibility(elements, expectations) { const hidden = {}; const visible = {}; for (const propertyName in expectations) { ok( elements.hasOwnProperty(propertyName), `Expected panel elements to have property ${propertyName}` ); if (expectations[propertyName]) { visible[propertyName] = elements[propertyName]; } else { hidden[propertyName] = elements[propertyName]; } } assertVisibility({ hidden, visible }); } /** * 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. * * Optionally executes a postEventAssertion function once the event occurs. * * @param {string} elementId - The Id of the element to wait for the event on. * @param {string} eventName - The name of the event to wait for. * @param {Function} callback - A callback function to execute immediately before waiting for the event. * This is often used to trigger the event on the expected element. * @param {Function|null} [postEventAssertion=null] - An optional callback function to execute after * the event has occurred. * @param {ChromeWindow} [win] * @throws Throws if the element with the specified `elementId` does not exist. * @returns {Promise} */ static async _waitForPopupEvent( elementId, eventName, callback, postEventAssertion = null, win = window ) { const element = win.document.getElementById(elementId); if (!element) { throw new Error( `Unable to find the ${elementId} element in the document.` ); } const promise = BrowserTestUtils.waitForEvent(element, eventName); await callback(); info(`Waiting for the ${elementId} ${eventName} event`); await promise; if (postEventAssertion) { await postEventAssertion(); } // Wait a single tick on the event loop. await new Promise(resolve => setTimeout(resolve, 0)); } } /** * A class containing test utility functions specific to testing full-page translations. */ class FullPageTranslationsTestUtils { /** * A collection of element visibility expectations for the default panel view. */ static #defaultViewVisibilityExpectations = { cancelButton: true, fromMenuList: true, fromLabel: true, header: true, langSelection: true, toMenuList: true, toLabel: true, translateButton: true, }; /** * Asserts that the state of a checkbox with a given dataL10nId is * checked or not, based on the value of expected being true or false. * * @param {string} dataL10nId - The data-l10n-id of the checkbox. * @param {object} expectations * @param {string} expectations.langTag - A BCP-47 language tag. * @param {boolean} expectations.checked - Whether the checkbox is expected to be checked. * @param {boolean} expectations.disabled - Whether the menuitem is expected to be disabled. */ static async #assertCheckboxState( dataL10nId, { langTag = null, checked = true, disabled = false } ) { const menuItems = getAllByL10nId(dataL10nId); for (const menuItem of menuItems) { if (langTag) { const { args: { language }, } = document.l10n.getAttributes(menuItem); is( language, getIntlDisplayName(langTag), `Should match expected language display name for ${dataL10nId}` ); } is( menuItem.disabled, disabled, `Should match expected disabled state for ${dataL10nId}` ); await waitForCondition( () => menuItem.getAttribute("checked") === (checked ? "true" : "false"), "Waiting for checkbox state" ); is( menuItem.getAttribute("checked"), checked ? "true" : "false", `Should match expected checkbox state for ${dataL10nId}` ); } } /** * Asserts that the always-offer-translations checkbox matches the expected checked state. * * @param {boolean} checked */ static async assertIsAlwaysOfferTranslationsEnabled(checked) { info( `Checking that always-offer-translations is ${ checked ? "enabled" : "disabled" }` ); await FullPageTranslationsTestUtils.#assertCheckboxState( "translations-panel-settings-always-offer-translation", { checked } ); } /** * Asserts that the always-translate-language checkbox matches the expected checked state. * * @param {string} langTag - A BCP-47 language tag * @param {object} expectations * @param {boolean} expectations.checked - Whether the checkbox is expected to be checked. * @param {boolean} expectations.disabled - Whether the menuitem is expected to be disabled. */ static async assertIsAlwaysTranslateLanguage( langTag, { checked = true, disabled = false } ) { info( `Checking that always-translate is ${ checked ? "enabled" : "disabled" } for "${langTag}"` ); await FullPageTranslationsTestUtils.#assertCheckboxState( "translations-panel-settings-always-translate-language", { langTag, checked, disabled } ); } /** * Asserts that the never-translate-language checkbox matches the expected checked state. * * @param {string} langTag - A BCP-47 language tag * @param {object} expectations * @param {boolean} expectations.checked - Whether the checkbox is expected to be checked. * @param {boolean} expectations.disabled - Whether the menuitem is expected to be disabled. */ static async assertIsNeverTranslateLanguage( langTag, { checked = true, disabled = false } ) { info( `Checking that never-translate is ${ checked ? "enabled" : "disabled" } for "${langTag}"` ); await FullPageTranslationsTestUtils.#assertCheckboxState( "translations-panel-settings-never-translate-language", { langTag, checked, disabled } ); } /** * Asserts that the never-translate-site checkbox matches the expected checked state. * * @param {string} url - The url of a website * @param {object} expectations * @param {boolean} expectations.checked - Whether the checkbox is expected to be checked. * @param {boolean} expectations.disabled - Whether the menuitem is expected to be disabled. */ static async assertIsNeverTranslateSite( url, { checked = true, disabled = false } ) { info( `Checking that never-translate is ${ checked ? "enabled" : "disabled" } for "${url}"` ); await FullPageTranslationsTestUtils.#assertCheckboxState( "translations-panel-settings-never-translate-site", { checked, disabled } ); } /** * Asserts that the proper language tags are shown on the translations button. * * @param {string} fromLanguage - The BCP-47 language tag being translated from. * @param {string} toLanguage - The BCP-47 language tag being translated into. * @param {ChromeWindow} win */ static async assertLangTagIsShownOnTranslationsButton( fromLanguage, toLanguage, win = window ) { info( `Ensuring that the translations button displays the language tag "${toLanguage}"` ); const { button, locale } = await FullPageTranslationsTestUtils.assertTranslationsButton( { button: true, circleArrows: false, locale: true, icon: true }, "The icon presents the locale.", win ); is( locale.innerText, toLanguage, `The expected language tag "${toLanguage}" is shown.` ); is( button.getAttribute("data-l10n-id"), "urlbar-translations-button-translated" ); const fromLangDisplay = getIntlDisplayName(fromLanguage); const toLangDisplay = getIntlDisplayName(toLanguage); is( button.getAttribute("data-l10n-args"), `{"fromLanguage":"${fromLangDisplay}","toLanguage":"${toLangDisplay}"}` ); } /** * Asserts that the Spanish test page has been translated by checking * that the H1 element has been modified from its original form. * * @param {string} fromLanguage - The BCP-47 language tag being translated from. * @param {string} toLanguage - The BCP-47 language tag being translated into. * @param {Function} runInPage - Allows running a closure in the content page. * @param {string} message - An optional message to log to info. * @param {ChromeWindow} [win] */ static async assertPageIsTranslated( fromLanguage, toLanguage, runInPage, message = null, win = window ) { if (message) { info(message); } info("Checking that the page is translated"); const callback = async (TranslationsTest, { fromLang, toLang }) => { const { getH1 } = TranslationsTest.getSelectors(); await TranslationsTest.assertTranslationResult( "The page's H1 is translated.", getH1, `DON QUIJOTE DE LA MANCHA [${fromLang} to ${toLang}, html]` ); }; await runInPage(callback, { fromLang: fromLanguage, toLang: toLanguage }); await FullPageTranslationsTestUtils.assertLangTagIsShownOnTranslationsButton( fromLanguage, toLanguage, win ); } /** * Asserts that the Spanish test page is untranslated by checking * that the H1 element is still in its original Spanish form. * * @param {Function} runInPage - Allows running a closure in the content page. * @param {string} message - An optional message to log to info. */ static async assertPageIsUntranslated(runInPage, message = null) { if (message) { info(message); } info("Checking that the page is untranslated"); await runInPage(async TranslationsTest => { const { getH1 } = TranslationsTest.getSelectors(); await TranslationsTest.assertTranslationResult( "The page's H1 is untranslated and in the original Spanish.", getH1, "Don Quijote de La Mancha" ); }); } /** * Asserts that for each provided expectation, the visible state of the corresponding * element in FullPageTranslationsPanel.elements both exists and matches the visibility expectation. * * @param {object} expectations * A list of expectations for the visibility of any subset of FullPageTranslationsPanel.elements */ static #assertPanelElementVisibility(expectations = {}) { SharedTranslationsTestUtils._assertPanelElementVisibility( FullPageTranslationsPanel.elements, { cancelButton: false, changeSourceLanguageButton: false, dismissErrorButton: false, error: false, errorMessage: false, errorMessageHint: false, errorHintAction: false, fromMenuList: false, fromLabel: false, header: false, intro: false, introLearnMoreLink: false, langSelection: false, restoreButton: false, toLabel: false, toMenuList: false, translateButton: false, unsupportedHeader: false, unsupportedHint: false, unsupportedLearnMoreLink: false, // Overwrite any of the above defaults with the passed in expectations. ...expectations, } ); } /** * Asserts that the FullPageTranslationsPanel header has the expected l10nId. * * @param {string} l10nId - The expected data-l10n-id of the header. */ static #assertPanelHeaderL10nId(l10nId) { const { header } = FullPageTranslationsPanel.elements; SharedTranslationsTestUtils._assertL10nId(header, l10nId); } /** * Asserts that the FullPageTranslationsPanel error has the expected l10nId. * * @param {string} l10nId - The expected data-l10n-id of the error. */ static #assertPanelErrorL10nId(l10nId) { const { errorMessage } = FullPageTranslationsPanel.elements; SharedTranslationsTestUtils._assertL10nId(errorMessage, l10nId); } /** * Asserts that the mainViewId of the panel matches the given string. * * @param {string} expectedId */ static #assertPanelMainViewId(expectedId) { SharedTranslationsTestUtils._assertPanelMainViewId( FullPageTranslationsPanel, expectedId ); } /** * Asserts that panel element visibility matches the default panel view. */ static assertPanelViewDefault() { info("Checking that the panel shows the default view"); FullPageTranslationsTestUtils.#assertPanelMainViewId( "full-page-translations-panel-view-default" ); FullPageTranslationsTestUtils.#assertPanelElementVisibility({ ...FullPageTranslationsTestUtils.#defaultViewVisibilityExpectations, }); FullPageTranslationsTestUtils.#assertPanelHeaderL10nId( "translations-panel-header" ); } /** * Asserts that panel element visibility matches the initialization-failure view. */ static assertPanelViewInitFailure() { info("Checking that the panel shows the default view"); const { translateButton } = FullPageTranslationsPanel.elements; FullPageTranslationsTestUtils.#assertPanelMainViewId( "full-page-translations-panel-view-default" ); FullPageTranslationsTestUtils.#assertPanelElementVisibility({ cancelButton: true, error: true, errorMessage: true, errorMessageHint: true, errorHintAction: true, header: true, translateButton: true, }); is( translateButton.disabled, true, "The translate button should be disabled." ); FullPageTranslationsTestUtils.#assertPanelHeaderL10nId( "translations-panel-header" ); } /** * Asserts that panel element visibility matches the panel error view. */ static assertPanelViewError() { info("Checking that the panel shows the error view"); FullPageTranslationsTestUtils.#assertPanelMainViewId( "full-page-translations-panel-view-default" ); FullPageTranslationsTestUtils.#assertPanelElementVisibility({ error: true, errorMessage: true, ...FullPageTranslationsTestUtils.#defaultViewVisibilityExpectations, }); FullPageTranslationsTestUtils.#assertPanelHeaderL10nId( "translations-panel-header" ); FullPageTranslationsTestUtils.#assertPanelErrorL10nId( "translations-panel-error-translating" ); } /** * Asserts that the panel element visibility matches the panel loading view. */ static assertPanelViewLoading() { info("Checking that the panel shows the loading view"); FullPageTranslationsTestUtils.assertPanelViewDefault(); const loadingButton = getByL10nId( "translations-panel-translate-button-loading" ); ok(loadingButton, "The loading button is present"); ok(loadingButton.disabled, "The loading button is disabled"); } /** * Asserts that panel element visibility matches the panel first-show view. */ static assertPanelViewFirstShow() { info("Checking that the panel shows the first-show view"); FullPageTranslationsTestUtils.#assertPanelMainViewId( "full-page-translations-panel-view-default" ); FullPageTranslationsTestUtils.#assertPanelElementVisibility({ intro: true, introLearnMoreLink: true, ...FullPageTranslationsTestUtils.#defaultViewVisibilityExpectations, }); FullPageTranslationsTestUtils.#assertPanelHeaderL10nId( "translations-panel-intro-header" ); } /** * Asserts that panel element visibility matches the panel first-show error view. */ static assertPanelViewFirstShowError() { info("Checking that the panel shows the first-show error view"); FullPageTranslationsTestUtils.#assertPanelMainViewId( "full-page-translations-panel-view-default" ); FullPageTranslationsTestUtils.#assertPanelElementVisibility({ error: true, intro: true, introLearnMoreLink: true, ...FullPageTranslationsTestUtils.#defaultViewVisibilityExpectations, }); FullPageTranslationsTestUtils.#assertPanelHeaderL10nId( "translations-panel-intro-header" ); } /** * Asserts that panel element visibility matches the panel revisit view. */ static assertPanelViewRevisit() { info("Checking that the panel shows the revisit view"); FullPageTranslationsTestUtils.#assertPanelMainViewId( "full-page-translations-panel-view-default" ); FullPageTranslationsTestUtils.#assertPanelElementVisibility({ header: true, langSelection: true, restoreButton: true, toLabel: true, toMenuList: true, translateButton: true, }); FullPageTranslationsTestUtils.#assertPanelHeaderL10nId( "translations-panel-revisit-header" ); } /** * Asserts that panel element visibility matches the panel unsupported language view. */ static assertPanelViewUnsupportedLanguage() { info("Checking that the panel shows the unsupported-language view"); FullPageTranslationsTestUtils.#assertPanelMainViewId( "full-page-translations-panel-view-unsupported-language" ); FullPageTranslationsTestUtils.#assertPanelElementVisibility({ changeSourceLanguageButton: true, dismissErrorButton: true, unsupportedHeader: true, unsupportedHint: true, unsupportedLearnMoreLink: true, }); } /** * Asserts that the selected from-language matches the provided language tag. * * @param {object} options - Options containing 'langTag' and 'l10nId' to assert against. * @param {string} [options.langTag] - The BCP-47 language tag to match. * @param {string} [options.l10nId] - The localization Id to match. */ static assertSelectedFromLanguage({ langTag, l10nId }) { const { fromMenuList } = FullPageTranslationsPanel.elements; SharedTranslationsTestUtils._assertSelectedLanguage(fromMenuList, { langTag, l10nId, }); } /** * Asserts that the selected to-language matches the provided language tag. * * @param {object} options - Options containing 'langTag' and 'l10nId' to assert against. * @param {string} [options.langTag] - The BCP-47 language tag to match. * @param {string} [options.l10nId] - The localization Id to match. */ static assertSelectedToLanguage({ langTag, l10nId }) { const { toMenuList } = FullPageTranslationsPanel.elements; SharedTranslationsTestUtils._assertSelectedLanguage(toMenuList, { langTag, l10nId, }); } /** * Assert some property about the translations button. * * @param {Record} visibleAssertions * @param {string} message The message for the assertion. * @param {ChromeWindow} [win] * @returns {HTMLElement} */ static async assertTranslationsButton( visibleAssertions, message, win = window ) { const elements = { button: win.document.getElementById("translations-button"), icon: win.document.getElementById("translations-button-icon"), circleArrows: win.document.getElementById( "translations-button-circle-arrows" ), locale: win.document.getElementById("translations-button-locale"), }; for (const [name, element] of Object.entries(elements)) { if (!element) { throw new Error("Could not find the " + name); } } try { // Test that the visibilities match. await waitForCondition(() => { for (const [name, visible] of Object.entries(visibleAssertions)) { if (elements[name].hidden === visible) { return false; } } return true; }, message); } catch (error) { // On a mismatch, report it. for (const [name, expected] of Object.entries(visibleAssertions)) { is(!elements[name].hidden, expected, `Visibility for "${name}"`); } } ok(true, message); return elements; } /** * Simulates the effect of clicking the always-offer-translations menuitem. * Requires that the settings menu of the translations panel is open, * otherwise the test will fail. */ static async clickAlwaysOfferTranslations() { logAction(); await FullPageTranslationsTestUtils.#clickSettingsMenuItemByL10nId( "translations-panel-settings-always-offer-translation" ); } /** * Simulates the effect of clicking the always-translate-language menuitem. * Requires that the settings menu of the translations panel is open, * otherwise the test will fail. */ static async clickAlwaysTranslateLanguage({ downloadHandler = null, pivotTranslation = false, } = {}) { logAction(); await FullPageTranslationsTestUtils.#clickSettingsMenuItemByL10nId( "translations-panel-settings-always-translate-language" ); if (downloadHandler) { await FullPageTranslationsTestUtils.assertTranslationsButton( { button: true, circleArrows: true, locale: false, icon: true }, "The icon presents the loading indicator." ); await downloadHandler(pivotTranslation ? 2 : 1); } } /** * Simulates clicking the cancel button. */ static async clickCancelButton() { logAction(); const { cancelButton } = FullPageTranslationsPanel.elements; assertVisibility({ visible: { cancelButton } }); await FullPageTranslationsTestUtils.waitForPanelPopupEvent( "popuphidden", () => { click(cancelButton, "Clicking the cancel button"); } ); } /** * Simulates clicking the change-source-language button. * * @param {object} config * @param {boolean} config.firstShow * - True if the first-show view should be expected * False if the default view should be expected */ static async clickChangeSourceLanguageButton({ firstShow = false } = {}) { logAction(); const { changeSourceLanguageButton } = FullPageTranslationsPanel.elements; assertVisibility({ visible: { changeSourceLanguageButton } }); await FullPageTranslationsTestUtils.waitForPanelPopupEvent( "popupshown", () => { click( changeSourceLanguageButton, "Click the change-source-language button" ); }, firstShow ? FullPageTranslationsTestUtils.assertPanelViewFirstShow : FullPageTranslationsTestUtils.assertPanelViewDefault ); } /** * Simulates clicking the dismiss-error button. */ static async clickDismissErrorButton() { logAction(); const { dismissErrorButton } = FullPageTranslationsPanel.elements; assertVisibility({ visible: { dismissErrorButton } }); await FullPageTranslationsTestUtils.waitForPanelPopupEvent( "popuphidden", () => { click(dismissErrorButton, "Click the dismiss-error button"); } ); } /** * Simulates the effect of clicking the manage-languages menuitem. * Requires that the settings menu of the translations panel is open, * otherwise the test will fail. */ static async clickManageLanguages() { logAction(); await FullPageTranslationsTestUtils.#clickSettingsMenuItemByL10nId( "translations-panel-settings-manage-languages" ); } /** * Simulates the effect of clicking the never-translate-language menuitem. * Requires that the settings menu of the translations panel is open, * otherwise the test will fail. */ static async clickNeverTranslateLanguage() { logAction(); await FullPageTranslationsTestUtils.#clickSettingsMenuItemByL10nId( "translations-panel-settings-never-translate-language" ); } /** * Simulates the effect of clicking the never-translate-site menuitem. * Requires that the settings menu of the translations panel is open, * otherwise the test will fail. */ static async clickNeverTranslateSite() { logAction(); await FullPageTranslationsTestUtils.#clickSettingsMenuItemByL10nId( "translations-panel-settings-never-translate-site" ); } /** * Simulates clicking the restore-page button. */ static async clickRestoreButton() { logAction(); const { restoreButton } = FullPageTranslationsPanel.elements; assertVisibility({ visible: { restoreButton } }); await FullPageTranslationsTestUtils.waitForPanelPopupEvent( "popuphidden", () => { click(restoreButton, "Click the restore-page button"); } ); } /* * Simulates the effect of toggling a menu item in the translations panel * settings menu. Requires that the settings menu is currently open, * otherwise the test will fail. */ static async #clickSettingsMenuItemByL10nId(l10nId) { info(`Toggling the "${l10nId}" settings menu item.`); click(getByL10nId(l10nId), `Clicking the "${l10nId}" settings menu item.`); await closeFullPagePanelSettingsMenuIfOpen(); } /** * Simulates clicking the translate button. * * @param {object} config * @param {Function} config.downloadHandler * - The function handle expected downloads, resolveDownloads() or rejectDownloads() * Leave as null to test more granularly, such as testing opening the loading view, * or allowing for the automatic downloading of files. * @param {boolean} config.pivotTranslation * - True if the expected translation is a pivot translation, otherwise false. * Affects the number of expected downloads. * @param {ChromeWindow} [config.win] * - An optional ChromeWindow, for multi-window tests. */ static async clickTranslateButton({ downloadHandler = null, pivotTranslation = false, win = window, } = {}) { logAction(); const { translateButton } = win.FullPageTranslationsPanel.elements; assertVisibility({ visible: { translateButton } }); await FullPageTranslationsTestUtils.waitForPanelPopupEvent( "popuphidden", () => { click(translateButton); }, null /* postEventAssertion */, win ); if (downloadHandler) { await FullPageTranslationsTestUtils.assertTranslationsButton( { button: true, circleArrows: true, locale: false, icon: true }, "The icon presents the loading indicator.", win ); await downloadHandler(pivotTranslation ? 2 : 1); } } /** * Opens the translations panel. * * @param {object} config * @param {Function} config.onOpenPanel * - A function to run as soon as the panel opens. * @param {boolean} config.openFromAppMenu * - Open the panel from the app menu. If false, uses the translations button. * @param {boolean} config.openWithKeyboard * - Open the panel by synthesizing the keyboard. If false, synthesizes the mouse. * @param {ChromeWindow} [config.win] * - An optional window for multi-window tests. */ static async openPanel({ onOpenPanel = null, openFromAppMenu = false, openWithKeyboard = false, win = window, }) { logAction(); await closeAllOpenPanelsAndMenus(win); if (openFromAppMenu) { await FullPageTranslationsTestUtils.#openPanelViaAppMenu({ win, onOpenPanel, openWithKeyboard, }); } else { await FullPageTranslationsTestUtils.#openPanelViaTranslationsButton({ win, onOpenPanel, openWithKeyboard, }); } } /** * Opens the translations panel via the app menu. * * @param {object} config * @param {Function} config.onOpenPanel * - A function to run as soon as the panel opens. * @param {boolean} config.openWithKeyboard * - Open the panel by synthesizing the keyboard. If false, synthesizes the mouse. * @param {ChromeWindow} [config.win] */ static async #openPanelViaAppMenu({ onOpenPanel = null, openWithKeyboard = false, win = window, }) { logAction(); const appMenuButton = getById("PanelUI-menu-button", win.document); if (openWithKeyboard) { hitEnterKey(appMenuButton, "Opening the app-menu button with keyboard"); } else { click(appMenuButton, "Opening the app-menu button"); } await BrowserTestUtils.waitForEvent(win.PanelUI.mainView, "ViewShown"); const translateSiteButton = getById( "appMenu-translate-button", win.document ); is( translateSiteButton.disabled, false, "The app-menu translate button should be enabled" ); await FullPageTranslationsTestUtils.waitForPanelPopupEvent( "popupshown", () => { if (openWithKeyboard) { hitEnterKey(translateSiteButton, "Opening the popup with keyboard"); } else { click(translateSiteButton, "Opening the popup"); } }, onOpenPanel ); } /** * Opens the translations panel via the translations button. * * @param {object} config * @param {Function} config.onOpenPanel * - A function to run as soon as the panel opens. * @param {boolean} config.openWithKeyboard * - Open the panel by synthesizing the keyboard. If false, synthesizes the mouse. * @param {ChromeWindow} [config.win] */ static async #openPanelViaTranslationsButton({ onOpenPanel = null, openWithKeyboard = false, win = window, }) { logAction(); const { button } = await FullPageTranslationsTestUtils.assertTranslationsButton( { button: true }, "The translations button is visible.", win ); await FullPageTranslationsTestUtils.waitForPanelPopupEvent( "popupshown", () => { if (openWithKeyboard) { hitEnterKey(button, "Opening the popup with keyboard"); } else { click(button, "Opening the popup"); } }, onOpenPanel, win ); } /** * Opens the translations panel settings menu. * Requires that the translations panel is already open. */ static async openTranslationsSettingsMenu() { logAction(); const gearIcons = getAllByL10nId("translations-panel-settings-button"); for (const gearIcon of gearIcons) { if (BrowserTestUtils.isHidden(gearIcon)) { continue; } click(gearIcon, "Open the settings menu"); info("Waiting for settings menu to open."); const manageLanguages = await waitForCondition(() => maybeGetByL10nId("translations-panel-settings-manage-languages") ); ok( manageLanguages, "The manage languages item should be visible in the settings menu." ); return; } } /** * Switches the selected from-language to the provided language tag. * * @param {string} langTag - A BCP-47 language tag. */ static changeSelectedFromLanguage(langTag) { logAction(langTag); const { fromMenuList } = FullPageTranslationsPanel.elements; fromMenuList.value = langTag; fromMenuList.dispatchEvent(new Event("command")); } /** * Switches the selected to-language to the provided language tag. * * @param {string} langTag - A BCP-47 language tag. */ static changeSelectedToLanguage(langTag) { logAction(langTag); const { toMenuList } = FullPageTranslationsPanel.elements; toMenuList.value = langTag; toMenuList.dispatchEvent(new Event("command")); } /** * XUL popups will fire the popupshown and popuphidden events. These will fire for * any type of popup in the browser. This function waits for one of those events, and * checks that the viewId of the popup is PanelUI-profiler * * @param {"popupshown" | "popuphidden"} eventName * @param {Function} callback * @param {Function} postEventAssertion * An optional assertion to be made immediately after the event occurs. * @param {ChromeWindow} [win] * @returns {Promise} */ static async waitForPanelPopupEvent( eventName, callback, postEventAssertion = null, win = window ) { // De-lazify the panel elements. win.FullPageTranslationsPanel.elements; await SharedTranslationsTestUtils._waitForPopupEvent( "full-page-translations-panel", eventName, callback, postEventAssertion, win ); } } /** * A class containing test utility functions specific to testing select translations. */ class SelectTranslationsTestUtils { /** * Opens the context menu then asserts properties of the translate-selection item in the context menu. * * @param {Function} runInPage - A content-exposed function to run within the context of the page. * @param {object} options - Options for how to open the context menu and what properties to assert about the translate-selection item. * * @param {boolean} options.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

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. * @param {boolean} options.selectFrenchSentence - Selects a French sentence. * @param {boolean} options.selectEnglishSentence - Selects an English sentence. * @param {boolean} options.selectSpanishSentence - Selects a Spanish sentence. * @param {boolean} options.openAtFrenchSection - Opens the context menu at the section of French text. * @param {boolean} options.openAtEnglishSection - Opens the context menu at the section of English text. * @param {boolean} options.openAtSpanishSection - Opens the context menu at the section of Spanish text. * @param {boolean} options.openAtFrenchSentence - Opens the context menu at a French sentence. * @param {boolean} options.openAtEnglishSentence - Opens the context menu at an English sentence. * @param {boolean} options.openAtSpanishSentence - Opens the context menu at a Spanish sentence. * @param {boolean} options.openAtFrenchHyperlink - Opens the context menu at a hyperlinked French text. * @param {boolean} options.openAtEnglishHyperlink - Opens the context menu at an hyperlinked English text. * @param {boolean} options.openAtSpanishHyperlink - Opens the context menu at a hyperlinked Spanish text. * @param {string} [message] - A message to log to info. * @throws Throws an error if the properties of the translate-selection item do not match the expected options. */ static async assertContextMenuTranslateSelectionItem( runInPage, { expectMenuItemVisible, expectedTargetLanguage, selectH1, selectPdfSpan, selectFrenchSection, selectEnglishSection, selectSpanishSection, selectFrenchSentence, selectEnglishSentence, selectSpanishSentence, openAtH1, openAtPdfSpan, openAtFrenchSection, openAtEnglishSection, openAtSpanishSection, openAtFrenchSentence, openAtEnglishSentence, openAtSpanishSentence, openAtFrenchHyperlink, openAtEnglishHyperlink, openAtSpanishHyperlink, }, message ) { logAction(); if (message) { info(message); } await closeAllOpenPanelsAndMenus(); await SelectTranslationsTestUtils.openContextMenu(runInPage, { expectMenuItemVisible, expectedTargetLanguage, selectH1, selectPdfSpan, selectFrenchSection, selectEnglishSection, selectSpanishSection, selectFrenchSentence, selectEnglishSentence, selectSpanishSentence, openAtH1, openAtPdfSpan, openAtFrenchSection, openAtEnglishSection, openAtSpanishSection, openAtFrenchSentence, openAtEnglishSentence, openAtSpanishSentence, openAtFrenchHyperlink, openAtEnglishHyperlink, openAtSpanishHyperlink, }); const menuItem = maybeGetById( "context-translate-selection", /* ensureIsVisible */ false ); if (expectMenuItemVisible !== undefined) { const visibility = expectMenuItemVisible ? "visible" : "hidden"; assertVisibility({ [visibility]: { menuItem } }); } if (expectMenuItemVisible === true) { if (expectedTargetLanguage) { // Target language expected, check for the data-l10n-id with a `{$language}` argument. const expectedL10nId = selectFrenchSection || selectEnglishSection || selectSpanishSection || selectFrenchSentence || selectEnglishSentence || selectSpanishSentence ? "main-context-menu-translate-selection-to-language" : "main-context-menu-translate-link-text-to-language"; await waitForCondition( () => menuItem.getAttribute("data-l10n-id") === expectedL10nId, `Waiting for translate-selection context menu item to localize with target language ${expectedTargetLanguage}` ); is( menuItem.getAttribute("data-l10n-id"), expectedL10nId, "Expected the translate-selection context menu item to be localized with a target language." ); const l10nArgs = JSON.parse(menuItem.getAttribute("data-l10n-args")); is( l10nArgs.language, getIntlDisplayName(expectedTargetLanguage), `Expected the translate-selection context menu item to have the target language '${expectedTargetLanguage}'.` ); } else { // No target language expected, check for the data-l10n-id that has no `{$language}` argument. const expectedL10nId = selectFrenchSection || selectEnglishSection || selectSpanishSection || selectFrenchSentence || selectEnglishSentence || selectSpanishSentence ? "main-context-menu-translate-selection" : "main-context-menu-translate-link-text"; await waitForCondition( () => menuItem.getAttribute("data-l10n-id") === expectedL10nId, "Waiting for translate-selection context menu item to localize without target language." ); is( menuItem.getAttribute("data-l10n-id"), expectedL10nId, "Expected the translate-selection context menu item to be localized without a target language." ); } } } /** * Asserts that for each provided expectation, the visible state of the corresponding * element in FullPageTranslationsPanel.elements both exists and matches the visibility expectation. * * @param {object} expectations * A list of expectations for the visibility of any subset of SelectTranslationsPanel.elements */ static #assertPanelElementVisibility(expectations = {}) { SharedTranslationsTestUtils._assertPanelElementVisibility( SelectTranslationsPanel.elements, { 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, } ); } /** * 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 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({ 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({ copyButton: 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(); 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) { const expectedTextDirection = langTag ? Services.intl.getScriptDirection(langTag) : null; const { textArea } = SelectTranslationsPanel.elements; const actualTextDirection = textArea.getAttribute("dir"); is( actualTextDirection, expectedTextDirection, `The text direction should be ${expectedTextDirection}` ); } /** * 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 async #assertPanelTextAreaOverflow() { const { textArea } = SelectTranslationsPanel.elements; 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); } } /** * Asserts that the SelectTranslationsPanel translated text area is * the correct height for the length of the translated text. */ static #assertPanelTextAreaHeight() { const { textArea } = SelectTranslationsPanel.elements; if ( SelectTranslationsPanel.getSourceText().length < SelectTranslationsPanel.textLengthThreshold ) { is( textArea.style.height, SelectTranslationsPanel.shortTextHeight, "The panel text area should have the short-text height" ); } else { is( textArea.style.height, SelectTranslationsPanel.longTextHeight, "The panel text area should have the long-text height" ); } } /** * Asserts that the SelectTranslationsPanel UI matches the expected * state when the panel is actively translating text. */ 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({ 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 contains the * translating placeholder text. */ static async #assertPanelHasTranslatingPlaceholder() { 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, "Active translation text area should have the translating placeholder." ); SelectTranslationsTestUtils.#assertPanelTextAreaDirection(); SelectTranslationsTestUtils.#assertConditionalUIEnabled({ textArea: true, copyButton: false, doneButtonPrimary: true, translateFullPageButton: fromMenuList.value !== toMenuList.value && !isFullPageTranslationsRestrictedForPage, }); } /** * Asserts that the SelectTranslationsPanel UI contains the * translated text. */ static #assertPanelHasTranslatedText() { const { textArea, fromMenuList, toMenuList } = 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), `Translated text should match ${fromLanguage} to ${toLanguage}` ); is( SelectTranslationsPanel.getSourceText().length, SelectTranslationsPanel.getTranslatedText().length - translatedSuffix.length, "Expected translated text length to correspond to the source text length." ); } /** * Asserts the enabled state of action buttons in the SelectTranslationsPanel. * * @param {Record} enabledStates * - An object that maps whether each element should be enabled (true) or disabled (false). */ 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" }.` ); } } /** * Asserts that the selected from-language matches the provided language tag. * * @param {string} langTag - A BCP-47 language tag. */ static assertSelectedFromLanguage(langTag = null) { const { fromMenuList } = SelectTranslationsPanel.elements; SelectTranslationsTestUtils.#assertSelectedLanguage(fromMenuList, langTag); } /** * Asserts that the selected to-language matches the provided language tag. * * @param {string} langTag - A BCP-47 language tag. */ static assertSelectedToLanguage(langTag = null) { const { toMenuList } = SelectTranslationsPanel.elements; SelectTranslationsTestUtils.#assertSelectedLanguage(toMenuList, langTag); } /** * Asserts the selected language in the given menu list if a langTag is provided. * If no langTag is given, asserts that the menulist displays the localized placeholder. * * @param {object} menuList - The menu list object to check. * @param {string} [langTag] - The optional language tag to assert against. */ static #assertSelectedLanguage(menuList, langTag) { if (langTag) { SharedTranslationsTestUtils._assertSelectedLanguage(menuList, { langTag, }); } else { SharedTranslationsTestUtils._assertSelectedLanguage(menuList, { l10nId: "translations-panel-choose-language", }); SharedTranslationsTestUtils._assertHasFocus(menuList); } } /** * Simulates clicking the done button and waits for the panel to close. */ static async clickDoneButton() { logAction(); 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(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

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. * @param {boolean} options.selectFrenchSentence - Selects a French sentence. * @param {boolean} options.selectEnglishSentence - Selects an English sentence. * @param {boolean} options.selectSpanishSentence - Selects a Spanish sentence. * @param {boolean} options.openAtFrenchSection - Opens the context menu at the section of French text. * @param {boolean} options.openAtEnglishSection - Opens the context menu at the section of English text. * @param {boolean} options.openAtSpanishSection - Opens the context menu at the section of Spanish text. * @param {boolean} options.openAtFrenchSentence - Opens the context menu at a French sentence. * @param {boolean} options.openAtEnglishSentence - Opens the context menu at an English sentence. * @param {boolean} options.openAtSpanishSentence - Opens the context menu at a Spanish sentence. * @param {boolean} options.openAtFrenchHyperlink - Opens the context menu at a hyperlinked French text. * @param {boolean} options.openAtEnglishHyperlink - Opens the context menu at an hyperlinked English text. * @param {boolean} options.openAtSpanishHyperlink - Opens the context menu at a hyperlinked Spanish text. * @throws Throws an error if no valid option was provided for opening the menu. */ static async openContextMenu(runInPage, options) { logAction(); const maybeSelectContentFrom = async keyword => { const conditionVariableName = `select${keyword}`; const selectorFunctionName = `get${keyword}`; if (options[conditionVariableName]) { await runInPage( async (TranslationsTest, data) => { const selectorFunction = TranslationsTest.getSelectors()[data.selectorFunctionName]; if (typeof selectorFunction === "function") { const element = await selectorFunction(); TranslationsTest.selectContentElement(element); } }, { selectorFunctionName } ); } }; await maybeSelectContentFrom("H1"); await maybeSelectContentFrom("PdfSpan"); await maybeSelectContentFrom("FrenchSection"); await maybeSelectContentFrom("EnglishSection"); await maybeSelectContentFrom("SpanishSection"); await maybeSelectContentFrom("FrenchSentence"); await maybeSelectContentFrom("EnglishSentence"); await maybeSelectContentFrom("SpanishSentence"); const maybeOpenContextMenuAt = async keyword => { const optionVariableName = `openAt${keyword}`; const selectorFunctionName = `get${keyword}`; if (options[optionVariableName]) { await SharedTranslationsTestUtils._waitForPopupEvent( "contentAreaContextMenu", "popupshown", async () => { await runInPage( async (TranslationsTest, data) => { const selectorFunction = TranslationsTest.getSelectors()[data.selectorFunctionName]; if (typeof selectorFunction === "function") { const element = await selectorFunction(); await TranslationsTest.rightClickContentElement(element); } }, { selectorFunctionName } ); } ); } }; await maybeOpenContextMenuAt("H1"); await maybeOpenContextMenuAt("PdfSpan"); await maybeOpenContextMenuAt("FrenchSection"); await maybeOpenContextMenuAt("EnglishSection"); await maybeOpenContextMenuAt("SpanishSection"); await maybeOpenContextMenuAt("FrenchSentence"); await maybeOpenContextMenuAt("EnglishSentence"); await maybeOpenContextMenuAt("SpanishSentence"); await maybeOpenContextMenuAt("FrenchHyperlink"); await maybeOpenContextMenuAt("EnglishHyperlink"); await maybeOpenContextMenuAt("SpanishHyperlink"); } /** * Handles language-model downloads for the SelectTranslationsPanel, ensuring that expected * UI states match based on the resolved download state. * * @param {object} options - Configuration options for downloads. * @param {function(number): Promise} options.downloadHandler - The function to resolve or reject the downloads. * @param {boolean} [options.pivotTranslation] - Whether to expect a pivot translation. * * @returns {Promise} */ static async handleDownloads({ downloadHandler, pivotTranslation }) { if (downloadHandler) { await SelectTranslationsTestUtils.assertPanelViewActivelyTranslating(); await downloadHandler(pivotTranslation ? 2 : 1); } } /** * Switches the selected from-language to the provided language tags * * @param {string[]} langTags - An array of BCP-47 language tags. * @param {object} options - Configuration options for the language change. * @param {boolean} options.openDropdownMenu - Determines whether the language change should be made via a dropdown menu or directly. * * @returns {Promise} */ static async changeSelectedFromLanguage(langTags, options) { logAction(langTags); const { fromMenuList, fromMenuPopup } = SelectTranslationsPanel.elements; const { openDropdownMenu } = options; const switchFn = openDropdownMenu ? SelectTranslationsTestUtils.#changeSelectedLanguageViaDropdownMenu : SelectTranslationsTestUtils.#changeSelectedLanguageDirectly; await switchFn( langTags, { menuList: fromMenuList, menuPopup: fromMenuPopup }, options ); } /** * 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. * @param {object} options - Options for selecting paragraphs and opening the context menu. * @param {boolean} options.openDropdownMenu - Determines whether the language change should be made via a dropdown menu or directly. * @param {Function} options.downloadHandler - Handler for initiating downloads post language change, if applicable. * @param {Function} options.onChangeLanguage - Callback function to be executed after the language change. * * @returns {Promise} */ static async changeSelectedToLanguage(langTags, options) { logAction(langTags); const { toMenuList, toMenuPopup } = SelectTranslationsPanel.elements; const { openDropdownMenu } = options; const switchFn = openDropdownMenu ? SelectTranslationsTestUtils.#changeSelectedLanguageViaDropdownMenu : SelectTranslationsTestUtils.#changeSelectedLanguageDirectly; await switchFn( langTags, { menuList: toMenuList, menuPopup: toMenuPopup }, options ); } /** * Directly changes the selected language to each provided language tag without using a dropdown menu. * * @param {string[]} langTags - An array of BCP-47 language tags for direct selection. * @param {object} elements - Elements required for changing the selected language. * @param {Element} elements.menuList - The menu list element where languages are directly changed. * @param {object} options - Configuration options for language change and additional actions. * @param {Function} options.downloadHandler - Handler for initiating downloads post language change, if applicable. * @param {Function} options.onChangeLanguage - Callback function to be executed after the language change. * * @returns {Promise} */ static async #changeSelectedLanguageDirectly(langTags, elements, options) { const { menuList } = elements; const { textArea } = SelectTranslationsPanel.elements; const { onChangeLanguage, downloadHandler } = options; for (const langTag of langTags) { const menuListUpdated = BrowserTestUtils.waitForMutationCondition( menuList, { attributes: true, attributeFilter: ["value"] }, () => menuList.value === langTag ); menuList.focus(); menuList.value = langTag; menuList.dispatchEvent(new Event("command")); await menuListUpdated; } // 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); } if (onChangeLanguage) { await onChangeLanguage(); } } /** * Changes the selected language by opening the dropdown menu for each provided language tag. * * @param {string[]} langTags - An array of BCP-47 language tags for selection via dropdown. * @param {object} elements - Elements involved in the dropdown language selection process. * @param {Element} elements.menuList - The element that triggers the dropdown menu. * @param {Element} elements.menuPopup - The dropdown menu element containing selectable languages. * @param {object} options - Configuration options for language change and additional actions. * @param {Function} options.downloadHandler - Handler for initiating downloads post language change, if applicable. * @param {Function} options.onChangeLanguage - Callback function to be executed after the language change. * * @returns {Promise} */ static async #changeSelectedLanguageViaDropdownMenu( langTags, elements, options ) { const { menuList, menuPopup } = elements; const { onChangeLanguage } = options; for (const langTag of langTags) { await SelectTranslationsTestUtils.waitForPanelPopupEvent( "popupshown", () => click(menuList) ); const menuItem = menuPopup.querySelector(`[value="${langTag}"]`); await SelectTranslationsTestUtils.waitForPanelPopupEvent( "popuphidden", () => { click(menuItem); // Synthesizing a click on the menuitem isn't closing the popup // as a click normally would, so this tab keypress is added to // ensure the popup closes. EventUtils.synthesizeKey("KEY_Tab"); } ); await SelectTranslationsTestUtils.handleDownloads(options); if (onChangeLanguage) { await onChangeLanguage(); } } } /** * Opens the Select Translations panel via the context menu based on specified options. * * @param {Function} runInPage - A content-exposed function to run within the context of the page. * @param {object} options - Options for selecting paragraphs and opening the context menu. * * The following options will only work when testing SELECT_TEST_PAGE_URL. * * @param {string} options.expectedFromLanguage - The expected from-language tag. * @param {string} options.expectedToLanguage - The expected to-language tag. * @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. * @param {boolean} options.selectFrenchSentence - Selects a French sentence. * @param {boolean} options.selectEnglishSentence - Selects an English sentence. * @param {boolean} options.selectSpanishSentence - Selects a Spanish sentence. * @param {boolean} options.openAtFrenchSection - Opens the context menu at the section of French text. * @param {boolean} options.openAtEnglishSection - Opens the context menu at the section of English text. * @param {boolean} options.openAtSpanishSection - Opens the context menu at the section of Spanish text. * @param {boolean} options.openAtFrenchSentence - Opens the context menu at a French sentence. * @param {boolean} options.openAtEnglishSentence - Opens the context menu at an English sentence. * @param {boolean} options.openAtSpanishSentence - Opens the context menu at a Spanish sentence. * @param {boolean} options.openAtFrenchHyperlink - Opens the context menu at a hyperlinked French text. * @param {boolean} options.openAtEnglishHyperlink - Opens the context menu at an hyperlinked English text. * @param {boolean} options.openAtSpanishHyperlink - Opens the context menu at a hyperlinked Spanish text. * @param {Function} [options.onOpenPanel] - An optional callback function to execute after the panel opens. * @param {string|null} [message] - An optional message to log to info. * @throws Throws an error if the context menu could not be opened with the provided options. * @returns {Promise} */ static async openPanel(runInPage, options, message) { logAction(); if (message) { info(message); } await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem( runInPage, options, message ); const menuItem = getById("context-translate-selection"); await SelectTranslationsTestUtils.waitForPanelPopupEvent( "popupshown", async () => { click(menuItem); await closeContextMenuIfOpen(); }, async () => { const { onOpenPanel } = options; await SelectTranslationsTestUtils.handleDownloads(options); if (onOpenPanel) { await onOpenPanel(); } } ); const { expectedFromLanguage, expectedToLanguage } = options; if (expectedFromLanguage !== undefined) { SelectTranslationsTestUtils.assertSelectedFromLanguage( expectedFromLanguage ); } if (expectedToLanguage !== undefined) { SelectTranslationsTestUtils.assertSelectedToLanguage(expectedToLanguage); } } /** * XUL popups will fire the popupshown and popuphidden events. These will fire for * any type of popup in the browser. This function waits for one of those events, and * checks that the viewId of the popup is PanelUI-profiler * * @param {"popupshown" | "popuphidden"} eventName * @param {Function} callback * @param {Function} postEventAssertion * An optional assertion to be made immediately after the event occurs. * @returns {Promise} */ static async waitForPanelPopupEvent( eventName, callback, postEventAssertion = null ) { // De-lazify the panel elements. SelectTranslationsPanel.elements; await SharedTranslationsTestUtils._waitForPopupEvent( "select-translations-panel", eventName, callback, postEventAssertion ); } } class TranslationsSettingsTestUtils { /** * Opens the Translation Settings page by clicking the settings button sent in the argument. * * @param {HTMLElement} settingsButton * @returns {Element} */ static async openAboutPreferencesTranslationsSettingsPane(settingsButton) { const document = gBrowser.selectedBrowser.contentDocument; const promise = BrowserTestUtils.waitForEvent( document, "paneshown", false, event => event.detail.category === "paneTranslations" ); click(settingsButton, "Click settings button"); await promise; const elements = { backButton: document.getElementById("translations-settings-back-button"), header: document.getElementById("translations-settings-header"), translationsSettingsDescription: document.getElementById( "translations-settings-description" ), translateAlwaysHeader: document.getElementById( "translations-settings-always-translate" ), translateNeverHeader: document.getElementById( "translations-settings-never-translate" ), translateAlwaysMenuList: document.getElementById( "translations-settings-always-translate-list" ), translateNeverMenuList: document.getElementById( "translations-settings-never-translate-list" ), translateNeverSiteHeader: document.getElementById( "translations-settings-never-sites-header" ), translateNeverSiteDesc: document.getElementById( "translations-settings-never-sites" ), translateDownloadLanguagesHeader: document .getElementById("translations-settings-download-section") .querySelector("h2"), translateDownloadLanguagesLearnMore: document.getElementById( "download-languages-learn-more" ), translateDownloadLanguagesList: document.getElementById( "translations-settings-download-section" ), }; return elements; } }