commit add0e3b257912fc6760b8858ce7985f4f30bda06 parent 07f707056f5482261285d99265ef04ccfd460ca7 Author: Erik Nordin <enordin@mozilla.com> Date: Thu, 18 Dec 2025 01:17:28 +0000 Bug 2002127 - Part 14: Implement Translations Settings Tests r=translations-reviewers,hjones This commit implements automated testing for the redesigned Translations settings and subpage, compatible with the settings redesign initiative. Differential Revision: https://phabricator.services.mozilla.com/D274231 Diffstat:
21 files changed, 4525 insertions(+), 82 deletions(-)
diff --git a/browser/components/preferences/translations.js b/browser/components/preferences/translations.js @@ -64,6 +64,22 @@ const DOWNLOAD_ERROR_ICON = "chrome://global/skin/icons/error.svg"; /** @type {string} */ const DOWNLOAD_WARNING_ICON = "chrome://global/skin/icons/warning.svg"; +/** + * Dispatches a test-only event when running under automation. + * + * @param {string} name - Event name without the "TranslationsSettingsTest:" prefix. + * @param {object} [detail] - Optional event detail. + */ +function dispatchTestEvent(name, detail) { + if (!globalThis.Cu?.isInAutomation) { + return; + } + const options = detail ? { detail } : undefined; + document.dispatchEvent( + new CustomEvent(`TranslationsSettingsTest:${name}`, options) + ); +} + const TranslationsSettings = { /** * True once initialization has completed. @@ -321,6 +337,7 @@ const TranslationsSettings = { await this.refreshNeverLanguages(); this.refreshNeverSites(); await this.refreshDownloadedLanguages(); + this.dispatchInitializedTestEvent(); return; } @@ -329,6 +346,7 @@ const TranslationsSettings = { await this.refreshNeverLanguages(); this.refreshNeverSites(); await this.refreshDownloadedLanguages(); + this.dispatchInitializedTestEvent(); return; } @@ -399,6 +417,7 @@ const TranslationsSettings = { !this.elements?.downloadLanguagesButton || !this.elements?.downloadLanguagesNoneRow ) { + this.dispatchInitializedTestEvent(); return; } @@ -419,6 +438,7 @@ const TranslationsSettings = { this.elements.neverTranslateLanguagesSelect.disabled = true; this.elements.downloadLanguagesSelect.disabled = true; this.setDownloadButtonDisabledState(true); + this.dispatchInitializedTestEvent(); return; } @@ -454,6 +474,15 @@ const TranslationsSettings = { await this.refreshNeverLanguages(); this.refreshNeverSites(); this.initialized = true; + + this.dispatchInitializedTestEvent(); + }, + + /** + * Dispatch the test-only Initialized event and mark the document as ready. + */ + dispatchInitializedTestEvent() { + dispatchTestEvent("Initialized"); }, /** @@ -688,6 +717,7 @@ const TranslationsSettings = { } else { this.resetDownloadSelect(); } + dispatchTestEvent("DownloadedLanguagesSelectOptionsUpdated"); }, /** @@ -799,6 +829,10 @@ const TranslationsSettings = { item.remove(); } + const previousEmptyStateVisible = + alwaysTranslateLanguagesNoneRow && + !alwaysTranslateLanguagesNoneRow.hidden; + if (alwaysTranslateLanguagesNoneRow) { const hasLanguages = !!langTags.length; alwaysTranslateLanguagesNoneRow.hidden = hasLanguages; @@ -856,6 +890,20 @@ const TranslationsSettings = { alwaysTranslateLanguagesGroup.appendChild(item); } } + + dispatchTestEvent("AlwaysLanguagesRendered", { + languages: langTags, + count: langTags.length, + }); + + const currentEmptyStateVisible = + alwaysTranslateLanguagesNoneRow && + !alwaysTranslateLanguagesNoneRow.hidden; + if (previousEmptyStateVisible && !currentEmptyStateVisible) { + dispatchTestEvent("AlwaysTranslateLanguagesEmptyStateHidden"); + } else if (!previousEmptyStateVisible && currentEmptyStateVisible) { + dispatchTestEvent("AlwaysTranslateLanguagesEmptyStateShown"); + } }, /** @@ -928,6 +976,8 @@ const TranslationsSettings = { } await this.resetAlwaysSelect(); + + dispatchTestEvent("AlwaysTranslateLanguagesSelectOptionsUpdated"); }, /** @@ -1002,6 +1052,9 @@ const TranslationsSettings = { item.remove(); } + const previousEmptyStateVisible = + neverTranslateLanguagesNoneRow && !neverTranslateLanguagesNoneRow.hidden; + if (neverTranslateLanguagesNoneRow) { const hasLanguages = Boolean(langTags.length); neverTranslateLanguagesNoneRow.hidden = hasLanguages; @@ -1056,6 +1109,19 @@ const TranslationsSettings = { neverTranslateLanguagesGroup.appendChild(item); } } + + dispatchTestEvent("NeverLanguagesRendered", { + languages: langTags, + count: langTags.length, + }); + + const currentEmptyStateVisible = + neverTranslateLanguagesNoneRow && !neverTranslateLanguagesNoneRow.hidden; + if (previousEmptyStateVisible && !currentEmptyStateVisible) { + dispatchTestEvent("NeverTranslateLanguagesEmptyStateHidden"); + } else if (!previousEmptyStateVisible && currentEmptyStateVisible) { + dispatchTestEvent("NeverTranslateLanguagesEmptyStateShown"); + } }, /** @@ -1113,6 +1179,8 @@ const TranslationsSettings = { } await this.resetNeverSelect(); + + dispatchTestEvent("NeverTranslateLanguagesSelectOptionsUpdated"); }, /** @@ -1153,6 +1221,9 @@ const TranslationsSettings = { item.remove(); } + const previousEmptyStateVisible = + neverTranslateSitesNoneRow && !neverTranslateSitesNoneRow.hidden; + if (neverTranslateSitesNoneRow) { const hasSites = Boolean(siteOrigins.length); neverTranslateSitesNoneRow.hidden = hasSites; @@ -1196,6 +1267,19 @@ const TranslationsSettings = { neverTranslateSitesGroup.appendChild(item); } } + + dispatchTestEvent("NeverSitesRendered", { + sites: siteOrigins, + count: siteOrigins.length, + }); + + const currentEmptyStateVisible = + neverTranslateSitesNoneRow && !neverTranslateSitesNoneRow.hidden; + if (previousEmptyStateVisible && !currentEmptyStateVisible) { + dispatchTestEvent("NeverTranslateSitesEmptyStateHidden"); + } else if (!previousEmptyStateVisible && currentEmptyStateVisible) { + dispatchTestEvent("NeverTranslateSitesEmptyStateShown"); + } }, /** @@ -1262,7 +1346,7 @@ const TranslationsSettings = { }, /** - * Set the download button state. + * Set the download button state and dispatch test events when it changes. * * @param {boolean} isDisabled */ @@ -1272,7 +1356,14 @@ const TranslationsSettings = { return; } + const wasDisabled = button.disabled; button.disabled = isDisabled; + + if (wasDisabled !== isDisabled) { + dispatchTestEvent( + isDisabled ? "DownloadButtonDisabled" : "DownloadButtonEnabled" + ); + } }, /** @@ -1297,6 +1388,7 @@ const TranslationsSettings = { this.currentDownloadLangTag = langTag; this.downloadingLanguageTags.add(langTag); this.setDownloadControlsDisabled(true); + dispatchTestEvent("DownloadStarted", { langTag }); await this.renderDownloadLanguages(); this.updateDownloadSelectOptionState({ preserveSelection: true }); @@ -1305,7 +1397,9 @@ const TranslationsSettings = { await TranslationsParent.downloadLanguageFiles(langTag); this.downloadedLanguageTags.add(langTag); downloadSucceeded = true; + dispatchTestEvent("DownloadCompleted", { langTag }); } catch (error) { + dispatchTestEvent("DownloadFailed", { langTag }); console.error("Failed to download language files", error); this.downloadFailedLanguageTags.add(langTag); } finally { @@ -1552,6 +1646,9 @@ const TranslationsSettings = { return; } + const previousEmptyStateVisible = + downloadLanguagesNoneRow && !downloadLanguagesNoneRow.hidden; + for (const item of downloadLanguagesGroup.querySelectorAll( `.${DOWNLOAD_LANGUAGE_ITEM_CLASS}` )) { @@ -1579,6 +1676,14 @@ const TranslationsSettings = { } } + const currentEmptyStateVisible = + downloadLanguagesNoneRow && !downloadLanguagesNoneRow.hidden; + if (previousEmptyStateVisible && !currentEmptyStateVisible) { + dispatchTestEvent("DownloadedLanguagesEmptyStateHidden"); + } else if (!previousEmptyStateVisible && currentEmptyStateVisible) { + dispatchTestEvent("DownloadedLanguagesEmptyStateShown"); + } + const sortedLangTags = [...langTags].sort((lhs, rhs) => { const labelA = this.formatLanguageLabel(lhs) ?? lhs; const labelB = this.formatLanguageLabel(rhs) ?? rhs; @@ -1624,6 +1729,14 @@ const TranslationsSettings = { downloadLanguagesGroup.appendChild(item); } } + + dispatchTestEvent("DownloadedLanguagesRendered", { + languages: sortedLangTags, + count: sortedLangTags.length, + downloading: sortedLangTags.filter(langTag => + this.downloadingLanguageTags.has(langTag) + ), + }); }, /** @@ -1657,6 +1770,7 @@ const TranslationsSettings = { try { await TranslationsParent.deleteLanguageFiles(langTag); this.downloadedLanguageTags.delete(langTag); + dispatchTestEvent("DownloadDeleted", { langTag }); } catch (error) { console.error("Failed to remove downloaded language files", error); await this.renderDownloadLanguages(); @@ -1698,6 +1812,7 @@ const TranslationsSettings = { this.currentDownloadLangTag = langTag; this.downloadingLanguageTags.add(langTag); this.setDownloadControlsDisabled(true); + dispatchTestEvent("DownloadStarted", { langTag }); await this.renderDownloadLanguages(); this.updateDownloadSelectOptionState({ preserveSelection: true }); @@ -1706,9 +1821,11 @@ const TranslationsSettings = { await TranslationsParent.downloadLanguageFiles(langTag); this.downloadedLanguageTags.add(langTag); downloadSucceeded = true; + dispatchTestEvent("DownloadCompleted", { langTag }); } catch (error) { console.error("Failed to download language files", error); this.downloadFailedLanguageTags.add(langTag); + dispatchTestEvent("DownloadFailed", { langTag }); } finally { this.downloadingLanguageTags.delete(langTag); this.currentDownloadLangTag = null; diff --git a/browser/components/translations/tests/browser/browser.toml b/browser/components/translations/tests/browser/browser.toml @@ -8,6 +8,38 @@ support-files = [ ["browser_translations_about_preferences_manage_downloaded_languages.js"] +["browser_translations_about_settings_main_page_offer_checkbox.js"] + +["browser_translations_about_settings_subpage_always_translate_langs_a11y.js"] + +["browser_translations_about_settings_subpage_always_translate_langs_basic.js"] + +["browser_translations_about_settings_subpage_always_translate_langs_modify.js"] + +["browser_translations_about_settings_subpage_always_translate_langs_observe.js"] + +["browser_translations_about_settings_subpage_back_button.js"] + +["browser_translations_about_settings_subpage_default.js"] + +["browser_translations_about_settings_subpage_download_langs_basic.js"] + +["browser_translations_about_settings_subpage_download_langs_errors.js"] + +["browser_translations_about_settings_subpage_download_langs_sorting.js"] + +["browser_translations_about_settings_subpage_never_translate_langs_a11y.js"] + +["browser_translations_about_settings_subpage_never_translate_langs_basic.js"] + +["browser_translations_about_settings_subpage_never_translate_langs_modify.js"] + +["browser_translations_about_settings_subpage_never_translate_langs_observe.js"] + +["browser_translations_about_settings_subpage_never_translate_sites_basic.js"] + +["browser_translations_about_settings_subpage_never_translate_sites_observe.js"] + ["browser_translations_e2e_full_page_translate_with_lexical_shortlist.js"] ["browser_translations_e2e_full_page_translate_without_lexical_shortlist.js"] diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_main_page_offer_checkbox.js b/browser/components/translations/tests/browser/browser_translations_about_settings_main_page_offer_checkbox.js @@ -0,0 +1,163 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the "Always offer to translate" checkbox in about:preferences + * properly updates the browser.translations.automaticallyPopup preference + * when toggled by the user. + */ +add_task(async function test_offer_translations_checkbox_toggle() { + const PREF_NAME = "browser.translations.automaticallyPopup"; + + const { cleanup } = await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [ + ["browser.settings-redesign.enabled", true], + [PREF_NAME, true], + ], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Waiting for the offerTranslations checkbox to be available"); + const checkbox = await waitForCondition( + () => document.getElementById("offerTranslations"), + "Waiting for offerTranslations checkbox to be visible" + ); + + await ensureVisibility({ + message: "offerTranslations checkbox should be visible", + visible: { checkbox }, + }); + + info("Scrolling checkbox into view"); + checkbox.scrollIntoView({ behavior: "instant", block: "center" }); + + info("Verifying initial state matches the preference"); + let prefValue = Services.prefs.getBoolPref(PREF_NAME); + is(prefValue, true, "Initial pref value should be true"); + + const checkboxInput = + checkbox.tagName === "INPUT" + ? checkbox + : checkbox.querySelector('input[type="checkbox"]'); + + if (checkboxInput) { + is( + checkboxInput.checked, + prefValue, + "Checkbox checked state should match preference value" + ); + } else { + is( + checkbox.checked, + prefValue, + "Checkbox checked state should match preference value" + ); + } + + info("Clicking checkbox to toggle it OFF (true → false)"); + let prefChanged = TestUtils.waitForPrefChange(PREF_NAME); + click(checkbox, "Toggling offerTranslations checkbox to false"); + await prefChanged; + + prefValue = Services.prefs.getBoolPref(PREF_NAME); + is(prefValue, false, "Pref should now be false after clicking"); + + if (checkboxInput) { + is(checkboxInput.checked, false, "Checkbox should be unchecked"); + } else { + is(checkbox.checked, false, "Checkbox should be unchecked"); + } + + info("Clicking checkbox to toggle it back ON (false → true)"); + prefChanged = TestUtils.waitForPrefChange(PREF_NAME); + click(checkbox, "Toggling offerTranslations checkbox to true"); + await prefChanged; + + prefValue = Services.prefs.getBoolPref(PREF_NAME); + is(prefValue, true, "Pref should now be true after clicking again"); + + if (checkboxInput) { + is(checkboxInput.checked, true, "Checkbox should be checked"); + } else { + is(checkbox.checked, true, "Checkbox should be checked"); + } + + await cleanup(); +}); + +/** + * Tests that changing the browser.translations.automaticallyPopup preference + * directly updates the checkbox state in the UI automatically. + */ +add_task(async function test_offer_translations_pref_updates_checkbox() { + const PREF_NAME = "browser.translations.automaticallyPopup"; + + const { cleanup } = await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [ + ["browser.settings-redesign.enabled", true], + [PREF_NAME, true], + ], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Waiting for the offerTranslations checkbox to be available"); + const checkbox = await waitForCondition( + () => document.getElementById("offerTranslations"), + "Waiting for offerTranslations checkbox to be visible" + ); + + await ensureVisibility({ + message: "offerTranslations checkbox should be visible", + visible: { checkbox }, + }); + + info("Scrolling checkbox into view"); + checkbox.scrollIntoView({ behavior: "instant", block: "center" }); + + const checkboxInput = + checkbox.tagName === "INPUT" + ? checkbox + : checkbox.querySelector('input[type="checkbox"]'); + + const getCheckedState = () => + checkboxInput ? checkboxInput.checked : checkbox.checked; + + info("Verifying initial state is checked (pref is true)"); + is(getCheckedState(), true, "Checkbox should initially be checked"); + + info("Setting pref to false directly"); + Services.prefs.setBoolPref(PREF_NAME, false); + + info("Waiting for checkbox to update to unchecked state"); + await waitForCondition( + () => getCheckedState() === false, + "Waiting for checkbox to become unchecked after pref change" + ); + + is( + getCheckedState(), + false, + "Checkbox should be unchecked after pref set to false" + ); + + info("Setting pref to true directly"); + Services.prefs.setBoolPref(PREF_NAME, true); + + info("Waiting for checkbox to update to checked state"); + await waitForCondition( + () => getCheckedState() === true, + "Waiting for checkbox to become checked after pref change" + ); + + is( + getCheckedState(), + true, + "Checkbox should be checked after pref set to true" + ); + + await cleanup(); +}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_always_translate_langs_a11y.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_always_translate_langs_a11y.js @@ -0,0 +1,173 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests keyboard activation (Enter key) of remove buttons in the always-translate language list. + */ +add_task(async function test_always_translate_languages_keyboard_activation() { + const { cleanup, translationsSettingsTestUtils } = + await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [ + ["browser.settings-redesign.enabled", true], + [ALWAYS_TRANSLATE_LANGS_PREF, "es,fr,uk"], + ], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Navigating to translations subpage"); + const manageButton = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton" + ); + + info("Scrolling button into view"); + manageButton.scrollIntoView({ behavior: "instant", block: "center" }); + + info("Clicking manage button and waiting for initialization"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.Initialized]], + }, + async () => { + click( + manageButton, + "Clicking manage button to open translations subpage" + ); + } + ); + + info("Verifying three language items are rendered"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ + languages: ["es", "fr", "uk"], + count: 3, + }); + + info("Getting first remove button"); + const firstRemoveButton = document.querySelector( + ".translations-always-remove-button" + ); + ok(firstRemoveButton, "First remove button should exist"); + + info("Testing keyboard activation with Enter key"); + firstRemoveButton.focus(); + is( + document.activeElement, + firstRemoveButton, + "Remove button should be focused" + ); + + info("Pressing Enter to remove first language"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [TranslationsSettingsTestUtils.Events.AlwaysLanguagesRendered], + ], + }, + async () => { + const prefChanged = TestUtils.waitForPrefChange( + ALWAYS_TRANSLATE_LANGS_PREF + ); + const enterEvent = new KeyboardEvent("keydown", { + key: "Enter", + bubbles: true, + }); + firstRemoveButton.dispatchEvent(enterEvent); + click(firstRemoveButton, "Activating remove via Enter key"); + await prefChanged; + } + ); + + info("Verifying language was removed"); + const langs = getAlwaysTranslateLanguagesFromPref(); + is(langs.length, 2, "Should have 2 languages after removal"); + + info("Verifying UI updated to show 2 items"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ + count: 2, + }); + + await cleanup(); +}); + +/** + * Tests accessibility features including ARIA labels and keyboard-only operation. + */ +add_task(async function test_always_translate_languages_accessibility() { + const { cleanup, translationsSettingsTestUtils } = + await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [ + ["browser.settings-redesign.enabled", true], + [ALWAYS_TRANSLATE_LANGS_PREF, "es,fr"], + ], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Navigating to translations subpage"); + const manageButton = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton" + ); + + info("Scrolling button into view"); + manageButton.scrollIntoView({ behavior: "instant", block: "center" }); + + info("Clicking manage button and waiting for initialization"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.Initialized]], + }, + async () => { + click( + manageButton, + "Clicking manage button to open translations subpage" + ); + } + ); + + info("Verifying two language items are rendered"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ + languages: ["es", "fr"], + count: 2, + }); + + info("Getting language items and remove buttons"); + const items = document.querySelectorAll(".translations-always-language-item"); + is(items.length, 2, "Should have 2 language items"); + + info("Verifying remove buttons have accessible labels"); + const removeButtons = document.querySelectorAll( + ".translations-always-remove-button" + ); + is(removeButtons.length, 2, "Should have 2 remove buttons"); + + for (const button of removeButtons) { + const ariaLabel = + button.getAttribute("aria-label") || + button.ariaLabel || + button.getAttribute("data-l10n-id"); + ok( + ariaLabel, + "Remove button should have aria-label or data-l10n-id for accessibility" + ); + if (typeof ariaLabel === "string") { + Assert.greater( + ariaLabel.length, + 0, + "Remove button accessibility label should not be empty" + ); + } + } + + info("Verifying language items have proper semantic structure"); + for (const item of items) { + const label = item.getAttribute("label"); + ok(label, "Each language item should have a label attribute"); + Assert.greater(label.length, 0, "Label should not be empty"); + } + + await cleanup(); +}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_always_translate_langs_basic.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_always_translate_langs_basic.js @@ -0,0 +1,160 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that pre-populated languages load correctly when opening the subpage. + */ +add_task(async function test_always_translate_languages_prepopulated() { + const { cleanup, translationsSettingsTestUtils } = + await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [ + ["browser.settings-redesign.enabled", true], + [ALWAYS_TRANSLATE_LANGS_PREF, "uk,es,fr"], + ], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Navigating to translations subpage"); + const manageButton = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton" + ); + + info("Scrolling button into view"); + manageButton.scrollIntoView({ behavior: "instant", block: "center" }); + + info("Clicking manage button and waiting for initialization"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.Initialized]], + }, + async () => { + click( + manageButton, + "Clicking manage button to open translations subpage" + ); + } + ); + + info("Verifying three language items are displayed"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ + languages: ["es", "fr", "uk"], + count: 3, + }); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguagesOrder({ + languages: ["fr", "es", "uk"], + }); + + info("Verifying empty state is not visible"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguagesEmptyState({ + visible: false, + }); + + await cleanup(); +}); + +/** + * Tests that the dropdown state is managed correctly - already-added languages + * should be disabled in the dropdown. + */ +add_task(async function test_always_translate_languages_dropdown_state() { + const { cleanup, translationsSettingsTestUtils } = + await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [ + ["browser.settings-redesign.enabled", true], + [ALWAYS_TRANSLATE_LANGS_PREF, "es,fr"], + ], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Navigating to translations subpage"); + const manageButton = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton" + ); + + info("Scrolling button into view"); + manageButton.scrollIntoView({ behavior: "instant", block: "center" }); + + info("Clicking manage button and waiting for initialization"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.Initialized]], + }, + async () => { + click( + manageButton, + "Clicking manage button to open translations subpage" + ); + } + ); + + info("Verifying two language items are rendered"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ + languages: ["es", "fr"], + count: 2, + }); + + info("Getting the dropdown element"); + const dropdown = + translationsSettingsTestUtils.getAlwaysTranslateLanguagesSelect(); + + info("Verifying Spanish and French options are disabled"); + const spanishOption = dropdown.querySelector('moz-option[value="es"]'); + const frenchOption = dropdown.querySelector('moz-option[value="fr"]'); + const ukrainianOption = dropdown.querySelector('moz-option[value="uk"]'); + + ok(spanishOption, "Spanish option should exist in dropdown"); + ok(frenchOption, "French option should exist in dropdown"); + ok(ukrainianOption, "Ukrainian option should exist in dropdown"); + + ok( + spanishOption.disabled, + "Spanish option should be disabled (already added)" + ); + ok(frenchOption.disabled, "French option should be disabled (already added)"); + ok(!ukrainianOption.disabled, "Ukrainian option should not be disabled"); + + info("Adding Ukrainian via dropdown"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [TranslationsSettingsTestUtils.Events.AlwaysLanguagesRendered], + [ + TranslationsSettingsTestUtils.Events + .AlwaysTranslateLanguagesSelectOptionsUpdated, + ], + ], + }, + async () => { + const prefChanged = TestUtils.waitForPrefChange( + ALWAYS_TRANSLATE_LANGS_PREF + ); + await translationsSettingsTestUtils.addAlwaysTranslateLanguage("uk"); + await prefChanged; + } + ); + + info("Verifying Ukrainian was added"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ + languages: ["es", "fr", "uk"], + count: 3, + }); + + info("Verifying dropdown updated and Ukrainian is now disabled"); + ok( + ukrainianOption.disabled, + "Ukrainian option should now be disabled (just added)" + ); + + await dropdown.updateComplete; + + info("Verifying dropdown resets to placeholder"); + is(dropdown.value, "", "Dropdown should reset to empty value"); + + await cleanup(); +}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_always_translate_langs_modify.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_always_translate_langs_modify.js @@ -0,0 +1,424 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests basic adding and removing of languages in the Always Translate Languages + * section, including empty state transitions. + */ +add_task(async function test_always_translate_languages_add_and_remove() { + const { cleanup, translationsSettingsTestUtils } = + await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [ + ["browser.settings-redesign.enabled", true], + [ALWAYS_TRANSLATE_LANGS_PREF, ""], + ], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Navigating to translations subpage"); + const manageButton = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton" + ); + + info("Scrolling button into view"); + manageButton.scrollIntoView({ behavior: "instant", block: "center" }); + + info("Clicking manage button and waiting for initialization"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.Initialized]], + }, + async () => { + click( + manageButton, + "Clicking manage button to open translations subpage" + ); + } + ); + + info("Verifying empty state is visible initially"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguagesEmptyState({ + visible: true, + }); + + info("Adding Spanish (es) via dropdown"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [TranslationsSettingsTestUtils.Events.AlwaysLanguagesRendered], + [ + TranslationsSettingsTestUtils.Events + .AlwaysTranslateLanguagesEmptyStateHidden, + ], + ], + }, + async () => { + const prefChanged = TestUtils.waitForPrefChange( + ALWAYS_TRANSLATE_LANGS_PREF + ); + await translationsSettingsTestUtils.addAlwaysTranslateLanguage("es"); + await prefChanged; + } + ); + + info("Verifying Spanish was added"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ + languages: ["es"], + count: 1, + }); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguagesEmptyState({ + visible: false, + }); + + info("Adding French (fr) via dropdown"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [TranslationsSettingsTestUtils.Events.AlwaysLanguagesRendered], + ], + }, + async () => { + const prefChanged = TestUtils.waitForPrefChange( + ALWAYS_TRANSLATE_LANGS_PREF + ); + await translationsSettingsTestUtils.addAlwaysTranslateLanguage("fr"); + await prefChanged; + } + ); + + info("Verifying French was added"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ + languages: ["es", "fr"], + count: 2, + }); + + info("Adding Ukrainian (uk) via dropdown"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [TranslationsSettingsTestUtils.Events.AlwaysLanguagesRendered], + ], + }, + async () => { + const prefChanged = TestUtils.waitForPrefChange( + ALWAYS_TRANSLATE_LANGS_PREF + ); + await translationsSettingsTestUtils.addAlwaysTranslateLanguage("uk"); + await prefChanged; + } + ); + + info("Verifying Ukrainian was added"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ + languages: ["es", "fr", "uk"], + count: 3, + }); + + info("Removing middle item (French)"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [TranslationsSettingsTestUtils.Events.AlwaysLanguagesRendered], + ], + }, + async () => { + const prefChanged = TestUtils.waitForPrefChange( + ALWAYS_TRANSLATE_LANGS_PREF + ); + await translationsSettingsTestUtils.removeAlwaysTranslateLanguage("fr"); + await prefChanged; + } + ); + + info("Verifying French was removed"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ + languages: ["es", "uk"], + count: 2, + }); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguagesEmptyState({ + visible: false, + }); + + info("Removing Ukrainian"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [TranslationsSettingsTestUtils.Events.AlwaysLanguagesRendered], + ], + }, + async () => { + const prefChanged = TestUtils.waitForPrefChange( + ALWAYS_TRANSLATE_LANGS_PREF + ); + await translationsSettingsTestUtils.removeAlwaysTranslateLanguage("uk"); + await prefChanged; + } + ); + + info("Verifying Ukrainian was removed"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ + languages: ["es"], + count: 1, + }); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguagesEmptyState({ + visible: false, + }); + + info("Removing Spanish (last language)"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [TranslationsSettingsTestUtils.Events.AlwaysLanguagesRendered], + [ + TranslationsSettingsTestUtils.Events + .AlwaysTranslateLanguagesEmptyStateShown, + ], + ], + }, + async () => { + const prefChanged = TestUtils.waitForPrefChange( + ALWAYS_TRANSLATE_LANGS_PREF + ); + await translationsSettingsTestUtils.removeAlwaysTranslateLanguage("es"); + await prefChanged; + } + ); + + info("Verifying all languages removed and empty state reappears"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ + languages: [], + count: 0, + }); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguagesEmptyState({ + visible: true, + }); + + await cleanup(); +}); + +/** + * Tests that invalid language tags don't break the UI and valid languages + * are still rendered correctly. + */ +add_task(async function test_always_translate_languages_invalid_tags() { + const { cleanup, translationsSettingsTestUtils } = + await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [ + ["browser.settings-redesign.enabled", true], + [ALWAYS_TRANSLATE_LANGS_PREF, "es,fr,uk"], + ], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Navigating to translations subpage"); + const manageButton = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton" + ); + + info("Scrolling button into view"); + manageButton.scrollIntoView({ behavior: "instant", block: "center" }); + + info("Clicking manage button and waiting for initialization"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.Initialized]], + }, + async () => { + click( + manageButton, + "Clicking manage button to open translations subpage" + ); + } + ); + + info("Verifying three valid languages are displayed"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ + languages: ["es", "fr", "uk"], + count: 3, + }); + + info("Testing UI doesn't break when pref has mixed valid/invalid tags"); + info("Adding invalid tags via pref change"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [TranslationsSettingsTestUtils.Events.AlwaysLanguagesRendered], + ], + }, + async () => { + Services.prefs.setCharPref( + ALWAYS_TRANSLATE_LANGS_PREF, + "es,INVALID,fr,uk" + ); + } + ); + + info("Verifying UI still works and valid languages remain accessible"); + ok( + document.querySelector('[data-lang-tag="es"]'), + "Spanish item should still exist" + ); + ok( + document.querySelector('[data-lang-tag="fr"]'), + "French item should still exist" + ); + ok( + document.querySelector('[data-lang-tag="uk"]'), + "Ukrainian item should still exist" + ); + + await cleanup(); +}); + +/** + * Tests that adding a language to always-translate automatically removes it + * from the never-translate list ("stealing" behavior). + */ +add_task(async function test_always_translate_languages_stealing() { + const { cleanup, translationsSettingsTestUtils } = + await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [ + ["browser.settings-redesign.enabled", true], + [NEVER_TRANSLATE_LANGS_PREF, "es,fr,uk"], + [ALWAYS_TRANSLATE_LANGS_PREF, ""], + ], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Navigating to translations subpage"); + const manageButton = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton" + ); + + info("Scrolling button into view"); + manageButton.scrollIntoView({ behavior: "instant", block: "center" }); + + info("Clicking manage button and waiting for initialization"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.Initialized]], + }, + async () => { + click( + manageButton, + "Clicking manage button to open translations subpage" + ); + } + ); + + info("Verifying always-translate section is empty"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguagesEmptyState({ + visible: true, + }); + + info("Verifying never-translate has three languages"); + let neverLangs = getNeverTranslateLanguagesFromPref(); + is(neverLangs.length, 3, "Should have 3 never-translate languages"); + ok( + neverLangs.includes("es") && + neverLangs.includes("fr") && + neverLangs.includes("uk"), + "Never-translate should include es, fr, uk" + ); + + info("Adding Spanish to always-translate via UI"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [TranslationsSettingsTestUtils.Events.AlwaysLanguagesRendered], + [ + TranslationsSettingsTestUtils.Events + .AlwaysTranslateLanguagesEmptyStateHidden, + ], + ], + }, + async () => { + const alwaysPrefChanged = TestUtils.waitForPrefChange( + ALWAYS_TRANSLATE_LANGS_PREF + ); + await translationsSettingsTestUtils.addAlwaysTranslateLanguage("es"); + await alwaysPrefChanged; + } + ); + + info("Verifying Spanish appears in always-translate list"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ + languages: ["es"], + count: 1, + }); + + info("Verifying Spanish was removed from never-translate pref"); + neverLangs = getNeverTranslateLanguagesFromPref(); + is(neverLangs.length, 2, "Should have 2 never-translate languages"); + ok(!neverLangs.includes("es"), "Never-translate should not include Spanish"); + ok( + neverLangs.includes("fr") && neverLangs.includes("uk"), + "Never-translate should still include French and Ukrainian" + ); + + info("Adding French to always-translate via UI"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [TranslationsSettingsTestUtils.Events.AlwaysLanguagesRendered], + ], + }, + async () => { + const alwaysPrefChanged = TestUtils.waitForPrefChange( + ALWAYS_TRANSLATE_LANGS_PREF + ); + await translationsSettingsTestUtils.addAlwaysTranslateLanguage("fr"); + await alwaysPrefChanged; + } + ); + + info("Verifying both Spanish and French in always-translate list"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ + languages: ["es", "fr"], + count: 2, + }); + + info("Verifying only Ukrainian remains in never-translate pref"); + neverLangs = getNeverTranslateLanguagesFromPref(); + is(neverLangs.length, 1, "Should have 1 never-translate language"); + ok( + neverLangs.includes("uk"), + "Never-translate should only include Ukrainian" + ); + + info("Adding Ukrainian to always-translate via UI"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [TranslationsSettingsTestUtils.Events.AlwaysLanguagesRendered], + ], + }, + async () => { + const alwaysPrefChanged = TestUtils.waitForPrefChange( + ALWAYS_TRANSLATE_LANGS_PREF + ); + await translationsSettingsTestUtils.addAlwaysTranslateLanguage("uk"); + await alwaysPrefChanged; + } + ); + + info("Verifying all three languages now in always-translate"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ + languages: ["es", "fr", "uk"], + count: 3, + }); + + info("Verifying never-translate is empty after stealing"); + neverLangs = getNeverTranslateLanguagesFromPref(); + is(neverLangs.length, 0, "Never-translate should be empty after stealing"); + + await cleanup(); +}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_always_translate_langs_observe.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_always_translate_langs_observe.js @@ -0,0 +1,245 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the UI reactively updates when prefs are changed externally + * while the subpage is open. + */ +add_task(async function test_always_translate_languages_observe_pref_changes() { + const { cleanup, translationsSettingsTestUtils } = + await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [ + ["browser.settings-redesign.enabled", true], + [ALWAYS_TRANSLATE_LANGS_PREF, ""], + ], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Navigating to translations subpage"); + const manageButton = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton" + ); + + info("Scrolling button into view"); + manageButton.scrollIntoView({ behavior: "instant", block: "center" }); + + info("Clicking manage button and waiting for initialization"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.Initialized]], + }, + async () => { + click( + manageButton, + "Clicking manage button to open translations subpage" + ); + } + ); + + info("Verifying empty state initially visible"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguagesEmptyState({ + visible: true, + }); + + info("Adding Spanish (es) via pref directly"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [TranslationsSettingsTestUtils.Events.AlwaysLanguagesRendered], + [ + TranslationsSettingsTestUtils.Events + .AlwaysTranslateLanguagesEmptyStateHidden, + ], + ], + }, + async () => { + Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, "es"); + } + ); + + info("Verifying Spanish was added"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ + languages: ["es"], + count: 1, + }); + + info("Adding more languages via pref (es,fr,uk)"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [TranslationsSettingsTestUtils.Events.AlwaysLanguagesRendered], + ], + }, + async () => { + Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, "es,fr,uk"); + } + ); + + info("Verifying all three languages are displayed"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ + languages: ["es", "fr", "uk"], + count: 3, + }); + + info("Removing French via pref (es,uk)"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [TranslationsSettingsTestUtils.Events.AlwaysLanguagesRendered], + ], + }, + async () => { + Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, "es,uk"); + } + ); + + info("Verifying French was removed"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ + languages: ["es", "uk"], + count: 2, + }); + + info("Clearing all languages via pref"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [TranslationsSettingsTestUtils.Events.AlwaysLanguagesRendered], + [ + TranslationsSettingsTestUtils.Events + .AlwaysTranslateLanguagesEmptyStateShown, + ], + ], + }, + async () => { + Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, ""); + } + ); + + info("Verifying all languages removed and empty state returns"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ + languages: [], + count: 0, + }); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguagesEmptyState({ + visible: true, + }); + + await cleanup(); +}); + +/** + * Tests that both UI lists update correctly when simulating a stealing scenario + * by manually adding a language to one pref and removing it from the other. + */ +add_task(async function test_always_translate_languages_simulated_stealing() { + const { cleanup, translationsSettingsTestUtils } = + await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [ + ["browser.settings-redesign.enabled", true], + [NEVER_TRANSLATE_LANGS_PREF, "es,fr"], + [ALWAYS_TRANSLATE_LANGS_PREF, ""], + ], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Navigating to translations subpage"); + const manageButton = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton" + ); + + info("Scrolling button into view"); + manageButton.scrollIntoView({ behavior: "instant", block: "center" }); + + info("Clicking manage button and waiting for initialization"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.Initialized]], + }, + async () => { + click( + manageButton, + "Clicking manage button to open translations subpage" + ); + } + ); + + info("Verifying always-translate section is empty"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguagesEmptyState({ + visible: true, + }); + + info( + "Simulating stealing by adding Spanish to always-translate and removing from never-translate" + ); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [TranslationsSettingsTestUtils.Events.NeverLanguagesRendered], + [TranslationsSettingsTestUtils.Events.AlwaysLanguagesRendered], + [ + TranslationsSettingsTestUtils.Events + .AlwaysTranslateLanguagesEmptyStateHidden, + ], + ], + }, + async () => { + Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, "es"); + Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, "fr"); + } + ); + + info("Verifying Spanish appears in always-translate UI"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ + languages: ["es"], + count: 1, + }); + + info("Verifying never-translate UI updated to show only French"); + await translationsSettingsTestUtils.assertNeverTranslateLanguages({ + languages: ["fr"], + count: 1, + }); + + info( + "Simulating stealing French by adding to always-translate and removing from never-translate" + ); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [TranslationsSettingsTestUtils.Events.NeverLanguagesRendered], + [ + TranslationsSettingsTestUtils.Events + .NeverTranslateLanguagesEmptyStateShown, + ], + [TranslationsSettingsTestUtils.Events.AlwaysLanguagesRendered], + ], + }, + async () => { + Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, "es,fr"); + Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, ""); + } + ); + + info("Verifying both Spanish and French in always-translate UI"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ + languages: ["es", "fr"], + count: 2, + }); + + info("Verifying never-translate UI shows empty state"); + await translationsSettingsTestUtils.assertNeverTranslateLanguages({ + languages: [], + count: 0, + }); + await translationsSettingsTestUtils.assertNeverTranslateLanguagesEmptyState({ + visible: true, + }); + + await cleanup(); +}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_back_button.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_back_button.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the translations subpage back button returns to the main page. + */ +add_task(async function test_translations_subpage_back_button() { + const { cleanup, translationsSettingsTestUtils } = + await TranslationsSettingsTestUtils.openTranslationsSettingsSubpage(); + + const document = gBrowser.selectedBrowser.contentDocument; + const pane = translationsSettingsTestUtils.getTranslationsPane(); + + ok(pane, "Translations setting pane should exist"); + ok( + translationsSettingsTestUtils.getBackButton(), + "Translations subpage should include a back button" + ); + + await translationsSettingsTestUtils.clickBackButton(); + + is( + document.location.hash, + "#general", + "Hash should return to the General pane after clicking back" + ); + ok(pane?.hidden, "Translations pane should hide after navigating back"); + + const manageButton = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton on main page" + ); + ok( + BrowserTestUtils.isVisible(manageButton), + "Main page translations section should be visible" + ); + + await cleanup(); +}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_default.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_default.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the "More translation settings" button in about:preferences + * properly navigates to the translations subpage and that all default-state + * elements are visible. + */ +add_task( + async function test_translations_subpage_button_and_default_elements() { + const { cleanup } = await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [["browser.settings-redesign.enabled", true]], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Waiting for the translationsManageButton to be available"); + const button = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton to be visible" + ); + + await ensureVisibility({ + message: "translationsManageButton should be visible", + visible: { button }, + }); + + info("Scrolling button into view"); + button.scrollIntoView({ behavior: "instant", block: "center" }); + + info("Clicking button to navigate to translations subpage"); + click(button, "Navigating to translations subpage"); + + info("Waiting for default-state element in Always Translate section"); + const alwaysTranslateDefaultRow = await waitForCondition( + () => + document.getElementById("translationsAlwaysTranslateLanguagesNoneRow"), + "Waiting for translationsAlwaysTranslateLanguagesNoneRow to be visible" + ); + + await ensureVisibility({ + message: "Always Translate default row should be visible", + visible: { alwaysTranslateDefaultRow }, + }); + is( + alwaysTranslateDefaultRow.getAttribute("data-l10n-id"), + "settings-translations-subpage-no-languages-added", + "Always Translate default row has correct l10n ID" + ); + + info("Waiting for default-state element in Never Translate section"); + const neverTranslateDefaultRow = await waitForCondition( + () => + document.getElementById("translationsNeverTranslateLanguagesNoneRow"), + "Waiting for translationsNeverTranslateLanguagesNoneRow to be visible" + ); + + await ensureVisibility({ + message: "Never Translate default row should be visible", + visible: { neverTranslateDefaultRow }, + }); + is( + neverTranslateDefaultRow.getAttribute("data-l10n-id"), + "settings-translations-subpage-no-languages-added", + "Never Translate default row has correct l10n ID" + ); + + info("Waiting for default-state element in Never Translate Sites section"); + const neverTranslateSitesDefaultRow = await waitForCondition( + () => document.getElementById("translationsNeverTranslateSitesNoneRow"), + "Waiting for translationsNeverTranslateSitesNoneRow to be visible" + ); + + await ensureVisibility({ + message: "Never Translate Sites default row should be visible", + visible: { neverTranslateSitesDefaultRow }, + }); + is( + neverTranslateSitesDefaultRow.getAttribute("data-l10n-id"), + "settings-translations-subpage-no-sites-added", + "Never Translate Sites default row has correct l10n ID" + ); + + info("Waiting for default-state element in Download Languages section"); + const downloadLanguagesDefaultRow = await waitForCondition( + () => document.getElementById("translationsDownloadLanguagesNoneRow"), + "Waiting for translationsDownloadLanguagesNoneRow to be visible" + ); + + await ensureVisibility({ + message: "Download Languages default row should be visible", + visible: { downloadLanguagesDefaultRow }, + }); + is( + downloadLanguagesDefaultRow.getAttribute("data-l10n-id"), + "settings-translations-subpage-no-languages-downloaded", + "Download Languages default row has correct l10n ID" + ); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_download_langs_basic.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_download_langs_basic.js @@ -0,0 +1,169 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +add_task(async function test_download_languages_basic_flow() { + const { cleanup, remoteClients, translationsSettingsTestUtils } = + await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [["browser.settings-redesign.enabled", true]], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Waiting for translationsManageButton"); + const manageButton = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton" + ); + + manageButton.scrollIntoView({ behavior: "instant", block: "center" }); + + info("Opening translations subpage"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.Initialized]], + }, + async () => { + click(manageButton, "Open translations subpage"); + } + ); + + const downloadSelect = + translationsSettingsTestUtils.getDownloadedLanguagesSelect(); + const downloadButton = translationsSettingsTestUtils.getDownloadButton(); + + ok(downloadSelect, "Download languages select should exist"); + ok(downloadButton, "Download languages button should exist"); + + await translationsSettingsTestUtils.assertDownloadedLanguagesEmptyState({ + visible: true, + }); + ok( + downloadButton.disabled, + "Download button disabled before selecting a language" + ); + + const frenchOption = downloadSelect.querySelector('moz-option[value="fr"]'); + ok(frenchOption, "French option should be available"); + ok(!frenchOption.disabled, "French option should start enabled"); + + info("Select French to enable download button"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.DownloadButtonEnabled]], + }, + async () => { + await translationsSettingsTestUtils.selectDownloadLanguage("fr"); + } + ); + ok(!downloadButton.disabled, "Download button enabled after selecting"); + + const expectedModelDownloads = languageModelNames([ + { fromLang: "fr", toLang: "en" }, + { fromLang: "en", toLang: "fr" }, + ]); + + info("Download French language models"); + const downloadEvents = [ + translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadButtonDisabled + ), + translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadStarted, + { expectedDetail: { langTag: "fr" } } + ), + translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, + { expectedDetail: { languages: ["fr"], count: 1, downloading: ["fr"] } } + ), + translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events + .DownloadedLanguagesSelectOptionsUpdated + ), + translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadCompleted, + { expectedDetail: { langTag: "fr" } } + ), + translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, + { expectedDetail: { languages: ["fr"], count: 1, downloading: [] } } + ), + translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events + .DownloadedLanguagesSelectOptionsUpdated + ), + ]; + + await click(downloadButton, "Start French download"); + Assert.deepEqual( + await remoteClients.translationModels.resolvePendingDownloads( + expectedModelDownloads.length + ), + expectedModelDownloads, + "French models were downloaded." + ); + await Promise.all(downloadEvents); + + await translationsSettingsTestUtils.assertDownloadedLanguages({ + languages: ["fr"], + downloading: [], + count: 1, + }); + ok(frenchOption.disabled, "French option disabled after download"); + + info("Open delete confirmation then cancel"); + await translationsSettingsTestUtils.openDownloadDeleteConfirmation("fr"); + const warningButton = + translationsSettingsTestUtils.getDownloadWarningButton("fr"); + ok(warningButton, "Warning icon should be shown during delete confirmation"); + ok( + warningButton.getAttribute("iconsrc")?.includes("warning"), + "Warning icon should use warning asset" + ); + const cancelRender = translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, + { expectedDetail: { languages: ["fr"], count: 1, downloading: [] } } + ); + await translationsSettingsTestUtils.cancelDownloadDelete("fr"); + await cancelRender; + + await translationsSettingsTestUtils.assertDownloadedLanguages({ + languages: ["fr"], + downloading: [], + count: 1, + }); + const deleteIcon = + translationsSettingsTestUtils.getDownloadRemoveButton("fr"); + ok(deleteIcon, "Delete icon should return after cancel"); + ok( + deleteIcon.getAttribute("iconsrc")?.includes("delete"), + "Delete icon should use delete asset" + ); + + info("Confirm deletion after second attempt"); + const deleted = translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadDeleted, + { expectedDetail: { langTag: "fr" } } + ); + const renderAfterDelete = translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, + { expectedDetail: { languages: [], count: 0, downloading: [] } } + ); + const optionsAfterDelete = translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadedLanguagesSelectOptionsUpdated + ); + + await translationsSettingsTestUtils.openDownloadDeleteConfirmation("fr"); + await translationsSettingsTestUtils.confirmDownloadDelete("fr"); + await Promise.all([deleted, renderAfterDelete, optionsAfterDelete]); + + await translationsSettingsTestUtils.assertDownloadedLanguagesEmptyState({ + visible: true, + }); + ok(!frenchOption.disabled, "French option re-enabled after removal"); + + await cleanup(); +}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_download_langs_errors.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_download_langs_errors.js @@ -0,0 +1,185 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +add_task( + async function test_download_error_retry_via_selector_and_main_button() { + const { cleanup, remoteClients, translationsSettingsTestUtils } = + await TranslationsSettingsTestUtils.openTranslationsSettingsSubpage(); + + const document = gBrowser.selectedBrowser.contentDocument; + const downloadButton = translationsSettingsTestUtils.getDownloadButton(); + + await translationsSettingsTestUtils.assertDownloadedLanguagesEmptyState({ + visible: true, + }); + + await translationsSettingsTestUtils.startDownloadFailure({ + langTag: "fr", + remoteClients, + }); + + const errorButton = + translationsSettingsTestUtils.getDownloadErrorButton("fr"); + const retryButton = + translationsSettingsTestUtils.getDownloadRetryButton("fr"); + ok(errorButton, "Error icon should be visible"); + ok(retryButton, "Retry button should be visible"); + const errorMessage = getByL10nId( + "settings-translations-subpage-download-error", + document + ); + ok(errorMessage, "Error message should be shown"); + is( + translationsSettingsTestUtils.getSelectedDownloadLanguage(), + "fr", + "French should stay selected after failed download" + ); + ok( + !downloadButton.disabled, + "Download button should stay enabled after failed download" + ); + + info("Retry French download via main button"); + await translationsSettingsTestUtils.selectDownloadLanguage("fr"); + await click(downloadButton, "Retry French download via main button"); + + const modelNames = + TranslationsSettingsTestUtils.getLanguageModelNames("fr"); + await remoteClients.translationModels.resolvePendingDownloads( + modelNames.length + ); + + info("Waiting for French retry to finish"); + await waitForCondition( + () => + translationsSettingsTestUtils.getDownloadRemoveButton("fr") && + !translationsSettingsTestUtils.getDownloadRetryButton("fr"), + "Waiting for French download to succeed after retry" + ); + + await translationsSettingsTestUtils.assertDownloadedLanguages({ + languages: ["fr"], + downloading: [], + count: 1, + }); + is( + translationsSettingsTestUtils.getSelectedDownloadLanguage(), + "", + "Download selection should reset after successful retry" + ); + await translationsSettingsTestUtils.assertDownloadedLanguagesEmptyState({ + visible: false, + }); + + await cleanup(); + } +); + +add_task(async function test_download_error_retry_via_retry_button() { + const { cleanup, remoteClients, translationsSettingsTestUtils } = + await TranslationsSettingsTestUtils.openTranslationsSettingsSubpage(); + + const document = gBrowser.selectedBrowser.contentDocument; + const downloadButton = translationsSettingsTestUtils.getDownloadButton(); + + await translationsSettingsTestUtils.assertDownloadedLanguagesEmptyState({ + visible: true, + }); + + await translationsSettingsTestUtils.startDownloadFailure({ + langTag: "es", + remoteClients, + }); + + const errorButton = + translationsSettingsTestUtils.getDownloadErrorButton("es"); + const retryButton = + translationsSettingsTestUtils.getDownloadRetryButton("es"); + ok(errorButton, "Error icon should be visible"); + ok(retryButton, "Retry button should be visible"); + const errorMessage = getByL10nId( + "settings-translations-subpage-download-error", + document + ); + ok(errorMessage, "Error message should be shown"); + is( + translationsSettingsTestUtils.getSelectedDownloadLanguage(), + "es", + "Spanish should stay selected after failed download" + ); + ok( + !downloadButton.disabled, + "Download button should stay enabled after failed download" + ); + + info("Retry Spanish download via inline retry button"); + await translationsSettingsTestUtils.clickDownloadRetry("es"); + + const modelNames = TranslationsSettingsTestUtils.getLanguageModelNames("es"); + await remoteClients.translationModels.resolvePendingDownloads( + modelNames.length + ); + + info("Waiting for Spanish retry to finish"); + await waitForCondition( + () => + translationsSettingsTestUtils.getDownloadRemoveButton("es") && + !translationsSettingsTestUtils.getDownloadRetryButton("es"), + "Waiting for Spanish download to succeed after retry button" + ); + + await translationsSettingsTestUtils.assertDownloadedLanguages({ + languages: ["es"], + downloading: [], + count: 1, + }); + is( + translationsSettingsTestUtils.getSelectedDownloadLanguage(), + "", + "Download selection should reset after retry success" + ); + await translationsSettingsTestUtils.assertDownloadedLanguagesEmptyState({ + visible: false, + }); + + await cleanup(); +}); + +add_task(async function test_download_delete_cancel_restores_state() { + const { cleanup, remoteClients, translationsSettingsTestUtils } = + await TranslationsSettingsTestUtils.openTranslationsSettingsSubpage(); + + const downloadButton = translationsSettingsTestUtils.getDownloadButton(); + + await translationsSettingsTestUtils.selectDownloadLanguage("uk"); + info("Download Ukrainian"); + await click(downloadButton, "Download Ukrainian"); + + const modelNames = TranslationsSettingsTestUtils.getLanguageModelNames("uk"); + await remoteClients.translationModels.resolvePendingDownloads( + modelNames.length + ); + info("Waiting for Ukrainian download to complete"); + await waitForCondition( + () => + translationsSettingsTestUtils.getDownloadRemoveButton("uk") && + !translationsSettingsTestUtils.getDownloadRetryButton("uk"), + "Waiting for Ukrainian download to complete" + ); + + await translationsSettingsTestUtils.openDownloadDeleteConfirmation("uk"); + info("Cancel delete for Ukrainian"); + await translationsSettingsTestUtils.cancelDownloadDelete("uk"); + + await translationsSettingsTestUtils.assertDownloadedLanguages({ + languages: ["uk"], + downloading: [], + count: 1, + }); + + await cleanup(); +}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_download_langs_sorting.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_download_langs_sorting.js @@ -0,0 +1,132 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_download_languages_sorting_and_batch_resolution() { + const { cleanup, remoteClients, translationsSettingsTestUtils } = + await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [["browser.settings-redesign.enabled", true]], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Waiting for translationsManageButton"); + const manageButton = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton" + ); + manageButton.scrollIntoView({ behavior: "instant", block: "center" }); + + const initialDownloadsRendered = translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, + { + expectedDetail: { languages: [], count: 0, downloading: [] }, + } + ); + + info("Opening translations subpage"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.Initialized]], + }, + async () => { + click(manageButton, "Open translations subpage"); + } + ); + await initialDownloadsRendered; + + const downloadButton = translationsSettingsTestUtils.getDownloadButton(); + + info("Verify empty state before downloads"); + await translationsSettingsTestUtils.assertDownloadedLanguagesEmptyState({ + visible: true, + }); + + async function downloadAndResolve(langTag, inProgressLangs, finalOrder) { + await translationsSettingsTestUtils.selectDownloadLanguage(langTag); + + const started = translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadStarted, + { expectedDetail: { langTag } } + ); + const renderInProgress = translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, + { + expectedDetail: { + languages: inProgressLangs, + count: inProgressLangs.length, + downloading: [langTag], + }, + } + ); + const optionsUpdated = translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events + .DownloadedLanguagesSelectOptionsUpdated + ); + + const completed = translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadCompleted, + { expectedDetail: { langTag } } + ); + const renderComplete = translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, + { + expectedDetail: { + languages: finalOrder, + count: finalOrder.length, + downloading: [], + }, + } + ); + const optionsUpdatedAfter = translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events + .DownloadedLanguagesSelectOptionsUpdated + ); + + await click(downloadButton, `Start ${langTag} download`); + await Promise.all([started, renderInProgress, optionsUpdated]); + + const modelNames = languageModelNames([ + { fromLang: langTag, toLang: "en" }, + { fromLang: "en", toLang: langTag }, + ]); + await remoteClients.translationModels.resolvePendingDownloads( + modelNames.length + ); + + await Promise.all([completed, renderComplete, optionsUpdatedAfter]); + } + + info("Download French first"); + await downloadAndResolve("fr", ["fr"], ["fr"]); + await translationsSettingsTestUtils.assertDownloadedLanguagesEmptyState({ + visible: false, + }); + await translationsSettingsTestUtils.assertDownloadedLanguagesOrder({ + languages: ["fr"], + }); + + info("Download Ukrainian second"); + await downloadAndResolve("uk", ["fr", "uk"], ["fr", "uk"]); + await translationsSettingsTestUtils.assertDownloadedLanguagesOrder({ + languages: ["fr", "uk"], + }); + + info("Download Spanish; expect it to sort between French and Ukrainian"); + await downloadAndResolve("es", ["fr", "es", "uk"], ["fr", "es", "uk"]); + + await translationsSettingsTestUtils.assertDownloadedLanguages({ + languages: ["es", "fr", "uk"], + downloading: [], + count: 3, + }); + await translationsSettingsTestUtils.assertDownloadedLanguagesOrder({ + languages: ["fr", "es", "uk"], + }); + await translationsSettingsTestUtils.assertDownloadedLanguagesEmptyState({ + visible: false, + }); + + await cleanup(); +}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_never_translate_langs_a11y.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_never_translate_langs_a11y.js @@ -0,0 +1,171 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests keyboard activation (Enter key) of remove buttons in the never-translate language list. + */ +add_task(async function test_never_translate_languages_keyboard_activation() { + const { cleanup, translationsSettingsTestUtils } = + await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [ + ["browser.settings-redesign.enabled", true], + [NEVER_TRANSLATE_LANGS_PREF, "es,fr,uk"], + ], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Navigating to translations subpage"); + const manageButton = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton" + ); + + info("Scrolling button into view"); + manageButton.scrollIntoView({ behavior: "instant", block: "center" }); + + info("Clicking manage button and waiting for initialization"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.Initialized]], + }, + async () => { + click( + manageButton, + "Clicking manage button to open translations subpage" + ); + } + ); + + info("Verifying three language items are rendered"); + await translationsSettingsTestUtils.assertNeverTranslateLanguages({ + languages: ["es", "fr", "uk"], + count: 3, + }); + + info("Getting first remove button"); + const firstRemoveButton = document.querySelector( + ".translations-never-remove-button" + ); + ok(firstRemoveButton, "First remove button should exist"); + + info("Testing keyboard activation with Enter key"); + firstRemoveButton.focus(); + is( + document.activeElement, + firstRemoveButton, + "Remove button should be focused" + ); + + info("Pressing Enter to remove first language"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.NeverLanguagesRendered]], + }, + async () => { + const prefChanged = TestUtils.waitForPrefChange( + NEVER_TRANSLATE_LANGS_PREF + ); + const enterEvent = new KeyboardEvent("keydown", { + key: "Enter", + bubbles: true, + }); + firstRemoveButton.dispatchEvent(enterEvent); + click(firstRemoveButton, "Activating remove via Enter key"); + await prefChanged; + } + ); + + info("Verifying language was removed"); + const langs = getNeverTranslateLanguagesFromPref(); + is(langs.length, 2, "Should have 2 languages after removal"); + + info("Verifying UI updated to show 2 items"); + await translationsSettingsTestUtils.assertNeverTranslateLanguages({ + count: 2, + }); + + await cleanup(); +}); + +/** + * Tests accessibility features including ARIA labels and keyboard-only operation. + */ +add_task(async function test_never_translate_languages_accessibility() { + const { cleanup, translationsSettingsTestUtils } = + await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [ + ["browser.settings-redesign.enabled", true], + [NEVER_TRANSLATE_LANGS_PREF, "es,fr"], + ], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Navigating to translations subpage"); + const manageButton = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton" + ); + + info("Scrolling button into view"); + manageButton.scrollIntoView({ behavior: "instant", block: "center" }); + + info("Clicking manage button and waiting for initialization"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.Initialized]], + }, + async () => { + click( + manageButton, + "Clicking manage button to open translations subpage" + ); + } + ); + + info("Verifying two language items are rendered"); + await translationsSettingsTestUtils.assertNeverTranslateLanguages({ + languages: ["es", "fr"], + count: 2, + }); + + info("Getting language items and remove buttons"); + const items = document.querySelectorAll(".translations-never-language-item"); + is(items.length, 2, "Should have 2 language items"); + + info("Verifying remove buttons have accessible labels"); + const removeButtons = document.querySelectorAll( + ".translations-never-remove-button" + ); + is(removeButtons.length, 2, "Should have 2 remove buttons"); + + for (const button of removeButtons) { + const ariaLabel = + button.getAttribute("aria-label") || + button.ariaLabel || + button.getAttribute("data-l10n-id"); + ok( + ariaLabel, + "Remove button should have aria-label or data-l10n-id for accessibility" + ); + if (typeof ariaLabel === "string") { + Assert.greater( + ariaLabel.length, + 0, + "Remove button accessibility label should not be empty" + ); + } + } + + info("Verifying language items have proper semantic structure"); + for (const item of items) { + const label = item.getAttribute("label"); + ok(label, "Each language item should have a label attribute"); + Assert.greater(label.length, 0, "Label should not be empty"); + } + + await cleanup(); +}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_never_translate_langs_basic.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_never_translate_langs_basic.js @@ -0,0 +1,160 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that pre-populated languages load correctly when opening the subpage. + */ +add_task(async function test_never_translate_languages_prepopulated() { + const { cleanup, translationsSettingsTestUtils } = + await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [ + ["browser.settings-redesign.enabled", true], + [NEVER_TRANSLATE_LANGS_PREF, "uk,es,fr"], + ], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Navigating to translations subpage"); + const manageButton = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton" + ); + + info("Scrolling button into view"); + manageButton.scrollIntoView({ behavior: "instant", block: "center" }); + + info("Clicking manage button and waiting for initialization"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.Initialized]], + }, + async () => { + click( + manageButton, + "Clicking manage button to open translations subpage" + ); + } + ); + + info("Verifying three language items are displayed"); + await translationsSettingsTestUtils.assertNeverTranslateLanguages({ + languages: ["es", "fr", "uk"], + count: 3, + }); + await translationsSettingsTestUtils.assertNeverTranslateLanguagesOrder({ + languages: ["fr", "es", "uk"], + }); + + info("Verifying empty state is not visible"); + await translationsSettingsTestUtils.assertNeverTranslateLanguagesEmptyState({ + visible: false, + }); + + await cleanup(); +}); + +/** + * Tests that the dropdown state is managed correctly - already-added languages + * should be disabled in the dropdown. + */ +add_task(async function test_never_translate_languages_dropdown_state() { + const { cleanup, translationsSettingsTestUtils } = + await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [ + ["browser.settings-redesign.enabled", true], + [NEVER_TRANSLATE_LANGS_PREF, "es,fr"], + ], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Navigating to translations subpage"); + const manageButton = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton" + ); + + info("Scrolling button into view"); + manageButton.scrollIntoView({ behavior: "instant", block: "center" }); + + info("Clicking manage button and waiting for initialization"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.Initialized]], + }, + async () => { + click( + manageButton, + "Clicking manage button to open translations subpage" + ); + } + ); + + info("Verifying two language items are rendered"); + await translationsSettingsTestUtils.assertNeverTranslateLanguages({ + languages: ["es", "fr"], + count: 2, + }); + + info("Getting the dropdown element"); + const dropdown = + translationsSettingsTestUtils.getNeverTranslateLanguagesSelect(); + + info("Verifying Spanish and French options are disabled"); + const spanishOption = dropdown.querySelector('moz-option[value="es"]'); + const frenchOption = dropdown.querySelector('moz-option[value="fr"]'); + const ukrainianOption = dropdown.querySelector('moz-option[value="uk"]'); + + ok(spanishOption, "Spanish option should exist in dropdown"); + ok(frenchOption, "French option should exist in dropdown"); + ok(ukrainianOption, "Ukrainian option should exist in dropdown"); + + ok( + spanishOption.disabled, + "Spanish option should be disabled (already added)" + ); + ok(frenchOption.disabled, "French option should be disabled (already added)"); + ok(!ukrainianOption.disabled, "Ukrainian option should not be disabled"); + + info("Adding Ukrainian via dropdown"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [TranslationsSettingsTestUtils.Events.NeverLanguagesRendered], + [ + TranslationsSettingsTestUtils.Events + .NeverTranslateLanguagesSelectOptionsUpdated, + ], + ], + }, + async () => { + const prefChanged = TestUtils.waitForPrefChange( + NEVER_TRANSLATE_LANGS_PREF + ); + await translationsSettingsTestUtils.addNeverTranslateLanguage("uk"); + await prefChanged; + } + ); + + info("Verifying Ukrainian was added"); + await translationsSettingsTestUtils.assertNeverTranslateLanguages({ + languages: ["es", "fr", "uk"], + count: 3, + }); + + info("Verifying dropdown updated and Ukrainian is now disabled"); + ok( + ukrainianOption.disabled, + "Ukrainian option should now be disabled (just added)" + ); + + await dropdown.updateComplete; + + info("Verifying dropdown resets to placeholder"); + is(dropdown.value, "", "Dropdown should reset to empty value"); + + await cleanup(); +}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_never_translate_langs_modify.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_never_translate_langs_modify.js @@ -0,0 +1,413 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests basic adding and removing of languages in the Never Translate Languages + * section, including empty state transitions. + */ +add_task(async function test_never_translate_languages_add_and_remove() { + const { cleanup, translationsSettingsTestUtils } = + await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [ + ["browser.settings-redesign.enabled", true], + [NEVER_TRANSLATE_LANGS_PREF, ""], + ], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Navigating to translations subpage"); + const manageButton = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton" + ); + + info("Scrolling button into view"); + manageButton.scrollIntoView({ behavior: "instant", block: "center" }); + + info("Clicking manage button and waiting for initialization"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.Initialized]], + }, + async () => { + click( + manageButton, + "Clicking manage button to open translations subpage" + ); + } + ); + + info("Verifying empty state is visible initially"); + await translationsSettingsTestUtils.assertNeverTranslateLanguagesEmptyState({ + visible: true, + }); + + info("Adding Spanish (es) via dropdown"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [TranslationsSettingsTestUtils.Events.NeverLanguagesRendered], + [ + TranslationsSettingsTestUtils.Events + .NeverTranslateLanguagesEmptyStateHidden, + ], + ], + }, + async () => { + const prefChanged = TestUtils.waitForPrefChange( + NEVER_TRANSLATE_LANGS_PREF + ); + await translationsSettingsTestUtils.addNeverTranslateLanguage("es"); + await prefChanged; + } + ); + + info("Verifying Spanish was added"); + await translationsSettingsTestUtils.assertNeverTranslateLanguages({ + languages: ["es"], + count: 1, + }); + await translationsSettingsTestUtils.assertNeverTranslateLanguagesEmptyState({ + visible: false, + }); + + info("Adding French (fr) via dropdown"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.NeverLanguagesRendered]], + }, + async () => { + const prefChanged = TestUtils.waitForPrefChange( + NEVER_TRANSLATE_LANGS_PREF + ); + await translationsSettingsTestUtils.addNeverTranslateLanguage("fr"); + await prefChanged; + } + ); + + info("Verifying French was added"); + await translationsSettingsTestUtils.assertNeverTranslateLanguages({ + languages: ["es", "fr"], + count: 2, + }); + + info("Adding Ukrainian (uk) via dropdown"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.NeverLanguagesRendered]], + }, + async () => { + const prefChanged = TestUtils.waitForPrefChange( + NEVER_TRANSLATE_LANGS_PREF + ); + await translationsSettingsTestUtils.addNeverTranslateLanguage("uk"); + await prefChanged; + } + ); + + info("Verifying Ukrainian was added"); + await translationsSettingsTestUtils.assertNeverTranslateLanguages({ + languages: ["es", "fr", "uk"], + count: 3, + }); + + info("Removing middle item (French)"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.NeverLanguagesRendered]], + }, + async () => { + const prefChanged = TestUtils.waitForPrefChange( + NEVER_TRANSLATE_LANGS_PREF + ); + await translationsSettingsTestUtils.removeNeverTranslateLanguage("fr"); + await prefChanged; + } + ); + + info("Verifying French was removed"); + await translationsSettingsTestUtils.assertNeverTranslateLanguages({ + languages: ["es", "uk"], + count: 2, + }); + await translationsSettingsTestUtils.assertNeverTranslateLanguagesEmptyState({ + visible: false, + }); + + info("Removing Ukrainian"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.NeverLanguagesRendered]], + }, + async () => { + const prefChanged = TestUtils.waitForPrefChange( + NEVER_TRANSLATE_LANGS_PREF + ); + await translationsSettingsTestUtils.removeNeverTranslateLanguage("uk"); + await prefChanged; + } + ); + + info("Verifying Ukrainian was removed"); + await translationsSettingsTestUtils.assertNeverTranslateLanguages({ + languages: ["es"], + count: 1, + }); + await translationsSettingsTestUtils.assertNeverTranslateLanguagesEmptyState({ + visible: false, + }); + + info("Removing Spanish (last language)"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [TranslationsSettingsTestUtils.Events.NeverLanguagesRendered], + [ + TranslationsSettingsTestUtils.Events + .NeverTranslateLanguagesEmptyStateShown, + ], + ], + }, + async () => { + const prefChanged = TestUtils.waitForPrefChange( + NEVER_TRANSLATE_LANGS_PREF + ); + await translationsSettingsTestUtils.removeNeverTranslateLanguage("es"); + await prefChanged; + } + ); + + info("Verifying all languages removed and empty state reappears"); + await translationsSettingsTestUtils.assertNeverTranslateLanguages({ + languages: [], + count: 0, + }); + await translationsSettingsTestUtils.assertNeverTranslateLanguagesEmptyState({ + visible: true, + }); + + await cleanup(); +}); + +/** + * Tests that invalid language tags don't break the UI and valid languages + * are still rendered correctly. + */ +add_task(async function test_never_translate_languages_invalid_tags() { + const { cleanup, translationsSettingsTestUtils } = + await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [ + ["browser.settings-redesign.enabled", true], + [NEVER_TRANSLATE_LANGS_PREF, "es,fr,uk"], + ], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Navigating to translations subpage"); + const manageButton = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton" + ); + + info("Scrolling button into view"); + manageButton.scrollIntoView({ behavior: "instant", block: "center" }); + + info("Clicking manage button and waiting for initialization"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.Initialized]], + }, + async () => { + click( + manageButton, + "Clicking manage button to open translations subpage" + ); + } + ); + + info("Verifying three valid languages are displayed"); + await translationsSettingsTestUtils.assertNeverTranslateLanguages({ + languages: ["es", "fr", "uk"], + count: 3, + }); + + info("Testing UI doesn't break when pref has mixed valid/invalid tags"); + info("Adding invalid tags via pref change"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.NeverLanguagesRendered]], + }, + async () => { + Services.prefs.setCharPref( + NEVER_TRANSLATE_LANGS_PREF, + "es,INVALID,fr,uk" + ); + } + ); + + info("Verifying UI still works and valid languages remain accessible"); + ok( + document.querySelector('[data-lang-tag="es"]'), + "Spanish item should still exist" + ); + ok( + document.querySelector('[data-lang-tag="fr"]'), + "French item should still exist" + ); + ok( + document.querySelector('[data-lang-tag="uk"]'), + "Ukrainian item should still exist" + ); + + await cleanup(); +}); + +/** + * Tests that adding a language to never-translate automatically removes it + * from the always-translate list ("stealing" behavior). + */ +add_task(async function test_never_translate_languages_stealing() { + const { cleanup, translationsSettingsTestUtils } = + await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [ + ["browser.settings-redesign.enabled", true], + [ALWAYS_TRANSLATE_LANGS_PREF, "es,fr,uk"], + [NEVER_TRANSLATE_LANGS_PREF, ""], + ], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Navigating to translations subpage"); + const manageButton = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton" + ); + + info("Scrolling button into view"); + manageButton.scrollIntoView({ behavior: "instant", block: "center" }); + + info("Clicking manage button and waiting for initialization"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.Initialized]], + }, + async () => { + click( + manageButton, + "Clicking manage button to open translations subpage" + ); + } + ); + + info("Verifying never-translate section is empty"); + await translationsSettingsTestUtils.assertNeverTranslateLanguagesEmptyState({ + visible: true, + }); + + info("Verifying always-translate has three languages"); + let alwaysLangs = getAlwaysTranslateLanguagesFromPref(); + is(alwaysLangs.length, 3, "Should have 3 always-translate languages"); + ok( + alwaysLangs.includes("es") && + alwaysLangs.includes("fr") && + alwaysLangs.includes("uk"), + "Always-translate should include es, fr, uk" + ); + + info("Adding Spanish to never-translate via UI"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [TranslationsSettingsTestUtils.Events.NeverLanguagesRendered], + [ + TranslationsSettingsTestUtils.Events + .NeverTranslateLanguagesEmptyStateHidden, + ], + ], + }, + async () => { + const neverPrefChanged = TestUtils.waitForPrefChange( + NEVER_TRANSLATE_LANGS_PREF + ); + await translationsSettingsTestUtils.addNeverTranslateLanguage("es"); + await neverPrefChanged; + } + ); + + info("Verifying Spanish appears in never-translate list"); + await translationsSettingsTestUtils.assertNeverTranslateLanguages({ + languages: ["es"], + count: 1, + }); + + info("Verifying Spanish was removed from always-translate pref"); + alwaysLangs = getAlwaysTranslateLanguagesFromPref(); + is(alwaysLangs.length, 2, "Should have 2 always-translate languages"); + ok( + !alwaysLangs.includes("es"), + "Always-translate should not include Spanish" + ); + ok( + alwaysLangs.includes("fr") && alwaysLangs.includes("uk"), + "Always-translate should still include French and Ukrainian" + ); + + info("Adding French to never-translate via UI"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.NeverLanguagesRendered]], + }, + async () => { + const neverPrefChanged = TestUtils.waitForPrefChange( + NEVER_TRANSLATE_LANGS_PREF + ); + await translationsSettingsTestUtils.addNeverTranslateLanguage("fr"); + await neverPrefChanged; + } + ); + + info("Verifying both Spanish and French in never-translate list"); + await translationsSettingsTestUtils.assertNeverTranslateLanguages({ + languages: ["es", "fr"], + count: 2, + }); + + info("Verifying only Ukrainian remains in always-translate pref"); + alwaysLangs = getAlwaysTranslateLanguagesFromPref(); + is(alwaysLangs.length, 1, "Should have 1 always-translate language"); + ok( + alwaysLangs.includes("uk"), + "Always-translate should only include Ukrainian" + ); + + info("Verifying stealing also works via UI for Ukrainian"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.NeverLanguagesRendered]], + }, + async () => { + const neverPrefChanged = TestUtils.waitForPrefChange( + NEVER_TRANSLATE_LANGS_PREF + ); + await translationsSettingsTestUtils.addNeverTranslateLanguage("uk"); + await neverPrefChanged; + } + ); + + info("Verifying all three languages now in never-translate"); + await translationsSettingsTestUtils.assertNeverTranslateLanguages({ + languages: ["es", "fr", "uk"], + count: 3, + }); + + info("Verifying always-translate is empty after stealing"); + alwaysLangs = getAlwaysTranslateLanguagesFromPref(); + is(alwaysLangs.length, 0, "Always-translate should be empty after stealing"); + + await cleanup(); +}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_never_translate_langs_observe.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_never_translate_langs_observe.js @@ -0,0 +1,241 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the UI reactively updates when prefs are changed externally + * while the subpage is open. + */ +add_task(async function test_never_translate_languages_observe_pref_changes() { + const { cleanup, translationsSettingsTestUtils } = + await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [ + ["browser.settings-redesign.enabled", true], + [NEVER_TRANSLATE_LANGS_PREF, ""], + ], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Navigating to translations subpage"); + const manageButton = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton" + ); + + info("Scrolling button into view"); + manageButton.scrollIntoView({ behavior: "instant", block: "center" }); + + info("Clicking manage button and waiting for initialization"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.Initialized]], + }, + async () => { + click( + manageButton, + "Clicking manage button to open translations subpage" + ); + } + ); + + info("Verifying empty state initially visible"); + await translationsSettingsTestUtils.assertNeverTranslateLanguagesEmptyState({ + visible: true, + }); + + info("Adding Spanish (es) via pref directly"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [TranslationsSettingsTestUtils.Events.NeverLanguagesRendered], + [ + TranslationsSettingsTestUtils.Events + .NeverTranslateLanguagesEmptyStateHidden, + ], + ], + }, + async () => { + Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, "es"); + } + ); + + info("Verifying Spanish was added"); + await translationsSettingsTestUtils.assertNeverTranslateLanguages({ + languages: ["es"], + count: 1, + }); + + info("Adding more languages via pref (es,fr,uk)"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.NeverLanguagesRendered]], + }, + async () => { + Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, "es,fr,uk"); + } + ); + + info("Verifying all three languages are displayed"); + await translationsSettingsTestUtils.assertNeverTranslateLanguages({ + languages: ["es", "fr", "uk"], + count: 3, + }); + + info("Removing French via pref (es,uk)"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.NeverLanguagesRendered]], + }, + async () => { + Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, "es,uk"); + } + ); + + info("Verifying French was removed"); + await translationsSettingsTestUtils.assertNeverTranslateLanguages({ + languages: ["es", "uk"], + count: 2, + }); + + info("Clearing all languages via pref"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [TranslationsSettingsTestUtils.Events.NeverLanguagesRendered], + [ + TranslationsSettingsTestUtils.Events + .NeverTranslateLanguagesEmptyStateShown, + ], + ], + }, + async () => { + Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, ""); + } + ); + + info("Verifying all languages removed and empty state returns"); + await translationsSettingsTestUtils.assertNeverTranslateLanguages({ + languages: [], + count: 0, + }); + await translationsSettingsTestUtils.assertNeverTranslateLanguagesEmptyState({ + visible: true, + }); + + await cleanup(); +}); + +/** + * Tests that both UI lists update correctly when simulating a stealing scenario + * by manually adding a language to one pref and removing it from the other. + */ +add_task(async function test_never_translate_languages_simulated_stealing() { + const { cleanup, translationsSettingsTestUtils } = + await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [ + ["browser.settings-redesign.enabled", true], + [ALWAYS_TRANSLATE_LANGS_PREF, "es,fr"], + [NEVER_TRANSLATE_LANGS_PREF, ""], + ], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Navigating to translations subpage"); + const manageButton = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton" + ); + + info("Scrolling button into view"); + manageButton.scrollIntoView({ behavior: "instant", block: "center" }); + + info("Clicking manage button and waiting for initialization"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.Initialized]], + }, + async () => { + click( + manageButton, + "Clicking manage button to open translations subpage" + ); + } + ); + + info("Verifying never-translate section is empty"); + await translationsSettingsTestUtils.assertNeverTranslateLanguagesEmptyState({ + visible: true, + }); + + info( + "Simulating stealing by adding Spanish to never-translate and removing from always-translate" + ); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [TranslationsSettingsTestUtils.Events.NeverLanguagesRendered], + [ + TranslationsSettingsTestUtils.Events + .NeverTranslateLanguagesEmptyStateHidden, + ], + [TranslationsSettingsTestUtils.Events.AlwaysLanguagesRendered], + ], + }, + async () => { + Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, "es"); + Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, "fr"); + } + ); + + info("Verifying Spanish appears in never-translate UI"); + await translationsSettingsTestUtils.assertNeverTranslateLanguages({ + languages: ["es"], + count: 1, + }); + + info("Verifying always-translate UI updated to show only French"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ + languages: ["fr"], + count: 1, + }); + + info( + "Simulating stealing French by adding to never-translate and removing from always-translate" + ); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [TranslationsSettingsTestUtils.Events.NeverLanguagesRendered], + [TranslationsSettingsTestUtils.Events.AlwaysLanguagesRendered], + [ + TranslationsSettingsTestUtils.Events + .AlwaysTranslateLanguagesEmptyStateShown, + ], + ], + }, + async () => { + Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, "es,fr"); + Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, ""); + } + ); + + info("Verifying both Spanish and French in never-translate UI"); + await translationsSettingsTestUtils.assertNeverTranslateLanguages({ + languages: ["es", "fr"], + count: 2, + }); + + info("Verifying always-translate UI shows empty state"); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguages({ + languages: [], + count: 0, + }); + await translationsSettingsTestUtils.assertAlwaysTranslateLanguagesEmptyState({ + visible: true, + }); + + await cleanup(); +}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_never_translate_sites_basic.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_never_translate_sites_basic.js @@ -0,0 +1,265 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that never-translate site permissions load when opening the settings subpage. + */ +add_task(async function test_never_translate_sites_prepopulated() { + const exampleComOrigin = new URL(ENGLISH_PAGE_URL).origin; + const exampleOrgOrigin = new URL(SPANISH_PAGE_URL_DOT_ORG).origin; + const exampleNetOrigin = new URL("https://example.net").origin; + const siteOrigins = [exampleOrgOrigin, exampleComOrigin, exampleNetOrigin]; + const expectedSites = TranslationsSettingsTestUtils.sortOrigins(siteOrigins); + + const { cleanup, translationsSettingsTestUtils } = + await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [["browser.settings-redesign.enabled", true]], + }); + + info("Adding never-translate site permissions before opening settings"); + for (const origin of siteOrigins) { + TranslationsParent.setNeverTranslateSiteByOrigin(true, origin); + } + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Navigating to the translations settings subpage"); + const manageButton = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton" + ); + + info("Scrolling manage button into view"); + manageButton.scrollIntoView({ behavior: "instant", block: "center" }); + + info( + "Opening settings subpage and waiting for never-translate sites to render" + ); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [ + TranslationsSettingsTestUtils.Events.NeverSitesRendered, + { sites: expectedSites, count: expectedSites.length }, + ], + [ + TranslationsSettingsTestUtils.Events + .NeverTranslateSitesEmptyStateHidden, + ], + [TranslationsSettingsTestUtils.Events.Initialized], + ], + }, + async () => { + click(manageButton, "Opening translations settings subpage"); + } + ); + + info("Verifying pre-populated never-translate sites are displayed"); + await translationsSettingsTestUtils.assertNeverTranslateSites({ + sites: expectedSites, + count: expectedSites.length, + }); + + info( + "Verifying never-translate sites empty state is hidden when sites exist" + ); + await translationsSettingsTestUtils.assertNeverTranslateSitesEmptyState({ + visible: false, + }); + + await cleanup(); +}); + +/** + * Tests deleting never-translate sites and showing the empty-state row. + */ +add_task(async function test_never_translate_sites_delete_and_empty_state() { + const exampleComOrigin = new URL(ENGLISH_PAGE_URL).origin; + const exampleOrgOrigin = new URL(SPANISH_PAGE_URL_DOT_ORG).origin; + const initialSites = [exampleComOrigin, exampleOrgOrigin]; + const expectedInitialSites = + TranslationsSettingsTestUtils.sortOrigins(initialSites); + + const { cleanup, translationsSettingsTestUtils } = + await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [["browser.settings-redesign.enabled", true]], + }); + + info("Adding never-translate site permissions before opening settings"); + for (const origin of initialSites) { + TranslationsParent.setNeverTranslateSiteByOrigin(true, origin); + } + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Navigating to the translations settings subpage"); + const manageButton = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton" + ); + + info("Scrolling manage button into view"); + manageButton.scrollIntoView({ behavior: "instant", block: "center" }); + + info( + "Opening settings subpage and waiting for never-translate sites to render" + ); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [ + TranslationsSettingsTestUtils.Events.NeverSitesRendered, + { + sites: expectedInitialSites, + count: expectedInitialSites.length, + }, + ], + [ + TranslationsSettingsTestUtils.Events + .NeverTranslateSitesEmptyStateHidden, + ], + [TranslationsSettingsTestUtils.Events.Initialized], + ], + }, + async () => { + click(manageButton, "Opening translations settings subpage"); + } + ); + + info("Verifying both never-translate sites are shown"); + await translationsSettingsTestUtils.assertNeverTranslateSites({ + sites: expectedInitialSites, + count: expectedInitialSites.length, + }); + + const removedOrigin = expectedInitialSites[0]; + const remainingOrigin = expectedInitialSites[1]; + + info(`Removing never-translate site ${removedOrigin} via delete button`); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [ + TranslationsSettingsTestUtils.Events.NeverSitesRendered, + { sites: [remainingOrigin], count: 1 }, + ], + ], + }, + async () => { + await translationsSettingsTestUtils.removeNeverTranslateSite( + removedOrigin + ); + } + ); + + info("Verifying single site remains after deletion"); + await translationsSettingsTestUtils.assertNeverTranslateSites({ + sites: [remainingOrigin], + count: 1, + }); + await translationsSettingsTestUtils.assertNeverTranslateSitesEmptyState({ + visible: false, + }); + + info(`Removing final never-translate site ${remainingOrigin}`); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [ + TranslationsSettingsTestUtils.Events.NeverSitesRendered, + { sites: [], count: 0 }, + ], + [ + TranslationsSettingsTestUtils.Events + .NeverTranslateSitesEmptyStateShown, + ], + ], + }, + async () => { + await translationsSettingsTestUtils.removeNeverTranslateSite( + remainingOrigin + ); + } + ); + + info("Verifying empty state row appears after last deletion"); + await translationsSettingsTestUtils.assertNeverTranslateSites({ + sites: [], + count: 0, + }); + await translationsSettingsTestUtils.assertNeverTranslateSitesEmptyState({ + visible: true, + }); + + await cleanup(); +}); + +/** + * Tests that never-translate sites are sorted by domain ignoring the scheme. + */ +add_task(async function test_never_translate_sites_sorted_ignoring_scheme() { + const httpEsOrigin = new URL("https://es.wikipedia.org").origin; + const httpsEnOrigin = new URL("https://en.wikipedia.org").origin; + const httpsJaOrigin = new URL("https://ja.wikipedia.org").origin; + const siteOrigins = [httpsJaOrigin, httpEsOrigin, httpsEnOrigin]; + const expectedSites = TranslationsSettingsTestUtils.sortOrigins(siteOrigins); + + const { cleanup, translationsSettingsTestUtils } = + await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [["browser.settings-redesign.enabled", true]], + }); + + info("Adding never-translate site permissions before opening settings"); + for (const origin of siteOrigins) { + TranslationsParent.setNeverTranslateSiteByOrigin(true, origin); + } + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Navigating to the translations settings subpage"); + const manageButton = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton" + ); + + info("Scrolling manage button into view"); + manageButton.scrollIntoView({ behavior: "instant", block: "center" }); + + info( + "Opening settings subpage and waiting for never-translate sites to render" + ); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [ + TranslationsSettingsTestUtils.Events.NeverSitesRendered, + { sites: expectedSites, count: expectedSites.length }, + ], + [ + TranslationsSettingsTestUtils.Events + .NeverTranslateSitesEmptyStateHidden, + ], + [TranslationsSettingsTestUtils.Events.Initialized], + ], + }, + async () => { + click(manageButton, "Opening translations settings subpage"); + } + ); + + info("Verifying never-translate sites are sorted by domain ignoring scheme"); + await translationsSettingsTestUtils.assertNeverTranslateSites({ + sites: expectedSites, + count: expectedSites.length, + }); + await translationsSettingsTestUtils.assertNeverTranslateSitesOrder({ + sites: expectedSites, + }); + await translationsSettingsTestUtils.assertNeverTranslateSitesEmptyState({ + visible: false, + }); + + await cleanup(); +}); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_never_translate_sites_observe.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_never_translate_sites_observe.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that adding never-translate site permissions while the settings subpage is open + * updates the UI via the permissions observer. + */ +add_task( + async function test_never_translate_sites_observe_permission_changes() { + const exampleComOrigin = new URL(ENGLISH_PAGE_URL).origin; + const exampleOrgOrigin = new URL(SPANISH_PAGE_URL_DOT_ORG).origin; + + const { cleanup, translationsSettingsTestUtils } = + await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [["browser.settings-redesign.enabled", true]], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + + info("Navigating to the translations settings subpage"); + const manageButton = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton" + ); + + info("Scrolling manage button into view"); + manageButton.scrollIntoView({ behavior: "instant", block: "center" }); + + info("Opening settings subpage and waiting for initial empty render"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [ + TranslationsSettingsTestUtils.Events.NeverSitesRendered, + { sites: [], count: 0 }, + ], + [TranslationsSettingsTestUtils.Events.Initialized], + ], + }, + async () => { + click(manageButton, "Opening translations settings subpage"); + } + ); + + info("Verifying never-translate sites empty state is visible initially"); + await translationsSettingsTestUtils.assertNeverTranslateSitesEmptyState({ + visible: true, + }); + + info("Adding example.com to never-translate sites while settings are open"); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [ + TranslationsSettingsTestUtils.Events.NeverSitesRendered, + { sites: [exampleComOrigin], count: 1 }, + ], + [ + TranslationsSettingsTestUtils.Events + .NeverTranslateSitesEmptyStateHidden, + ], + ], + }, + async () => { + TranslationsParent.setNeverTranslateSiteByOrigin( + true, + exampleComOrigin + ); + } + ); + + info("Verifying example.com is rendered in the never-translate sites list"); + await translationsSettingsTestUtils.assertNeverTranslateSites({ + sites: [exampleComOrigin], + count: 1, + }); + await translationsSettingsTestUtils.assertNeverTranslateSitesEmptyState({ + visible: false, + }); + + info("Adding example.org to never-translate sites while settings are open"); + const expectedSites = TranslationsSettingsTestUtils.sortOrigins([ + exampleComOrigin, + exampleOrgOrigin, + ]); + await translationsSettingsTestUtils.assertEvents( + { + expected: [ + [ + TranslationsSettingsTestUtils.Events.NeverSitesRendered, + { sites: expectedSites, count: expectedSites.length }, + ], + ], + }, + async () => { + TranslationsParent.setNeverTranslateSiteByOrigin( + true, + exampleOrgOrigin + ); + } + ); + + info("Verifying both sites are displayed after observer update"); + await translationsSettingsTestUtils.assertNeverTranslateSites({ + sites: expectedSites, + count: expectedSites.length, + }); + await translationsSettingsTestUtils.assertNeverTranslateSitesEmptyState({ + visible: false, + }); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/head.js b/browser/components/translations/tests/browser/head.js @@ -4205,68 +4205,3 @@ class SelectTranslationsTestUtils { ); } } - -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 translationsPane = - content.window.gCategoryModules.get("paneTranslations"); - const promise = BrowserTestUtils.waitForEvent( - document, - "paneshown", - false, - event => event.detail.category === "paneTranslations" - ); - - click(settingsButton, "Click settings button"); - await promise; - - return translationsPane.elements; - } - - /** - * Utility function to handle the click event for a `moz-button` element that controls - * the Download/Remove Language functionality. - * - * The button's icon reflects the current state of the language (downloaded, loading, or removed), - * which is represented by a corresponding CSS class. - * - * When this button is clicked for any language, the function waits for the button's state and icon - * to update. It then checks whether the button's state and icon match the expected state as defined - * by the test case, and logs the respective message provided by the test case. - * - * @param {Element} langButton - The `moz-button` element representing the download/remove button. - * @param {string} buttonIcon - The expected CSS class representing the button's state/icon (e.g., download, loading, or remove icon). - * @param {string} logMsg - A custom log message provided by the test case indicating the expected result. - */ - - static async downaloadButtonClick(langButton, buttonIcon, logMsg) { - if ( - !langButton.parentNode - .querySelector("moz-button") - .classList.contains(buttonIcon) - ) { - await BrowserTestUtils.waitForMutationCondition( - langButton.parentNode.querySelector("moz-button"), - { attributes: true, attributeFilter: ["class"] }, - () => - langButton.parentNode - .querySelector("moz-button") - .classList.contains(buttonIcon) - ); - } - ok( - langButton.parentNode - .querySelector("moz-button") - .classList.contains(buttonIcon), - logMsg - ); - } -} diff --git a/toolkit/components/translations/actors/TranslationsParent.sys.mjs b/toolkit/components/translations/actors/TranslationsParent.sys.mjs @@ -2813,6 +2813,7 @@ export class TranslationsParent extends JSWindowActorParent { language, /* includePivotRecords */ true )) { + await chaosMode(1 / 6); const download = () => { lazy.console.log("Downloading record", record.name, record.id); return client.attachments.download(record); @@ -4626,7 +4627,9 @@ async function downloadManager(queue) { const newRetriesLeft = retriesLeft - 1; - if (retriesLeft > 0) { + // Skip retries in automation to avoid slow test timeouts, + // especially when running in chaos mode when things take longer. + if (retriesLeft > 0 && !Cu.isInAutomation) { lazy.console.log( `Queueing another attempt. ${newRetriesLeft} attempts left.` ); @@ -4658,6 +4661,9 @@ async function downloadManager(queue) { } // Wait for any active downloads to complete. + if (!pendingDownloadAttempts.size) { + break; + } await Promise.race(pendingDownloadAttempts); } diff --git a/toolkit/components/translations/tests/browser/shared-head.js b/toolkit/components/translations/tests/browser/shared-head.js @@ -353,6 +353,1127 @@ function upperCaseNode(node) { } /** + * Test utility class for translations settings UI tests. + * Provides methods for interacting with and asserting the state of + * the translations settings page in about:preferences. + */ +class TranslationsSettingsTestUtils { + /** + * @param {Document} document - The settings document + */ + constructor(document) { + this.document = document; + } + + async openTranslationsSubpageFromDocument() { + const manageButton = await waitForCondition( + () => this.document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton" + ); + manageButton.scrollIntoView({ behavior: "instant", block: "center" }); + + await this.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.Initialized]], + }, + async () => { + click(manageButton, "Open translations subpage"); + } + ); + } + + /** + * Opens the translations settings subpage and returns helpers. + * + * @param {Array} [lexicalShortlistPrefs] + * @returns {Promise<{cleanup: Function, remoteClients: object, translationsSettingsTestUtils: TranslationsSettingsTestUtils}>} + */ + static async openTranslationsSettingsSubpage(lexicalShortlistPrefs = []) { + const { cleanup, remoteClients, translationsSettingsTestUtils } = + await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [ + ["browser.settings-redesign.enabled", true], + ...lexicalShortlistPrefs, + ], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + const manageButton = await waitForCondition( + () => document.getElementById("translationsManageButton"), + "Waiting for translationsManageButton" + ); + manageButton.scrollIntoView({ behavior: "instant", block: "center" }); + + await translationsSettingsTestUtils.assertEvents( + { + expected: [[TranslationsSettingsTestUtils.Events.Initialized]], + }, + async () => { + click(manageButton, "Open translations subpage"); + } + ); + + return { cleanup, remoteClients, translationsSettingsTestUtils }; + } + + static getLanguageModelNames(langTag) { + return languageModelNames([ + { fromLang: langTag, toLang: "en" }, + { fromLang: "en", toLang: langTag }, + ]); + } + + /** + * Returns origins sorted alphabetically while ignoring schemes. + * + * @param {string[]} origins + * @returns {string[]} + */ + static sortOrigins(origins) { + const stripScheme = origin => + origin.replace(/^[a-z][a-z0-9+.-]*:\/\//i, ""); + return [...origins].sort((originA, originB) => + stripScheme(originA).localeCompare(stripScheme(originB)) + ); + } + + /** + * Static Events class for event name constants. + */ + static Events = class Events { + static AlwaysLanguagesRendered = + "TranslationsSettingsTest:AlwaysLanguagesRendered"; + static NeverLanguagesRendered = + "TranslationsSettingsTest:NeverLanguagesRendered"; + static NeverSitesRendered = "TranslationsSettingsTest:NeverSitesRendered"; + static DownloadedLanguagesRendered = + "TranslationsSettingsTest:DownloadedLanguagesRendered"; + + static AlwaysTranslateLanguagesEmptyStateShown = + "TranslationsSettingsTest:AlwaysTranslateLanguagesEmptyStateShown"; + static AlwaysTranslateLanguagesEmptyStateHidden = + "TranslationsSettingsTest:AlwaysTranslateLanguagesEmptyStateHidden"; + static NeverTranslateLanguagesEmptyStateShown = + "TranslationsSettingsTest:NeverTranslateLanguagesEmptyStateShown"; + static NeverTranslateLanguagesEmptyStateHidden = + "TranslationsSettingsTest:NeverTranslateLanguagesEmptyStateHidden"; + static NeverTranslateSitesEmptyStateShown = + "TranslationsSettingsTest:NeverTranslateSitesEmptyStateShown"; + static NeverTranslateSitesEmptyStateHidden = + "TranslationsSettingsTest:NeverTranslateSitesEmptyStateHidden"; + static DownloadedLanguagesEmptyStateShown = + "TranslationsSettingsTest:DownloadedLanguagesEmptyStateShown"; + static DownloadedLanguagesEmptyStateHidden = + "TranslationsSettingsTest:DownloadedLanguagesEmptyStateHidden"; + + static AlwaysTranslateLanguagesSelectOptionsUpdated = + "TranslationsSettingsTest:AlwaysTranslateLanguagesSelectOptionsUpdated"; + static NeverTranslateLanguagesSelectOptionsUpdated = + "TranslationsSettingsTest:NeverTranslateLanguagesSelectOptionsUpdated"; + static DownloadedLanguagesSelectOptionsUpdated = + "TranslationsSettingsTest:DownloadedLanguagesSelectOptionsUpdated"; + + static DownloadStarted = "TranslationsSettingsTest:DownloadStarted"; + static DownloadProgress = "TranslationsSettingsTest:DownloadProgress"; + static DownloadCompleted = "TranslationsSettingsTest:DownloadCompleted"; + static DownloadFailed = "TranslationsSettingsTest:DownloadFailed"; + static DownloadDeleted = "TranslationsSettingsTest:DownloadDeleted"; + + static Initialized = "TranslationsSettingsTest:Initialized"; + static InitializationFailed = + "TranslationsSettingsTest:InitializationFailed"; + + static DownloadButtonEnabled = + "TranslationsSettingsTest:DownloadButtonEnabled"; + static DownloadButtonDisabled = + "TranslationsSettingsTest:DownloadButtonDisabled"; + }; + + /** + * Waits for a translations settings event to be dispatched. + * + * @param {string} eventName - The event name to wait for + * @param {object} options + * @param {object} [options.expectedDetail] - Expected detail properties + * @returns {Promise<CustomEvent>} + */ + async waitForEvent(eventName, options = {}) { + const { expectedDetail } = options; + + return BrowserTestUtils.waitForEvent( + this.document, + eventName, + false, + event => { + if (expectedDetail) { + for (const key of Object.keys(expectedDetail)) { + const actual = event.detail?.[key]; + const expected = expectedDetail[key]; + if (JSON.stringify(actual) !== JSON.stringify(expected)) { + return false; + } + } + } + return true; + } + ); + } + + /** + * Asserts that specific events occur (or don't occur) during an action. + * + * @param {object} assertions + * @param {Array<[string, object?]>} assertions.expected - Events that must occur + * @param {Array<string>} [assertions.unexpected] - Events that must not occur + * @param {number} [assertions.timeout=10000] - Timeout in milliseconds + * @param {Function} callback - The action to perform + * @returns {Promise<void>} + */ + async assertEvents( + { expected = [], unexpected = [], timeout = 10000 }, + callback + ) { + const firedEvents = []; + const unexpectedEventsFired = []; + + const handlers = new Map(); + + const isInitializedFlagSet = + this.document?.defaultView?.wrappedJSObject?.TranslationsSettings + ?.initialized; + + const preseedEventsIfAlreadySatisfied = () => { + for (const [eventName] of expected) { + if ( + eventName === TranslationsSettingsTestUtils.Events.Initialized && + isInitializedFlagSet && + !firedEvents.some(([name]) => name === eventName) + ) { + firedEvents.push([eventName, null]); + } + } + }; + + preseedEventsIfAlreadySatisfied(); + + const maybeAddSyntheticInitializationEvent = () => { + if ( + expected.some( + ([name]) => name === TranslationsSettingsTestUtils.Events.Initialized + ) && + !firedEvents.some( + ([name]) => name === TranslationsSettingsTestUtils.Events.Initialized + ) && + this.document?.defaultView?.wrappedJSObject?.TranslationsSettings + ?.initialized + ) { + firedEvents.push([ + TranslationsSettingsTestUtils.Events.Initialized, + null, + ]); + } + }; + + for (const [eventName] of expected) { + const handler = event => { + firedEvents.push([eventName, event.detail]); + }; + handlers.set(eventName, handler); + this.document.addEventListener(eventName, handler); + } + + for (const eventName of unexpected) { + const handler = event => { + unexpectedEventsFired.push([eventName, event.detail]); + }; + handlers.set(eventName, handler); + this.document.addEventListener(eventName, handler); + } + + try { + await callback(); + + maybeAddSyntheticInitializationEvent(); + preseedEventsIfAlreadySatisfied(); + + const interval = 100; + const maxTries = Math.ceil(timeout / interval); + const expectedEventNames = expected.map(([name]) => name).join(", "); + try { + await TestUtils.waitForCondition( + () => { + maybeAddSyntheticInitializationEvent(); + return firedEvents.length >= expected.length; + }, + `Waiting for ${expected.length} expected event(s): ${expectedEventNames}`, + interval, + maxTries + ); + } catch (error) { + throw new Error( + error?.message ?? + error ?? + `Timed out waiting for expected event(s): ${expectedEventNames}` + ); + } + + for (let i = 0; i < expected.length; i++) { + const [expectedEventName, expectedDetail] = expected[i]; + const [firedEventName, firedDetail] = firedEvents[i] || []; + + is( + firedEventName, + expectedEventName, + `Expected event ${i}: ${expectedEventName}` + ); + + if (expectedDetail) { + for (const key of Object.keys(expectedDetail)) { + Assert.deepEqual( + firedDetail?.[key], + expectedDetail[key], + `Event ${expectedEventName} detail.${key} matches` + ); + } + } + } + + const unexpectedNames = unexpectedEventsFired + .map(([name]) => name) + .join(", "); + is( + unexpectedEventsFired.length, + 0, + `No unexpected events should fire. Fired: ${unexpectedNames}` + ); + } finally { + for (const [eventName, handler] of handlers.entries()) { + this.document.removeEventListener(eventName, handler); + } + } + } + + /** + * Gets the translations setting pane element. + * + * @returns {HTMLElement|null} + */ + getTranslationsPane() { + return this.document.querySelector( + 'setting-pane[data-category="paneTranslations"]' + ); + } + + /** + * Gets the translations subpage back button element. + * + * @returns {HTMLElement|null} + */ + getBackButton() { + return this.getTranslationsPane()?.pageHeaderEl?.backButtonEl ?? null; + } + + /** + * Clicks the translations subpage back button and waits for the main pane. + * + * @returns {Promise<void>} + */ + async clickBackButton() { + const pane = this.getTranslationsPane(); + if (!pane) { + throw new Error("Translations pane not found"); + } + + if (pane.getUpdateComplete) { + await pane.getUpdateComplete(); + } + + const backButton = pane.pageHeaderEl?.backButtonEl; + if (!backButton) { + throw new Error("Translations back button not found"); + } + + const paneShown = BrowserTestUtils.waitForEvent( + this.document, + "paneshown", + event => event.detail?.category === "paneGeneral" + ); + + await click(backButton, "Navigate back to main settings"); + await paneShown; + + await TestUtils.waitForCondition( + () => pane.hidden, + "Waiting for translations pane to hide" + ); + } + + /** + * Gets the always-translate languages select element. + * + * @returns {HTMLSelectElement|null} + */ + getAlwaysTranslateLanguagesSelect() { + return this.document.getElementById( + "translationsAlwaysTranslateLanguagesSelect" + ); + } + + /** + * Gets the never-translate languages select element. + * + * @returns {HTMLSelectElement|null} + */ + getNeverTranslateLanguagesSelect() { + return this.document.getElementById( + "translationsNeverTranslateLanguagesSelect" + ); + } + + /** + * Gets the download languages select element. + * + * @returns {HTMLSelectElement|null} + */ + getDownloadedLanguagesSelect() { + return this.document.getElementById("translationsDownloadLanguagesSelect"); + } + + getSelectedDownloadLanguage() { + return this.getDownloadedLanguagesSelect()?.value ?? ""; + } + + /** + * Gets the download button element. + * + * @returns {HTMLButtonElement|null} + */ + getDownloadButton() { + return this.document.getElementById("translationsDownloadLanguagesButton"); + } + + /** + * Gets the download languages group element. + * + * @returns {HTMLElement|null} + */ + getDownloadedLanguagesGroup() { + return this.document.getElementById("translationsDownloadLanguagesGroup"); + } + + /** + * Selects a language in the download dropdown. + * + * @param {string} langTag + */ + async selectDownloadLanguage(langTag) { + const dropdown = this.getDownloadedLanguagesSelect(); + dropdown.value = langTag; + dropdown.dispatchEvent(new Event("change", { bubbles: true })); + } + + async downloadLanguage({ + langTag, + remoteClients, + inProgressLanguages, + finalLanguages, + }) { + await this.selectDownloadLanguage(langTag); + + const started = this.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadStarted, + { expectedDetail: { langTag } } + ); + const renderInProgress = this.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, + { + expectedDetail: { + languages: inProgressLanguages, + count: inProgressLanguages.length, + downloading: [langTag], + }, + } + ); + const optionsUpdated = this.waitForEvent( + TranslationsSettingsTestUtils.Events + .DownloadedLanguagesSelectOptionsUpdated + ); + + await click(this.getDownloadButton(), `Start ${langTag} download`); + await Promise.all([started, renderInProgress, optionsUpdated]); + + const completed = this.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadCompleted, + { expectedDetail: { langTag } } + ); + const renderComplete = this.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, + { + expectedDetail: { + languages: finalLanguages, + count: finalLanguages.length, + downloading: [], + }, + } + ); + const optionsAfter = this.waitForEvent( + TranslationsSettingsTestUtils.Events + .DownloadedLanguagesSelectOptionsUpdated + ); + + await remoteClients.translationModels.resolvePendingDownloads( + TranslationsSettingsTestUtils.getLanguageModelNames(langTag).length + ); + await Promise.all([completed, renderComplete, optionsAfter]); + } + + /** + * Starts a download expected to fail and waits for the failure state. + * + * @param {object} options + * @param {string} options.langTag + * @param {object} options.remoteClients + * @param {string[]} [options.inProgressLanguages] + * @param {string[]} [options.failedLanguages] + */ + async startDownloadFailure({ + langTag, + remoteClients, + inProgressLanguages = [langTag], + failedLanguages = [langTag], + }) { + await this.selectDownloadLanguage(langTag); + + const started = this.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadStarted, + { expectedDetail: { langTag } } + ); + const renderInProgress = this.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, + { + expectedDetail: { + languages: inProgressLanguages, + count: inProgressLanguages.length, + downloading: [langTag], + }, + } + ); + const optionsUpdated = this.waitForEvent( + TranslationsSettingsTestUtils.Events + .DownloadedLanguagesSelectOptionsUpdated + ); + + await click( + this.getDownloadButton(), + `Start ${langTag} download (expect failure)` + ); + await Promise.all([started, renderInProgress, optionsUpdated]); + + const failed = this.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadFailed, + { expectedDetail: { langTag } } + ); + const renderFailed = this.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, + { + expectedDetail: { + languages: failedLanguages, + count: failedLanguages.length, + downloading: [], + }, + } + ); + const optionsAfterFail = this.waitForEvent( + TranslationsSettingsTestUtils.Events + .DownloadedLanguagesSelectOptionsUpdated + ); + + const modelNames = + TranslationsSettingsTestUtils.getLanguageModelNames(langTag); + await remoteClients.translationModels.waitForPendingDownloads( + modelNames.length + ); + await remoteClients.translationModels.rejectPendingDownloads( + modelNames.length + ); + await Promise.all([failed, renderFailed, optionsAfterFail]); + } + + /** + * Waits for a language to appear in the download languages list. + * + * @param {string} langTag + * @returns {Promise<Element>} + */ + async waitForDownloadedLanguageItem(langTag) { + return waitForCondition( + () => + this.document.querySelector( + `.translations-download-language-item[data-lang-tag="${langTag}"]` + ), + `Waiting for downloaded language item: ${langTag}` + ); + } + + /** + * Asserts the current state of the downloaded languages list. + * + * @param {object} expected + * @param {string[]} [expected.languages] - Expected language tags + * @param {string[]} [expected.downloading] - Expected language tags that are downloading + * @param {number} [expected.count] - Expected count of languages + * @returns {Promise<void>} + */ + async assertDownloadedLanguages({ languages, downloading, count }) { + const items = this.document.querySelectorAll( + ".translations-download-language-item" + ); + + if (count !== undefined) { + is(items.length, count, `Should have ${count} downloaded language(s)`); + } + + const langTags = Array.from(items).map(item => item.dataset.langTag); + + if (languages) { + Assert.deepEqual( + langTags.sort(), + [...languages].sort(), + "Downloaded languages match" + ); + } + + if (downloading) { + const downloadingLangs = Array.from(items) + .filter(item => + item + .querySelector(".translations-download-remove-button") + ?.hasAttribute("disabled") + ) + .map(item => item.dataset.langTag); + Assert.deepEqual( + downloadingLangs.sort(), + [...downloading].sort(), + "Downloading languages match" + ); + } + } + + /** + * Asserts the current order of the downloaded languages list. + * + * @param {object} expected + * @param {string[]} expected.languages - Expected language tags in order + * @returns {Promise<void>} + */ + async assertDownloadedLanguagesOrder({ languages }) { + const items = this.document.querySelectorAll( + ".translations-download-language-item" + ); + const actualLanguages = Array.from(items).map(item => item.dataset.langTag); + Assert.deepEqual( + actualLanguages, + languages, + "Downloaded languages order matches" + ); + } + + /** + * Asserts the visibility state of the downloaded languages empty state. + * + * @param {object} expected + * @param {boolean} expected.visible - Whether empty state should be visible + * @returns {Promise<void>} + */ + async assertDownloadedLanguagesEmptyState({ visible }) { + const emptyRow = this.document.getElementById( + "translationsDownloadLanguagesNoneRow" + ); + if (visible) { + ok( + emptyRow && !emptyRow.hidden, + "Downloaded languages empty state should be visible" + ); + } else { + ok( + !emptyRow || emptyRow.hidden, + "Downloaded languages empty state should be hidden" + ); + } + } + + /** + * Removes a language from the downloaded languages list. + * + * @param {string} langTag + * @returns {Promise<void>} + */ + async removeDownloadedLanguage(langTag) { + const removeButton = await waitForCondition( + () => this.getDownloadDeleteIconButton(langTag), + `Waiting for download delete icon button for ${langTag}` + ); + removeButton.click(); + await waitForCondition( + () => this.getDownloadDeleteConfirmButton(langTag), + `Waiting for delete confirmation for ${langTag}` + ); + } + + getDownloadRemoveButton(langTag) { + return this.document.querySelector( + `[data-lang-tag="${langTag}"].translations-download-remove-button` + ); + } + + getDownloadDeleteConfirmButton(langTag) { + return this.document.querySelector( + `[data-lang-tag="${langTag}"].translations-download-delete-confirm-button` + ); + } + + getDownloadDeleteCancelButton(langTag) { + return this.document.querySelector( + `[data-lang-tag="${langTag}"].translations-download-delete-cancel-button` + ); + } + + getDownloadRetryButton(langTag) { + return this.document.querySelector( + `[data-lang-tag="${langTag}"].translations-download-retry-button` + ); + } + + getDownloadErrorButton(langTag) { + return this.document.querySelector( + `[data-lang-tag="${langTag}"].translations-download-remove-button[iconsrc*="error"]` + ); + } + + getDownloadWarningButton(langTag) { + return this.document.querySelector( + `[data-lang-tag="${langTag}"].translations-download-remove-button[iconsrc*="warning"]` + ); + } + + getDownloadDeleteIconButton(langTag) { + return this.document.querySelector( + `[data-lang-tag="${langTag}"].translations-download-remove-button[iconsrc*="delete"]` + ); + } + + async openDownloadDeleteConfirmation(langTag) { + const removeButton = await waitForCondition( + () => this.getDownloadDeleteIconButton(langTag), + `Waiting for download delete icon button for ${langTag}` + ); + removeButton.click(); + await waitForCondition( + () => this.getDownloadDeleteConfirmButton(langTag), + `Waiting for delete confirmation for ${langTag}` + ); + } + + async cancelDownloadDelete(langTag) { + const cancelButton = await waitForCondition( + () => this.getDownloadDeleteCancelButton(langTag), + `Waiting for delete cancel button for ${langTag}` + ); + cancelButton.click(); + await waitForCondition( + () => this.getDownloadDeleteIconButton(langTag), + `Waiting for delete icon button to return for ${langTag}` + ); + } + + async confirmDownloadDelete(langTag) { + const confirmButton = await waitForCondition( + () => this.getDownloadDeleteConfirmButton(langTag), + `Waiting for delete confirm button for ${langTag}` + ); + const deleted = this.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadDeleted, + { expectedDetail: { langTag } } + ); + confirmButton.click(); + await deleted; + } + + async clickDownloadRetry(langTag) { + const retryButton = await waitForCondition( + () => this.getDownloadRetryButton(langTag), + `Waiting for retry button for ${langTag}` + ); + const started = this.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadStarted, + { expectedDetail: { langTag } } + ); + retryButton.click(); + await started; + } + + /** + * Adds a language to the always-translate list. + * + * @param {string} langTag + * @returns {Promise<void>} + */ + async addAlwaysTranslateLanguage(langTag) { + const dropdown = this.getAlwaysTranslateLanguagesSelect(); + dropdown.value = langTag; + dropdown.dispatchEvent(new Event("change", { bubbles: true })); + } + + /** + * Removes a language from the always-translate list. + * + * @param {string} langTag + * @returns {Promise<void>} + */ + async removeAlwaysTranslateLanguage(langTag) { + const removeButton = this.document.querySelector( + `[data-lang-tag="${langTag}"].translations-always-remove-button` + ); + if (!removeButton) { + throw new Error(`Remove button not found for language: ${langTag}`); + } + const rendered = this.waitForEvent( + TranslationsSettingsTestUtils.Events.AlwaysTranslateLanguagesRendered + ); + removeButton.click(); + await rendered; + } + + /** + * Waits for a language to appear in the always-translate languages list. + * + * @param {string} langTag + * @returns {Promise<Element>} + */ + async waitForAlwaysTranslateLanguageItem(langTag) { + return TestUtils.waitForCondition( + () => + this.document.querySelector( + `[data-lang-tag="${langTag}"].translations-always-language-item` + ), + `Waiting for always-translate language item: ${langTag}` + ); + } + + /** + * Asserts the current state of the always-translate languages list. + * + * @param {object} expected + * @param {string[]} [expected.languages] - Expected language tags + * @param {number} [expected.count] - Expected count of languages + * @returns {Promise<void>} + */ + async assertAlwaysTranslateLanguages({ languages, count }) { + const items = this.document.querySelectorAll( + ".translations-always-language-item" + ); + + if (count !== undefined) { + is( + items.length, + count, + `Should have ${count} always-translate language(s)` + ); + } + + if (languages) { + const actualLanguages = Array.from(items).map( + item => item.dataset.langTag + ); + Assert.deepEqual( + actualLanguages.sort(), + [...languages].sort(), + "Always-translate languages match" + ); + } + } + + /** + * Asserts the current order of the always-translate languages list. + * + * @param {object} expected + * @param {string[]} expected.languages - Expected language tags in order + * @returns {Promise<void>} + */ + async assertAlwaysTranslateLanguagesOrder({ languages }) { + const items = this.document.querySelectorAll( + ".translations-always-language-item" + ); + const actualLanguages = Array.from(items).map(item => item.dataset.langTag); + Assert.deepEqual( + actualLanguages, + languages, + "Always-translate languages order matches" + ); + } + + /** + * Asserts the visibility state of the always-translate languages empty state. + * + * @param {object} expected + * @param {boolean} expected.visible - Whether empty state should be visible + * @returns {Promise<void>} + */ + async assertAlwaysTranslateLanguagesEmptyState({ visible }) { + const emptyRow = this.document.getElementById( + "translationsAlwaysTranslateLanguagesNoneRow" + ); + if (visible) { + ok( + emptyRow && !emptyRow.hidden, + "Always-translate languages empty state should be visible" + ); + } else { + ok( + !emptyRow || emptyRow.hidden, + "Always-translate languages empty state should be hidden" + ); + } + } + + /** + * Adds a language to the never-translate list. + * + * @param {string} langTag + * @returns {Promise<void>} + */ + async addNeverTranslateLanguage(langTag) { + const dropdown = this.getNeverTranslateLanguagesSelect(); + dropdown.value = langTag; + dropdown.dispatchEvent(new Event("change", { bubbles: true })); + } + + /** + * Removes a language from the never-translate list. + * + * @param {string} langTag + * @returns {Promise<void>} + */ + async removeNeverTranslateLanguage(langTag) { + const removeButton = this.document.querySelector( + `[data-lang-tag="${langTag}"].translations-never-remove-button` + ); + if (!removeButton) { + throw new Error(`Remove button not found for language: ${langTag}`); + } + const rendered = this.waitForEvent( + TranslationsSettingsTestUtils.Events.NeverTranslateLanguagesRendered + ); + removeButton.click(); + await rendered; + } + + /** + * Waits for a language to appear in the never-translate languages list. + * + * @param {string} langTag + * @returns {Promise<Element>} + */ + async waitForNeverTranslateLanguageItem(langTag) { + return TestUtils.waitForCondition( + () => + this.document.querySelector( + `[data-lang-tag="${langTag}"].translations-never-language-item` + ), + `Waiting for never-translate language item: ${langTag}` + ); + } + + /** + * Asserts the current state of the never-translate languages list. + * + * @param {object} expected + * @param {string[]} [expected.languages] - Expected language tags + * @param {number} [expected.count] - Expected count of languages + * @returns {Promise<void>} + */ + async assertNeverTranslateLanguages({ languages, count }) { + const items = this.document.querySelectorAll( + ".translations-never-language-item" + ); + + if (count !== undefined) { + is( + items.length, + count, + `Should have ${count} never-translate language(s)` + ); + } + + if (languages) { + const actualLanguages = Array.from(items).map( + item => item.dataset.langTag + ); + Assert.deepEqual( + actualLanguages.sort(), + [...languages].sort(), + "Never-translate languages match" + ); + } + } + + /** + * Asserts the current order of the never-translate languages list. + * + * @param {object} expected + * @param {string[]} expected.languages - Expected language tags in order + * @returns {Promise<void>} + */ + async assertNeverTranslateLanguagesOrder({ languages }) { + const items = this.document.querySelectorAll( + ".translations-never-language-item" + ); + const actualLanguages = Array.from(items).map(item => item.dataset.langTag); + Assert.deepEqual( + actualLanguages, + languages, + "Never-translate languages order matches" + ); + } + + /** + * Asserts the visibility state of the never-translate languages empty state. + * + * @param {object} expected + * @param {boolean} expected.visible - Whether empty state should be visible + * @returns {Promise<void>} + */ + async assertNeverTranslateLanguagesEmptyState({ visible }) { + const emptyRow = this.document.getElementById( + "translationsNeverTranslateLanguagesNoneRow" + ); + if (visible) { + ok( + emptyRow && !emptyRow.hidden, + "Never-translate languages empty state should be visible" + ); + } else { + ok( + !emptyRow || emptyRow.hidden, + "Never-translate languages empty state should be hidden" + ); + } + } + + /** + * Gets the never-translate sites list element. + * + * @returns {HTMLElement|null} + */ + getNeverTranslateSitesGroup() { + return this.document.getElementById("translationsNeverTranslateSitesGroup"); + } + + /** + * Waits for a site to appear in the never-translate sites list. + * + * @param {string} origin + * @returns {Promise<Element>} + */ + async waitForNeverTranslateSiteItem(origin) { + return waitForCondition( + () => + this.document.querySelector( + `[data-origin="${origin}"].translations-never-site-item` + ), + `Waiting for never-translate site item: ${origin}` + ); + } + + /** + * Removes a site from the never-translate list. + * + * @param {string} origin + * @returns {Promise<void>} + */ + async removeNeverTranslateSite(origin) { + const removeButton = await waitForCondition( + () => + this.document.querySelector( + `[data-origin="${origin}"].translations-never-site-remove-button` + ), + `Waiting for remove button for ${origin}` + ); + const rendered = this.waitForEvent( + TranslationsSettingsTestUtils.Events.NeverTranslateSitesRendered + ); + removeButton.click(); + await rendered; + } + + /** + * Asserts the current state of the never-translate sites list. + * + * @param {object} expected + * @param {string[]} [expected.sites] - Expected site origins + * @param {number} [expected.count] - Expected count of sites + * @returns {Promise<void>} + */ + async assertNeverTranslateSites({ sites, count }) { + const items = this.document.querySelectorAll( + ".translations-never-site-item" + ); + + if (count !== undefined) { + is(items.length, count, `Should have ${count} never-translate site(s)`); + } + + if (sites) { + const actualSites = Array.from(items).map(item => item.dataset.origin); + Assert.deepEqual( + actualSites.sort(), + [...sites].sort(), + "Never-translate sites match" + ); + } + } + + /** + * Asserts the current order of the never-translate sites list. + * + * @param {object} expected + * @param {string[]} expected.sites - Expected site origins in order + * @returns {Promise<void>} + */ + async assertNeverTranslateSitesOrder({ sites }) { + const items = this.document.querySelectorAll( + ".translations-never-site-item" + ); + const actualSites = Array.from(items).map(item => item.dataset.origin); + Assert.deepEqual(actualSites, sites, "Never-translate sites order matches"); + } + + /** + * Asserts the visibility state of the never-translate sites empty state. + * + * @param {object} expected + * @param {boolean} expected.visible - Whether empty state should be visible + * @returns {Promise<void>} + */ + async assertNeverTranslateSitesEmptyState({ visible }) { + const emptyRow = this.document.getElementById( + "translationsNeverTranslateSitesNoneRow" + ); + if (visible) { + ok( + emptyRow && !emptyRow.hidden, + "Never-translate sites empty state should be visible" + ); + } else { + ok( + !emptyRow || emptyRow.hidden, + "Never-translate sites empty state should be hidden" + ); + } + } +} + +/** * Recursively transforms all child nodes to have diacriticized text. This is useful * to spot multiple translations. * @@ -1761,6 +2882,23 @@ function createAttachmentMock( autoDownloadFromRemoteSettings ) { const pendingDownloads = []; + const pendingDownloadsWaiters = []; + + function notifyPendingDownload() { + const ready = pendingDownloadsWaiters.filter( + waiter => pendingDownloads.length >= waiter.count + ); + for (const waiter of ready) { + waiter.resolve(); + } + for (const waiter of ready) { + const index = pendingDownloadsWaiters.indexOf(waiter); + if (index !== -1) { + pendingDownloadsWaiters.splice(index, 1); + } + } + } + client.attachments.download = record => new Promise((resolve, reject) => { console.log("Download requested:", client.collectionName, record.name); @@ -1773,6 +2911,7 @@ function createAttachmentMock( resolve({ buffer }); } else { pendingDownloads.push({ record, resolve, reject }); + notifyPendingDownload(); } }); @@ -1790,11 +2929,47 @@ function createAttachmentMock( `Intentionally rejecting ${expectedDownloadCount} mocked downloads for "${client.collectionName}"` ); - // Add 1 to account for the original attempt. - const attempts = TranslationsParent.MAX_DOWNLOAD_RETRIES + 1; - return downloadHandler(expectedDownloadCount * attempts, download => - download.reject(new Error("Intentionally rejecting downloads.")) - ); + const names = []; + const waitTick = () => new Promise(resolve => setTimeout(resolve, 0)); + + const rejectNext = () => { + const download = pendingDownloads.shift(); + if (!download) { + return false; + } + console.log(`Handling download:`, client.collectionName); + download.reject(new Error("Intentionally rejecting downloads.")); + names.push(download.record.name); + return true; + }; + + // Wait for the expected downloads to start arriving and reject them as they do. + while (names.length < expectedDownloadCount) { + await waitForPendingDownloads(names.length + 1); + while (names.length < expectedDownloadCount && rejectNext()) { + // Keep rejecting until we reach the expected count. + } + } + + // Drain any retries until the queue stays empty for a short idle window. + let idleTicks = 0; + const idleWindow = 20; + while (idleTicks < idleWindow) { + await waitTick(); + if (rejectNext()) { + idleTicks = 0; + } else { + idleTicks++; + } + } + + if (pendingDownloads.length) { + throw new Error( + `An unexpected download was found, only expected ${expectedDownloadCount} downloads` + ); + } + + return names.sort((a, b) => a.localeCompare(b)); } async function downloadHandler(expectedDownloadCount, action) { @@ -1835,12 +3010,22 @@ function createAttachmentMock( ); } + function waitForPendingDownloads(expectedCount) { + if (pendingDownloads.length >= expectedCount) { + return Promise.resolve(); + } + return new Promise(resolve => { + pendingDownloadsWaiters.push({ count: expectedCount, resolve }); + }); + } + return { client, pendingDownloads, resolvePendingDownloads, rejectPendingDownloads, assertNoNewDownloads, + waitForPendingDownloads, }; } @@ -1881,15 +3066,6 @@ function createRecordsForLanguagePair(fromLang, toLang, splitVocab = false) { : [{ fileType: "vocab", name: `vocab.${lang}.spm` }]), ]; - const attachment = { - hash: `${crypto.randomUUID()}`, - size: `123`, - filename: `model.${lang}.intgemm.alphas.bin`, - location: `main-workspace/translations-models/${crypto.randomUUID()}.bin`, - mimetype: "application/octet-stream", - isDownloaded: false, - }; - const expectedLength = splitVocab ? RECORDS_PER_LANGUAGE_PAIR_SPLIT_VOCAB : RECORDS_PER_LANGUAGE_PAIR_SHARED_VOCAB; @@ -1901,6 +3077,15 @@ function createRecordsForLanguagePair(fromLang, toLang, splitVocab = false) { ); for (const { fileType, name } of models) { + const attachment = { + hash: `${crypto.randomUUID()}`, + size: "123", + filename: name, + location: `main-workspace/translations-models/${crypto.randomUUID()}.bin`, + mimetype: "application/octet-stream", + isDownloaded: false, + }; + records.push({ id: crypto.randomUUID(), name, @@ -1910,7 +3095,7 @@ function createRecordsForLanguagePair(fromLang, toLang, splitVocab = false) { version: TranslationsParent.LANGUAGE_MODEL_MAJOR_VERSION_MAX + ".0", last_modified: Date.now(), schema: Date.now(), - attachment: JSON.parse(JSON.stringify(attachment)), // Making a deep copy. + attachment: JSON.parse(JSON.stringify(attachment)), // Making a deep copy }); } return records; @@ -2281,6 +3466,11 @@ async function setupAboutPreferences( const elements = await selectAboutPreferencesElements(); + const document = gBrowser.selectedBrowser.contentDocument; + const translationsSettingsTestUtils = new TranslationsSettingsTestUtils( + document + ); + async function cleanup() { Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, ""); Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, ""); @@ -2298,6 +3488,7 @@ async function setupAboutPreferences( cleanup, remoteClients, elements, + translationsSettingsTestUtils, }; }