diff --git a/tests/test_downloads_tab_workflow_route.mjs b/tests/test_downloads_tab_workflow_route.mjs index a69ba48..7c63881 100644 --- a/tests/test_downloads_tab_workflow_route.mjs +++ b/tests/test_downloads_tab_workflow_route.mjs @@ -1952,6 +1952,55 @@ test('Apply reuses the content-preserving workflow analysis path', () => { assert.doesNotMatch(applyPendingResolutionList, /refreshAnalysisInBackground\(/); }); +test('Apply stays busy after linking until the old card is removed', async () => { + const applyQueuedByKey = eval(`(${extractMethod(queueMethodsSource, 'applyQueuedByKey')})`); + const window = new Window(); + const button = window.document.createElement('button'); + button.textContent = 'Apply'; + window.document.body.append(button); + let finishApply; + let calls = 0; + const dialog = { + pendingIndex: new Map([['model', 0]]), + pendingResolutions: [{}], + async applyPendingResolutionList() { + calls += 1; + await new Promise(resolve => { finishApply = resolve; }); + this.pendingIndex.delete('model'); + } + }; + const pending = applyQueuedByKey.call(dialog, 'model', button); + assert.equal(button.textContent, 'Applying...'); + assert.equal(button.disabled, true); + await applyQueuedByKey.call(dialog, 'model', button); + assert.equal(calls, 1); + finishApply(); + await pending; + assert.equal(button.isConnected, true); + assert.equal(button.textContent, 'Applying...'); + assert.equal(button.disabled, true); + assert.equal(button.classList.contains('mr-btn-is-disabled'), true); + button.remove(); + await window.happyDOM.close(); +}); + +test('Apply becomes available again when linking fails and selection stays queued', async () => { + const applyQueuedByKey = eval(`(${extractMethod(queueMethodsSource, 'applyQueuedByKey')})`); + const window = new Window(); + const button = window.document.createElement('button'); + window.document.body.append(button); + const dialog = { + pendingIndex: new Map([['model', 0]]), + pendingResolutions: [{}], + async applyPendingResolutionList() { return null; } + }; + await applyQueuedByKey.call(dialog, 'model', button); + assert.equal(button.textContent, 'Apply'); + assert.equal(button.disabled, false); + assert.equal(button.classList.contains('mr-btn-is-disabled'), false); + await window.happyDOM.close(); +}); + test('Apply keeps the workflow slot key when resolved path identity changes', () => { const doesResolvedModelMatchAlias = eval( `(${extractMethod(queueMethodsSource, 'doesResolvedModelMatchAlias')})` diff --git a/web/resolver/actions/queue_methods.js b/web/resolver/actions/queue_methods.js index adfee1e..a310218 100644 --- a/web/resolver/actions/queue_methods.js +++ b/web/resolver/actions/queue_methods.js @@ -2096,6 +2096,7 @@ export const queueMethods = { * Apply one queued resolution from the selected bar. */ async applyQueuedByKey(key, button = null) { + if (button?.disabled) return; if (!key || !this.pendingIndex.has(key)) { this.showNotification('Selected match is no longer queued.', 'error'); return; @@ -2117,7 +2118,9 @@ export const queueMethods = { try { await this.applyPendingResolutionList([selection], { clearAll: false }); } finally { - if (button?.isConnected) { + // A successful link removes the selection before the background + // analysis replaces this card. Keep it busy until that render. + if (button?.isConnected && this.pendingIndex.has(key)) { button.disabled = false; button.classList.remove('mr-btn-is-disabled'); button.textContent = 'Apply';