mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-14 01:33:21 -03:00
feat(delete): add undo toasts and harden delete modals
This commit is contained in:
@@ -1,11 +1,18 @@
|
||||
import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest';
|
||||
|
||||
const showToastMock = vi.fn();
|
||||
const showActionToastMock = vi.fn();
|
||||
const handleUndoDeleteMock = vi.fn();
|
||||
const recreateVirtualScrollMock = vi.fn();
|
||||
const translateMock = vi.fn((key) => key);
|
||||
|
||||
vi.mock('../../../static/js/utils/uiHelpers.js', () => ({
|
||||
showToast: showToastMock,
|
||||
showActionToast: showActionToastMock,
|
||||
}));
|
||||
|
||||
vi.mock('../../../static/js/utils/undoHelpers.js', () => ({
|
||||
handleUndoDelete: handleUndoDeleteMock,
|
||||
}));
|
||||
|
||||
vi.mock('../../../static/js/utils/i18nHelpers.js', () => ({
|
||||
@@ -20,6 +27,17 @@ vi.mock('../../../static/js/components/RecipeCard.js', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../../static/js/utils/modalUtils.js', () => ({
|
||||
armDeleteButton: (modalElement) => {
|
||||
if (!modalElement) return null;
|
||||
const buttons = modalElement.querySelectorAll('.delete-btn');
|
||||
buttons.forEach((button) => { button.disabled = true; });
|
||||
return setTimeout(() => {
|
||||
buttons.forEach((button) => { button.disabled = false; });
|
||||
}, 1500);
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../../static/js/utils/infiniteScroll.js', () => ({
|
||||
recreateVirtualScroll: recreateVirtualScrollMock,
|
||||
}));
|
||||
@@ -211,3 +229,152 @@ describe('DuplicatesManager prompt matching toggle', () => {
|
||||
expect(document.getElementById('duplicatesBasis').textContent).toBe('recipes.duplicates.basis.loraCombo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DuplicatesManager confirmDeleteDuplicates undo flows', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
setCurrentPageType('recipes');
|
||||
setupDom();
|
||||
state.pendingLayoutRecreate = false;
|
||||
state.virtualScroller = { enable: vi.fn(), disable: vi.fn() };
|
||||
handleUndoDeleteMock.mockResolvedValue(true);
|
||||
globalThis.modalManager = { showModal: vi.fn(), closeModal: vi.fn() };
|
||||
globalThis.recipeManager = { loadRecipes: vi.fn() };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
state.pendingLayoutRecreate = false;
|
||||
state.virtualScroller = null;
|
||||
delete globalThis.modalManager;
|
||||
delete globalThis.recipeManager;
|
||||
delete globalThis.fetch;
|
||||
});
|
||||
|
||||
function mockBulkDelete(payload) {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => payload,
|
||||
});
|
||||
}
|
||||
|
||||
function lastActionToastOptions() {
|
||||
const call = showActionToastMock.mock.calls[showActionToastMock.mock.calls.length - 1];
|
||||
return call[3];
|
||||
}
|
||||
|
||||
it('shows the undo action toast with the batch id and reloads recipes on undo', async () => {
|
||||
mockBulkDelete({ success: true, total_deleted: 2, batch_id: 'recipe-batch-1' });
|
||||
|
||||
const manager = new DuplicatesManager({});
|
||||
manager.inDuplicateMode = true;
|
||||
manager.selectedForDeletion.add('r1');
|
||||
manager.selectedForDeletion.add('r2');
|
||||
|
||||
await manager.confirmDeleteDuplicates();
|
||||
|
||||
expect(showActionToastMock).toHaveBeenCalledTimes(1);
|
||||
expect(showActionToastMock).toHaveBeenCalledWith(
|
||||
'toast.undo.deletedBulk',
|
||||
{ count: 2 },
|
||||
'success',
|
||||
expect.objectContaining({
|
||||
actionText: 'toast.undo.action',
|
||||
onAction: expect.any(Function),
|
||||
})
|
||||
);
|
||||
// The legacy duplicates success toast is replaced, not duplicated
|
||||
expect(showToastMock).not.toHaveBeenCalledWith(
|
||||
'toast.duplicates.deleteSuccess',
|
||||
expect.anything(),
|
||||
expect.anything()
|
||||
);
|
||||
// exitDuplicateMode still runs for successful deletions
|
||||
expect(manager.inDuplicateMode).toBe(false);
|
||||
|
||||
lastActionToastOptions().onAction();
|
||||
|
||||
expect(handleUndoDeleteMock).toHaveBeenCalledTimes(1);
|
||||
expect(handleUndoDeleteMock).toHaveBeenCalledWith('recipe-batch-1', expect.any(Function));
|
||||
|
||||
const refreshFn = handleUndoDeleteMock.mock.calls[0][1];
|
||||
refreshFn();
|
||||
expect(globalThis.recipeManager.loadRecipes).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('undoes the batch_ids fallback sequentially with one final refresh and restored toast', async () => {
|
||||
mockBulkDelete({ success: true, total_deleted: 2, batch_ids: ['rb-1', 'rb-2'] });
|
||||
|
||||
const manager = new DuplicatesManager({});
|
||||
manager.inDuplicateMode = true;
|
||||
manager.selectedForDeletion.add('r1');
|
||||
manager.selectedForDeletion.add('r2');
|
||||
|
||||
await manager.confirmDeleteDuplicates();
|
||||
|
||||
expect(showActionToastMock).toHaveBeenCalledTimes(1);
|
||||
await lastActionToastOptions().onAction();
|
||||
|
||||
expect(handleUndoDeleteMock).toHaveBeenCalledTimes(2);
|
||||
expect(handleUndoDeleteMock.mock.calls[0]).toEqual(['rb-1', null, { showToast: false, refresh: false }]);
|
||||
expect(handleUndoDeleteMock.mock.calls[1]).toEqual(['rb-2', null, { showToast: false, refresh: false }]);
|
||||
expect(globalThis.recipeManager.loadRecipes).toHaveBeenCalledTimes(1);
|
||||
expect(globalThis.recipeManager.loadRecipes).toHaveBeenCalledWith(true);
|
||||
expect(showToastMock).toHaveBeenCalledTimes(1);
|
||||
expect(showToastMock).toHaveBeenCalledWith('toast.undo.restored', {}, 'success');
|
||||
});
|
||||
|
||||
it('keeps the legacy success toast when the response carries no batch field', async () => {
|
||||
mockBulkDelete({ success: true, total_deleted: 1 });
|
||||
|
||||
const manager = new DuplicatesManager({});
|
||||
manager.inDuplicateMode = true;
|
||||
manager.selectedForDeletion.add('r1');
|
||||
|
||||
await manager.confirmDeleteDuplicates();
|
||||
|
||||
expect(showActionToastMock).not.toHaveBeenCalled();
|
||||
expect(showToastMock).toHaveBeenCalledWith(
|
||||
'toast.duplicates.deleteSuccess',
|
||||
{ count: 1, type: 'recipes' },
|
||||
'success'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DuplicatesManager deleteSelectedDuplicates delay-activate', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
setCurrentPageType('recipes');
|
||||
setupDom();
|
||||
document.body.insertAdjacentHTML('beforeend', `
|
||||
<div id="duplicateDeleteModal" class="modal delete-modal">
|
||||
<div class="delete-model-info"><p><span id="duplicateDeleteCount">0</span></p></div>
|
||||
<button class="cancel-btn">Cancel</button>
|
||||
<button class="delete-btn">Delete</button>
|
||||
</div>
|
||||
`);
|
||||
globalThis.modalManager = { showModal: vi.fn(), closeModal: vi.fn() };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
delete globalThis.modalManager;
|
||||
});
|
||||
|
||||
it('opens with the delete button disabled and enables it after 1500ms', async () => {
|
||||
const manager = new DuplicatesManager({});
|
||||
manager.selectedForDeletion.add('r1');
|
||||
|
||||
await manager.deleteSelectedDuplicates();
|
||||
|
||||
expect(globalThis.modalManager.showModal).toHaveBeenCalledWith('duplicateDeleteModal');
|
||||
const deleteBtn = document.querySelector('#duplicateDeleteModal .delete-btn');
|
||||
expect(deleteBtn.disabled).toBe(true);
|
||||
|
||||
deleteBtn.click();
|
||||
expect(deleteBtn.disabled).toBe(true);
|
||||
|
||||
vi.advanceTimersByTime(1500);
|
||||
expect(deleteBtn.disabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user