diff --git a/qBitControl/ViewModels/TorrentView/TorrentListHelperViewModel.swift b/qBitControl/ViewModels/TorrentView/TorrentListHelperViewModel.swift index 69fc95b..a2c26e8 100644 --- a/qBitControl/ViewModels/TorrentView/TorrentListHelperViewModel.swift +++ b/qBitControl/ViewModels/TorrentView/TorrentListHelperViewModel.swift @@ -23,7 +23,7 @@ class TorrentListHelperViewModel: ObservableObject { @Published public var isTorrentAddView: Bool = false @Published public var isSelectionMode: Bool = false - @Published public var selectedTorrents: Set = Set() + @Published public var selectedTorrents: Set = Set() @Published public var filteredTorrents: [Torrent] = [] @@ -316,17 +316,12 @@ class TorrentListHelperViewModel: ObservableObject { func checkAllTorrents() { self.torrents.forEach { torrent in - self.selectedTorrents.insert(torrent) + self.selectedTorrents.insert(torrent.hash) } } func doForSelectedTorrents(action: ([String]) -> Void) { - let selectedHashes = self.selectedTorrents.compactMap { - torrent in - torrent.hash - } - - action(selectedHashes) + action(Array(self.selectedTorrents)) self.quitSelectionMode() } diff --git a/qBitControl/Views/TorrentViews/TorrentListHelperView.swift b/qBitControl/Views/TorrentViews/TorrentListHelperView.swift index 9a2f440..2e72a8c 100644 --- a/qBitControl/Views/TorrentViews/TorrentListHelperView.swift +++ b/qBitControl/Views/TorrentViews/TorrentListHelperView.swift @@ -54,7 +54,7 @@ struct TorrentListHelperView: View { } func torrentSelectionModeRowView(torrent: Torrent) -> some View { - let isTorrentSelected = viewModel.selectedTorrents.contains(torrent) + let isTorrentSelected = viewModel.selectedTorrents.contains(torrent.hash) return HStack { Image(systemName: isTorrentSelected ? "checkmark.circle.fill" : "circle") @@ -75,9 +75,9 @@ struct TorrentListHelperView: View { .onTapGesture { withAnimation(.spring(response: 0.2, dampingFraction: 0.6)) { if isTorrentSelected { - viewModel.selectedTorrents.remove(torrent) + viewModel.selectedTorrents.remove(torrent.hash) } else { - viewModel.selectedTorrents.insert(torrent) + viewModel.selectedTorrents.insert(torrent.hash) } } } diff --git a/qBitControlTests/TorrentListHelperViewModelTests.swift b/qBitControlTests/TorrentListHelperViewModelTests.swift index 5079ef1..1960e3c 100644 --- a/qBitControlTests/TorrentListHelperViewModelTests.swift +++ b/qBitControlTests/TorrentListHelperViewModelTests.swift @@ -1,5 +1,6 @@ import XCTest import SwiftUI +import Combine @testable import qBitManager final class TorrentListHelperViewModelTests: XCTestCase { @@ -355,10 +356,59 @@ final class TorrentListHelperViewModelTests: XCTestCase { XCTAssertEqual(client.deleteFilesFlag, false) // 4. Test multi delete selected torrents - sut.selectedTorrents = [mockTorrents[0], mockTorrents[1]] + sut.selectedTorrents = [mockTorrents[0].hash, mockTorrents[1].hash] sut.deleteSelectedTorrents(isDeleteFiles: true) try? await Task.sleep(nanoseconds: 50_000_000) XCTAssertNotNil(client.deleteCalledWithHash) XCTAssertEqual(client.deleteFilesFlag, true) } + + @MainActor + func testSelection_PersistsAcrossTelemetryRefresh() async throws { + // Given: A view model loaded with a mock torrent + let mockTorrents = getTestTorrents() + let mockTorrent = mockTorrents[0] + + let client = TestTorrentClient(torrents: [mockTorrent]) + let sut = TorrentListHelperViewModel(client: client) + + // Setup expectation to wait for initial Combine emission FIRST + var cancellables = Set() + let initialExpectation = XCTestExpectation(description: "Initial Combine emission") + sut.$filteredTorrents + .dropFirst() + .sink { _ in initialExpectation.fulfill() } + .store(in: &cancellables) + + // Inject initial torrent into the cache SECOND + qBitData.shared.cacheManager.torrents = [mockTorrent.hash: mockTorrent] + + await fulfillment(of: [initialExpectation], timeout: 1.0) + + // Select the torrent hash + sut.selectedTorrents = [mockTorrent.hash] + XCTAssertTrue(sut.selectedTorrents.contains(mockTorrent.hash)) + + // Setup expectation to wait for refresh Combine emission + let refreshExpectation = XCTestExpectation(description: "Refresh Combine emission") + sut.$filteredTorrents + .dropFirst() + .sink { _ in refreshExpectation.fulfill() } + .store(in: &cancellables) + + // When: Telemetry updates properties (speed and progress change) + var updatedTorrent = mockTorrent + updatedTorrent.progress = 0.55 + updatedTorrent.dlspeed = 200 + updatedTorrent.upspeed = 80 + + // Merge the update into the cache + qBitData.shared.cacheManager.torrents = [updatedTorrent.hash: updatedTorrent] + + // Await the update event reactively (sleep-free!) + await fulfillment(of: [refreshExpectation], timeout: 1.0) + + // Then: The selection MUST still persist, even though properties changed + XCTAssertTrue(sut.selectedTorrents.contains(mockTorrent.hash), "Selection should persist after telemetry updates properties") + } }