commit e034951ed4167b2109175d39f8f9ca307585c53e parent 53700a63ae0fd67c1b3676905160a966588e1db2 Author: Erik Nordin <enordin@mozilla.com> Date: Thu, 18 Dec 2025 01:17:29 +0000 Bug 2002127 - Part 19: Improve Language-Download States r=translations-reviewers,desktop-theme-reviewers,hjones This commit improves the logic around the various states of downloaded languages, from the download-failed message to the delete-downloaded-language confirmation. This change ensures that only one confirmation state can be shown at a time, opening another closes the previous, and that any confirmation or failure states are cleaned up on reload. Differential Revision: https://phabricator.services.mozilla.com/D274831 Diffstat:
9 files changed, 451 insertions(+), 41 deletions(-)
diff --git a/browser/components/preferences/translations.js b/browser/components/preferences/translations.js @@ -56,6 +56,8 @@ const DOWNLOAD_LANGUAGE_REMOVE_BUTTON_CLASS = const DOWNLOAD_LANGUAGE_RETRY_BUTTON_CLASS = "translations-download-retry-button"; /** @type {string} */ +const DOWNLOAD_LANGUAGE_FAILED_CLASS = "translations-download-language-error"; +/** @type {string} */ const DOWNLOAD_LANGUAGE_DELETE_CONFIRM_BUTTON_CLASS = "translations-download-delete-confirm-button"; /** @type {string} */ @@ -254,7 +256,7 @@ const TranslationsSettings = { target === this.elements?.downloadLanguagesButton || target.closest?.("#translationsDownloadLanguagesButton") ) { - this.onDownloadButtonClicked(); + this.onDownloadLanguageButtonClicked(); break; } @@ -464,7 +466,7 @@ const TranslationsSettings = { this.elements.neverTranslateLanguagesSelect.disabled = true; this.elements.neverTranslateLanguagesButton.disabled = true; this.elements.downloadLanguagesSelect.disabled = true; - this.setDownloadButtonDisabledState(true); + this.setDownloadLanguageButtonDisabledState(true); this.dispatchInitializedTestEvent(); return; } @@ -475,7 +477,7 @@ const TranslationsSettings = { this.elements.neverTranslateLanguagesButton.disabled = true; this.elements.downloadLanguagesSelect.disabled = false; this.resetDownloadSelect(); - this.setDownloadButtonDisabledState(true); + this.setDownloadLanguageButtonDisabledState(true); await this.buildAlwaysTranslateSelectOptions(); await this.buildNeverTranslateSelectOptions(); await this.buildDownloadSelectOptions(); @@ -753,7 +755,7 @@ const TranslationsSettings = { } if (preserveSelection) { - this.updateDownloadButtonDisabled(); + this.updateDownloadLanguageButtonDisabled(); } else { this.resetDownloadSelect(); } @@ -1492,7 +1494,7 @@ const TranslationsSettings = { * Handle a selection change in the download dropdown. */ onDownloadSelectionChanged() { - this.updateDownloadButtonDisabled(); + this.updateDownloadLanguageButtonDisabled(); }, /** @@ -1500,7 +1502,7 @@ const TranslationsSettings = { * * @returns {boolean} */ - isDownloadButtonDisabled() { + shouldDisableDownloadLanguageButton() { const select = this.elements?.downloadLanguagesSelect; if (!select || this.currentDownloadLangTag) { return true; @@ -1522,7 +1524,7 @@ const TranslationsSettings = { * * @param {boolean} isDisabled */ - setDownloadButtonDisabledState(isDisabled) { + setDownloadLanguageButtonDisabledState(isDisabled) { const button = this.elements?.downloadLanguagesButton; if (!button) { return; @@ -1533,7 +1535,9 @@ const TranslationsSettings = { if (wasDisabled !== isDisabled) { dispatchTestEvent( - isDisabled ? "DownloadButtonDisabled" : "DownloadButtonEnabled" + isDisabled + ? "DownloadLanguageButtonDisabled" + : "DownloadLanguageButtonEnabled" ); } }, @@ -1541,8 +1545,10 @@ const TranslationsSettings = { /** * Update the enabled state of the download button. */ - updateDownloadButtonDisabled() { - this.setDownloadButtonDisabledState(this.isDownloadButtonDisabled()); + updateDownloadLanguageButtonDisabled() { + this.setDownloadLanguageButtonDisabledState( + this.shouldDisableDownloadLanguageButton() + ); }, /** @@ -1550,12 +1556,13 @@ const TranslationsSettings = { * * @returns {Promise<void>} */ - async onDownloadButtonClicked() { + async onDownloadLanguageButtonClicked() { const langTag = this.elements?.downloadLanguagesSelect?.value; if (!langTag || this.currentDownloadLangTag) { return; } + this.downloadPendingDeleteLanguageTags.clear(); this.downloadFailedLanguageTags.clear(); this.currentDownloadLangTag = langTag; this.downloadingLanguageTags.add(langTag); @@ -1582,7 +1589,7 @@ const TranslationsSettings = { this.updateDownloadSelectOptionState({ preserveSelection: !downloadSucceeded, }); - this.updateDownloadButtonDisabled(); + this.updateDownloadLanguageButtonDisabled(); } }, @@ -1595,8 +1602,8 @@ const TranslationsSettings = { if (this.elements?.downloadLanguagesSelect) { this.elements.downloadLanguagesSelect.disabled = isDisabled; } - this.setDownloadButtonDisabledState( - isDisabled || this.isDownloadButtonDisabled() + this.setDownloadLanguageButtonDisabledState( + isDisabled || this.shouldDisableDownloadLanguageButton() ); }, @@ -1629,7 +1636,7 @@ const TranslationsSettings = { if (setting) { setting.value = ""; } - this.updateDownloadButtonDisabled(); + this.updateDownloadLanguageButtonDisabled(); }, /** @@ -1642,6 +1649,7 @@ const TranslationsSettings = { return; } + this.downloadPendingDeleteLanguageTags.clear(); const downloaded = await Promise.all( this.languageList.map(async (/** @type {LanguageInfo} */ { langTag }) => { try { @@ -1673,7 +1681,7 @@ const TranslationsSettings = { await this.renderDownloadLanguages(); this.updateDownloadSelectOptionState(); - this.updateDownloadButtonDisabled(); + this.updateDownloadLanguageButtonDisabled(); }, /** @@ -1683,7 +1691,7 @@ const TranslationsSettings = { * @param {HTMLElement} item - The moz-box-item element to populate. * @returns {Promise<void>} */ - async createDeleteConfirmationItem(langTag, item) { + async createDeleteConfirmationItem(langTag, item, disableActions = false) { const warningButton = document.createElement("moz-button"); warningButton.setAttribute("slot", "actions-start"); warningButton.setAttribute("type", "icon"); @@ -1707,8 +1715,12 @@ const TranslationsSettings = { { language: languageLabel, size: sizeLabel } ); + const buttonGroup = document.createElement("moz-button-group"); + const deleteButton = document.createElement("moz-button"); - deleteButton.setAttribute("type", "default"); + deleteButton.setAttribute("type", "destructive"); + deleteButton.setAttribute("size", "small"); + deleteButton.disabled = disableActions; document.l10n.setAttributes( deleteButton, "settings-translations-subpage-download-delete-button" @@ -1718,6 +1730,8 @@ const TranslationsSettings = { const cancelButton = document.createElement("moz-button"); cancelButton.setAttribute("type", "default"); + cancelButton.setAttribute("size", "small"); + cancelButton.disabled = disableActions; document.l10n.setAttributes( cancelButton, "settings-translations-subpage-download-cancel-button" @@ -1726,8 +1740,18 @@ const TranslationsSettings = { cancelButton.dataset.langTag = langTag; confirmContent.appendChild(confirmText); - confirmContent.appendChild(deleteButton); - confirmContent.appendChild(cancelButton); + buttonGroup.append(deleteButton, cancelButton); + confirmContent.appendChild(buttonGroup); + + if (!deleteButton.disabled) { + requestAnimationFrame(() => { + requestAnimationFrame(() => { + if (deleteButton.isConnected) { + deleteButton.focus({ focusVisible: true }); + } + }); + }); + } item.appendChild(warningButton); item.appendChild(confirmContent); @@ -1740,7 +1764,7 @@ const TranslationsSettings = { * @param {HTMLElement} item - The moz-box-item element to populate. * @returns {Promise<void>} */ - async createFailedDownloadItem(langTag, item) { + async createFailedDownloadItem(langTag, item, disableActions = false) { const errorButton = document.createElement("moz-button"); errorButton.setAttribute("slot", "actions-start"); errorButton.setAttribute("type", "icon"); @@ -1767,6 +1791,8 @@ const TranslationsSettings = { const retryButton = document.createElement("moz-button"); retryButton.setAttribute("type", "text"); + retryButton.setAttribute("size", "small"); + retryButton.disabled = disableActions; document.l10n.setAttributes( retryButton, "settings-translations-subpage-download-retry-button" @@ -1777,6 +1803,16 @@ const TranslationsSettings = { errorContent.appendChild(errorText); errorContent.appendChild(retryButton); + if (!retryButton.disabled) { + requestAnimationFrame(() => { + requestAnimationFrame(() => { + if (retryButton.isConnected) { + retryButton.focus({ focusVisible: true }); + } + }); + }); + } + item.appendChild(errorButton); item.appendChild(errorContent); }, @@ -1794,7 +1830,8 @@ const TranslationsSettings = { langTag, isDownloading, item, - progressLabel + progressLabel, + disableActions = false ) { const label = await this.formatDownloadLabel(langTag); if (!label) { @@ -1813,6 +1850,9 @@ const TranslationsSettings = { removeButton.setAttribute("aria-label", label); if (isDownloading) { removeButton.style.pointerEvents = "none"; + removeButton.disabled = false; + } else { + removeButton.disabled = disableActions; } this.setIconButtonGhostState( removeButton, @@ -1841,6 +1881,7 @@ const TranslationsSettings = { return; } + const isDownloadInProgress = Boolean(this.currentDownloadLangTag); const previousEmptyStateVisible = downloadLanguagesNoneRow && !downloadLanguagesNoneRow.hidden; @@ -1900,15 +1941,25 @@ const TranslationsSettings = { item.dataset.langTag = langTag; if (isPendingDelete) { - await this.createDeleteConfirmationItem(langTag, item); + await this.createDeleteConfirmationItem( + langTag, + item, + isDownloadInProgress + ); } else if (isFailed) { - await this.createFailedDownloadItem(langTag, item); + item.classList.add(DOWNLOAD_LANGUAGE_FAILED_CLASS); + await this.createFailedDownloadItem( + langTag, + item, + isDownloadInProgress + ); } else { const shouldAdd = await this.createDownloadLanguageItem( langTag, isDownloading, item, - progressLabel + progressLabel, + isDownloadInProgress ); if (!shouldAdd) { continue; @@ -1945,6 +1996,8 @@ const TranslationsSettings = { return; } + this.downloadFailedLanguageTags.clear(); + this.downloadPendingDeleteLanguageTags.clear(); this.downloadPendingDeleteLanguageTags.add(langTag); await this.renderDownloadLanguages(); }, @@ -1974,7 +2027,7 @@ const TranslationsSettings = { await this.renderDownloadLanguages(); this.updateDownloadSelectOptionState(); - this.updateDownloadButtonDisabled(); + this.updateDownloadLanguageButtonDisabled(); }, /** @@ -2029,7 +2082,7 @@ const TranslationsSettings = { this.updateDownloadSelectOptionState({ preserveSelection: !downloadSucceeded, }); - this.updateDownloadButtonDisabled(); + this.updateDownloadLanguageButtonDisabled(); } }, diff --git a/browser/components/translations/tests/browser/browser.toml b/browser/components/translations/tests/browser/browser.toml @@ -24,8 +24,12 @@ support-files = [ ["browser_translations_about_settings_subpage_download_langs_basic.js"] +["browser_translations_about_settings_subpage_download_langs_delete_confirmation.js"] + ["browser_translations_about_settings_subpage_download_langs_errors.js"] +["browser_translations_about_settings_subpage_download_langs_reset_states.js"] + ["browser_translations_about_settings_subpage_download_langs_sorting.js"] ["browser_translations_about_settings_subpage_never_translate_langs_a11y.js"] 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 @@ -33,7 +33,8 @@ add_task(async function test_download_languages_basic_flow() { const downloadSelect = translationsSettingsTestUtils.getDownloadedLanguagesSelect(); - const downloadButton = translationsSettingsTestUtils.getDownloadButton(); + const downloadButton = + translationsSettingsTestUtils.getDownloadLanguageButton(); ok(downloadSelect, "Download languages select should exist"); ok(downloadButton, "Download languages button should exist"); @@ -53,7 +54,9 @@ add_task(async function test_download_languages_basic_flow() { info("Select French to enable download button"); await translationsSettingsTestUtils.assertEvents( { - expected: [[TranslationsSettingsTestUtils.Events.DownloadButtonEnabled]], + expected: [ + [TranslationsSettingsTestUtils.Events.DownloadLanguageButtonEnabled], + ], }, async () => { await translationsSettingsTestUtils.selectDownloadLanguage("fr"); @@ -68,7 +71,7 @@ add_task(async function test_download_languages_basic_flow() { info("Download French language models"); const downloadButtonDisabled = translationsSettingsTestUtils.waitForEvent( - TranslationsSettingsTestUtils.Events.DownloadButtonDisabled + TranslationsSettingsTestUtils.Events.DownloadLanguageButtonDisabled ); const downloadStarted = translationsSettingsTestUtils.waitForEvent( TranslationsSettingsTestUtils.Events.DownloadStarted, diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_download_langs_delete_confirmation.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_download_langs_delete_confirmation.js @@ -0,0 +1,180 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task( + async function test_delete_confirmation_replaced_by_new_confirmation() { + const { cleanup, remoteClients, translationsSettingsTestUtils } = + await TranslationsSettingsTestUtils.openTranslationsSettingsSubpage(); + + await translationsSettingsTestUtils.assertDownloadedLanguagesEmptyState({ + visible: true, + }); + + info("Download French"); + await translationsSettingsTestUtils.downloadLanguage({ + langTag: "fr", + remoteClients, + inProgressLanguages: ["fr"], + finalLanguages: ["fr"], + }); + + info("Download Spanish"); + await translationsSettingsTestUtils.downloadLanguage({ + langTag: "es", + remoteClients, + inProgressLanguages: ["fr", "es"], + finalLanguages: ["fr", "es"], + }); + + await translationsSettingsTestUtils.openDownloadDeleteConfirmation("fr"); + ok( + translationsSettingsTestUtils.getDownloadDeleteConfirmButton("fr"), + "French delete confirmation should be shown" + ); + + await translationsSettingsTestUtils.openDownloadDeleteConfirmation("es"); + ok( + translationsSettingsTestUtils.getDownloadDeleteConfirmButton("es"), + "Spanish delete confirmation should be shown" + ); + ok( + !translationsSettingsTestUtils.getDownloadDeleteConfirmButton("fr"), + "French delete confirmation should close when Spanish opens" + ); + ok( + translationsSettingsTestUtils.getDownloadRemoveButton("fr"), + "French delete icon should return after switching confirmations" + ); + + await cleanup(); + } +); + +add_task(async function test_delete_confirmation_closes_when_download_starts() { + const { cleanup, remoteClients, translationsSettingsTestUtils } = + await TranslationsSettingsTestUtils.openTranslationsSettingsSubpage(); + + await translationsSettingsTestUtils.downloadLanguage({ + langTag: "fr", + remoteClients, + inProgressLanguages: ["fr"], + finalLanguages: ["fr"], + }); + + await translationsSettingsTestUtils.openDownloadDeleteConfirmation("fr"); + ok( + translationsSettingsTestUtils.getDownloadDeleteConfirmButton("fr"), + "French delete confirmation should be open before starting new download" + ); + + const renderInProgress = translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, + { + expectedDetail: { + languages: ["fr", "es"], + count: 2, + downloading: ["es"], + }, + } + ); + const optionsUpdated = translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadedLanguagesSelectOptionsUpdated + ); + const started = translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadStarted, + { expectedDetail: { langTag: "es" } } + ); + + await translationsSettingsTestUtils.selectDownloadLanguage("es"); + await click( + translationsSettingsTestUtils.getDownloadLanguageButton(), + "Start Spanish download" + ); + await Promise.all([renderInProgress, optionsUpdated, started]); + + ok( + !translationsSettingsTestUtils.getDownloadDeleteConfirmButton("fr"), + "Delete confirmation should close when download begins" + ); + const frenchRemoveButton = + translationsSettingsTestUtils.getDownloadRemoveButton("fr"); + ok( + frenchRemoveButton?.disabled, + "Other delete buttons should be disabled during download" + ); + + const completed = translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadCompleted, + { expectedDetail: { langTag: "es" } } + ); + const renderComplete = translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, + { + expectedDetail: { + languages: ["fr", "es"], + count: 2, + downloading: [], + }, + } + ); + const optionsAfter = translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadedLanguagesSelectOptionsUpdated + ); + + await remoteClients.translationModels.resolvePendingDownloads( + TranslationsSettingsTestUtils.getLanguageModelNames("es").length + ); + await Promise.all([completed, renderComplete, optionsAfter]); + + await cleanup(); +}); + +add_task( + async function test_failed_download_closes_when_delete_confirmation_opens() { + const { cleanup, remoteClients, translationsSettingsTestUtils } = + await TranslationsSettingsTestUtils.openTranslationsSettingsSubpage(); + + await translationsSettingsTestUtils.downloadLanguage({ + langTag: "es", + remoteClients, + inProgressLanguages: ["es"], + finalLanguages: ["es"], + }); + + info("Start French download expecting failure"); + await translationsSettingsTestUtils.startDownloadFailure({ + langTag: "fr", + remoteClients, + inProgressLanguages: ["fr", "es"], + failedLanguages: ["fr", "es"], + }); + + ok( + translationsSettingsTestUtils.getDownloadErrorButton("fr"), + "French error should be visible before opening delete confirmation" + ); + + await translationsSettingsTestUtils.openDownloadDeleteConfirmation("es"); + ok( + translationsSettingsTestUtils.getDownloadDeleteConfirmButton("es"), + "Spanish delete confirmation should be shown" + ); + ok( + !translationsSettingsTestUtils.getDownloadErrorButton("fr"), + "French failure state should close when delete confirmation opens" + ); + ok( + !translationsSettingsTestUtils.getDownloadRetryButton("fr"), + "Retry button should close with error state" + ); + await translationsSettingsTestUtils.assertDownloadedLanguages({ + languages: ["es"], + downloading: [], + count: 1, + }); + + 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 @@ -11,7 +11,8 @@ add_task( await TranslationsSettingsTestUtils.openTranslationsSettingsSubpage(); const document = gBrowser.selectedBrowser.contentDocument; - const downloadButton = translationsSettingsTestUtils.getDownloadButton(); + const downloadButton = + translationsSettingsTestUtils.getDownloadLanguageButton(); await translationsSettingsTestUtils.assertDownloadedLanguagesEmptyState({ visible: true, @@ -89,7 +90,8 @@ add_task(async function test_download_error_retry_via_retry_button() { await TranslationsSettingsTestUtils.openTranslationsSettingsSubpage(); const document = gBrowser.selectedBrowser.contentDocument; - const downloadButton = translationsSettingsTestUtils.getDownloadButton(); + const downloadButton = + translationsSettingsTestUtils.getDownloadLanguageButton(); await translationsSettingsTestUtils.assertDownloadedLanguagesEmptyState({ visible: true, @@ -116,6 +118,12 @@ add_task(async function test_download_error_retry_via_retry_button() { document ); ok(errorMessage, "Error message should be shown"); + const errorRow = + await translationsSettingsTestUtils.waitForDownloadedLanguageItem("es"); + ok( + errorRow.classList.contains("translations-download-language-error"), + "Error row should have the error class" + ); is( translationsSettingsTestUtils.getSelectedDownloadLanguage(), "es", @@ -163,7 +171,8 @@ add_task(async function test_download_delete_cancel_restores_state() { const { cleanup, remoteClients, translationsSettingsTestUtils } = await TranslationsSettingsTestUtils.openTranslationsSettingsSubpage(); - const downloadButton = translationsSettingsTestUtils.getDownloadButton(); + const downloadButton = + translationsSettingsTestUtils.getDownloadLanguageButton(); await translationsSettingsTestUtils.selectDownloadLanguage("uk"); info("Download Ukrainian"); diff --git a/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_download_langs_reset_states.js b/browser/components/translations/tests/browser/browser_translations_about_settings_subpage_download_langs_reset_states.js @@ -0,0 +1,152 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_other_actions_disable_during_active_download() { + const { cleanup, remoteClients, translationsSettingsTestUtils } = + await TranslationsSettingsTestUtils.openTranslationsSettingsSubpage(); + + await translationsSettingsTestUtils.downloadLanguage({ + langTag: "fr", + remoteClients, + inProgressLanguages: ["fr"], + finalLanguages: ["fr"], + }); + + const started = translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadStarted, + { expectedDetail: { langTag: "es" } } + ); + const renderInProgress = translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, + { + expectedDetail: { + languages: ["fr", "es"], + count: 2, + downloading: ["es"], + }, + } + ); + const optionsUpdated = translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadedLanguagesSelectOptionsUpdated + ); + + await translationsSettingsTestUtils.selectDownloadLanguage("es"); + await click( + translationsSettingsTestUtils.getDownloadLanguageButton(), + "Start Spanish download while French exists" + ); + await Promise.all([started, renderInProgress, optionsUpdated]); + + ok( + translationsSettingsTestUtils.getDownloadLanguageButton().disabled, + "Download button should be disabled during active download" + ); + ok( + translationsSettingsTestUtils.getDownloadedLanguagesSelect().disabled, + "Download select should be disabled during active download" + ); + + const frenchRemoveButton = + translationsSettingsTestUtils.getDownloadRemoveButton("fr"); + ok( + frenchRemoveButton?.disabled, + "Other delete buttons should stay disabled during download" + ); + ok( + !translationsSettingsTestUtils.getDownloadDeleteConfirmButton("fr"), + "Delete confirmation should not open while download in progress" + ); + + const completed = translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadCompleted, + { expectedDetail: { langTag: "es" } } + ); + const renderComplete = translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadedLanguagesRendered, + { + expectedDetail: { + languages: ["fr", "es"], + count: 2, + downloading: [], + }, + } + ); + const optionsAfter = translationsSettingsTestUtils.waitForEvent( + TranslationsSettingsTestUtils.Events.DownloadedLanguagesSelectOptionsUpdated + ); + + await remoteClients.translationModels.resolvePendingDownloads( + TranslationsSettingsTestUtils.getLanguageModelNames("es").length + ); + await Promise.all([completed, renderComplete, optionsAfter]); + + await cleanup(); +}); + +add_task(async function test_states_reset_after_reload() { + const { cleanup, remoteClients, translationsSettingsTestUtils } = + await TranslationsSettingsTestUtils.openTranslationsSettingsSubpage(); + + await translationsSettingsTestUtils.downloadLanguage({ + langTag: "es", + remoteClients, + inProgressLanguages: ["es"], + finalLanguages: ["es"], + }); + + info("Trigger French download failure before reloading"); + await translationsSettingsTestUtils.startDownloadFailure({ + langTag: "fr", + remoteClients, + inProgressLanguages: ["fr", "es"], + failedLanguages: ["fr", "es"], + }); + + ok( + translationsSettingsTestUtils.getDownloadErrorButton("fr"), + "French error should be visible before opening delete confirmation" + ); + + await translationsSettingsTestUtils.openDownloadDeleteConfirmation("es"); + ok( + translationsSettingsTestUtils.getDownloadDeleteConfirmButton("es"), + "Spanish delete confirmation should be open before reload" + ); + ok( + !translationsSettingsTestUtils.getDownloadErrorButton("fr"), + "French error should close when another delete confirmation opens" + ); + + info("Reload about:preferences"); + await loadNewPage(gBrowser.selectedBrowser, "about:preferences"); + + const reloadedTestUtils = new TranslationsSettingsTestUtils( + gBrowser.selectedBrowser.contentDocument + ); + + await reloadedTestUtils.openTranslationsSubpageFromDocument(); + await reloadedTestUtils.assertDownloadedLanguagesEmptyState({ + visible: true, + }); + is( + reloadedTestUtils.getSelectedDownloadLanguage(), + "", + "Download selection should reset after reload" + ); + ok( + !reloadedTestUtils.getDownloadDeleteConfirmButton("es"), + "Delete confirmation should reset after reload" + ); + ok( + !reloadedTestUtils.getDownloadErrorButton("fr"), + "Failed download state should reset after reload" + ); + ok( + !reloadedTestUtils.getDownloadRetryButton("fr"), + "Retry button should not persist after reload" + ); + + 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 @@ -36,7 +36,8 @@ add_task(async function test_download_languages_sorting_and_batch_resolution() { ); await initialDownloadsRendered; - const downloadButton = translationsSettingsTestUtils.getDownloadButton(); + const downloadButton = + translationsSettingsTestUtils.getDownloadLanguageButton(); info("Verify empty state before downloads"); await translationsSettingsTestUtils.assertDownloadedLanguagesEmptyState({ diff --git a/browser/themes/shared/preferences/preferences.css b/browser/themes/shared/preferences/preferences.css @@ -1450,6 +1450,10 @@ setting-group[groupid="home"] { margin: var(--space-large) 0; } +.translations-download-language-error { + background-color: var(--background-color-critical); +} + .hidden-category { display: none; } @@ -1551,3 +1555,7 @@ setting-group[groupid="home"] { #payments-list-header { --box-label-font-weight: var(--heading-font-weight); } + +.box-header-bold { + --box-label-font-weight: var(--heading-font-weight); +} diff --git a/toolkit/components/translations/tests/browser/shared-head.js b/toolkit/components/translations/tests/browser/shared-head.js @@ -492,10 +492,10 @@ class TranslationsSettingsTestUtils { static InitializationFailed = "TranslationsSettingsTest:InitializationFailed"; - static DownloadButtonEnabled = - "TranslationsSettingsTest:DownloadButtonEnabled"; - static DownloadButtonDisabled = - "TranslationsSettingsTest:DownloadButtonDisabled"; + static DownloadLanguageButtonEnabled = + "TranslationsSettingsTest:DownloadLanguageButtonEnabled"; + static DownloadLanguageButtonDisabled = + "TranslationsSettingsTest:DownloadLanguageButtonDisabled"; }; /** @@ -779,7 +779,7 @@ class TranslationsSettingsTestUtils { * * @returns {HTMLButtonElement|null} */ - getDownloadButton() { + getDownloadLanguageButton() { return this.document.getElementById("translationsDownloadLanguagesButton"); } @@ -830,7 +830,7 @@ class TranslationsSettingsTestUtils { .DownloadedLanguagesSelectOptionsUpdated ); - await click(this.getDownloadButton(), `Start ${langTag} download`); + await click(this.getDownloadLanguageButton(), `Start ${langTag} download`); await Promise.all([started, renderInProgress, optionsUpdated]); const completed = this.waitForEvent( @@ -895,7 +895,7 @@ class TranslationsSettingsTestUtils { ); await click( - this.getDownloadButton(), + this.getDownloadLanguageButton(), `Start ${langTag} download (expect failure)` ); await Promise.all([started, renderInProgress, optionsUpdated]);