Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,17 @@ 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) {
configDispatch({ type: CONFIG_ACTIONS.DELETE_INSTALLATION_BACKUP, payload: { id: installation.id, backupId: backup.id } })
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) {
Expand Down Expand Up @@ -153,10 +157,10 @@ function ManageInstallationBackups(): JSX.Element {
<ThinSeparator />

<div className="shrink-0 w-fit flex gap-1 text-lg">
<NormalButton className="p-1" title={t("features.backups.restoreBackup")} onClick={() => setBackupToRestore(backup)}>
<NormalButton className="p-1" title={t("features.backups.restoreBackup")} onClick={() => setBackupToRestore(backup)} disabled={backup._deleting || backup._restoring}>
<PiArrowCounterClockwiseDuotone />
</NormalButton>
<NormalButton onClick={() => setBackupToDelete(backup)} title={t("generic.delete")} className="p-1">
<NormalButton onClick={() => setBackupToDelete(backup)} title={t("generic.delete")} className="p-1" disabled={backup._deleting || backup._restoring}>
<PiTrashDuotone />
</NormalButton>
<NormalButton onClick={() => openPathInExplorer(backup.path, { parentOfFile: true })} title={`${t("generic.openOnFileExplorer")} · ${backup.path}`} className="p-1">
Expand Down
91 changes: 91 additions & 0 deletions tests/renderer-dom/installationsRestoreBackup.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<BridgeAPI["pathsManager"]["deletePath"]>(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<boolean>((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<BridgeAPI["pathsManager"]["deletePath"]>().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))
})
})
Loading