diff --git a/tests/test_downloads_tab_workflow_route.mjs b/tests/test_downloads_tab_workflow_route.mjs index 0b9b545..0cd83c2 100644 --- a/tests/test_downloads_tab_workflow_route.mjs +++ b/tests/test_downloads_tab_workflow_route.mjs @@ -4259,6 +4259,45 @@ test('progress polling explicitly handles cancelling as a non-interactive state' ); }); +test('Missing Models download progress preserves hovered controls between polling updates', () => { + const renderDownloadSnapshot = extractMethod( + resolveDownloadMethodsSource, + 'renderDownloadSnapshot' + ); + const renderDownloadProgressGroupForMissing = extractMethod( + resolveDownloadMethodsSource, + 'renderDownloadProgressGroupForMissing' + ); + const patchDownloadProgressContent = extractMethod( + resolveDownloadMethodsSource, + 'patchDownloadProgressContent' + ); + const pollDownloadProgress = extractMethod( + resolveDownloadMethodsSource, + 'pollDownloadProgress' + ); + + assert.match(renderDownloadSnapshot, /patchDownloadProgressContent\(/); + assert.doesNotMatch(renderDownloadSnapshot, /progressDiv\.innerHTML\s*=/); + assert.match(renderDownloadProgressGroupForMissing, /patchDownloadProgressContent\(/); + assert.doesNotMatch(renderDownloadProgressGroupForMissing, /progressDiv\.innerHTML\s*=/); + assert.match( + patchDownloadProgressContent, + /patchDownloadsPanelElement\(progressDiv,\s*nextProgressDiv\)/ + ); + + const activeProgressBranch = pollDownloadProgress.match( + /if \(progress\.status === 'downloading'[\s\S]*?\} else if \(progress\.status === 'cancelling'\)/ + )?.[0] || ''; + const cancellingBranch = pollDownloadProgress.match( + /else if \(progress\.status === 'cancelling'\)[\s\S]*?\} else if \(progress\.status === 'completed'\)/ + )?.[0] || ''; + assert.ok(activeProgressBranch); + assert.ok(cancellingBranch); + assert.doesNotMatch(activeProgressBranch, /refreshLocalMatchesUiForMissing/); + assert.doesNotMatch(cancellingBranch, /refreshLocalMatchesUiForMissing/); +}); + test('native Xet progress polling refreshes every 200 milliseconds', () => { const getDownloadProgressPollDelay = eval(`(${extractMethod(resolveDownloadMethodsSource, 'getDownloadProgressPollDelay')})`); diff --git a/web/resolver/actions/resolve_download_methods.js b/web/resolver/actions/resolve_download_methods.js index 504191c..a834849 100644 --- a/web/resolver/actions/resolve_download_methods.js +++ b/web/resolver/actions/resolve_download_methods.js @@ -1253,6 +1253,25 @@ export const resolveDownloadMethods = { `; }, + patchDownloadProgressContent(progressDiv, html) { + if (!progressDiv) return; + + const nextHtml = String(html || ''); + if ( + typeof progressDiv.cloneNode !== 'function' + || typeof this.patchDownloadsPanelElement !== 'function' + ) { + if (progressDiv.innerHTML !== nextHtml) { + progressDiv.innerHTML = nextHtml; + } + return; + } + + const nextProgressDiv = progressDiv.cloneNode(false); + nextProgressDiv.innerHTML = nextHtml; + this.patchDownloadsPanelElement(progressDiv, nextProgressDiv); + }, + renderDownloadProgressGroupForMissing(missing, progressDiv, { includeDownloadId = '', includeSnapshot = null } = {}) { if (!missing || !progressDiv) return false; @@ -1271,11 +1290,24 @@ export const resolveDownloadMethods = { progressDiv.classList.remove('mr-is-hidden'); progressDiv.classList.add('mr-is-visible'); - progressDiv.innerHTML = `
${items.join('')}
`; + this.patchDownloadProgressContent( + progressDiv, + `
${items.join('')}
` + ); this.attachDownloadActionHandlers(progressDiv, includeDownloadId); return true; }, + ensureSearchDownloadButtonIcon(downloadBtn) { + if ( + !downloadBtn?.classList?.contains('search-download-btn') + || downloadBtn.querySelector?.('svg') + ) { + return; + } + downloadBtn.innerHTML = getSvgIcon('download'); + }, + updateDownloadButtonForSnapshot(downloadBtn, progress, status, shouldRenderProgress) { if (!downloadBtn) return; @@ -1293,7 +1325,7 @@ export const resolveDownloadMethods = { ? `Paused ${percentLabel}`.trim() : (percentLabel ? `Downloading ${percentLabel}` : 'Starting download...')); if (downloadBtn.classList.contains('search-download-btn')) { - downloadBtn.innerHTML = getSvgIcon('download'); + this.ensureSearchDownloadButtonIcon(downloadBtn); downloadBtn.setAttribute('data-tooltip', label); downloadBtn.setAttribute('aria-label', label); } else { @@ -1307,7 +1339,7 @@ export const resolveDownloadMethods = { downloadBtn.disabled = true; downloadBtn.classList.remove('mr-is-success-action', 'mr-btn-primary'); if (downloadBtn.classList.contains('search-download-btn')) { - downloadBtn.innerHTML = getSvgIcon('download'); + this.ensureSearchDownloadButtonIcon(downloadBtn); downloadBtn.setAttribute('data-tooltip', 'Cancelling download...'); downloadBtn.setAttribute('aria-label', 'Cancelling download'); } else { @@ -1317,7 +1349,7 @@ export const resolveDownloadMethods = { downloadBtn.disabled = false; downloadBtn.classList.remove('mr-is-success-action', 'mr-btn-primary'); if (downloadBtn.classList.contains('search-download-btn')) { - downloadBtn.innerHTML = getSvgIcon('download'); + this.ensureSearchDownloadButtonIcon(downloadBtn); downloadBtn.setAttribute('data-tooltip', 'Retry download'); downloadBtn.setAttribute('aria-label', 'Retry download'); } else { @@ -1354,7 +1386,10 @@ export const resolveDownloadMethods = { if (progressDiv) { progressDiv.classList.remove('mr-is-hidden'); progressDiv.classList.add('mr-is-visible'); - progressDiv.innerHTML = this.renderDownloadSnapshotMarkup(downloadId, snapshot); + this.patchDownloadProgressContent( + progressDiv, + this.renderDownloadSnapshotMarkup(downloadId, snapshot) + ); this.attachDownloadActionHandlers(progressDiv, downloadId); } @@ -1873,7 +1908,6 @@ export const resolveDownloadMethods = { if (progress.status === 'downloading' || progress.status === 'starting' || progress.status === 'paused') { this.renderDownloadSnapshot(downloadId, snapshot, { progressDiv, downloadBtn }); - this.refreshLocalMatchesUiForMissing?.(missing); if (typeof this.requestQueuePanelUpdate === 'function') { this.requestQueuePanelUpdate(); } else { @@ -1886,7 +1920,6 @@ export const resolveDownloadMethods = { } else if (progress.status === 'cancelling') { this.renderDownloadSnapshot(downloadId, snapshot, { progressDiv, downloadBtn }); - this.refreshLocalMatchesUiForMissing?.(missing); this.updateQueuePanel?.(); setTimeout(() => this.pollDownloadProgress(downloadId), 500);