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 @@ -23,7 +23,7 @@ class TorrentListHelperViewModel: ObservableObject {
@Published public var isTorrentAddView: Bool = false
@Published public var isSelectionMode: Bool = false

@Published public var selectedTorrents: Set<Torrent> = Set()
@Published public var selectedTorrents: Set<String> = Set()

@Published public var filteredTorrents: [Torrent] = []

Expand Down Expand Up @@ -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()
}

Expand Down
6 changes: 3 additions & 3 deletions qBitControl/Views/TorrentViews/TorrentListHelperView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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)
}
}
}
Expand Down
52 changes: 51 additions & 1 deletion qBitControlTests/TorrentListHelperViewModelTests.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import XCTest
import SwiftUI
import Combine
@testable import qBitManager

final class TorrentListHelperViewModelTests: XCTestCase {
Expand Down Expand Up @@ -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<AnyCancellable>()
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")
}
}
Loading