diff --git a/src/renderer/src/features/installations/pages/ManageInstallationBackups.tsx b/src/renderer/src/features/installations/pages/ManageInstallationBackups.tsx index 7f975f66..6662bcd8 100644 --- a/src/renderer/src/features/installations/pages/ManageInstallationBackups.tsx +++ b/src/renderer/src/features/installations/pages/ManageInstallationBackups.tsx @@ -96,6 +96,7 @@ function ManageInstallationBackups(): JSX.Element { if (!installation) return addNotification(t("features.installations.noInstallationFound"), "error") if (!backup) return addNotification(t("features.backups.cantDeleteWhileinUse"), "error") + configDispatch({ type: CONFIG_ACTIONS.EDIT_INSTALLATION_BACKUP, payload: { id: installation.id, backupId: backup.id, updates: { _deleting: true } } }) const result = await deleteInstallationBackup(createBackupDeletionPorts(), { backup: toBackupSnapshot(backup) }) if (result.ok) { @@ -103,6 +104,9 @@ function ManageInstallationBackups(): JSX.Element { return addNotification(t("features.backups.backupDeletedSuccesfully"), "success") } + if (result.reason !== "backup-in-use") { + configDispatch({ type: CONFIG_ACTIONS.EDIT_INSTALLATION_BACKUP, payload: { id: installation.id, backupId: backup.id, updates: { _deleting: false } } }) + } const { messageKey, logged } = describeBackupDeletionFailure(result.reason) if (logged) { @@ -153,10 +157,10 @@ function ManageInstallationBackups(): JSX.Element {
- setBackupToRestore(backup)}> + setBackupToRestore(backup)} disabled={backup._deleting || backup._restoring}> - setBackupToDelete(backup)} title={t("generic.delete")} className="p-1"> + setBackupToDelete(backup)} title={t("generic.delete")} className="p-1" disabled={backup._deleting || backup._restoring}> openPathInExplorer(backup.path, { parentOfFile: true })} title={`${t("generic.openOnFileExplorer")} ยท ${backup.path}`} className="p-1"> diff --git a/tests/renderer-dom/installationsRestoreBackup.test.tsx b/tests/renderer-dom/installationsRestoreBackup.test.tsx index 31f2da09..27c182dd 100644 --- a/tests/renderer-dom/installationsRestoreBackup.test.tsx +++ b/tests/renderer-dom/installationsRestoreBackup.test.tsx @@ -96,4 +96,95 @@ describe("ManageInstallationBackups", () => { // The confirm dialog closes as part of the same click. await waitFor(() => expect(screen.queryByText("Are you sure you want to restore this Backup?")).toBeNull()) }) + + it("deletes the backup archive after the delete confirmation", async () => { + const user = userEvent.setup() + const deletePath = vi.fn(async () => true) + installMockWindowApi({ + configManager: { getConfig: vi.fn(async () => createMockConfig({ installations: [anInstallationWithBackup()] })) }, + pathsManager: { + deletePath, + extractOnPath: vi.fn(async () => true) + } + }) + + renderManageBackups("install-a") + + await user.click(await screen.findByTitle("Delete")) + await screen.findByText("Are you sure you want to delete this Backup?") + await user.click(screen.getAllByTitle("Delete")[1]!) + + await waitFor(() => expect(deletePath).toHaveBeenCalledTimes(1)) + expect(deletePath.mock.calls[0]?.[0]).toBe("/backups/a/backup-1.zip") + await waitFor(() => expect(screen.queryByTitle("Delete")).toBeNull()) + }) + + it("disables the row trash button while a deletion is in flight", async () => { + const user = userEvent.setup() + let resolveDelete: (result: boolean) => void = () => {} + const deletePath = vi.fn( + () => + new Promise((resolve) => { + resolveDelete = resolve + }) + ) + installMockWindowApi({ + configManager: { getConfig: vi.fn(async () => createMockConfig({ installations: [anInstallationWithBackup()] })) }, + pathsManager: { + deletePath, + extractOnPath: vi.fn(async () => true) + } + }) + + renderManageBackups("install-a") + + // Acquire the row's trash button. Before deletion it must be enabled. + const trashButton = await screen.findByTitle("Delete") + expect(trashButton.hasAttribute("disabled")).toBe(false) + + // Start the first deletion through the confirm dialog. + await user.click(trashButton) + await screen.findByText("Are you sure you want to delete this Backup?") + const confirmButtons = screen.getAllByTitle("Delete") + await user.click(confirmButtons[confirmButtons.length - 1]!) + + await waitFor(() => expect(deletePath).toHaveBeenCalledTimes(1)) + + // While the deletion is in flight, the row trash button must be disabled. + // This assertion fails on dev (where _deleting is never set) and passes on + // this branch (where configDispatch sets _deleting: true before the call). + await waitFor(() => { + const buttons = screen.getAllByRole("button") + const disabledButtons = buttons.filter((btn) => btn.hasAttribute("disabled")) + expect(disabledButtons.length).toBeGreaterThan(0) + }) + + resolveDelete(true) + await waitFor(() => expect(deletePath).toHaveBeenCalledTimes(1)) + }) + + it("clears the deleting state when the archive deletion fails", async () => { + const user = userEvent.setup() + const deletePath = vi.fn().mockResolvedValueOnce(false).mockResolvedValueOnce(true) + installMockWindowApi({ + configManager: { getConfig: vi.fn(async () => createMockConfig({ installations: [anInstallationWithBackup()] })) }, + pathsManager: { + deletePath, + extractOnPath: vi.fn(async () => true) + } + }) + + renderManageBackups("install-a") + + await user.click(await screen.findByTitle("Delete")) + await screen.findByText("Are you sure you want to delete this Backup?") + await user.click(screen.getAllByTitle("Delete")[1]!) + await waitFor(() => expect(deletePath).toHaveBeenCalledTimes(1)) + await waitFor(() => expect(screen.getByTitle("Delete")).toBeTruthy()) + + await user.click(screen.getByTitle("Delete")) + await screen.findByText("Are you sure you want to delete this Backup?") + await user.click(screen.getAllByTitle("Delete")[1]!) + await waitFor(() => expect(deletePath).toHaveBeenCalledTimes(2)) + }) })